@@ -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,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(
12571368function 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+ }
0 commit comments