Skip to content

Commit 9be2b33

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(compiler-cli): handle transformed classes when generating HMR code (angular#60298)
We had several places where we were trying to get the source file of a class for which we're generating HMR-related code. These calls will fail if the class was transformed so we have to get its source file through the original node. Fixes angular#60287. PR Close angular#60298
1 parent 7f85d18 commit 9be2b33

File tree

5 files changed

+77
-5
lines changed

5 files changed

+77
-5
lines changed

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1896,7 +1896,7 @@ export class ComponentDecoratorHandler
18961896
const res = compileResults(fac, def, classMetadata, 'ɵcmp', null, null, debugInfo, null);
18971897
return hmrMeta === null || res.length === 0
18981898
? null
1899-
: getHmrUpdateDeclaration(res, pool.statements, hmrMeta, node.getSourceFile());
1899+
: getHmrUpdateDeclaration(res, pool.statements, hmrMeta, node);
19001900
}
19011901

19021902
/**

packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function extractHmrDependencies(
4242
} | null {
4343
const name = ts.isClassDeclaration(node) && node.name ? node.name.text : null;
4444
const visitor = new PotentialTopLevelReadsVisitor();
45-
const sourceFile = node.getSourceFile();
45+
const sourceFile = ts.getOriginalNode(node).getSourceFile();
4646

4747
// Visit all of the compiled expressions to look for potential
4848
// local references that would have to be retained.

packages/compiler-cli/src/ngtsc/hmr/src/metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function extractHmrMetatadata(
4747
return null;
4848
}
4949

50-
const sourceFile = clazz.getSourceFile();
50+
const sourceFile = ts.getOriginalNode(clazz).getSourceFile();
5151
const filePath =
5252
getProjectRelativePath(sourceFile.fileName, rootDirs, compilerHost) ||
5353
compilerHost.getCanonicalFileName(sourceFile.fileName);

packages/compiler-cli/src/ngtsc/hmr/src/update_declaration.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import {
1313
presetImportManagerForceNamespaceImports,
1414
translateStatement,
1515
} from '../../translator';
16+
import {ClassDeclaration} from '../../reflection';
1617
import ts from 'typescript';
1718

1819
/**
1920
* Gets the declaration for the function that replaces the metadata of a class during HMR.
2021
* @param compilationResults Code generated for the class during compilation.
2122
* @param meta HMR metadata about the class.
22-
* @param sourceFile File in which the class is defined.
23+
* @param declaration Class for which the update declaration is being generated.
2324
*/
2425
export function getHmrUpdateDeclaration(
2526
compilationResults: CompileResult[],
2627
constantStatements: o.Statement[],
2728
meta: R3HmrMetadata,
28-
sourceFile: ts.SourceFile,
29+
declaration: ClassDeclaration,
2930
): ts.FunctionDeclaration {
3031
const namespaceSpecifiers = meta.namespaceDependencies.reduce((result, current) => {
3132
result.set(current.moduleName, current.assignedName);
@@ -37,6 +38,7 @@ export function getHmrUpdateDeclaration(
3738
rewriter: importRewriter,
3839
});
3940
const callback = compileHmrUpdateCallback(compilationResults, constantStatements, meta);
41+
const sourceFile = ts.getOriginalNode(declaration).getSourceFile();
4042
const node = translateStatement(sourceFile, callback, importManager) as ts.FunctionDeclaration;
4143

4244
// The output AST doesn't support modifiers so we have to emit to

packages/compiler-cli/test/ngtsc/hmr_spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
1010
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
1111
import {NgtscTestEnvironment} from './env';
12+
import {CompilerOptions} from '@angular/compiler-cli/src/transformers/api';
13+
import {createCompilerHost} from '@angular/compiler-cli/src/transformers/compiler_host';
14+
import {NgtscProgram} from '@angular/compiler-cli/src/ngtsc/program';
15+
import ts from 'typescript';
1216

1317
runInEachFileSystem(() => {
1418
describe('HMR code generation', () => {
@@ -901,5 +905,71 @@ runInEachFileSystem(() => {
901905
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, Foo, Component) {',
902906
);
903907
});
908+
909+
it('should generate HMR code for a transformed class', () => {
910+
env.write(
911+
'test.ts',
912+
`
913+
import {Component} from '@angular/core';
914+
915+
@Component({template: ''})
916+
export class Cmp {}
917+
`,
918+
);
919+
920+
const options: CompilerOptions = {
921+
target: ts.ScriptTarget.Latest,
922+
module: ts.ModuleKind.ESNext,
923+
_enableHmr: true,
924+
};
925+
926+
const program = new NgtscProgram(['/test.ts'], options, createCompilerHost({options}));
927+
const transformers = program.compiler.prepareEmit().transformers;
928+
929+
transformers.before!.unshift((ctx) => (sourceFile) => {
930+
const visitor = (node: ts.Node) => {
931+
if (ts.isClassDeclaration(node) && node.name?.getText() === 'Cmp') {
932+
const newMember = ctx.factory.createPropertyDeclaration(
933+
undefined,
934+
'newProp',
935+
undefined,
936+
undefined,
937+
ctx.factory.createNumericLiteral(123),
938+
);
939+
940+
return ctx.factory.updateClassDeclaration(
941+
node,
942+
node.modifiers,
943+
node.name,
944+
node.typeParameters,
945+
node.heritageClauses,
946+
[newMember, ...node.members],
947+
);
948+
}
949+
return ts.visitEachChild(node, visitor, ctx);
950+
};
951+
return ts.visitEachChild(sourceFile, visitor, ctx);
952+
});
953+
954+
const {diagnostics, emitSkipped} = program
955+
.getTsProgram()
956+
.emit(undefined, undefined, undefined, undefined, transformers);
957+
const declaration = program
958+
.getTsProgram()
959+
.getSourceFile('/test.ts')
960+
?.statements.find(
961+
(stmt) => ts.isClassDeclaration(stmt) && stmt.name?.getText() === 'Cmp',
962+
) as ts.ClassDeclaration;
963+
964+
const jsContents = env.getContents('/test.js');
965+
const hmrContents = program.compiler.emitHmrUpdateModule(declaration);
966+
967+
expect(diagnostics.length).toBe(0);
968+
expect(emitSkipped).toBe(false);
969+
970+
expect(jsContents).toContain('ɵreplaceMetadata(Cmp');
971+
expect(jsContents).toContain('newProp = 123');
972+
expect(hmrContents).toContain('export default function Cmp_UpdateMetadata');
973+
});
904974
});
905975
});

0 commit comments

Comments
 (0)