@@ -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,203 @@ 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+ } else {
1132+ const diag = makeDiagnostic (
1133+ ErrorCode . WARN_NGMODULE_ID_UNNECESSARY ,
1134+ idExpr ,
1135+ `Using 'module.id' for NgModule.id is a common anti-pattern that is ignored by the Angular compiler.` ,
1136+ ) ;
1137+ diag . category = ts . DiagnosticCategory . Warning ;
1138+ diagnostics . push ( diag ) ;
1139+ }
1140+ }
1141+
1142+ const type = wrapTypeReference ( node ) ;
1143+
1144+ // Builds a single type-tuple element for the `.d.ts` from a raw `imports`/`exports`/
1145+ // `declarations` array element, without statically resolving it:
1146+ // - `Foo` -> `typeof Foo`
1147+ // - `Foo.forRoot()` / `fn()` -> `ReturnType<typeof Foo.forRoot>` / `ReturnType<typeof fn>`
1148+ // - `forwardRef(() => Foo)` -> unwrapped, then handled as `Foo`
1149+ const transformToTypeTupleElement = ( el : ts . Expression ) : Expression => {
1150+ el = unwrapExpression ( el ) ;
1151+
1152+ const forwardRefUnwrapped = tryUnwrapForwardRef ( el , this . reflector ) ;
1153+ if ( forwardRefUnwrapped !== null ) {
1154+ return transformToTypeTupleElement ( forwardRefUnwrapped ) ;
1155+ }
1156+
1157+ // A call expression (e.g. `Foo.forRoot()` or a bare `fn()`) cannot be referenced with a
1158+ // `typeof` query directly. Instead emit `ReturnType<typeof callee>` so that the `.d.ts`
1159+ // reader can resolve the (potentially `ModuleWithProviders<T>`) return type later.
1160+ if ( ts . isCallExpression ( el ) ) {
1161+ const callee = expressionToEntityName ( el . expression ) ;
1162+ if ( callee !== null ) {
1163+ return new WrappedNodeExpr (
1164+ ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( 'ReturnType' ) , [
1165+ ts . factory . createTypeQueryNode ( callee ) ,
1166+ ] ) ,
1167+ ) ;
1168+ }
1169+ }
1170+
1171+ if ( expressionToEntityName ( el ) === null ) {
1172+ const diag = makeDiagnostic (
1173+ ErrorCode . LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION ,
1174+ el ,
1175+ `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.` ,
1176+ ) ;
1177+ diagnostics . push ( diag ) ;
1178+ return new WrappedNodeExpr ( ts . factory . createKeywordTypeNode ( ts . SyntaxKind . NeverKeyword ) ) ;
1179+ }
1180+
1181+ return new TypeofExpr ( new WrappedNodeExpr ( el ) ) ;
1182+ } ;
1183+
1184+ const resolvedToTypeTupleElement = (
1185+ originalEl : ts . Expression ,
1186+ resolved : ResolvedValue ,
1187+ ) : Expression => {
1188+ if ( resolved instanceof Reference ) {
1189+ const emitted = this . refEmitter . emit ( resolved , node . getSourceFile ( ) ) ;
1190+ if ( emitted . kind === ReferenceEmitKind . Success ) {
1191+ return new TypeofExpr ( emitted . expression ) ;
1192+ }
1193+ }
1194+
1195+ if ( Array . isArray ( resolved ) ) {
1196+ const elements : Expression [ ] = [ ] ;
1197+ let allValid = true ;
1198+ for ( const item of resolved ) {
1199+ if ( item instanceof Reference ) {
1200+ const emitted = this . refEmitter . emit ( item , node . getSourceFile ( ) ) ;
1201+ if ( emitted . kind === ReferenceEmitKind . Success ) {
1202+ elements . push ( new TypeofExpr ( emitted . expression ) ) ;
1203+ continue ;
1204+ }
1205+ }
1206+ allValid = false ;
1207+ break ;
1208+ }
1209+ if ( allValid && elements . length > 0 ) {
1210+ return new LiteralArrayExpr ( elements ) ;
1211+ }
1212+ }
1213+
1214+ // Fallback to syntactic transform
1215+ return transformToTypeTupleElement ( originalEl ) ;
1216+ } ;
1217+
1218+ const transformToTypeTupleExpression = ( expr : ts . Expression ) : Expression | null => {
1219+ if ( ts . isArrayLiteralExpression ( expr ) ) {
1220+ // An empty array is treated like an omitted slot (emitted as `never` by
1221+ // `createNgModuleType`), matching the standard compilation path.
1222+ if ( expr . elements . length === 0 ) {
1223+ return null ;
1224+ }
1225+ return new LiteralArrayExpr (
1226+ expr . elements . map ( ( el ) => {
1227+ const resolved = this . evaluator . evaluate ( el ) ;
1228+ return resolvedToTypeTupleElement ( el , resolved ) ;
1229+ } ) ,
1230+ ) ;
1231+ }
1232+ const resolved = this . evaluator . evaluate ( expr ) ;
1233+ return resolvedToTypeTupleElement ( expr , resolved ) ;
1234+ } ;
1235+
1236+ const ngModuleMetadata : R3NgModuleMetadata = {
1237+ kind : R3NgModuleMetadataKind . Isolated ,
1238+ type,
1239+ bootstrapExpression : rawBootstrap ? new WrappedNodeExpr ( rawBootstrap ) : null ,
1240+ declarationsExpression : rawDeclarations
1241+ ? transformToTypeTupleExpression ( rawDeclarations )
1242+ : null ,
1243+ importsExpression : rawImports ? transformToTypeTupleExpression ( rawImports ) : null ,
1244+ exportsExpression : rawExports ? transformToTypeTupleExpression ( rawExports ) : null ,
1245+ id,
1246+ selectorScopeMode : R3SelectorScopeMode . Omit ,
1247+ schemas : [ ] ,
1248+ } ;
1249+
1250+ // Providers are emitted as-is - they are needed for the injector but don't go through any
1251+ // resolution at this stage.
1252+ let wrappedProviders : WrappedNodeExpr < ts . Expression > | null = null ;
1253+ if (
1254+ rawProviders !== null &&
1255+ ( ! ts . isArrayLiteralExpression ( rawProviders ) || rawProviders . elements . length > 0 )
1256+ ) {
1257+ wrappedProviders = new WrappedNodeExpr (
1258+ this . annotateForClosureCompiler
1259+ ? wrapFunctionExpressionsInParens ( rawProviders )
1260+ : rawProviders ,
1261+ ) ;
1262+ }
1263+
1264+ const injectorMetadata : R3InjectorMetadata = {
1265+ name,
1266+ type,
1267+ providers : wrappedProviders ,
1268+ imports : [ ] ,
1269+ } ;
1270+
1271+ const factoryMetadata : R3FactoryMetadata = {
1272+ name,
1273+ type,
1274+ typeArgumentCount : 0 ,
1275+ deps : getValidConstructorDependencies ( node , this . reflector , this . isCore ) ,
1276+ target : FactoryTarget . NgModule ,
1277+ } ;
1278+
1279+ return {
1280+ diagnostics : diagnostics . length > 0 ? diagnostics : undefined ,
1281+ analysis : {
1282+ id,
1283+ schemas : [ ] ,
1284+ mod : ngModuleMetadata ,
1285+ inj : injectorMetadata ,
1286+ fac : factoryMetadata ,
1287+ declarations : [ ] ,
1288+ rawDeclarations,
1289+ imports : [ ] ,
1290+ rawImports,
1291+ importRefs : [ ] ,
1292+ exports : [ ] ,
1293+ rawExports,
1294+ providers : rawProviders ,
1295+ providersRequiringFactory : null ,
1296+ classMetadata : null ,
1297+ factorySymbolName : node . name . text ,
1298+ remoteScopesMayRequireCycleProtection : false ,
1299+ decorator : ( decorator ?. node as ts . Decorator | null ) ?? null ,
1300+ } ,
1301+ } ;
1302+ }
1303+
10961304 // Verify that a "Declaration" reference is a `ClassDeclaration` reference.
10971305 private isClassDeclarationReference ( ref : Reference ) : ref is Reference < ClassDeclaration > {
10981306 return this . reflector . isClass ( ref . node ) ;
@@ -1176,17 +1384,6 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
11761384 } else if ( entry instanceof DynamicValue && allowUnresolvedReferences ) {
11771385 dynamicValueSet . add ( entry ) ;
11781386 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- ) ;
11901387 } else {
11911388 // TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
11921389 throw createValueHasWrongTypeError (
@@ -1257,3 +1454,20 @@ function makeStandaloneBootstrapDiagnostic(
12571454function isSyntheticReference ( ref : Reference < DeclarationNode > ) : boolean {
12581455 return ref . synthetic ;
12591456}
1457+
1458+ /**
1459+ * Converts a value expression that is an identifier or a chain of property accesses on identifiers
1460+ * (e.g. `Foo` or `Foo.bar`) into the equivalent `ts.EntityName`, reusing the original identifier
1461+ * nodes so that any imports they reference are preserved by TypeScript's declaration emitter.
1462+ * Returns `null` for any other shape of expression.
1463+ */
1464+ function expressionToEntityName ( expr : ts . Expression ) : ts . EntityName | null {
1465+ if ( ts . isIdentifier ( expr ) ) {
1466+ return expr ;
1467+ }
1468+ if ( ts . isPropertyAccessExpression ( expr ) && ts . isIdentifier ( expr . name ) ) {
1469+ const left = expressionToEntityName ( expr . expression ) ;
1470+ return left === null ? null : ts . factory . createQualifiedName ( left , expr . name . text ) ;
1471+ }
1472+ return null ;
1473+ }
0 commit comments