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
54 changes: 34 additions & 20 deletions packages/compiler-cli/test/ngtsc/hmr_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,19 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain(`import * as i0 from "@angular/core";`);
expect(jsContents).toContain('const id = "test.ts%40Cmp";');
expect(jsContents).toContain('function Cmp_HmrLoad(t) {');
expect(jsContents).toContain(
'import(/* @vite-ignore */\nnew URL("./@ng/component?c=test.ts%40Cmp&t=" + encodeURIComponent(t), import.meta.url).href)',
'import(/* @vite-ignore */\nnew URL("./@ng/component?c=" + id + "&t=" + encodeURIComponent(t), import.meta.url).href)',
);
expect(jsContents).toContain(
').then(m => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, [i0], ' +
'[Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input]));',
'[Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input], import.meta, id));',
);
expect(jsContents).toContain('Cmp_HmrLoad(Date.now());');
expect(jsContents).toContain(
'import.meta.hot && import.meta.hot.on("angular:component-update", ' +
'd => d.id === "test.ts%40Cmp" && Cmp_HmrLoad(d.timestamp)',
'd => d.id === id && Cmp_HmrLoad(d.timestamp)',
);

expect(hmrContents).toContain(
Expand Down Expand Up @@ -171,18 +172,19 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');
expect(jsContents).toContain(`import * as i0 from "@angular/core";`);
expect(jsContents).toContain(`import * as i1 from "./dep";`);
expect(jsContents).toContain('const id = "test.ts%40Cmp";');
expect(jsContents).toContain('function Cmp_HmrLoad(t) {');
expect(jsContents).toContain(
'import(/* @vite-ignore */\nnew URL("./@ng/component?c=test.ts%40Cmp&t=" + encodeURIComponent(t), import.meta.url).href)',
'import(/* @vite-ignore */\nnew URL("./@ng/component?c=" + id + "&t=" + encodeURIComponent(t), import.meta.url).href)',
);
expect(jsContents).toContain(
').then(m => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], ' +
'[DepModule, Component]));',
'[DepModule, Component], import.meta, id));',
);
expect(jsContents).toContain('Cmp_HmrLoad(Date.now());');
expect(jsContents).toContain(
'import.meta.hot && import.meta.hot.on("angular:component-update", ' +
'd => d.id === "test.ts%40Cmp" && Cmp_HmrLoad(d.timestamp)',
'd => d.id === id && Cmp_HmrLoad(d.timestamp)',
);

expect(hmrContents).toContain(
Expand Down Expand Up @@ -340,7 +342,9 @@ runInEachFileSystem(() => {
expect(jsContents).toContain('const Cmp_Defer_1_DepsFn = () => [Dep];');
expect(jsContents).toContain('function Cmp_Defer_0_Template(rf, ctx) { if (rf & 1) {');
expect(jsContents).toContain('i0.ɵɵdefer(1, 0, Cmp_Defer_1_DepsFn);');
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0], [Dep]));');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [Dep], import.meta, id));',
);
expect(jsContents).not.toContain('setClassMetadata');

expect(hmrContents).toContain(
Expand Down Expand Up @@ -422,7 +426,9 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
const hmrContents = env.driveHmr('test.ts', 'Cmp');
expect(jsContents).toContain('dependencies: [Cmp]');
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0], [Component]));');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Component) {',
);
Expand All @@ -445,7 +451,9 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
const hmrContents = env.driveHmr('test.ts', 'Cmp');
expect(jsContents).not.toContain('dependencies');
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0], [Component]));');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Component) {',
);
Expand All @@ -471,7 +479,7 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [providers, Component]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [providers, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, providers, Component) {',
Expand Down Expand Up @@ -508,7 +516,7 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Component]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, value, Component) {',
Expand Down Expand Up @@ -542,7 +550,7 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Component]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, value, Component) {',
Expand Down Expand Up @@ -574,7 +582,7 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [condition, providersA, providersB, Component]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [condition, providersA, providersB, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, condition, providersA, providersB, Component) {',
Expand Down Expand Up @@ -608,7 +616,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
const hmrContents = env.driveHmr('test.ts', 'Cmp');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, otherValue, Component]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, otherValue, Component], import.meta, id));',
);
expect(jsContents).toContain('useFactory: () => [(value), ((((otherValue))))]');
expect(hmrContents).toContain(
Expand Down Expand Up @@ -646,7 +654,7 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Optional, dep, Component]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Optional, dep, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, value, Optional, dep, Component) {',
Expand Down Expand Up @@ -697,7 +705,9 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain('dependencies: [Dep]');
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0], [Dep]));');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [Dep], import.meta, id));',
);
expect(hmrContents).toContain('function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Dep) {');
});

