Skip to content

Commit 231e6ff

Browse files
crisbetodevversion
authored andcommitted
feat(compiler-cli): generate the HMR replacement module (#58205)
Adds the ability to generate the function that replaces the component's metadata during HMR. The HMR update module is a function that is loaded dynamically and as such it has some special considerations: * It isn't bundled, because doing so will result in multiple version of core. * Since it isn't bundled, all dependencies have to be passed in as parameters. These changes include some special logic to determine and output those dependencies. * While HMR is enabled, we have to disable the functionality that generates dynamic imports and drop the dependencies inside `@defer` blocks, because we need to retain the ability to refer to them in case they're needed inside the HMR update function. * The function is returned by the `NgCompiler` as a string for the CLI's sake. PR Close #58205
1 parent ca651d1 commit 231e6ff

File tree

18 files changed

+909
-166
lines changed

18 files changed

+909
-166
lines changed

packages/compiler-cli/src/ngtsc/annotations/common/src/debug_info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {literal, R3ClassDebugInfo, WrappedNodeExpr} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {DeclarationNode, ReflectionHost} from '../../../reflection';
13-
import {getProjectRelativePath} from './util';
13+
import {getProjectRelativePath} from '../../../util/src/path';
1414

1515
export function extractClassDebugInfo(
1616
clazz: DeclarationNode,

packages/compiler-cli/src/ngtsc/annotations/common/src/util.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
Statement,
2121
WrappedNodeExpr,
2222
} from '@angular/compiler';
23-
import {relative} from 'path';
2423
import ts from 'typescript';
2524

2625
import {
@@ -510,31 +509,3 @@ export function isAbstractClassDeclaration(clazz: ClassDeclaration): boolean {
510509
? clazz.modifiers.some((mod) => mod.kind === ts.SyntaxKind.AbstractKeyword)
511510
: false;
512511
}
513-
514-
/**
515-
* Attempts to generate a project-relative path
516-
* @param sourceFile
517-
* @param rootDirs
518-
* @param compilerHost
519-
* @returns
520-
*/
521-
export function getProjectRelativePath(
522-
sourceFile: ts.SourceFile,
523-
rootDirs: readonly string[],
524-
compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
525-
): string | null {
526-
// Note: we need to pass both the file name and the root directories through getCanonicalFileName,
527-
// because the root directories might've been passed through it already while the source files
528-
// definitely have not. This can break the relative return value, because in some platforms
529-
// getCanonicalFileName lowercases the path.
530-
const filePath = compilerHost.getCanonicalFileName(sourceFile.fileName);
531-
532-
for (const rootDir of rootDirs) {
533-
const rel = relative(compilerHost.getCanonicalFileName(rootDir), filePath);
534-
if (!rel.startsWith('..')) {
535-
return rel;
536-
}
537-
}
538-
539-
return null;
540-
}

packages/compiler-cli/src/ngtsc/annotations/component/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ts_library(
1515
"//packages/compiler-cli/src/ngtsc/cycles",
1616
"//packages/compiler-cli/src/ngtsc/diagnostics",
1717
"//packages/compiler-cli/src/ngtsc/file_system",
18+
"//packages/compiler-cli/src/ngtsc/hmr",
1819
"//packages/compiler-cli/src/ngtsc/imports",
1920
"//packages/compiler-cli/src/ngtsc/incremental:api",
2021
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",

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

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
AnimationTriggerNames,
1111
BoundTarget,
1212
compileClassDebugInfo,
13-
compileClassHmrInitializer,
13+
compileHmrInitializer,
1414
compileComponentClassMetadata,
1515
compileComponentDeclareClassMetadata,
1616
compileComponentFromMetadata,
@@ -180,7 +180,7 @@ import {
180180
} from './util';
181181
import {getTemplateDiagnostics} from '../../../typecheck';
182182
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
183-
import {extractHmrInitializerMeta} from './hmr';
183+
import {extractHmrMetatadata, getHmrUpdateDeclaration} from '../../../hmr';
184184

185185
const EMPTY_ARRAY: any[] = [];
186186

@@ -267,6 +267,11 @@ export class ComponentDecoratorHandler
267267
enableLetSyntax: this.enableLetSyntax,
268268
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
269269
};
270+
271+
// Dependencies can't be deferred during HMR, because the HMR update module can't have
272+
// dynamic imports and its dependencies need to be passed in directly. If dependencies
273+
// are deferred, their imports will be deleted so we won't may lose the reference to them.
274+
this.canDeferDeps = !enableHmr;
270275
}
271276

272277
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
@@ -280,6 +285,9 @@ export class ComponentDecoratorHandler
280285
private preanalyzeTemplateCache = new Map<DeclarationNode, ParsedTemplateWithSource>();
281286
private preanalyzeStylesCache = new Map<DeclarationNode, string[] | null>();
282287

288+
/** Whether generated code for a component can defer its dependencies. */
289+
private readonly canDeferDeps: boolean;
290+
283291
private extractTemplateOptions: {
284292
enableI18nLegacyMessageIdFormat: boolean;
285293
i18nNormalizeLineEndingsInICUs: boolean;
@@ -876,9 +884,6 @@ export class ComponentDecoratorHandler
876884
this.rootDirs,
877885
/* forbidOrphanRenderering */ this.forbidOrphanRendering,
878886
),
879-
hmrInitializerMeta: this.enableHmr
880-
? extractHmrInitializerMeta(node, this.reflector, this.compilerHost, this.rootDirs)
881-
: null,
882887
template,
883888
providersRequiringFactory,
884889
viewProvidersRequiringFactory,
@@ -1600,15 +1605,19 @@ export class ComponentDecoratorHandler
16001605
return [];
16011606
}
16021607

