Last active
December 30, 2024 09:55
-
-
Save migajek/adaa1e56a03f913f18aee05bc12c2e09 to your computer and use it in GitHub Desktop.
Aurelia 2 Autoinject Webpack Transformer - automatically annotate classes with @Inject(Dep1, Dep2, ..DepN) decorator based on the class constructor parameter types
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Aurelia 2 Autoinject Webpack Transformer | |
Have you improved it? Please share! | |
https://gist.github.com/migajek/adaa1e56a03f913f18aee05bc12c2e09 | |
Aurelia 2 beta 15 implemented TC 39 decorators, which currently doesn't support emitting metadata. | |
This forces developers to explicitly decorate class with @inject(Dep1, Dep2) | |
this code is a very limited transformer that handles the issue for MY codebase, and therefore might not work properly for your :) | |
However, in our case, the codebase followed a convention - no "resolve" parameter values, only type annotation. So each constructor looked like this: | |
constructor (private ea: EventAggregator, private myService: InternalService) | |
This code does following | |
- for each class, take the constructor parameters | |
- if there are any parameter types that *might* be injectable, put the @inject(...types) on the class | |
- if necessary, import {inject} from 'aurelia' | |
the injectable types are simply the ones that are imported in the file, that is they are present in the import statements. | |
Again, this code is very limited and was crafted for very specific codebase. | |
It doesn't validate the number of parameters on the constructor vs on the final @inject statement. | |
It doesn't actually do any validation. | |
Use with caution. | |
Usage: | |
1. compile this file with tsc, put the result in your root directory | |
2. In the webpack config, find the rule for typescript files (ts). Add this custom loader AT THE END of the list (they're processed from right to left!) | |
for instance: | |
{ | |
test: /\.ts$/i, | |
use: [ | |
'ts-loader', | |
'@aurelia/webpack-loader', | |
{ | |
loader: path.resolve(__dirname, 'aurelia-inject-transformer/aurelia-inject-transformer.js') | |
} | |
], | |
exclude: /node_modules/ | |
}, | |
*/ | |
import * as ts from "typescript"; | |
import * as fs from 'fs'; | |
const allowedImplicitImports = new Set(['HTMLElement', 'Element']); | |
function addInjectDecoratorsWithImport(sourceCode: string): string { | |
const sourceFile = ts.createSourceFile( | |
'temp.ts', | |
sourceCode, | |
ts.ScriptTarget.Latest, | |
true, | |
ts.ScriptKind.TS | |
); | |
let injectAdded = false; | |
const transformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => { | |
function visit(node: ts.Node): ts.Node { | |
if (ts.isClassDeclaration(node)) { | |
const parent = node.parent; | |
const allNamedImports = ts.isSourceFile(parent) ? getAllNamedImports(parent) : undefined; | |
const constructor = node.members.find(m => ts.isConstructorDeclaration(m)) as ts.ConstructorDeclaration | undefined; | |
if (constructor && allNamedImports?.size > 0) { | |
// Extract parameter types from the constructor | |
const parameterTypes = constructor.parameters | |
.map(param => param.type ? param.type.getText() : '') | |
.filter(type => type) | |
.filter(t => allNamedImports.has(t) || allowedImplicitImports.has(t)); | |
if (parameterTypes.length > 0) { | |
// Create the @inject decorator with the parameter types | |
const injectDecorator = ts.factory.createDecorator( | |
ts.factory.createCallExpression( | |
ts.factory.createIdentifier('inject'), | |
undefined, | |
parameterTypes.map(type => ts.factory.createIdentifier(type)) | |
) | |
); | |
injectAdded = true; // Mark that @inject is added | |
const newModifiers = [ | |
injectDecorator, | |
...(node.modifiers || []) | |
] | |
// Add the @inject decorator to the class | |
return ts.factory.updateClassDeclaration( | |
node, | |
newModifiers, | |
node.name, | |
node.typeParameters, | |
node.heritageClauses, | |
node.members | |
); | |
} | |
} | |
} | |
return ts.visitEachChild(node, visit, context); | |
} | |
function addInjectImport(node: ts.SourceFile): ts.SourceFile { | |
if (injectAdded) { | |
// Check if an import for 'inject' already exists | |
const hasInjectImport = node.statements.some( | |
statement => | |
ts.isImportDeclaration(statement) && | |
getImportModuleSpecifierText(statement.moduleSpecifier) === "aurelia" && | |
statement.importClause?.namedBindings && | |
ts.isNamedImports(statement.importClause.namedBindings) && | |
statement.importClause.namedBindings.elements.some( | |
element => element.name.getText() === "inject" | |
) | |
); | |
if (!hasInjectImport) { | |
// Create the import { inject } from 'aurelia'; | |
const importStatement = ts.factory.createImportDeclaration( | |
undefined, | |
ts.factory.createImportClause( | |
false, | |
undefined, | |
ts.factory.createNamedImports([ | |
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('inject')) | |
]) | |
), | |
ts.factory.createStringLiteral('aurelia') | |
); | |
// Add the import statement to the top of the file | |
return ts.factory.updateSourceFile(node, [ | |
importStatement, | |
...node.statements | |
]); | |
} | |
} | |
return node; | |
} | |
return (node: ts.SourceFile) => { | |
const visitedNode = ts.visitNode(node, visit); | |
return ts.isSourceFile(visitedNode) ? addInjectImport(visitedNode) : visitedNode as ts.SourceFile; | |
} | |
} | |
const result = ts.transform(sourceFile, [transformer]); | |
const transformedSourceFile = result.transformed[0]; | |
const printer = ts.createPrinter(); | |
return printer.printFile(transformedSourceFile); | |
} | |
function getAllNamedImports(s: ts.SourceFile): Set<string> { | |
const imports = s.statements | |
.filter(x => ts.isImportDeclaration(x) && x.importClause?.namedBindings && | |
ts.isNamedImports(x.importClause.namedBindings)) | |
.map(x => ((x as ts.ImportDeclaration).importClause.namedBindings as ts.NamedImports).elements) | |
.flat() | |
.map(x => x.getText(s)); | |
; | |
return new Set(imports); | |
} | |
const filePath = process.argv[2]; | |
if (filePath && fs.existsSync(filePath)) { | |
fs.readFile(filePath, 'utf-8', (err, data) => { | |
if (err) { | |
console.error(`Error reading file: ${err.message}`); | |
process.exit(1); | |
} | |
const result = addInjectDecoratorsWithImport(data); | |
console.log(result); | |
}); | |
} | |
const writeTransformedResults = false; | |
// webpack transformer | |
module.exports = function (source: string) { | |
const transformed = addInjectDecoratorsWithImport(source); | |
if (writeTransformedResults) { | |
const logger = this.getLogger('inject-transformer'); | |
const fn = this.resourcePath; | |
const newFn = `${this.resourcePath}.transformed`; | |
fs.writeFileSync(newFn, transformed); | |
logger.info(`created ${newFn}`); | |
} | |
return transformed; | |
} | |
function getImportModuleSpecifierText(moduleSpecifier: ts.Expression): string { | |
return (ts.isStringLiteral(moduleSpecifier)) ? moduleSpecifier.text : moduleSpecifier.getText(); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment