Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Generate HMR initializer code #58150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
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
1 change: 1 addition & 0 deletions packages/bazel/src/ngc-wrapped/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export async function runOneBuild(
'onlyExplicitDeferDependencyImports',
'generateExtraImportsInLocalMode',
'_enableLetSyntax',
'_enableHmr',
]);

const userOverrides = Object.entries(userOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
*/

import {literal, R3ClassDebugInfo, WrappedNodeExpr} from '@angular/compiler';
import * as path from 'path';
import ts from 'typescript';

import {DeclarationNode, ReflectionHost} from '../../../reflection';
import {getProjectRelativePath} from './util';

export function extractClassDebugInfo(
clazz: DeclarationNode,
reflection: ReflectionHost,
compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
rootDirs: ReadonlyArray<string>,
forbidOrphanRendering: boolean,
): R3ClassDebugInfo | null {
Expand All @@ -22,7 +24,7 @@ export function extractClassDebugInfo(
}

const srcFile = clazz.getSourceFile();
const srcFileMaybeRelativePath = computeRelativePathIfPossible(srcFile.fileName, rootDirs);
const srcFileMaybeRelativePath = getProjectRelativePath(srcFile, rootDirs, compilerHost);

return {
type: new WrappedNodeExpr(clazz.name),
Expand All @@ -32,21 +34,3 @@ export function extractClassDebugInfo(
forbidOrphanRendering,
};
}

/**
* Computes a source file path relative to the project root folder if possible, otherwise returns
* null.
*/
function computeRelativePathIfPossible(
filePath: string,
rootDirs: ReadonlyArray<string>,
): string | null {
for (const rootDir of rootDirs) {
const rel = path.relative(rootDir, filePath);
if (!rel.startsWith('..')) {
return rel;
}
}

return null;
}
34 changes: 34 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/common/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Statement,
WrappedNodeExpr,
} from '@angular/compiler';
import {relative} from 'path';
import ts from 'typescript';

import {
Expand Down Expand Up @@ -413,6 +414,7 @@ export function compileResults(
additionalFields: CompileResult[] | null,
deferrableImports: Set<ts.ImportDeclaration> | null,
debugInfo: Statement | null = null,
hmrInitializer: Statement | null = null,
): CompileResult[] {
const statements = def.statements;

Expand All @@ -424,6 +426,10 @@ export function compileResults(
statements.push(debugInfo);
}

if (hmrInitializer !== null) {
statements.push(hmrInitializer);
}

const results = [
fac,
{
Expand Down Expand Up @@ -504,3 +510,31 @@ export function isAbstractClassDeclaration(clazz: ClassDeclaration): boolean {
? clazz.modifiers.some((mod) => mod.kind === ts.SyntaxKind.AbstractKeyword)
: false;
}

/**
* Attempts to generate a project-relative path
* @param sourceFile
* @param rootDirs
* @param compilerHost
* @returns
*/
export function getProjectRelativePath(
sourceFile: ts.SourceFile,
rootDirs: readonly string[],
compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
): string | null {
// Note: we need to pass both the file name and the root directories through getCanonicalFileName,
// because the root directories might've been passed through it already while the source files
// definitely have not. This can break the relative return value, because in some platforms
// getCanonicalFileName lowercases the path.
const filePath = compilerHost.getCanonicalFileName(sourceFile.fileName);

for (const rootDir of rootDirs) {
const rel = relative(compilerHost.getCanonicalFileName(rootDir), filePath);
if (!rel.startsWith('..')) {
return rel;
}
}

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AnimationTriggerNames,
BoundTarget,
compileClassDebugInfo,
compileClassHmrInitializer,
compileComponentClassMetadata,
compileComponentDeclareClassMetadata,
compileComponentFromMetadata,
Expand Down Expand Up @@ -179,6 +180,7 @@ import {
} from './util';
import {getTemplateDiagnostics} from '../../../typecheck';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {extractHmrInitializerMeta} from './hmr';

const EMPTY_ARRAY: any[] = [];

Expand Down Expand Up @@ -219,7 +221,7 @@ export class ComponentDecoratorHandler
private metaRegistry: MetadataRegistry,
private metaReader: MetadataReader,
private scopeReader: ComponentScopeReader,
private dtsScopeReader: DtsModuleScopeResolver,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
private scopeRegistry: LocalModuleScopeRegistry,
private typeCheckScopeRegistry: TypeCheckScopeRegistry,
private resourceRegistry: ResourceRegistry,
Expand Down Expand Up @@ -255,6 +257,7 @@ export class ComponentDecoratorHandler
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
private readonly i18nPreserveSignificantWhitespace: boolean,
private readonly strictStandalone: boolean,
private readonly enableHmr: boolean,
) {
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
Expand Down Expand Up @@ -869,9 +872,13 @@ export class ComponentDecoratorHandler
classDebugInfo: extractClassDebugInfo(
node,
this.reflector,
this.compilerHost,
this.rootDirs,
/* forbidOrphanRenderering */ this.forbidOrphanRendering,
),
hmrInitializerMeta: this.enableHmr
? extractHmrInitializerMeta(node, this.reflector, this.compilerHost, this.rootDirs)
: null,
template,
providersRequiringFactory,
viewProvidersRequiringFactory,
Expand Down Expand Up @@ -1613,6 +1620,10 @@ export class ComponentDecoratorHandler
analysis.classDebugInfo !== null
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
: null;
const hmrInitializer =
analysis.hmrInitializerMeta !== null
? compileClassHmrInitializer(analysis.hmrInitializerMeta).toStmt()
: null;
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
return compileResults(
fac,
Expand All @@ -1622,6 +1633,7 @@ export class ComponentDecoratorHandler
inputTransformFields,
deferrableImports,
debugInfo,
hmrInitializer,
);
}

Expand Down Expand Up @@ -1695,6 +1707,10 @@ export class ComponentDecoratorHandler
analysis.classDebugInfo !== null
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
: null;
const hmrInitializer =
analysis.hmrInitializerMeta !== null
? compileClassHmrInitializer(analysis.hmrInitializerMeta).toStmt()
: null;
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
return compileResults(
fac,
Expand All @@ -1704,6 +1720,7 @@ export class ComponentDecoratorHandler
inputTransformFields,
deferrableImports,
debugInfo,
hmrInitializer,
);
}

Expand Down
40 changes: 40 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {R3HmrInitializerMetadata, WrappedNodeExpr} from '@angular/compiler';
import {DeclarationNode, ReflectionHost} from '../../../reflection';
import {getProjectRelativePath} from '../../common';
import ts from 'typescript';

/**
* Extracts the metadata necessary to generate an HMR initializer.
*/
export function extractHmrInitializerMeta(
clazz: DeclarationNode,
reflection: ReflectionHost,
compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
rootDirs: readonly string[],
): R3HmrInitializerMetadata | null {
if (!reflection.isClass(clazz)) {
return null;
}

const sourceFile = clazz.getSourceFile();
const filePath =
getProjectRelativePath(sourceFile, rootDirs, compilerHost) ||
compilerHost.getCanonicalFileName(sourceFile.fileName);

const meta: R3HmrInitializerMetadata = {
type: new WrappedNodeExpr(clazz.name),
className: clazz.name.text,
timestamp: Date.now() + '',
filePath,
};

return meta;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
DeferBlockDepsEmitMode,
R3ClassDebugInfo,
R3ClassMetadata,
R3HmrInitializerMetadata,
R3ComponentMetadata,
R3DeferPerBlockDependency,
R3DeferPerComponentDependency,
Expand Down Expand Up @@ -56,6 +57,7 @@ export interface ComponentAnalysisData {
template: ParsedTemplateWithSource;
classMetadata: R3ClassMetadata | null;
classDebugInfo: R3ClassDebugInfo | null;
hmrInitializerMeta: R3HmrInitializerMetadata | null;

inputs: ClassPropertyMapping<InputMapping>;
inputFieldNamesFromMetadataArray: Set<string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ function setup(
metaRegistry,
metaReader,
scopeRegistry,
dtsResolver,
{
getCanonicalFileName: (fileName) => fileName,
},
scopeRegistry,
typeCheckScopeRegistry,
resourceRegistry,
Expand Down Expand Up @@ -156,6 +158,7 @@ function setup(
jitDeclarationRegistry,
/* i18nPreserveSignificantWhitespace */ true,
/* strictStandalone */ false,
/* enableHmr */ false,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
}
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/api/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export interface InternalOptions {
* @internal
*/
_angularCoreVersion?: string;

/**
* Whether to enable the necessary code generation for hot module reloading.
*
* @internal
*/
_enableHmr?: boolean;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export class NgCompiler {
private readonly enableBlockSyntax: boolean;
private readonly enableLetSyntax: boolean;
private readonly angularCoreVersion: string | null;
private readonly enableHmr: boolean;

/**
* `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each
Expand Down Expand Up @@ -462,6 +463,7 @@ export class NgCompiler {
this.enableBlockSyntax = options['_enableBlockSyntax'] ?? true;
this.enableLetSyntax = options['_enableLetSyntax'] ?? true;
this.angularCoreVersion = options['_angularCoreVersion'] ?? null;
this.enableHmr = !!options['_enableHmr'];
this.constructionDiagnostics.push(
...this.adapter.constructionDiagnostics,
...verifyCompatibleTypeCheckOptions(this.options),
Expand Down Expand Up @@ -1427,7 +1429,7 @@ export class NgCompiler {
metaRegistry,
metaReader,
scopeReader,
depScopeReader,
this.adapter,
ngModuleScopeRegistry,
typeCheckScopeRegistry,
resourceRegistry,
Expand Down Expand Up @@ -1463,6 +1465,7 @@ export class NgCompiler {
jitDeclarationRegistry,
this.options.i18nPreserveWhitespaceForLegacyExtraction ?? true,
!!this.options.strictStandalone,
this.enableHmr,
),

// TODO(alxhub): understand why the cast here is necessary (something to do with `null`
Expand Down
63 changes: 63 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10519,6 +10519,69 @@ runInEachFileSystem((os: string) => {
});
});

describe('HMR initializer', () => {
it('should not generate an HMR initializer by default', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';

@Component({
selector: 'cmp',
template: 'hello',
standalone: true,
})
export class Cmp {}
`,
);

env.driveMain();

const jsContents = env.getContents('test.js');
expect(jsContents).not.toContain('import.meta.hot');
expect(jsContents).not.toContain('replaceMetadata');
});

it('should generate an HMR initializer when enabled', () => {
env.write(
'tsconfig.json',
JSON.stringify({
extends: './tsconfig-base.json',
angularCompilerOptions: {
_enableHmr: true,
},
}),
);

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

@Component({
selector: 'cmp',
template: 'hello',
standalone: true,
})
export class Cmp {}
`,
);

env.driveMain();

const jsContents = env.getContents('test.js');

// We need a regex match here, because the file path changes based on
// the file system and the timestamp will be different for each test run.
expect(jsContents).toMatch(
/import\.meta\.hot && import\.meta\.hot\.on\("angular:component-update", d => { if \(d\.id == "test\.ts%40Cmp"\) {/,
);
expect(jsContents).toMatch(
/import\("\/@ng\/component\?c=test\.ts%40Cmp&t=\d+"\).then\(m => i0\.ɵɵreplaceMetadata\(Cmp, m\.default\)\);/,
);
});
});

describe('tsickle compatibility', () => {
it('should preserve fileoverview comments', () => {
env.write(
Expand Down
Loading