au-rogue
Version:
Conservative Aurelia 1 to 2 codemods. Changes only what is safe, reports everything.
152 lines (151 loc) • 7.07 kB
JavaScript
import { Node } from 'ts-morph';
function ensureImport(sf, module, names) {
const existing = sf.getImportDeclarations().find(i => i.getModuleSpecifierValue() === module);
if (existing) {
const toAdd = new Set(names);
for (const ni of existing.getNamedImports()) {
toAdd.delete(ni.getName());
}
for (const name of toAdd)
existing.addNamedImport(name);
return;
}
sf.addImportDeclaration({ moduleSpecifier: module, namedImports: names });
}
function removeNamedImports(sf, module, removeNames) {
for (const imp of sf.getImportDeclarations()) {
if (imp.getModuleSpecifierValue() !== module)
continue;
let removed = false;
for (const ni of [...imp.getNamedImports()]) {
if (removeNames.has(ni.getName())) {
ni.remove();
removed = true;
}
}
if (removed) {
if (imp.getNamedImports().length === 0 && !imp.getDefaultImport() && !imp.getNamespaceImport()) {
imp.remove();
}
}
}
}
function getRuntimeTokenTextForParam(sf, cls, paramName) {
// Not enough info here, use the type based function instead
return null;
}
function tokenNameForInterface(name) {
return `${name}Token`;
}
export function transformDI(project, reporter) {
const aureliaV1Modules = [
'aurelia-framework',
'aurelia-dependency-injection',
'aurelia-binding'
];
for (const sf of project.getSourceFiles()) {
let touched = false;
// Remove autoinject from imports
for (const mod of aureliaV1Modules) {
removeNamedImports(sf, mod, new Set(['autoinject']));
}
// Remove class-level @autoinject and convert parameter properties
for (const cls of sf.getClasses()) {
const autoDecorators = cls.getDecorators().filter(d => d.getName() === 'autoinject');
if (autoDecorators.length > 0) {
autoDecorators.forEach(d => d.remove());
touched = true;
reporter.edit(sf.getFilePath(), `Removed @autoinject on class ${cls.getName() || '(anonymous)'}`);
}
const ctor = cls.getConstructors()[0];
if (!ctor)
continue;
const params = ctor.getParameters().filter(p => p.isParameterProperty());
if (params.length === 0)
continue;
// Add imports for resolve as needed
let needResolve = false;
let needDI = false;
for (const p of params) {
const name = p.getName();
const typeNode = p.getTypeNode();
const type = p.getType();
const typeText = typeNode ? typeNode.getText() : null;
// Decide if the type resolves to a runtime value
const symbol = type.getSymbol() || type.getAliasSymbol();
const decls = symbol ? symbol.getDeclarations() : [];
const isClass = decls?.some(d => Node.isClassDeclaration(d) || Node.isClassExpression(d));
const isInterface = type.isInterface();
const isAnonymous = type.isAnonymous() && !typeText;
if (isClass && typeText) {
// Convert to class property with resolve(TypeName)
cls.insertProperty(0, {
name,
scope: p.getScope(),
isReadonly: p.getReadonlyKeyword() !== undefined,
type: typeText,
initializer: `resolve(${typeText})`
});
reporter.edit(sf.getFilePath(), `Converted parameter property '${name}: ${typeText}' to 'resolve(${typeText})' on class ${cls.getName() || '(anonymous)'}`);
p.remove();
needResolve = true;
touched = true;
}
else if (isInterface && typeText) {
// Generate a DI token at file level
const tokenConst = tokenNameForInterface(typeText.replace(/<.*>/, ''));
const hasToken = !!sf.getVariableDeclaration(tokenConst);
if (!hasToken) {
sf.insertVariableStatement(0, {
declarationKind: 'const',
declarations: [{
name: tokenConst,
initializer: `DI.createInterface<${typeText}>('${typeText}')`
}],
isExported: false
});
reporter.add(sf.getFilePath(), `Added interface token '${tokenConst}' for type '${typeText}'`);
needDI = true;
touched = true;
}
cls.insertProperty(0, {
name,
scope: p.getScope(),
isReadonly: p.getReadonlyKeyword() !== undefined,
type: typeText,
initializer: `resolve(${tokenConst})`
});
reporter.edit(sf.getFilePath(), `Converted parameter property '${name}: ${typeText}' to 'resolve(${tokenConst})' on class ${cls.getName() || '(anonymous)'} (generated token)`);
p.remove();
needResolve = true;
touched = true;
reporter.warn(sf.getFilePath(), `Generated DI token '${tokenConst}' for interface '${typeText}'. Confirm registrations match this token.`);
}
else {
// Skip, leave as is, note for manual work
reporter.warn(sf.getFilePath(), `Skipped converting parameter property '${name}${typeText ? ': ' + typeText : ''}' on class ${cls.getName() || '(anonymous)'} due to non-runtime type. Replace with resolve(...) or @inject manually.`);
}
}
// If constructor is now empty parameter list and empty body, leave it as is
if (needResolve) {
ensureImport(sf, 'aurelia', ['resolve']);
}
if (needDI) {
ensureImport(sf, 'aurelia', ['DI', 'resolve']);
}
}
if (touched) {
// Remove v1 only imports if they are now empty
for (const mod of aureliaV1Modules) {
for (const imp of [...sf.getImportDeclarations()]) {
if (imp.getModuleSpecifierValue() !== mod)
continue;
if (imp.getNamedImports().length === 0 && !imp.getDefaultImport() && !imp.getNamespaceImport()) {
imp.remove();
reporter.remove(sf.getFilePath(), `Removed empty import '${mod}'`);
}
}
}
}
}
}