@@ -31,6 +31,7 @@ import {
3131 ReturnStatement ,
3232 SchemaMetadata ,
3333 Statement ,
34+ TypeofExpr ,
3435 WrappedNodeExpr ,
3536} from '@angular/compiler' ;
3637import ts from 'typescript' ;
@@ -45,6 +46,7 @@ import {
4546 assertSuccessfulReferenceEmit ,
4647 LocalCompilationExtraImportsTracker ,
4748 Reference ,
49+ ReferenceEmitKind ,
4850 ReferenceEmitter ,
4951} from '../../../imports' ;
5052import {
@@ -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(
12571367function 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+ }
0 commit comments