Skip to content

Commit 38c8d71

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. These changes are internal refactorings to support isolated declarations and are not relevant to 3rd party Angular users.
1 parent 8b3973a commit 38c8d71

12 files changed

Lines changed: 1033 additions & 63 deletions

File tree

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)