Last active
August 27, 2023 18:50
-
-
Save ezzabuzaid/e190d9ffaa1c17f63e9741029a64087c to your computer and use it in GitHub Desktop.
Migrate Angular projects to use new inject fn
This file contains 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
import { writeFileSync } from 'fs'; | |
import * as path from 'path'; | |
import * as ts from 'typescript'; | |
/** | |
* Extracts files from tsconfig.json | |
* | |
* @param tsconfigPath | |
* @returns | |
*/ | |
function getFilesFromTsConfig(tsconfigPath: string) { | |
const parseConfigHost: ts.ParseConfigHost = { | |
fileExists: ts.sys.fileExists, | |
readDirectory: ts.sys.readDirectory, | |
readFile: ts.sys.readFile, | |
useCaseSensitiveFileNames: true, | |
}; | |
const result = ts.parseJsonConfigFileContent( | |
ts.readConfigFile(tsconfigPath, ts.sys.readFile).config, | |
parseConfigHost, | |
path.dirname(tsconfigPath) | |
); | |
if (result.errors && result.errors.length) { | |
throw ts.formatDiagnostics(result.errors, { | |
getCanonicalFileName: (fileName) => fileName, | |
getCurrentDirectory: ts.sys.getCurrentDirectory, | |
getNewLine: () => ts.sys.newLine, | |
}); | |
} | |
return result; | |
} | |
const tsconfigPath = './tsconfig.json'; // path to your tsconfig.json | |
const tsConfigParseResult = getFilesFromTsConfig(tsconfigPath); | |
const program = ts.createProgram({ | |
options: tsConfigParseResult.options, | |
rootNames: tsConfigParseResult.fileNames, | |
projectReferences: tsConfigParseResult.projectReferences, | |
configFileParsingDiagnostics: tsConfigParseResult.errors, | |
}); | |
let currentSourceFile: ts.SourceFile | undefined; | |
interface ITransformOptions { | |
removeEmptyConstructor?: boolean; | |
onlyDecorators: string[]; | |
onCandidate: (node: ts.Node) => void; | |
} | |
const transformer: ( | |
options: ITransformOptions | |
) => ts.TransformerFactory<ts.SourceFile> = (options) => (context) => { | |
let changes: ts.PropertyDeclaration[] = []; | |
let tokensMap: Record<string, ReturnType<typeof makeTokenMetadata>> = {}; | |
const visit: ts.Visitor = (node) => { | |
if (ts.isClassDeclaration(node)) { | |
if (options.onlyDecorators.some((d) => getDecorator(node, d))) { | |
tokensMap = {}; | |
// check if the class has super class | |
const hasSuperClass = node.heritageClauses?.some( | |
(h) => h.token === ts.SyntaxKind.ExtendsKeyword | |
); | |
if (hasSuperClass) { | |
// subclass migration is not supported | |
return node; | |
} | |
const cstr = node.members.find(ts.isConstructorDeclaration); | |
if (cstr) { | |
changes = []; | |
const updatedNode = ts.visitEachChild(node, visit, context); // visit the current node | |
const members = [ | |
...changes, | |
...updatedNode.members, // this will keep constructor after instance properties | |
]; | |
options.onCandidate(node); | |
return context.factory.createClassDeclaration( | |
node.modifiers, | |
node.name, | |
node.typeParameters, | |
node.heritageClauses, | |
members | |
); | |
} | |
} | |
// return the node as is: nothing to migrate | |
return node; | |
} | |
if (ts.isConstructorDeclaration(node)) { | |
const updatedNode = ts.visitEachChild(node, visit, context); | |
if ( | |
options.removeEmptyConstructor === false || | |
updatedNode.parameters.length || | |
updatedNode.body?.statements.length | |
) { | |
return ts.factory.updateConstructorDeclaration( | |
node, | |
node.modifiers, | |
updatedNode.parameters, | |
updatedNode.body | |
); | |
} | |
return undefined; | |
} | |
if (ts.isPropertyAccessExpression(node)) { | |
if (ts.isIdentifier(node.expression)) { | |
const token = tokensMap[node.expression.text]; | |
if (token && !token.ignore) { | |
return ts.factory.createPropertyAccessExpression( | |
ts.factory.createIdentifier(`this.${node.expression.text}`), | |
node.name | |
); | |
} | |
} | |
} | |
if (ts.isParameter(node)) { | |
if (!node.modifiers) { | |
// ignore non constructor parameters | |
return node; | |
} | |
const token = makeTokenMetadata(node); | |
tokensMap[node.name.getText(currentSourceFile)] = token; | |
if (token.ignore) { | |
// return the node as is since it cannot be migrated | |
return node; | |
} | |
changes.push(convertToInjectSyntax(makeParameterMetadata(node), token)); | |
return undefined; | |
} | |
return ts.visitEachChild(node, visit, context); | |
}; | |
return (node) => ts.visitEachChild(node, visit, context); | |
}; | |
function makeParameterMetadata(param: ts.ParameterDeclaration) { | |
let isPublic = false; | |
let isPrivate = false; | |
let isProtected = false; | |
let isReadonly = false; | |
for (const modifier of param.modifiers ?? []) { | |
if (modifier.kind === ts.SyntaxKind.PublicKeyword) { | |
isPublic = true; | |
} | |
if (modifier.kind === ts.SyntaxKind.PrivateKeyword) { | |
isPrivate = true; | |
} | |
if (modifier.kind === ts.SyntaxKind.ProtectedKeyword) { | |
isProtected = true; | |
} | |
if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) { | |
isReadonly = true; | |
} | |
} | |
let isOptional = getDecorator(param, 'Optional'); | |
let isSelf = getDecorator(param, 'Self'); | |
let isSkipSelf = getDecorator(param, 'SkipSelf'); | |
let isHost = getDecorator(param, 'Host'); | |
return { | |
isPublic, | |
isPrivate, | |
isProtected, | |
isReadonly, | |
isOptional, | |
isSelf, | |
isSkipSelf, | |
isHost, | |
name: (param.name as ts.Identifier).text, | |
}; | |
} | |
function makeTokenMetadata(param: ts.ParameterDeclaration) { | |
let token: string | undefined; | |
let type: string | undefined; | |
let ignore = false; | |
if (!ts.isIdentifier(param.name)) { | |
// ignore properties with destructuring | |
// @Inject(TOKEN) { someValue }: Interface | |
return { | |
ignore: true, | |
}; | |
} | |
let injectDecorator = getDecorator(param, 'Inject'); | |
if (injectDecorator) { | |
const args = getDecoratorArguments(injectDecorator); | |
if (ts.isIdentifier(args[0])) { | |
token = args[0].text; | |
} | |
type = | |
param.type && ts.isTypeReferenceNode(param.type) | |
? param.type.getText(currentSourceFile) | |
: undefined; | |
} else { | |
token = | |
param.type && | |
ts.isTypeReferenceNode(param.type) && | |
ts.isIdentifier(param.type.typeName) | |
? param.type.typeName.text | |
: undefined; | |
type = token; | |
// only ignore if there is no type in simple injection | |
ignore = !type ? true : false; | |
} | |
return { | |
type, | |
token, | |
ignore, | |
}; | |
} | |
function convertToInjectSyntax( | |
metadata: ReturnType<typeof makeParameterMetadata>, | |
token: ReturnType<typeof makeTokenMetadata> | |
) { | |
const { | |
name, | |
isOptional, | |
isSelf, | |
isSkipSelf, | |
isHost, | |
isPrivate, | |
isPublic, | |
isProtected, | |
isReadonly, | |
} = metadata; | |
const optionsProperties: ts.PropertyAssignment[] = []; | |
if (isOptional) { | |
optionsProperties.push( | |
ts.factory.createPropertyAssignment('optional', ts.factory.createTrue()) | |
); | |
} | |
if (isSelf) { | |
optionsProperties.push( | |
ts.factory.createPropertyAssignment('self', ts.factory.createTrue()) | |
); | |
} | |
if (isSkipSelf) { | |
optionsProperties.push( | |
ts.factory.createPropertyAssignment('skipSelf', ts.factory.createTrue()) | |
); | |
} | |
if (isHost) { | |
optionsProperties.push( | |
ts.factory.createPropertyAssignment('host', ts.factory.createTrue()) | |
); | |
} | |
const modifiers: ts.Modifier[] = [ | |
ts.factory.createModifier( | |
isPublic | |
? ts.SyntaxKind.PublicKeyword | |
: isProtected | |
? ts.SyntaxKind.ProtectedKeyword | |
: ts.SyntaxKind.PrivateKeyword | |
), | |
]; | |
if (isReadonly) { | |
modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)); | |
} | |
const depsType = | |
token.type && token.type !== token.token | |
? [ts.factory.createTypeReferenceNode(token.type, undefined)] | |
: undefined; | |
const injectArgs: ts.Expression[] = [ | |
ts.factory.createIdentifier(token.token ?? name), | |
]; | |
if (optionsProperties.length) { | |
injectArgs.push( | |
ts.factory.createObjectLiteralExpression(optionsProperties) | |
); | |
} | |
const injectFn = ts.factory.createCallExpression( | |
ts.factory.createIdentifier('inject'), | |
depsType, | |
injectArgs | |
); | |
return ts.factory.createPropertyDeclaration( | |
modifiers, | |
name, | |
undefined, | |
undefined, | |
injectFn | |
); | |
} | |
function getDecorator(node: ts.HasDecorators, decoratorName: string) { | |
const decorators = ts.getDecorators(node) ?? []; | |
return decorators.find((it) => { | |
if (ts.isCallExpression(it.expression)) { | |
return ( | |
ts.isIdentifier(it.expression.expression) && | |
it.expression.expression.text === decoratorName | |
); | |
} | |
return false; | |
}); | |
} | |
function getDecoratorArguments(node: ts.Decorator) { | |
if (ts.isCallExpression(node.expression)) { | |
return node.expression.arguments; | |
} | |
return []; | |
} | |
function migrateFile(sourceFile: ts.SourceFile) { | |
let sourceFileChanged = false; | |
const result = ts.transform<ts.SourceFile>( | |
sourceFile, | |
[ | |
transformer({ | |
removeEmptyConstructor: true, | |
onlyDecorators: ['Component', 'Pipe', 'Directive', 'Injectable'], | |
onCandidate: () => { | |
sourceFileChanged = true; | |
}, | |
}), | |
], | |
tsConfigParseResult.options | |
); | |
if (sourceFileChanged) { | |
const printer = ts.createPrinter({ | |
newLine: ts.NewLineKind.LineFeed, | |
}); | |
const transformedSource = printer.printNode( | |
ts.EmitHint.Unspecified, | |
result.transformed[0], | |
sourceFile | |
); | |
return transformedSource; | |
} | |
} | |
function run() { | |
for (const sourceFile of program.getSourceFiles()) { | |
const fileName = sourceFile.fileName; | |
try { | |
console.log(`Migrating ${fileName}`); | |
currentSourceFile = sourceFile; | |
const newSourceFileContent = migrateFile(sourceFile); | |
if (newSourceFileContent) { | |
writeFileSync(fileName, newSourceFileContent); | |
} | |
} catch (e) { | |
console.warn(`Could not migrate ${fileName}`); | |
console.error(e); | |
} finally { | |
console.log(`Migrated ${fileName}`); | |
} | |
} | |
} | |
run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment