refactor(compiler): add support for compiling NgModules under isolatedDeclarations#68680
Open
alxhub wants to merge 1 commit into
Open
refactor(compiler): add support for compiling NgModules under isolatedDeclarations#68680alxhub wants to merge 1 commit into
alxhub wants to merge 1 commit into
Conversation
179ab9b to
38c8d71
Compare
…dDeclarations 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. - Converted declaration-only compliance tests to use explicit goldens (*_isolated.golden.d.ts) and updated behavioral tests. These changes are internal refactorings to support isolated declarations and are not relevant to 3rd party Angular users.
Member
Author
atscott
requested changes
May 14, 2026
| const rawDeclarations = ngModule.get('declarations') ?? null; | ||
| const rawImports = ngModule.get('imports') ?? null; | ||
| const rawExports = ngModule.get('exports') ?? null; | ||
| const rawBootstrap = ngModule.get('bootstrap') ?? null; |
| @@ -116,6 +142,30 @@ export class DtsMetadataReader implements MetadataReader { | |||
| }; | |||
| } | |||
|
|
|||
| private extractReferencesFromResolvedValue( | |||
| value: ResolvedValue, | |||
| bestGuessOwningModule: OwningModule | null, | |||
| const rawImports = ngModule.get('imports') ?? null; | ||
| const rawExports = ngModule.get('exports') ?? null; | ||
| const rawBootstrap = ngModule.get('bootstrap') ?? null; | ||
| const rawProviders = ngModule.has('providers') ? ngModule.get('providers')! : null; |
Contributor
There was a problem hiding this comment.
Suggested change
| const rawProviders = ngModule.has('providers') ? ngModule.get('providers')! : null; | |
| const rawProviders = ngModule.get('providers') ?? null; |
| const idExpr = ngModule.get('id')!; | ||
| if (!isModuleIdExpression(idExpr)) { | ||
| id = new WrappedNodeExpr(idExpr); | ||
| } |
Contributor
There was a problem hiding this comment.
Should we have an else case with WARN_NGMODULE_ID_UNNECESSARY?
Comment on lines
+1396
to
+1399
| const forwardRefUnwrapped = tryUnwrapForwardRef(el, reflector); | ||
| if (forwardRefUnwrapped !== null) { | ||
| return transformToTypeTupleElement(forwardRefUnwrapped, reflector, diagnostics); | ||
| } |
Contributor
There was a problem hiding this comment.
nit: maybe use a loop instead of recursion
| } | ||
| if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) { | ||
| const left = expressionToEntityName(expr.expression); | ||
| return left === null ? null : ts.factory.createQualifiedName(left, expr.name.text); |
Contributor
There was a problem hiding this comment.
Suggested change
| return left === null ? null : ts.factory.createQualifiedName(left, expr.name.text); | |
| return left === null ? null : ts.factory.createQualifiedName(left, expr.name); |
nit
Also this looks quite similar to the existing getEntityTypeFromExpression?
Comment on lines
+87
to
+117
| const declarations = this.extractReferencesFromResolvedValue( | ||
| declarationMetadata.kind === ts.SyntaxKind.NeverKeyword | ||
| ? [] | ||
| : this.evaluator.evaluateType( | ||
| declarationMetadata, | ||
| ref.bestGuessOwningModule, | ||
| undefined, | ||
| foreignTypeResolver, | ||
| ), | ||
| ref.bestGuessOwningModule, | ||
| ); | ||
| const exports = extractReferencesFromType( | ||
| this.checker, | ||
| exportMetadata, | ||
| const exports = this.extractReferencesFromResolvedValue( | ||
| exportMetadata.kind === ts.SyntaxKind.NeverKeyword | ||
| ? [] | ||
| : this.evaluator.evaluateType( | ||
| exportMetadata, | ||
| ref.bestGuessOwningModule, | ||
| undefined, | ||
| foreignTypeResolver, | ||
| ), | ||
| ref.bestGuessOwningModule, | ||
| ); | ||
| const imports = extractReferencesFromType( | ||
| this.checker, | ||
| importMetadata, | ||
| const imports = this.extractReferencesFromResolvedValue( | ||
| importMetadata.kind === ts.SyntaxKind.NeverKeyword | ||
| ? [] | ||
| : this.evaluator.evaluateType( | ||
| importMetadata, | ||
| ref.bestGuessOwningModule, | ||
| undefined, | ||
| foreignTypeResolver, | ||
| ), |
Contributor
There was a problem hiding this comment.
Suggested change
| const declarations = this.extractReferencesFromResolvedValue( | |
| declarationMetadata.kind === ts.SyntaxKind.NeverKeyword | |
| ? [] | |
| : this.evaluator.evaluateType( | |
| declarationMetadata, | |
| ref.bestGuessOwningModule, | |
| undefined, | |
| foreignTypeResolver, | |
| ), | |
| ref.bestGuessOwningModule, | |
| ); | |
| const exports = extractReferencesFromType( | |
| this.checker, | |
| exportMetadata, | |
| const exports = this.extractReferencesFromResolvedValue( | |
| exportMetadata.kind === ts.SyntaxKind.NeverKeyword | |
| ? [] | |
| : this.evaluator.evaluateType( | |
| exportMetadata, | |
| ref.bestGuessOwningModule, | |
| undefined, | |
| foreignTypeResolver, | |
| ), | |
| ref.bestGuessOwningModule, | |
| ); | |
| const imports = extractReferencesFromType( | |
| this.checker, | |
| importMetadata, | |
| const imports = this.extractReferencesFromResolvedValue( | |
| importMetadata.kind === ts.SyntaxKind.NeverKeyword | |
| ? [] | |
| : this.evaluator.evaluateType( | |
| importMetadata, | |
| ref.bestGuessOwningModule, | |
| undefined, | |
| foreignTypeResolver, | |
| ), | |
| const evaluateMetadata = (metadataNode: ts.TypeNode) => { | |
| return metadataNode.kind === ts.SyntaxKind.NeverKeyword | |
| ? [] | |
| : this.evaluator.evaluateType( | |
| metadataNode, | |
| ref.bestGuessOwningModule, | |
| undefined, | |
| foreignTypeResolver, | |
| ); | |
| }; | |
| const declarations = this.extractReferencesFromResolvedValue( | |
| evaluateMetadata(declarationMetadata), | |
| ref.bestGuessOwningModule, | |
| ); | |
| const exports = this.extractReferencesFromResolvedValue( | |
| evaluateMetadata(exportMetadata), | |
| ref.bestGuessOwningModule, | |
| ); | |
| const imports = this.extractReferencesFromResolvedValue( | |
| evaluateMetadata(importMetadata), | |
| ref.bestGuessOwningModule, | |
| ); |
| return null; | ||
| } | ||
| const name = ts.isQualifiedName(type.typeName) ? type.typeName.right : type.typeName; | ||
| if (!ts.isIdentifier(name) || name.text !== 'ModuleWithProviders') { |
Contributor
There was a problem hiding this comment.
Suggested change
| if (!ts.isIdentifier(name) || name.text !== 'ModuleWithProviders') { | |
| if (name.text !== 'ModuleWithProviders') { |
Types guarantee this is an identifier
Comment on lines
+310
to
+316
| const typeArguments = node.typeArguments | ||
| ? ts.factory.createNodeArray( | ||
| node.typeArguments.map( | ||
| (arg) => ts.visitNode(arg, visit, ts.isTypeNode) as ts.TypeNode, | ||
| ), | ||
| ) | ||
| : undefined; |
Contributor
There was a problem hiding this comment.
Suggested change
| const typeArguments = node.typeArguments | |
| ? ts.factory.createNodeArray( | |
| node.typeArguments.map( | |
| (arg) => ts.visitNode(arg, visit, ts.isTypeNode) as ts.TypeNode, | |
| ), | |
| ) | |
| : undefined; | |
| const typeArguments = node.typeArguments | |
| ? ts.visitNodes(node.typeArguments, visit, ts.isTypeNode) | |
| : undefined; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: only really relevant for g3
attempt to statically evaluate @NgModule imports and exports in isolated declarations mode. if resolution succeeds, emit the resolved values as concrete references. if resolution fails, fall back to purely syntactic transforms, such as transforming calls to ReturnType<typeof ...>.
also add validation to ensure expressions falling back to typeof are valid entity names, producing a LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION diagnostic otherwise.
this allows emitting correct type references in .d.ts files without needing full resolution of external references, while supporting common patterns like forRoot() and local evaluation where possible.
TAG=agy
CONV=51a2b6d6-5679-49cf-8fa6-61fbc69628be