Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,7 @@ export class ComponentDecoratorHandler
? extractHmrMetatadata(
node,
this.reflector,
this.evaluator,
this.compilerHost,
this.rootDirs,
def,
Expand Down Expand Up @@ -1725,6 +1726,7 @@ export class ComponentDecoratorHandler
? extractHmrMetatadata(
node,
this.reflector,
this.evaluator,
this.compilerHost,
this.rootDirs,
def,
Expand Down Expand Up @@ -1787,6 +1789,7 @@ export class ComponentDecoratorHandler
? extractHmrMetatadata(
node,
this.reflector,
this.evaluator,
this.compilerHost,
this.rootDirs,
def,
Expand Down Expand Up @@ -1843,6 +1846,7 @@ export class ComponentDecoratorHandler
? extractHmrMetatadata(
node,
this.reflector,
this.evaluator,
this.compilerHost,
this.rootDirs,
def,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/hmr/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/translator",
Expand Down
108 changes: 100 additions & 8 deletions packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import {
R3HmrNamespaceDependency,
outputAst as o,
} from '@angular/compiler';
import {DeclarationNode} from '../../reflection';
import {DeclarationNode, ReflectionHost} from '../../reflection';
import {CompileResult} from '../../transform';
import ts from 'typescript';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';

/**
* Determines the file-level dependencies that the HMR initializer needs to capture and pass along.
Expand All @@ -33,7 +34,12 @@ export function extractHmrDependencies(
deferBlockMetadata: R3ComponentDeferMetadata,
classMetadata: o.Statement | null,
debugInfo: o.Statement | null,
): {local: string[]; external: R3HmrNamespaceDependency[]} {
reflection: ReflectionHost,
evaluator: PartialEvaluator,
): {
local: {name: string; runtimeRepresentation: o.Expression}[];
external: R3HmrNamespaceDependency[];
} | null {
const name = ts.isClassDeclaration(node) && node.name ? node.name.text : null;
const visitor = new PotentialTopLevelReadsVisitor();
const sourceFile = node.getSourceFile();
Expand All @@ -57,16 +63,81 @@ export function extractHmrDependencies(
// variables inside of functions. Note that we filter out the class name since it is always
// defined and it saves us having to repeat this logic wherever the locals are consumed.
const availableTopLevel = getTopLevelDeclarationNames(sourceFile);
const local: {name: string; runtimeRepresentation: o.Expression}[] = [];
const seenLocals = new Set<string>();

for (const readNode of visitor.allReads) {
const readName = readNode instanceof o.ReadVarExpr ? readNode.name : readNode.text;

if (readName !== name && !seenLocals.has(readName) && availableTopLevel.has(readName)) {
const runtimeRepresentation = getRuntimeRepresentation(readNode, reflection, evaluator);

if (runtimeRepresentation === null) {
return null;
}

local.push({name: readName, runtimeRepresentation});
seenLocals.add(readName);
}
}

return {
local: Array.from(visitor.allReads).filter((r) => r !== name && availableTopLevel.has(r)),
local,
external: Array.from(visitor.namespaceReads, (name, index) => ({
moduleName: name,
assignedName: `ɵhmr${index}`,
})),
};
}

/**
* Gets a node that can be used to represent an identifier in the HMR replacement code at runtime.
*/
function getRuntimeRepresentation(
node: o.ReadVarExpr | ts.Identifier,
reflection: ReflectionHost,
evaluator: PartialEvaluator,
): o.Expression | null {
if (node instanceof o.ReadVarExpr) {
return o.variable(node.name);
}

// Const enums can't be passed by reference, because their values are inlined.
// Pass in an object literal with all of the values instead.
if (isConstEnumReference(node, reflection)) {
const evaluated = evaluator.evaluate(node);

if (evaluated instanceof Map) {
const members: {key: string; quoted: boolean; value: o.Expression}[] = [];

for (const [name, value] of evaluated.entries()) {
if (
value instanceof EnumValue &&
(value.resolved == null ||
typeof value.resolved === 'string' ||
typeof value.resolved === 'boolean' ||
typeof value.resolved === 'number')
) {
members.push({
key: name,
quoted: false,
value: o.literal(value.resolved),
});
} else {
// TS is pretty restrictive about what values can be in a const enum so our evaluator
// should be able to handle them, however if we happen to hit such a case, we return null
// so the HMR update can be invalidated.
return null;
}
}

return o.literalMap(members);
}
}

return o.variable(node.text);
}

/**
* Gets the names of all top-level declarations within the file (imports, declared classes etc).
* @param sourceFile File in which to search for locals.
Expand All @@ -81,8 +152,7 @@ function getTopLevelDeclarationNames(sourceFile: ts.SourceFile): Set<string> {
if (
ts.isClassDeclaration(node) ||
ts.isFunctionDeclaration(node) ||
(ts.isEnumDeclaration(node) &&
!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ConstKeyword))
ts.isEnumDeclaration(node)
) {
if (node.name) {
results.add(node.name.text);
Expand Down Expand Up @@ -157,7 +227,7 @@ function trackBindingName(node: ts.BindingName, results: Set<string>): void {
* inside functions.
*/
class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
readonly allReads = new Set<string>();
readonly allReads = new Set<o.ReadVarExpr | ts.Identifier>();
readonly namespaceReads = new Set<string>();

override visitExternalExpr(ast: o.ExternalExpr, context: any) {
Expand All @@ -168,7 +238,7 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
}

override visitReadVarExpr(ast: o.ReadVarExpr, context: any) {
this.allReads.add(ast.name);
this.allReads.add(ast);
super.visitReadVarExpr(ast, context);
}

Expand All @@ -186,7 +256,7 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
*/
private addAllTopLevelIdentifiers = (node: ts.Node) => {
if (ts.isIdentifier(node) && this.isTopLevelIdentifierReference(node)) {
this.allReads.add(node.text);
this.allReads.add(node);
} else {
ts.forEachChild(node, this.addAllTopLevelIdentifiers);
}
Expand Down Expand Up @@ -326,3 +396,25 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
return !!value && typeof value.kind === 'number';
}
}

/** Checks whether a node is a reference to a const enum. */
function isConstEnumReference(node: ts.Identifier, reflection: ReflectionHost): boolean {
const parent = node.parent;

// Only check identifiers that are in the form of `Foo.bar` where `Foo` is the node being checked.
if (
!parent ||
!ts.isPropertyAccessExpression(parent) ||
parent.expression !== node ||
!ts.isIdentifier(parent.name)
) {
return false;
}

const declaration = reflection.getDeclarationOfIdentifier(node);
return (
declaration !== null &&
ts.isEnumDeclaration(declaration.node) &&
!!declaration.node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ConstKeyword)
);
}
9 changes: 9 additions & 0 deletions packages/compiler-cli/src/ngtsc/hmr/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {getProjectRelativePath} from '../../util/src/path';
import {CompileResult} from '../../transform';
import {extractHmrDependencies} from './extract_dependencies';
import ts from 'typescript';
import {PartialEvaluator} from '../../partial_evaluator';

/**
* Extracts the HMR metadata for a class declaration.
Expand All @@ -33,6 +34,7 @@ import ts from 'typescript';
export function extractHmrMetatadata(
clazz: DeclarationNode,
reflection: ReflectionHost,
evaluator: PartialEvaluator,
compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
rootDirs: readonly string[],
definition: R3CompiledExpression,
Expand All @@ -57,7 +59,14 @@ export function extractHmrMetatadata(
deferBlockMetadata,
classMetadata,
debugInfo,
reflection,
evaluator,
);

if (dependencies === null) {
return null;
}

const meta: R3HmrMetadata = {
type: new o.WrappedNodeExpr(clazz.name),
className: clazz.name.text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,10 @@ export class StaticInterpreter {
private visitEnumDeclaration(node: ts.EnumDeclaration, context: Context): ResolvedValue {
const enumRef = this.getReference(node, context);
const map = new Map<string, EnumValue>();
node.members.forEach((member) => {
node.members.forEach((member, index) => {
const name = this.stringNameFromPropertyName(member.name, context);
if (name !== undefined) {
const resolved = member.initializer && this.visit(member.initializer, context);
const resolved = member.initializer ? this.visit(member.initializer, context) : index;
map.set(name, new EnumValue(enumRef, name, resolved));
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ runInEachFileSystem(() => {
}
expect((result.enumRef.node as ts.EnumDeclaration).name.text).toBe('Foo');
expect(result.name).toBe('B');
expect(result.resolved).toBe(1);
});

it('variable declaration resolution works', () => {
Expand Down
87 changes: 87 additions & 0 deletions packages/compiler-cli/test/ngtsc/hmr_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,5 +800,92 @@ runInEachFileSystem(() => {
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], []));');
expect(hmrContents).toContain('function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces) {');
});

it('should pass const enums defined in the same file as an object literal', () => {
enableHmr();
env.write(
'test.ts',
`
import {Component, InjectionToken} from '@angular/core';

const token = new InjectionToken<number>('TEST');

const numberThree = 3;

export const enum Foo {
one,
two = '2',
three = numberThree
}

@Component({
template: '',
providers: [{
provide: token,
useValue: Foo.three
}]
})
export class Cmp {}
`,
);

env.driveMain();

const jsContents = env.getContents('test.js');
const hmrContents = env.driveHmr('test.ts', 'Cmp');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, { one: 0, two: "2", three: 3 }, Component]));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, Foo, Component) {',
);
});

it('should pass const enum defined in other file as an object literal', () => {
enableHmr();

env.write(
'deps.ts',
`
const numberThree = 3;

export const enum Foo {
one,
two = '2',
three = numberThree
}
`,
);

env.write(
'test.ts',
`
import {Component, InjectionToken} from '@angular/core';
import {Foo} from './deps';

const token = new InjectionToken<number>('TEST');

@Component({
template: '',
providers: [{
provide: token,
useValue: Foo.three
}]
})
export class Cmp {}
`,
);

env.driveMain();

const jsContents = env.getContents('test.js');
const hmrContents = env.driveHmr('test.ts', 'Cmp');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, { one: 0, two: "2", three: 3 }, Component]));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, Foo, Component) {',
);
});
});
});
Loading