1603-
const perComponentDeferredDeps = this.resolveAllDeferredDependencies(resolution);
1608+
const perComponentDeferredDeps = this.canDeferDeps
1609+
? this.resolveAllDeferredDependencies(resolution)
1610+
: null;
16041611
const meta: R3ComponentMetadata<R3TemplateDependency> = {
16051612
...analysis.meta,
16061613
...resolution,
16071614
defer: this.compileDeferBlocks(resolution),
16081615
};
16091616
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
16101617

1611-
removeDeferrableTypesFromComponentDecorator(analysis, perComponentDeferredDeps);
1618+
if (perComponentDeferredDeps !== null) {
1619+
removeDeferrableTypesFromComponentDecorator(analysis, perComponentDeferredDeps);
1620+
}
16121621

16131622
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
16141623
const inputTransformFields = compileInputTransformFields(analysis.inputs);
@@ -1620,11 +1629,22 @@ export class ComponentDecoratorHandler
16201629
analysis.classDebugInfo !== null
16211630
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
16221631
: null;
1623-
const hmrInitializer =
1624-
analysis.hmrInitializerMeta !== null
1625-
? compileClassHmrInitializer(analysis.hmrInitializerMeta).toStmt()
1626-
: null;
1627-
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
1632+
const hmrMeta = this.enableHmr
1633+
? extractHmrMetatadata(
1634+
node,
1635+
this.reflector,
1636+
this.compilerHost,
1637+
this.rootDirs,
1638+
def,
1639+
fac,
1640+
classMetadata,
1641+
debugInfo,
1642+
)
1643+
: null;
1644+
const hmrInitializer = hmrMeta ? compileHmrInitializer(hmrMeta).toStmt() : null;
1645+
const deferrableImports = this.canDeferDeps
1646+
? this.deferredSymbolTracker.getDeferrableImportDecls()
1647+
: null;
16281648
return compileResults(
16291649
fac,
16301650
def,
@@ -1655,7 +1675,9 @@ export class ComponentDecoratorHandler
16551675
: null,
16561676
};
16571677

1658-
const perComponentDeferredDeps = this.resolveAllDeferredDependencies(resolution);
1678+
const perComponentDeferredDeps = this.canDeferDeps
1679+
? this.resolveAllDeferredDependencies(resolution)
1680+
: null;
16591681
const meta: R3ComponentMetadata<R3TemplateDependencyMetadata> = {
16601682
...analysis.meta,
16611683
...resolution,
@@ -1671,8 +1693,32 @@ export class ComponentDecoratorHandler
16711693
perComponentDeferredDeps,
16721694
).toStmt()
16731695
: null;
1674-
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
1675-
return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports);
1696+
const hmrMeta = this.enableHmr
1697+
? extractHmrMetatadata(
1698+
node,
1699+
this.reflector,
1700+
this.compilerHost,
1701+
this.rootDirs,
1702+
def,
1703+
fac,
1704+
classMetadata,
1705+
null,
1706+
)
1707+
: null;
1708+
const hmrInitializer = hmrMeta ? compileHmrInitializer(hmrMeta).toStmt() : null;
1709+
const deferrableImports = this.canDeferDeps
1710+
? this.deferredSymbolTracker.getDeferrableImportDecls()
1711+
: null;
1712+
return compileResults(
1713+
fac,
1714+
def,
1715+
classMetadata,
1716+
'ɵcmp',
1717+
inputTransformFields,
1718+
deferrableImports,
1719+
null,
1720+
hmrInitializer,
1721+
);
16761722
}
16771723

