Skip to content

Instantly share code, notes, and snippets.

@Cliffback
Created November 24, 2025 09:00
Show Gist options
  • Select an option

  • Save Cliffback/ed717311b97b5643c9cb4e6212e212e7 to your computer and use it in GitHub Desktop.

Select an option

Save Cliffback/ed717311b97b5643c9cb4e6212e212e7 to your computer and use it in GitHub Desktop.
Transform componentWillLoad
// AST Transformer to fix angular initial props on versions after `@stencil/[email protected]`
// Can be run with:
// `find path/to/look/for/components -name '*.tsx' | xargs -n 1 npx tsx path/to/script/transform-componentWillLoad.ts`
import ts from 'typescript';
import fs from 'fs';
import path from 'path';
const inPath = process.argv[2];
if (!inPath) {
console.error('Usage: tsx transform-componentWillLoad.ts <file-path>');
process.exit(1);
}
const absInPath = path.resolve(inPath);
const sourceText = fs.readFileSync(absInPath, 'utf8');
const sourceFile = ts.createSourceFile(absInPath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
let replacementText: string | null = null;
let replaceStart = 0;
let replaceEnd = 0;
function isAlreadyPatched(node: ts.MethodDeclaration | ts.PropertyDeclaration): boolean {
const hasAsync = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
const returnsPromiseVoid =
node.type && ts.isTypeReferenceNode(node.type) && node.type.typeName.getText() === 'Promise' && node.type.typeArguments?.[0].kind === ts.SyntaxKind.VoidKeyword;
const body = ts.isMethodDeclaration(node)
? node.body
: ts.isPropertyDeclaration(node) && node.initializer && ts.isArrowFunction(node.initializer)
? node.initializer.body
: undefined;
const containsRAF = body?.getText().includes('requestAnimationFrame');
return !!(hasAsync && returnsPromiseVoid && containsRAF);
}
function buildAsyncComponentWillLoadMethod(f: ts.NodeFactory, originalStatements: ts.Statement[], preservedModifiers?: ts.ModifierLike[]): ts.MethodDeclaration {
// resolve: () => void
const resolveParam = f.createParameterDeclaration(
undefined,
undefined,
f.createIdentifier('resolve'),
undefined,
f.createFunctionTypeNode(undefined, [], f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)),
undefined,
);
// reject: (error: Error) => void
const rejectParam = f.createParameterDeclaration(
undefined,
undefined,
f.createIdentifier('reject'),
undefined,
f.createFunctionTypeNode(
undefined,
[f.createParameterDeclaration(undefined, undefined, f.createIdentifier('error'), undefined, f.createTypeReferenceNode('Error', undefined), undefined)],
f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
),
undefined,
);
// try { <originalStatements>; resolve(); } catch (e) { reject(e as Error); }
const tryBlock = f.createBlock([...originalStatements, f.createExpressionStatement(f.createCallExpression(f.createIdentifier('resolve'), undefined, []))], true);
const catchClause = f.createCatchClause(
f.createVariableDeclaration(f.createIdentifier('e')),
f.createBlock(
[
f.createExpressionStatement(
f.createCallExpression(f.createIdentifier('reject'), undefined, [f.createAsExpression(f.createIdentifier('e'), f.createTypeReferenceNode('Error', undefined))]),
),
],
true,
),
);
const tryStatement = f.createTryStatement(tryBlock, catchClause, undefined);
const rafCallback = f.createArrowFunction(undefined, undefined, [], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock([tryStatement], true));
const promiseExecutor = f.createArrowFunction(
undefined,
undefined,
[resolveParam, rejectParam],
undefined,
f.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
f.createBlock([f.createExpressionStatement(f.createCallExpression(f.createIdentifier('requestAnimationFrame'), undefined, [rafCallback]))], true),
);
const promiseCtor = f.createNewExpression(f.createIdentifier('Promise'), [f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)], [promiseExecutor]);
const asyncModifier = f.createModifier(ts.SyntaxKind.AsyncKeyword);
const finalModifiers = preservedModifiers ? [asyncModifier, ...preservedModifiers.filter(m => m.kind !== ts.SyntaxKind.AsyncKeyword)] : [asyncModifier];
return f.createMethodDeclaration(
finalModifiers,
undefined,
f.createIdentifier('componentWillLoad'),
undefined,
undefined,
[],
f.createTypeReferenceNode('Promise', [f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)]),
f.createBlock([f.createReturnStatement(promiseCtor)], true),
);
}
function createTransformer(): ts.TransformerFactory<ts.SourceFile> {
return context => {
const f = context.factory;
const visit: ts.Visitor = node => {
if ((ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node)) && ts.isIdentifier(node.name) && node.name.text === 'componentWillLoad') {
if (isAlreadyPatched(node)) {
console.log('ÔÜá´©Å componentWillLoad already patched. Skipping.');
return node;
}
let originalStatements: ts.Statement[] = [];
if (ts.isMethodDeclaration(node) && node.body) {
originalStatements = [...node.body.statements];
} else if (ts.isPropertyDeclaration(node) && node.initializer && ts.isArrowFunction(node.initializer)) {
originalStatements = ts.isBlock(node.initializer.body) ? [...node.initializer.body.statements] : [f.createExpressionStatement(node.initializer.body)];
}
const newMethod = buildAsyncComponentWillLoadMethod(f, originalStatements, node.modifiers ? [...node.modifiers] : undefined);
replacementText = printer.printNode(ts.EmitHint.Unspecified, newMethod, sourceFile);
replaceStart = node.getStart();
replaceEnd = node.getEnd();
return node;
}
return ts.visitEachChild(node, visit, context);
};
return sf => ts.visitNode(sf, visit) as ts.SourceFile;
};
}
ts.transform(sourceFile, [createTransformer()]);
if (replacementText && replaceStart > 0 && replaceEnd > replaceStart) {
const updatedText = sourceText.slice(0, replaceStart) + (replacementText as string) + sourceText.slice(replaceEnd);
fs.writeFileSync(absInPath, updatedText, 'utf8');
console.log(` Transformed componentWillLoad in: ${absInPath}`);
} else {
console.log('ÔÜá´©Å No componentWillLoad found or already patched.');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment