Skip to content

Instantly share code, notes, and snippets.

@migajek
Last active December 30, 2024 09:55
Show Gist options
  • Save migajek/adaa1e56a03f913f18aee05bc12c2e09 to your computer and use it in GitHub Desktop.
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
/*
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