16781724
compileLocal(
@@ -1684,16 +1730,16 @@ export class ComponentDecoratorHandler
16841730
// In the local compilation mode we can only rely on the information available
16851731
// within the `@Component.deferredImports` array, because in this mode compiler
16861732
// doesn't have information on which dependencies belong to which defer blocks.
1687-
const deferrableTypes = analysis.explicitlyDeferredTypes;
1733+
const deferrableTypes = this.canDeferDeps ? analysis.explicitlyDeferredTypes : null;
16881734

16891735
const meta = {
16901736
...analysis.meta,
16911737
...resolution,
16921738
defer: this.compileDeferBlocks(resolution),
16931739
} as R3ComponentMetadata<R3TemplateDependency>;
16941740

1695-
if (analysis.explicitlyDeferredTypes !== null) {
1696-
removeDeferrableTypesFromComponentDecorator(analysis, analysis.explicitlyDeferredTypes);
1741+
if (deferrableTypes !== null) {
1742+
removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes);
16971743
}
16981744

16991745
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
@@ -1707,11 +1753,22 @@ export class ComponentDecoratorHandler
17071753
analysis.classDebugInfo !== null
17081754
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
17091755
: null;
1710-
const hmrInitializer =
1711-
analysis.hmrInitializerMeta !== null
1712-
? compileClassHmrInitializer(analysis.hmrInitializerMeta).toStmt()
1713-
: null;
1714-
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
1756+
const hmrMeta = this.enableHmr
1757+
? extractHmrMetatadata(
1758+
node,
1759+
this.reflector,
1760+
this.compilerHost,
1761+
this.rootDirs,
1762+
def,
1763+
fac,
1764+
classMetadata,
1765+
debugInfo,
1766+
)
1767+
: null;
1768+
const hmrInitializer = hmrMeta ? compileHmrInitializer(hmrMeta).toStmt() : null;
1769+
const deferrableImports = this.canDeferDeps
1770+
? this.deferredSymbolTracker.getDeferrableImportDecls()
1771+
: null;
17151772
return compileResults(
17161773
fac,
17171774
def,
@@ -1724,6 +1781,50 @@ export class ComponentDecoratorHandler
17241781
);
17251782
}
17261783

1784+
compileHmrUpdateDeclaration(
1785+
node: ClassDeclaration,
1786+
analysis: Readonly<ComponentAnalysisData>,
1787+
resolution: Readonly<ComponentResolutionData>,
1788+
): ts.FunctionDeclaration | null {
1789+
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
1790+
return null;
1791+
}
1792+
1793+
// Create a brand-new constant pool since there shouldn't be any constant sharing.
1794+
const pool = new ConstantPool();
1795+
const meta: R3ComponentMetadata<R3TemplateDependency> = {
1796+
...analysis.meta,
1797+
...resolution,
1798+
defer: this.compileDeferBlocks(resolution),
1799+
};
1800+
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
1801+
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
1802+
const classMetadata =
1803+
analysis.classMetadata !== null
1804+
? compileComponentClassMetadata(analysis.classMetadata, null).toStmt()
1805+
: null;
1806+
const debugInfo =
1807+
analysis.classDebugInfo !== null
1808+
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
1809+
: null;
1810+
const hmrMeta = this.enableHmr
1811+
? extractHmrMetatadata(
1812+
node,
1813+
this.reflector,
1814+
this.compilerHost,
1815+
this.rootDirs,
1816+
def,
1817+
fac,
1818+
classMetadata,
1819+
debugInfo,
1820+
)
1821+
: null;
1822+
const res = compileResults(fac, def, classMetadata, 'ɵcmp', null, null, debugInfo, null);
1823+
return hmrMeta === null || res.length === 0
1824+
? null
1825+
: getHmrUpdateDeclaration(res, pool.statements, hmrMeta, node.getSourceFile());
1826+
}
1827+
17271828
/**
17281829
* Locates defer blocks in case scope information is not available.
17291830
* For example, this happens in the local compilation mode.

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

Lines changed: 0 additions & 39 deletions
This file was deleted.

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
DeferBlockDepsEmitMode,
1313
R3ClassDebugInfo,
1414
R3ClassMetadata,
15-
R3HmrInitializerMetadata,
1615
R3ComponentMetadata,
1716
R3DeferPerBlockDependency,
1817
R3DeferPerComponentDependency,
@@ -57,7 +56,6 @@ export interface ComponentAnalysisData {
5756
template: ParsedTemplateWithSource;
5857
classMetadata: R3ClassMetadata | null;
5958
classDebugInfo: R3ClassDebugInfo | null;
60-
hmrInitializerMeta: R3HmrInitializerMetadata | null;
6159

6260
inputs: ClassPropertyMapping<InputMapping>;
6361
inputFieldNamesFromMetadataArray: Set<string>;

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,34 @@ export class NgCompiler {
940940
compilation.traitCompiler.xi18n(ctx);
941941
}
942942

943+
/**
944+
* Emits the JavaScript module that can be used to replace the metadata of a class during HMR.
945+
* @param node Class for which to generate the update module.
946+
*/
947+
emitHmrUpdateModule(node: DeclarationNode): string | null {
948+
const {traitCompiler, reflector} = this.ensureAnalyzed();
949+
950+
if (!reflector.isClass(node)) {
951+
return null;
952+
}
953+
954+
const callback = traitCompiler.compileHmrUpdateCallback(node);
955+
956+
if (callback === null) {
957+
return null;
958+
}
959+
960+
const sourceFile = node.getSourceFile();
961+
const printer = ts.createPrinter();
962+
const nodeText = printer.printNode(ts.EmitHint.Unspecified, callback, sourceFile);
963+
964+
return ts.transpileModule(nodeText, {
965+
compilerOptions: this.options,
966+
fileName: sourceFile.fileName,
967+
reportDiagnostics: false,
968+
}).outputText;
969+
}
970+
943971
private ensureAnalyzed(this: NgCompiler): LazyCompilationState {
944972
if (this.compilation === null) {
945973
this.analyzeSync();

0 commit comments

Comments
 (0)