Skip to content

Commit 4f839cf

Browse files
committed
refactor(compiler): add support for compiling NgModules under isolatedDeclarations
This commit adds support for compiling NgModules in isolated declarations mode. Key changes: - Removed the unnecessary @NgModule({id}) warning diagnostic in isolated declarations mode. - Set declarations to never in the generated .d.ts file for isolated NgModules. - Extracted closures to standalone functions in NgModuleDecoratorHandler to avoid capturing state. - Removed bootstrap and declarations from R3NgModuleMetadataIsolated as they are not used or always never. - Extended the foreign function mechanism in PartialEvaluator to handle foreign types like ModuleWithProviders. - Converted declaration-only compliance tests to use explicit goldens (*_isolated.golden.d.ts) and updated behavioral tests. These changes are internal refactorings to support isolated declarations and are not relevant to 3rd party Angular users.
1 parent 8b3973a commit 4f839cf

613 files changed

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

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)