Expand Down Expand Up @@ -741,7 +751,9 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain('dependencies: [DepModule, i1.Dep]');
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], [DepModule]));');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], [DepModule], import.meta, id));',
);
expect(hmrContents).toContain('function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, DepModule) {');
});

Expand Down Expand Up @@ -797,7 +809,9 @@ runInEachFileSystem(() => {
const hmrContents = env.driveHmr('test.ts', 'Cmp');

expect(jsContents).toContain('dependencies: [i1.Dep]');
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], []));');
expect(jsContents).toContain(
'ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], [], import.meta, id));',
);
expect(hmrContents).toContain('function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces) {');
});

Expand Down Expand Up @@ -834,7 +848,7 @@ runInEachFileSystem(() => {
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]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, { one: 0, two: "2", three: 3 }, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, Foo, Component) {',
Expand Down Expand Up @@ -881,7 +895,7 @@ runInEachFileSystem(() => {
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]));',
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, { one: 0, two: "2", three: 3 }, Component], import.meta, id));',
);
expect(hmrContents).toContain(
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, Foo, Component) {',
Expand Down
24 changes: 17 additions & 7 deletions packages/compiler/src/render3/r3_hmr_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,10 @@ export interface R3HmrNamespaceDependency {
* @param meta HMR metadata extracted from the class.
*/
export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
const id = encodeURIComponent(`${meta.filePath}@${meta.className}`);
const urlPartial = `./@ng/component?c=${id}&t=`;
const moduleName = 'm';
const dataName = 'd';
const timestampName = 't';
const idName = 'id';
const importCallbackName = `${meta.className}_HmrLoad`;
const namespaces = meta.namespaceDependencies.map((dep) => {
return new o.ExternalExpr({moduleName: dep.moduleName, name: null});
Expand All @@ -66,22 +65,26 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
// m.default
const defaultRead = o.variable(moduleName).prop('default');

// ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals]);
// ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals], import.meta, id);
const replaceCall = o
.importExpr(R3.replaceMetadata)
.callFn([
meta.type,
defaultRead,
o.literalArr(namespaces),
o.literalArr(meta.localDependencies.map((l) => l.runtimeRepresentation)),
o.variable('import').prop('meta'),
o.variable(idName),
]);

// (m) => m.default && ɵɵreplaceMetadata(...)
const replaceCallback = o.arrowFn([new o.FnParam(moduleName)], defaultRead.and(replaceCall));

// '<urlPartial>' + encodeURIComponent(t)
// '<url>?c=' + id + '&t=' + encodeURIComponent(t)
const urlValue = o
.literal(urlPartial)
.literal(`./@ng/component?c=`)
.plus(o.variable(idName))
.plus(o.literal('&t='))
.plus(o.variable('encodeURIComponent').callFn([o.variable(timestampName)]));

// import.meta.url
Expand Down Expand Up @@ -109,13 +112,13 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
o.StmtModifier.Final,
);

// (d) => d.id === <id> && Cmp_HmrLoad(d.timestamp)
// (d) => d.id === id && Cmp_HmrLoad(d.timestamp)
const updateCallback = o.arrowFn(
[new o.FnParam(dataName)],
o
.variable(dataName)
.prop('id')
.identical(o.literal(id))
.identical(o.variable(idName))
.and(o.variable(importCallbackName).callFn([o.variable(dataName).prop('timestamp')])),
);

Expand All @@ -139,6 +142,13 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
.arrowFn(
[],
[
// const id = <id>;
new o.DeclareVarStmt(
idName,
o.literal(encodeURIComponent(`${meta.filePath}@${meta.className}`)),
null,
o.StmtModifier.Final,
),
// function Cmp_HmrLoad() {...}.
importCallback,
// ngDevMode && Cmp_HmrLoad(Date.now());
Expand Down
60 changes: 53 additions & 7 deletions packages/core/src/render3/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,32 @@ import {NgZone} from '../zone';
import {ViewEncapsulation} from '../metadata/view';
import {NG_COMP_DEF} from './fields';

/** Represents `import.meta` plus some information that's not in the built-in types. */
type ImportMetaExtended = ImportMeta & {
hot?: {
send?: (name: string, payload: unknown) => void;
};
};

/**
* Replaces the metadata of a component type and re-renders all live instances of the component.
* @param type Class whose metadata will be replaced.
* @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked.
* @param environment Syntehtic namespace imports that need to be passed along to the callback.
* @param locals Local symbols from the source location that have to be exposed to the callback.
* @param importMeta `import.meta` from the call site of the replacement function. Optional since
* it isn't used internally.
* @param id ID to the class being replaced. **Not** the same as the component definition ID.
* Optional since the ID might not be available internally.
* @codeGenApi
*/
export function ɵɵreplaceMetadata(
type: Type<unknown>,
applyMetadata: (...args: [Type<unknown>, unknown[], ...unknown[]]) => void,
namespaces: unknown[],
locals: unknown[],
importMeta: ImportMetaExtended | null = null,
id: string | null = null,
) {
ngDevMode && assertComponentDef(type);
const currentDef = getComponentDef(type)!;
Expand All @@ -84,7 +97,7 @@ export function ɵɵreplaceMetadata(
// Note: we have the additional check, because `IsRoot` can also indicate
// a component created through something like `createComponent`.
if (isRootView(root) && root[PARENT] === null) {
recreateMatchingLViews(newDef, oldDef, root);
recreateMatchingLViews(importMeta, id, newDef, oldDef, root);
}
}
}
Expand Down Expand Up @@ -129,10 +142,14 @@ function mergeWithExistingDefinition(

/**
* Finds all LViews matching a specific component definition and recreates them.
* @param importMeta `import.meta` information.
* @param id HMR ID of the component.
* @param oldDef Component definition to search for.
* @param rootLView View from which to start the search.
*/
function recreateMatchingLViews(
importMeta: ImportMetaExtended | null,
id: string | null,
newDef: ComponentDef<unknown>,
oldDef: ComponentDef<unknown>,
rootLView: LView,
Expand All @@ -149,7 +166,7 @@ function recreateMatchingLViews(
// produce false positives when using inheritance.
if (tView === oldDef.tView) {
ngDevMode && assertComponentDef(oldDef.type);
recreateLView(newDef, oldDef, rootLView);
recreateLView(importMeta, id, newDef, oldDef, rootLView);
return;
}

Expand All @@ -159,14 +176,14 @@ function recreateMatchingLViews(
if (isLContainer(current)) {
// The host can be an LView if a component is injecting `ViewContainerRef`.
if (isLView(current[HOST])) {
recreateMatchingLViews(newDef, oldDef, current[HOST]);
recreateMatchingLViews(importMeta, id, newDef, oldDef, current[HOST]);
}

for (let j = CONTAINER_HEADER_OFFSET; j < current.length; j++) {
recreateMatchingLViews(newDef, oldDef, current[j]);
recreateMatchingLViews(importMeta, id, newDef, oldDef, current[j]);
}
} else if (isLView(current)) {
recreateMatchingLViews(newDef, oldDef, current);
recreateMatchingLViews(importMeta, id, newDef, oldDef, current);
}
}
}
Expand All @@ -187,11 +204,15 @@ function clearRendererCache(factory: RendererFactory, def: ComponentDef<unknown>

/**
* Recreates an LView in-place from a new component definition.
* @param importMeta `import.meta` information.
* @param id HMR ID for the component.
* @param newDef Definition from which to recreate the view.
* @param oldDef Previous component definition being swapped out.
* @param lView View to be recreated.
*/
function recreateLView(
importMeta: ImportMetaExtended | null,
id: string | null,
newDef: ComponentDef<unknown>,
oldDef: ComponentDef<unknown>,
lView: LView<unknown>,
Expand Down Expand Up @@ -269,9 +290,34 @@ function recreateLView(

// The callback isn't guaranteed to be inside the Zone so we need to bring it in ourselves.
if (zone === null) {
recreate();
executeWithInvalidateFallback(importMeta, id, recreate);
} else {
zone.run(recreate);
zone.run(() => executeWithInvalidateFallback(importMeta, id, recreate));
}
}

/**
* Runs an HMR-related function and falls back to
* invalidating the HMR data if it throws an error.
*/
function executeWithInvalidateFallback(
importMeta: ImportMetaExtended | null,
id: string | null,
callback: () => void,
) {
try {
callback();
} catch (e) {
const errorMessage = (e as {message?: string}).message;

// If we have all the necessary information and APIs to send off the invalidation
// request, send it before rethrowing so the dev server can decide what to do.
if (id !== null && errorMessage) {
importMeta?.hot?.send?.('angular:invalidate', {id, message: errorMessage, error: true});
}

// Throw the error in case the page doesn't get refreshed.
throw e;
}
}

Expand Down
Loading