Skip to content

Instantly share code, notes, and snippets.

@ezzabuzaid
Last active August 27, 2023 18:50
Show Gist options
  • Save ezzabuzaid/e190d9ffaa1c17f63e9741029a64087c to your computer and use it in GitHub Desktop.
Save ezzabuzaid/e190d9ffaa1c17f63e9741029a64087c to your computer and use it in GitHub Desktop.
Migrate Angular projects to use new inject fn
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