@@ -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,119 @@ 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+ bootstrapExpression : rawBootstrap ? new WrappedNodeExpr ( rawBootstrap ) : null ,
1140+ declarationsExpression : null ,
1141+ importsExpression : rawImports
1142+ ? transformToTypeTupleExpression (
1143+ rawImports ,
1144+ this . evaluator ,
1145+ this . refEmitter ,
1146+ node . getSourceFile ( ) ,
1147+ this . reflector ,
1148+ diagnostics ,
1149+ )
1150+ : null ,
1151+ exportsExpression : rawExports
1152+ ? transformToTypeTupleExpression (
1153+ rawExports ,
1154+ this . evaluator ,
1155+ this . refEmitter ,
1156+ node . getSourceFile ( ) ,
1157+ this . reflector ,
1158+ diagnostics ,
1159+ )
1160+ : null ,
1161+ id,
1162+ selectorScopeMode : R3SelectorScopeMode . Omit ,
1163+ schemas : [ ] ,
1164+ } ;
1165+
1166+ // Providers are emitted as-is - they are needed for the injector but don't go through any
1167+ // resolution at this stage.
1168+ let wrappedProviders : WrappedNodeExpr < ts . Expression > | null = null ;
1169+ if (
1170+ rawProviders !== null &&
1171+ ( ! ts . isArrayLiteralExpression ( rawProviders ) || rawProviders . elements . length > 0 )
1172+ ) {
1173+ wrappedProviders = new WrappedNodeExpr (
1174+ this . annotateForClosureCompiler
1175+ ? wrapFunctionExpressionsInParens ( rawProviders )
1176+ : rawProviders ,
1177+ ) ;
1178+ }
1179+
1180+ const injectorMetadata : R3InjectorMetadata = {
1181+ name,
1182+ type,
1183+ providers : wrappedProviders ,
1184+ imports : [ ] ,
1185+ } ;
1186+
1187+ const factoryMetadata : R3FactoryMetadata = {
1188+ name,
1189+ type,
1190+ typeArgumentCount : 0 ,
1191+ deps : getValidConstructorDependencies ( node , this . reflector , this . isCore ) ,
1192+ target : FactoryTarget . NgModule ,
1193+ } ;
1194+
1195+ return {
1196+ diagnostics : diagnostics . length > 0 ? diagnostics : undefined ,
1197+ analysis : {
1198+ id,
1199+ schemas : [ ] ,
1200+ mod : ngModuleMetadata ,
1201+ inj : injectorMetadata ,
1202+ fac : factoryMetadata ,
1203+ declarations : [ ] ,
1204+ rawDeclarations,
1205+ imports : [ ] ,
1206+ rawImports,
1207+ importRefs : [ ] ,
1208+ exports : [ ] ,
1209+ rawExports,
1210+ providers : rawProviders ,
1211+ providersRequiringFactory : null ,
1212+ classMetadata : null ,
1213+ factorySymbolName : node . name . text ,
1214+ remoteScopesMayRequireCycleProtection : false ,
1215+ decorator : ( decorator ?. node as ts . Decorator | null ) ?? null ,
1216+ } ,
1217+ } ;
1218+ }
1219+
10961220 // Verify that a "Declaration" reference is a `ClassDeclaration` reference.
10971221 private isClassDeclarationReference ( ref : Reference ) : ref is Reference < ClassDeclaration > {
10981222 return this . reflector . isClass ( ref . node ) ;
@@ -1176,17 +1300,6 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
11761300 } else if ( entry instanceof DynamicValue && allowUnresolvedReferences ) {
11771301 dynamicValueSet . add ( entry ) ;
11781302 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- ) ;
11901303 } else {
11911304 // TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
11921305 throw createValueHasWrongTypeError (
@@ -1257,3 +1370,129 @@ function makeStandaloneBootstrapDiagnostic(
12571370function isSyntheticReference ( ref : Reference < DeclarationNode > ) : boolean {
12581371 return ref . synthetic ;
12591372}
1373+
1374+ /**
1375+ * Converts a value expression that is an identifier or a chain of property accesses on identifiers
1376+ * (e.g. `Foo` or `Foo.bar`) into the equivalent `ts.EntityName`, reusing the original identifier
1377+ * nodes so that any imports they reference are preserved by TypeScript's declaration emitter.
1378+ * Returns `null` for any other shape of expression.
1379+ */
1380+ function expressionToEntityName ( expr : ts . Expression ) : ts . EntityName | null {
1381+ if ( ts . isIdentifier ( expr ) ) {
1382+ return expr ;
1383+ }
1384+ if ( ts . isPropertyAccessExpression ( expr ) && ts . isIdentifier ( expr . name ) ) {
1385+ const left = expressionToEntityName ( expr . expression ) ;
1386+ return left === null ? null : ts . factory . createQualifiedName ( left , expr . name . text ) ;
1387+ }
1388+ return null ;
1389+ }
1390+
1391+ function transformToTypeTupleElement (
1392+ el : ts . Expression ,
1393+ reflector : ReflectionHost ,
1394+ diagnostics : ts . Diagnostic [ ] ,
1395+ ) : Expression {
1396+ el = unwrapExpression ( el ) ;
1397+
1398+ const forwardRefUnwrapped = tryUnwrapForwardRef ( el , reflector ) ;
1399+ if ( forwardRefUnwrapped !== null ) {
1400+ return transformToTypeTupleElement ( forwardRefUnwrapped , reflector , diagnostics ) ;
1401+ }
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