Skip to content

Commit 1d4f81c

Browse files
SkyZeroZxthePunderWoman
authored andcommitted
fix(compiler-cli): resolve import alias in defer blocks (#63966)
Fixes an error where using an alias in a defer block caused the compiler CLI to fail when parsing. The resolution logic in ComponentDecoratorHandler was updated to correctly handle deferred dependencies with aliased imports. PR Close #63966
1 parent 2d21524 commit 1d4f81c

File tree

8 files changed

+203
-11
lines changed

8 files changed

+203
-11
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,11 +2154,12 @@ export class ComponentDecoratorHandler
21542154
for (const [_, deps] of resolution.deferPerBlockDependencies) {
21552155
for (const deferBlockDep of deps) {
21562156
const node = deferBlockDep.declaration.node;
2157-
const importDecl = resolution.deferrableDeclToImportDecl.get(node) ?? null;
2158-
if (importDecl !== null && this.deferredSymbolTracker.canDefer(importDecl)) {
2157+
const importInfo = resolution.deferrableDeclToImportDecl.get(node) ?? null;
2158+
if (importInfo !== null && this.deferredSymbolTracker.canDefer(importInfo.node)) {
21592159
deferBlockDep.isDeferrable = true;
2160-
deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text;
2161-
deferBlockDep.isDefaultImport = isDefaultImport(importDecl);
2160+
deferBlockDep.symbolName = importInfo.name;
2161+
deferBlockDep.importPath = importInfo.from;
2162+
deferBlockDep.isDefaultImport = isDefaultImport(importInfo.node);
21622163

21632164
// The same dependency may be used across multiple deferred blocks. De-duplicate it
21642165
// because it can throw off other logic further down the compilation pipeline.
@@ -2408,9 +2409,10 @@ export class ComponentDecoratorHandler
24082409
return;
24092410
}
24102411

2411-
// Keep track of how this class made it into the current source file
2412-
// (which ts.ImportDeclaration was used for this symbol).
2413-
resolutionData.deferrableDeclToImportDecl.set(decl.node, imp.node);
2412+
// Keep track of how this class made it into the current source file.
2413+
// Store the full `Import` info so that callers can correctly determine the
2414+
// exported name (handling aliasing) and the module specifier.
2415+
resolutionData.deferrableDeclToImportDecl.set(decl.node, imp);
24142416

24152417
this.deferredSymbolTracker.markAsDeferrableCandidate(
24162418
node,

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
HostDirectiveMeta,
3030
InputMapping,
3131
} from '../../../metadata';
32-
import {ClassDeclaration} from '../../../reflection';
32+
import {ClassDeclaration, Import} from '../../../reflection';
3333
import {SubsetOfKeys} from '../../../util/src/typescript';
3434

3535
import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
@@ -129,10 +129,11 @@ export interface ComponentResolutionData {
129129

130130
/**
131131
* Map of all types that can be defer loaded (ts.ClassDeclaration) ->
132-
* corresponding import declaration (ts.ImportDeclaration) within
133-
* the current source file.
132+
* corresponding import information (reflection `Import`) within
133+
* the current source file. The `Import` preserves the exported name
134+
* as seen by the importing module so aliasing is handled correctly.
134135
*/
135-
deferrableDeclToImportDecl: Map<ClassDeclaration, ts.ImportDeclaration>;
136+
deferrableDeclToImportDecl: Map<ClassDeclaration, Import>;
136137

137138
/**
138139
* Map of `@defer` blocks -> their corresponding dependencies.

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,3 +1218,76 @@ export declare class MyApp {
12181218
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
12191219
}
12201220

1221+
/****************************************************************************************************
1222+
* PARTIAL FILE: counter.component.js
1223+
****************************************************************************************************/
1224+
import { Component } from '@angular/core';
1225+
import * as i0 from "@angular/core";
1226+
export class CounterComponent {
1227+
}
1228+
CounterComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: CounterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1229+
CounterComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: CounterComponent, isStandalone: true, selector: "my-counter-cmp", ngImport: i0, template: 'Counter!', isInline: true });
1230+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: CounterComponent, decorators: [{
1231+
type: Component,
1232+
args: [{
1233+
standalone: true,
1234+
selector: 'my-counter-cmp',
1235+
template: 'Counter!',
1236+
}]
1237+
}] });
1238+
1239+
/****************************************************************************************************
1240+
* PARTIAL FILE: counter.component.d.ts
1241+
****************************************************************************************************/
1242+
import * as i0 from "@angular/core";
1243+
export declare class CounterComponent {
1244+
static ɵfac: i0.ɵɵFactoryDeclaration<CounterComponent, never>;
1245+
static ɵcmp: i0.ɵɵComponentDeclaration<CounterComponent, "my-counter-cmp", never, {}, {}, never, never, true, never>;
1246+
}
1247+
1248+
/****************************************************************************************************
1249+
* PARTIAL FILE: deferred_import_alias_index.js
1250+
****************************************************************************************************/
1251+
export { CounterComponent as MyCounterCmp } from './counter.component';
1252+
1253+
/****************************************************************************************************
1254+
* PARTIAL FILE: deferred_import_alias_index.d.ts
1255+
****************************************************************************************************/
1256+
export { CounterComponent as MyCounterCmp } from './counter.component';
1257+
1258+
/****************************************************************************************************
1259+
* PARTIAL FILE: deferred_import_alias.js
1260+
****************************************************************************************************/
1261+
import { Component } from '@angular/core';
1262+
import * as i0 from "@angular/core";
1263+
export class TestCmp {
1264+
}
1265+
TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
1266+
TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, isStandalone: true, selector: "test-cmp", ngImport: i0, template: `
1267+
@defer {
1268+
<my-counter-cmp />
1269+
}
1270+
`, isInline: true, deferBlockDependencies: [() => [import("./deferred_import_alias_index").then(m => m.MyCounterCmp)]] });
1271+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, resolveDeferredDeps: () => [import("./deferred_import_alias_index").then(m => m.MyCounterCmp)], resolveMetadata: MyCounterCmp => ({ decorators: [{
1272+
type: Component,
1273+
args: [{
1274+
selector: 'test-cmp',
1275+
standalone: true,
1276+
imports: [MyCounterCmp],
1277+
template: `
1278+
@defer {
1279+
<my-counter-cmp />
1280+
}
1281+
`,
1282+
}]
1283+
}], ctorParameters: null, propDecorators: null }) });
1284+
1285+
/****************************************************************************************************
1286+
* PARTIAL FILE: deferred_import_alias.d.ts
1287+
****************************************************************************************************/
1288+
import * as i0 from "@angular/core";
1289+
export declare class TestCmp {
1290+
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmp, never>;
1291+
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmp, "test-cmp", never, {}, {}, never, never, true, never>;
1292+
}
1293+

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/TEST_CASES.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,26 @@
309309
"failureMessage": "Incorrect template"
310310
}
311311
]
312+
},
313+
{
314+
"description": "Defer alias re-export/import",
315+
"inputFiles": [
316+
"deferred_import_alias.ts",
317+
"deferred_import_alias_index.ts",
318+
"counter.component.ts"
319+
],
320+
"compilationModeFilter": ["declaration-only emit"],
321+
"expectations": [
322+
{
323+
"files": [
324+
{
325+
"expected": "deferred_import_alias.js",
326+
"generated": "deferred_import_alias_generated.js"
327+
}
328+
],
329+
"failureMessage": "Defer block output with import alias does not match deferred_import_alias.js (possible alias resolution regression)"
330+
}
331+
]
312332
}
313333
]
314334
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
standalone: true,
5+
selector: 'my-counter-cmp',
6+
template: 'Counter!',
7+
})
8+
export class CounterComponent {}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/****************************************************************************************************
2+
* PARTIAL FILE: counter.component.js
3+
****************************************************************************************************/
4+
import { Component } from '@angular/core';
5+
import * as i0 from "@angular/core";
6+
export class CounterComponent {
7+
}
8+
CounterComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: CounterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9+
CounterComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: CounterComponent, isStandalone: true, selector: "my-counter-cmp", ngImport: i0, template: 'Counter!', isInline: true });
10+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: CounterComponent, decorators: [{
11+
type: Component,
12+
args: [{
13+
standalone: true,
14+
selector: 'my-counter-cmp',
15+
template: 'Counter!',
16+
}]
17+
}] });
18+
19+
/****************************************************************************************************
20+
* PARTIAL FILE: counter.component.d.ts
21+
****************************************************************************************************/
22+
import * as i0 from "@angular/core";
23+
export declare class CounterComponent {
24+
static ɵfac: i0.ɵɵFactoryDeclaration<CounterComponent, never>;
25+
static ɵcmp: i0.ɵɵComponentDeclaration<CounterComponent, "my-counter-cmp", never, {}, {}, never, never, true, never>;
26+
}
27+
28+
/****************************************************************************************************
29+
* PARTIAL FILE: index.js
30+
****************************************************************************************************/
31+
export { CounterComponent as MyCounterCmp } from './counter.component';
32+
33+
/****************************************************************************************************
34+
* PARTIAL FILE: index.d.ts
35+
****************************************************************************************************/
36+
export { CounterComponent as MyCounterCmp } from './counter.component';
37+
38+
/****************************************************************************************************
39+
* PARTIAL FILE: test.js
40+
****************************************************************************************************/
41+
import { Component } from '@angular/core';
42+
import * as i0 from "@angular/core";
43+
export class TestCmp {
44+
}
45+
TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
46+
TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, isStandalone: true, selector: "test-cmp", ngImport: i0, template: `
47+
@defer {
48+
<my-counter-cmp />
49+
}
50+
`, isInline: true, deferBlockDependencies: [() => [import("./index").then(m => m.MyCounterCmp)]] });
51+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, resolveDeferredDeps: () => [import("./index").then(m => m.MyCounterCmp)], resolveMetadata: MyCounterCmp => ({ decorators: [{
52+
type: Component,
53+
args: [{
54+
selector: 'test-cmp',
55+
standalone: true,
56+
imports: [MyCounterCmp],
57+
template: `
58+
@defer {
59+
<my-counter-cmp />
60+
}
61+
`,
62+
}]
63+
}], ctorParameters: null, propDecorators: null }) });
64+
65+
/****************************************************************************************************
66+
* PARTIAL FILE: test.d.ts
67+
****************************************************************************************************/
68+
import * as i0 from "@angular/core";
69+
export declare class TestCmp {
70+
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmp, never>;
71+
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmp, "test-cmp", never, {}, {}, never, never, true, never>;
72+
}
73+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from '@angular/core';
2+
import {MyCounterCmp} from './deferred_import_alias_index';
3+
4+
@Component({
5+
selector: 'test-cmp',
6+
standalone: true,
7+
imports: [MyCounterCmp],
8+
template: `
9+
@defer {
10+
<my-counter-cmp />
11+
}
12+
`,
13+
})
14+
export class TestCmp {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {CounterComponent as MyCounterCmp} from './counter.component';

0 commit comments

Comments
 (0)