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 @@ -237,6 +237,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression>
: ChangeDetectionStrategy.Default,
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: this.sourceUrl,
relativeTemplatePath: null,
i18nUseExternalIds: false,
declarations,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function extractClassDebugInfo(
}

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

return {
type: new WrappedNodeExpr(clazz.name),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ import {
import {getTemplateDiagnostics} from '../../../typecheck';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {extractHmrMetatadata, getHmrUpdateDeclaration} from '../../../hmr';
import {getProjectRelativePath} from '../../../util/src/path';

const EMPTY_ARRAY: any[] = [];

Expand Down Expand Up @@ -714,6 +715,11 @@ export class ComponentDecoratorHandler
path: absoluteFrom(template.declaration.resolvedTemplateUrl),
expression: template.sourceMapping.node,
};
const relativeTemplatePath = getProjectRelativePath(
templateResource.path ?? ts.getOriginalNode(node).getSourceFile().fileName,
this.rootDirs,
this.compilerHost,
);

// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
// precede inline styles, and styles defined in the template override styles defined in the
Expand Down Expand Up @@ -868,6 +874,7 @@ export class ComponentDecoratorHandler
i18nUseExternalIds: this.i18nUseExternalIds,
relativeContextFilePath,
rawImports: rawImports !== null ? new o.WrappedNodeExpr(rawImports) : undefined,
relativeTemplatePath,
},
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
classMetadata: this.includeClassMetadata
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/hmr/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function extractHmrMetatadata(

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

const dependencies = extractHmrDependencies(clazz, definition, factory, classMetadata, debugInfo);
Expand Down
12 changes: 6 additions & 6 deletions packages/compiler-cli/src/ngtsc/util/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ export function normalizeSeparators(path: string): string {
}

/**
* Attempts to generate a project-relative path
* @param sourceFile
* @param rootDirs
* @param compilerHost
* Attempts to generate a project-relative path for a file.
* @param fileName Absolute path to the file.
* @param rootDirs Root directories of the project.
* @param compilerHost Host used to resolve file names.
* @returns
*/
export function getProjectRelativePath(
sourceFile: ts.SourceFile,
fileName: string,
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);
const filePath = compilerHost.getCanonicalFileName(fileName);

for (const rootDir of rootDirs) {
const rel = relative(compilerHost.getCanonicalFileName(rootDir), filePath);
Expand Down
140 changes: 140 additions & 0 deletions packages/compiler-cli/test/ngtsc/attach_source_location_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*!
* @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 {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
import {setEnableTemplateSourceLocations} from '@angular/compiler/src/render3/view/config';

const testFiles = loadStandardTestFiles({fakeCommon: true});

runInEachFileSystem(() => {
describe('source location instruction generation', () => {
let env!: NgtscTestEnvironment;

beforeEach(() => {
setEnableTemplateSourceLocations(true);
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig();
});

afterEach(() => {
setEnableTemplateSourceLocations(false);
});

it('should attach the source location in an inline template', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';

@Component({
template: \`
<div><span>
<strong>Hello</strong>
</span></div>
\`,
})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵelementStart(0, "div")(1, "span")(2, "strong");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.ts", [[0, 114, 5, 14], [1, 119, 5, 19], [2, 142, 6, 16]]);',
);
});

it('should attach the source location in an external template', () => {
env.write(
'test.html',
`
<div><span>
<strong>Hello</strong>
</span></div>
`,
);

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

@Component({templateUrl: './test.html'})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵelementStart(0, "div")(1, "span")(2, "strong");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.html", [[0, 9, 1, 8], [1, 14, 1, 13], [2, 31, 2, 10]]);',
);
});

it('should attach the source location to structural directives', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';
import {CommonModule} from '@angular/common';

@Component({
imports: [CommonModule],
template: \`
<div *ngIf="true">
<span></span>
</div>
\`,
})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵtemplate(0,');
expect(content).toContain('ɵɵelementStart(0, "div");');
expect(content).toContain('ɵɵelement(1, "span");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.ts", [[0, 207, 7, 14], [1, 242, 8, 16]]);',
);
});

it('should not attach the source location to ng-container', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';

@Component({
template: \`
<ng-container>
<div>
<ng-container>
<span></span>
</ng-container>
</div>
</ng-container>
\`,
})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵelementContainerStart(0);');
expect(content).toContain('ɵɵelementStart(1, "div");');
expect(content).toContain('ɵɵelementContainerStart(2);');
expect(content).toContain('ɵɵelement(3, "span");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.ts", [[1, 145, 6, 16], [3, 204, 8, 20]]);',
);
});
});
});
2 changes: 2 additions & 0 deletions packages/compiler/src/jit_compiler_facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export class CompilerFacadeImpl implements CompilerFacade {
facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) : null,
relativeContextFilePath: '',
i18nUseExternalIds: true,
relativeTemplatePath: null,
};
const jitExpressionSourceMap = `ng:///${facade.name}.js`;
return this.compileComponentFromMeta(angularCoreEnv, jitExpressionSourceMap, meta);
Expand Down Expand Up @@ -673,6 +674,7 @@ function convertDeclareComponentFacadeToMetadata(
declarationListEmitMode: DeclarationListEmitMode.ClosureResolved,
relativeContextFilePath: '',
i18nUseExternalIds: true,
relativeTemplatePath: null,
};
}

Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,11 @@ export class Identifiers {
static storeLet: o.ExternalReference = {name: 'ɵɵstoreLet', moduleName: CORE};
static readContextLet: o.ExternalReference = {name: 'ɵɵreadContextLet', moduleName: CORE};

static attachSourceLocations: o.ExternalReference = {
name: 'ɵɵattachSourceLocations',
moduleName: CORE,
};

static NgOnChangesFeature: o.ExternalReference = {name: 'ɵɵNgOnChangesFeature', moduleName: CORE};

static InheritDefinitionFeature: o.ExternalReference = {
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/render3/view/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ export interface R3ComponentMetadata<DeclarationT extends R3TemplateDependency>
*/
changeDetection: ChangeDetectionStrategy | o.Expression | null;

/**
* Relative path to the component's template from the root of the project.
* Used to generate debugging information.
*/
relativeTemplatePath: string | null;

/**
* The imports expression as appears on the component decorate for standalone component. This
* field is currently needed only for local compilation, and so in other compilation modes it may
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ import {
DeclarationListEmitMode,
DeferBlockDepsEmitMode,
R3ComponentMetadata,
R3DeferPerBlockDependency,
R3DeferPerComponentDependency,
R3DeferResolverFunctionMetadata,
R3DirectiveMetadata,
R3HostMetadata,
R3TemplateDependency,
} from './api';
import {getTemplateSourceLocationsEnabled} from './config';
import {createContentQueriesFunction, createViewQueriesFunction} from './query_generation';
import {makeBindingParser} from './template';
import {asLiteral, conditionallyCreateDirectiveBindingLiteral, DefinitionMap} from './util';
Expand Down Expand Up @@ -234,6 +233,8 @@ export function compileComponentFromMetadata(
meta.i18nUseExternalIds,
meta.defer,
allDeferrableDepsFn,
meta.relativeTemplatePath,
getTemplateSourceLocationsEnabled(),
);

// Then the IR is transformed to prepare it for cod egeneration.
Expand Down
28 changes: 28 additions & 0 deletions packages/compiler/src/render3/view/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*!
* @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
*/

/**
* Whether to produce instructions that will attach the source location to each DOM node.
*
* !!!Important!!! at the time of writing this flag isn't exposed externally, but internal debug
* tools enable it via a local change. Any modifications to this flag need to update the
* internal tooling as well.
*/
let ENABLE_TEMPLATE_SOURCE_LOCATIONS = false;

/**
* Utility function to enable source locations. Intended to be used **only** inside unit tests.
*/
export function setEnableTemplateSourceLocations(value: boolean): void {
ENABLE_TEMPLATE_SOURCE_LOCATIONS = value;
}

/** Gets whether template source locations are enabled. */
export function getTemplateSourceLocationsEnabled(): boolean {
return ENABLE_TEMPLATE_SOURCE_LOCATIONS;
}
5 changes: 5 additions & 0 deletions packages/compiler/src/template/pipeline/ir/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ export enum OpKind {
* A creation op that corresponds to i18n attributes on an element.
*/
I18nAttributes,

/**
* Creation op that attaches the location at which an element was defined in a template to it.
*/
SourceLocation,
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,7 @@ export function transformExpressionsInOp(
case OpKind.I18nAttributes:
case OpKind.IcuPlaceholder:
case OpKind.DeclareLet:
case OpKind.SourceLocation:
// These operations contain no expressions.
break;
default:
Expand Down
33 changes: 32 additions & 1 deletion packages/compiler/src/template/pipeline/ir/src/ops/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export type CreateOp =
| IcuPlaceholderOp
| I18nContextOp
| I18nAttributesOp
| DeclareLetOp;
| DeclareLetOp
| SourceLocationOp;

/**
* An operation representing the creation of an element or container.
Expand Down Expand Up @@ -1553,6 +1554,36 @@ export function createI18nAttributesOp(
};
}

/** Describes a location at which an element is defined within a template. */
export interface ElementSourceLocation {
targetSlot: SlotHandle;
offset: number;
line: number;
column: number;
}

/**
* Op that attaches the location at which each element is defined within the source template.
*/
export interface SourceLocationOp extends Op<CreateOp> {
kind: OpKind.SourceLocation;
templatePath: string;
locations: ElementSourceLocation[];
}

/** Create a `SourceLocationOp`. */
export function createSourceLocationOp(
templatePath: string,
locations: ElementSourceLocation[],
): SourceLocationOp {
return {
kind: OpKind.SourceLocation,
templatePath,
locations,
...NEW_OP,
};
}

/**
* An index into the `consts` array which is shared across the compilation of all views in a
* component.
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/template/pipeline/src/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export class ComponentCompilationJob extends CompilationJob {
readonly i18nUseExternalIds: boolean,
readonly deferMeta: R3ComponentDeferMetadata,
readonly allDeferrableDepsFn: o.ReadVarExpr | null,
readonly relativeTemplatePath: string | null,
readonly enableDebugLocations: boolean,
) {
super(componentName, pool, compatibility);
this.root = new ViewCompilationUnit(this, this.allocateXrefId(), null);
Expand Down
Loading