Skip to content

Commit 06b004e

Browse files
authored
refactor(compiler): add support for compiling NgModules under isolatedDeclarations
This commit adds support for compiling NgModules in isolated declarations mode.
1 parent df68a96 commit 06b004e

613 files changed

Lines changed: 7754 additions & 83 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 252 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ReturnStatement,
3232
SchemaMetadata,
3333
Statement,
34+
TypeofExpr,
3435
WrappedNodeExpr,
3536
} from '@angular/compiler';
3637
import ts from 'typescript';
@@ -45,6 +46,7 @@ import {
4546
assertSuccessfulReferenceEmit,
4647
LocalCompilationExtraImportsTracker,
4748
Reference,
49+
ReferenceEmitKind,
4850
ReferenceEmitter,
4951
} from '../../../imports';
5052
import {
@@ -104,6 +106,7 @@ import {
104106
ReferencesRegistry,
105107
resolveProvidersRequiringFactory,
106108
toR3Reference,
109+
tryUnwrapForwardRef,
107110
unwrapExpression,
108111
wrapFunctionExpressionsInParens,
109112
wrapTypeReference,
@@ -352,14 +355,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
352355
return {};
353356
}
354357

358+
// In declaration-only emission the `declarations`/`imports`/`exports` arrays are emitted via a
359+
// purely syntactic transform - we don't attempt static resolution at all (that machinery is
360+
// only needed for the regular emit path) and produce the `Isolated` metadata kind directly
361+
// from the raw decorator expressions.
362+
if (this.emitDeclarationOnly) {
363+
return this.analyzeForDeclarationOnly(node, name, ngModule, decorator);
364+
}
365+
355366
const forwardRefResolver = createForwardRefResolver(this.isCore);
356367
const moduleResolvers = combineResolvers([
357368
createModuleWithProvidersResolver(this.reflector, this.isCore),
358369
forwardRefResolver,
359370
]);
360371

361-
const allowUnresolvedReferences =
362-
this.compilationMode === CompilationMode.LOCAL && !this.emitDeclarationOnly;
372+
const allowUnresolvedReferences = this.compilationMode === CompilationMode.LOCAL;
363373
const diagnostics: ts.Diagnostic[] = [];
364374

365375
// Resolving declarations
@@ -552,6 +562,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
552562
const type = wrapTypeReference(node);
553563

554564
let ngModuleMetadata: R3NgModuleMetadata;
565+
555566
if (allowUnresolvedReferences) {
556567
ngModuleMetadata = {
557568
kind: R3NgModuleMetadataKind.Local,
@@ -1093,6 +1104,116 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
10931104
}
10941105
}
10951106

1107+
/**
1108+
* Analyze path used in `emitDeclarationOnly` (isolated declarations) mode. The
1109+
* `declarations`/`imports`/`exports` arrays are NOT statically resolved here - they're transformed
1110+
* syntactically into the `Isolated` metadata kind's type-tuple expressions, which downstream
1111+
* `.d.ts` metadata readers resolve. This skips the partial-evaluator path entirely.
1112+
*/
1113+
private analyzeForDeclarationOnly(
1114+
node: ClassDeclaration,
1115+
name: string,
1116+
ngModule: Map<string, ts.Expression>,
1117+
decorator: Readonly<Decorator>,
1118+
): AnalysisOutput<NgModuleAnalysis> {
1119+
const diagnostics: ts.Diagnostic[] = [];
1120+
const rawDeclarations = ngModule.get('declarations') ?? null;
1121+
const rawImports = ngModule.get('imports') ?? null;
1122+
const rawExports = ngModule.get('exports') ?? null;
1123+
const rawProviders = ngModule.get('providers') ?? null;
1124+
1125+
let id: Expression | null = null;
1126+
if (ngModule.has('id')) {
1127+
const idExpr = ngModule.get('id')!;
1128+
if (!isModuleIdExpression(idExpr)) {
1129+
id = new WrappedNodeExpr(idExpr);
1130+
}
1131+
}
1132+
1133+
const type = wrapTypeReference(node);
1134+
1135+
const ngModuleMetadata: R3NgModuleMetadata = {
1136+
kind: R3NgModuleMetadataKind.Isolated,
1137+
type,
1138+
importsExpression: rawImports
1139+
? transformToTypeTupleExpression(
1140+
rawImports,
1141+
this.evaluator,
1142+
this.refEmitter,
1143+
node.getSourceFile(),
1144+
this.reflector,
1145+
diagnostics,
1146+
)
1147+
: null,
1148+
exportsExpression: rawExports
1149+
? transformToTypeTupleExpression(
1150+
rawExports,
1151+
this.evaluator,
1152+
this.refEmitter,
1153+
node.getSourceFile(),
1154+
this.reflector,
1155+
diagnostics,
1156+
)
1157+
: null,
1158+
id,
1159+
selectorScopeMode: R3SelectorScopeMode.Omit,
1160+
schemas: [],
1161+
};
1162+
1163+
// Providers are emitted as-is - they are needed for the injector but don't go through any
1164+
// resolution at this stage.
1165+
let wrappedProviders: WrappedNodeExpr<ts.Expression> | null = null;
1166+
if (
1167+
rawProviders !== null &&
1168+
(!ts.isArrayLiteralExpression(rawProviders) || rawProviders.elements.length > 0)
1169+
) {
1170+
wrappedProviders = new WrappedNodeExpr(
1171+
this.annotateForClosureCompiler
1172+
? wrapFunctionExpressionsInParens(rawProviders)
1173+
: rawProviders,
1174+
);
1175+
}
1176+
1177+
const injectorMetadata: R3InjectorMetadata = {
1178+
name,
1179+
type,
1180+
providers: wrappedProviders,
1181+
imports: [],
1182+
};
1183+
1184+
const factoryMetadata: R3FactoryMetadata = {
1185+
name,
1186+
type,
1187+
typeArgumentCount: 0,
1188+
deps: getValidConstructorDependencies(node, this.reflector, this.isCore),
1189+
target: FactoryTarget.NgModule,
1190+
};
1191+
1192+
return {
1193+
diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
1194+
analysis: {
1195+
id,
1196+
schemas: [],
1197+
mod: ngModuleMetadata,
1198+
inj: injectorMetadata,
1199+
fac: factoryMetadata,
1200+
declarations: [],
1201+
rawDeclarations,
1202+
imports: [],
1203+
rawImports,
1204+
importRefs: [],
1205+
exports: [],
1206+
rawExports,
1207+
providers: rawProviders,
1208+
providersRequiringFactory: null,
1209+
classMetadata: null,
1210+
factorySymbolName: node.name.text,
1211+
remoteScopesMayRequireCycleProtection: false,
1212+
decorator: (decorator?.node as ts.Decorator | null) ?? null,
1213+
},
1214+
};
1215+
}
1216+
10961217
// Verify that a "Declaration" reference is a `ClassDeclaration` reference.
10971218
private isClassDeclarationReference(ref: Reference): ref is Reference<ClassDeclaration> {
10981219
return this.reflector.isClass(ref.node);
@@ -1176,17 +1297,6 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
11761297
} else if (entry instanceof DynamicValue && allowUnresolvedReferences) {
11771298
dynamicValueSet.add(entry);
11781299
continue;
1179-
} else if (
1180-
this.emitDeclarationOnly &&
1181-
entry instanceof DynamicValue &&
1182-
entry.isFromUnknownIdentifier()
1183-
) {
1184-
throw createValueHasWrongTypeError(
1185-
entry.node,
1186-
entry,
1187-
`Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is an external reference. ` +
1188-
'External references in @NgModule declarations are not supported in experimental declaration-only emission mode',
1189-
);
11901300
} else {
11911301
// TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
11921302
throw createValueHasWrongTypeError(
@@ -1257,3 +1367,132 @@ function makeStandaloneBootstrapDiagnostic(
12571367
function isSyntheticReference(ref: Reference<DeclarationNode>): boolean {
12581368
return ref.synthetic;
12591369
}
1370+
1371+
/**
1372+
* Converts a value expression that is an identifier or a chain of property accesses on identifiers
1373+
* (e.g. `Foo` or `Foo.bar`) into the equivalent `ts.EntityName`, reusing the original identifier
1374+
* nodes so that any imports they reference are preserved by TypeScript's declaration emitter.
1375+
* Returns `null` for any other shape of expression.
1376+
*/
1377+
function expressionToEntityName(expr: ts.Expression): ts.EntityName | null {
1378+
if (ts.isIdentifier(expr)) {
1379+
return expr;
1380+
}
1381+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
1382+
const left = expressionToEntityName(expr.expression);
1383+
return left === null ? null : ts.factory.createQualifiedName(left, expr.name);
1384+
}
1385+
return null;
1386+
}
1387+
1388+
function transformToTypeTupleElement(
1389+
el: ts.Expression,
1390+
reflector: ReflectionHost,
1391+
diagnostics: ts.Diagnostic[],
1392+
): Expression {
1393+
let current = unwrapExpression(el);
1394+
while (true) {
1395+
const unwrapped = tryUnwrapForwardRef(current, reflector);
1396+
if (unwrapped === null) {
1397+
break;
1398+
}
1399+
current = unwrapExpression(unwrapped);
1400+
}
1401+
el = current;
1402+
1403+
// A call expression (e.g. `Foo.forRoot()` or a bare `fn()`) cannot be referenced with a
1404+
// `typeof` query directly. Instead emit `ReturnType<typeof callee>` so that the `.d.ts`
1405+
// reader can resolve the (potentially `ModuleWithProviders<T>`) return type later.
1406+
if (ts.isCallExpression(el)) {
1407+
const callee = expressionToEntityName(el.expression);
1408+
if (callee !== null) {
1409+
return new WrappedNodeExpr(
1410+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('ReturnType'), [
1411+
ts.factory.createTypeQueryNode(callee),
1412+
]),
1413+
);
1414+
}
1415+
}
1416+
1417+
if (expressionToEntityName(el) === null) {
1418+
const diag = makeDiagnostic(
1419+
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
1420+
el,
1421+
`In experimental declaration-only emission mode, this expression is not supported in NgModule imports/exports as it cannot be referenced with 'typeof'. Use a direct reference or a supported call.`,
1422+
);
1423+
diagnostics.push(diag);
1424+
return new WrappedNodeExpr(ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword));
1425+
}
1426+
1427+
return new TypeofExpr(new WrappedNodeExpr(el));
1428+
}
1429+
1430+
function resolvedToTypeTupleElement(
1431+
originalEl: ts.Expression,
1432+
resolved: ResolvedValue,
1433+
refEmitter: ReferenceEmitter,
1434+
sourceFile: ts.SourceFile,
1435+
reflector: ReflectionHost,
1436+
diagnostics: ts.Diagnostic[],
1437+
): Expression {
1438+
if (resolved instanceof Reference) {
1439+
const emitted = refEmitter.emit(resolved, sourceFile);
1440+
if (emitted.kind === ReferenceEmitKind.Success) {
1441+
return new TypeofExpr(emitted.expression);
1442+
}
1443+
}
1444+
1445+
if (Array.isArray(resolved)) {
1446+
const elements: Expression[] = [];
1447+
let allValid = true;
1448+
for (const item of resolved) {
1449+
if (item instanceof Reference) {
1450+
const emitted = refEmitter.emit(item, sourceFile);
1451+
if (emitted.kind === ReferenceEmitKind.Success) {
1452+
elements.push(new TypeofExpr(emitted.expression));
1453+
continue;
1454+
}
1455+
}
1456+
allValid = false;
1457+
break;
1458+
}
1459+
if (allValid && elements.length > 0) {
1460+
return new LiteralArrayExpr(elements);
1461+
}
1462+
}
1463+
1464+
// Fallback to syntactic transform
1465+
return transformToTypeTupleElement(originalEl, reflector, diagnostics);
1466+
}
1467+
1468+
function transformToTypeTupleExpression(
1469+
expr: ts.Expression,
1470+
evaluator: PartialEvaluator,
1471+
refEmitter: ReferenceEmitter,
1472+
sourceFile: ts.SourceFile,
1473+
reflector: ReflectionHost,
1474+
diagnostics: ts.Diagnostic[],
1475+
): Expression | null {
1476+
if (ts.isArrayLiteralExpression(expr)) {
1477+
// An empty array is treated like an omitted slot (emitted as `never` by
1478+
// `createNgModuleType`), matching the standard compilation path.
1479+
if (expr.elements.length === 0) {
1480+
return null;
1481+
}
1482+
return new LiteralArrayExpr(
1483+
expr.elements.map((el) => {
1484+
const resolved = evaluator.evaluate(el);
1485+
return resolvedToTypeTupleElement(
1486+
el,
1487+
resolved,
1488+
refEmitter,
1489+
sourceFile,
1490+
reflector,
1491+
diagnostics,
1492+
);
1493+
}),
1494+
);
1495+
}
1496+
const resolved = evaluator.evaluate(expr);
1497+
return resolvedToTypeTupleElement(expr, resolved, refEmitter, sourceFile, reflector, diagnostics);
1498+
}

packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_project(
1212
"//packages/compiler",
1313
"//packages/compiler-cli/src/ngtsc/file_system",
1414
"//packages/compiler-cli/src/ngtsc/imports",
15+
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
1516
"//packages/compiler-cli/src/ngtsc/reflection",
1617
"//packages/compiler-cli/src/ngtsc/util",
1718
],

0 commit comments

Comments
 (0)