Skip to content

Commit c762ff3

Browse files
committed
feat(compiler): support isolated declarations for NgModule via syntactic transform
Support Isolated Declarations for @NgModule by bypassing static resolution of declarations, imports, and exports arrays in declaration-only mode, and instead performing a purely syntactic transform to generate the corresponding type tuples in the .d.ts file. This adds an Isolated metadata kind to R3NgModuleMetadata and updates createNgModuleType to use it. TAG=agy CONV=78b5e951-e08a-440f-896f-9aa8aad00083
1 parent 8b3973a commit c762ff3

9 files changed

Lines changed: 374 additions & 68 deletions

File tree

packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ReturnStatement,
3232
SchemaMetadata,
3333
Statement,
34+
TypeofExpr,
3435
WrappedNodeExpr,
3536
} from '@angular/compiler';
3637
import ts from 'typescript';
@@ -364,17 +365,20 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
364365

365366
// Resolving declarations
366367
let declarationRefs: Reference<ClassDeclaration>[] = [];
368+
let unresolvedDeclarations: DynamicValue[] = [];
367369
const rawDeclarations: ts.Expression | null = ngModule.get('declarations') ?? null;
368370
if (rawDeclarations !== null) {
369371
const declarationMeta = this.evaluator.evaluate(rawDeclarations, forwardRefResolver);
370-
declarationRefs = this.resolveTypeList(
372+
const result = this.resolveTypeList(
371373
rawDeclarations,
372374
declarationMeta,
373375
name,
374376
'declarations',
375377
0,
376378
allowUnresolvedReferences,
377-
).references;
379+
);
380+
declarationRefs = result.references;
381+
unresolvedDeclarations = result.dynamicValues;
378382

379383
// Look through the declarations to make sure they're all a part of the current compilation.
380384
for (const ref of declarationRefs) {
@@ -399,6 +403,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
399403

400404
// Resolving imports
401405
let importRefs: Reference<ClassDeclaration>[] = [];
406+
let unresolvedImports: DynamicValue[] = [];
402407
let rawImports: ts.Expression | null = ngModule.get('imports') ?? null;
403408
if (rawImports !== null) {
404409
const importsMeta = this.evaluator.evaluate(rawImports, moduleResolvers);
@@ -431,21 +436,25 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
431436
}
432437

433438
importRefs = result.references;
439+
unresolvedImports = result.dynamicValues;
434440
}
435441

436442
// Resolving exports
437443
let exportRefs: Reference<ClassDeclaration>[] = [];
444+
let unresolvedExports: DynamicValue[] = [];
438445
const rawExports: ts.Expression | null = ngModule.get('exports') ?? null;
439446
if (rawExports !== null) {
440447
const exportsMeta = this.evaluator.evaluate(rawExports, moduleResolvers);
441-
exportRefs = this.resolveTypeList(
448+
const result = this.resolveTypeList(
442449
rawExports,
443450
exportsMeta,
444451
name,
445452
'exports',
446453
0,
447454
allowUnresolvedReferences,
448-
).references;
455+
);
456+
exportRefs = result.references;
457+
unresolvedExports = result.dynamicValues;
449458
this.referencesRegistry.add(node, ...exportRefs);
450459
}
451460

@@ -534,12 +543,33 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
534543
exportedDeclarations.push(decl.type);
535544
}
536545
}
537-
const imports = importRefs.map((imp) =>
538-
this._toR3Reference(imp.getOriginForDiagnostics(meta, node.name), imp, valueContext),
539-
);
540-
const exports = exportRefs.map((exp) =>
541-
this._toR3Reference(exp.getOriginForDiagnostics(meta, node.name), exp, valueContext),
542-
);
546+
if (this.emitDeclarationOnly) {
547+
for (const d of unresolvedDeclarations) {
548+
const declExpr = new WrappedNodeExpr(d.node);
549+
declarations.push({
550+
value: declExpr,
551+
type: declExpr,
552+
});
553+
}
554+
}
555+
const imports = [
556+
...importRefs.map((imp) =>
557+
this._toR3Reference(imp.getOriginForDiagnostics(meta, node.name), imp, valueContext),
558+
),
559+
...unresolvedImports.map((d) => ({
560+
value: new WrappedNodeExpr(d.node),
561+
type: new WrappedNodeExpr(d.node),
562+
})),
563+
];
564+
const exports = [
565+
...exportRefs.map((exp) =>
566+
this._toR3Reference(exp.getOriginForDiagnostics(meta, node.name), exp, valueContext),
567+
),
568+
...unresolvedExports.map((d) => ({
569+
value: new WrappedNodeExpr(d.node),
570+
type: new WrappedNodeExpr(d.node),
571+
})),
572+
];
543573

544574
const isForwardReference = (ref: R3Reference) =>
545575
isExpressionForwardReference(ref.value, node.name!, valueContext);
@@ -552,7 +582,30 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
552582
const type = wrapTypeReference(node);
553583

554584
let ngModuleMetadata: R3NgModuleMetadata;
555-
if (allowUnresolvedReferences) {
585+
586+
const transformToTypeTupleExpression = (expr: ts.Expression) => {
587+
if (ts.isArrayLiteralExpression(expr)) {
588+
const types = expr.elements.map((el) => new TypeofExpr(new WrappedNodeExpr(el)));
589+
return new LiteralArrayExpr(types);
590+
}
591+
return new TypeofExpr(new WrappedNodeExpr(expr));
592+
};
593+
594+
if (this.emitDeclarationOnly) {
595+
ngModuleMetadata = {
596+
kind: R3NgModuleMetadataKind.Isolated,
597+
type,
598+
bootstrapExpression: rawBootstrap ? new WrappedNodeExpr(rawBootstrap) : null,
599+
declarationsExpression: rawDeclarations
600+
? transformToTypeTupleExpression(rawDeclarations)
601+
: null,
602+
importsExpression: rawImports ? transformToTypeTupleExpression(rawImports) : null,
603+
exportsExpression: rawExports ? transformToTypeTupleExpression(rawExports) : null,
604+
id,
605+
selectorScopeMode: R3SelectorScopeMode.Omit,
606+
schemas: [],
607+
};
608+
} else if (allowUnresolvedReferences) {
556609
ngModuleMetadata = {
557610
kind: R3NgModuleMetadataKind.Local,
558611
type,
@@ -1181,12 +1234,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<
11811234
entry instanceof DynamicValue &&
11821235
entry.isFromUnknownIdentifier()
11831236
) {
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-
);
1237+
dynamicValueSet.add(entry);
1238+
continue;
11901239
} else {
11911240
// TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
11921241
throw createValueHasWrongTypeError(

packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_project(
1212
"//packages/compiler",
1313
"//packages/compiler-cli/src/ngtsc/file_system",
1414
"//packages/compiler-cli/src/ngtsc/imports",
15+
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
1516
"//packages/compiler-cli/src/ngtsc/reflection",
1617
"//packages/compiler-cli/src/ngtsc/util",
1718
],

packages/compiler-cli/src/ngtsc/metadata/src/dts.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ReflectionHost,
1717
TypeValueReferenceKind,
1818
} from '../../reflection';
19+
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator/index';
1920
import {nodeDebugInfo} from '../../util/src/typescript';
2021

2122
import {
@@ -42,10 +43,14 @@ import {
4243
* from an upstream compilation already.
4344
*/
4445
export class DtsMetadataReader implements MetadataReader {
46+
private evaluator: PartialEvaluator;
47+
4548
constructor(
4649
private checker: ts.TypeChecker,
4750
private reflector: ReflectionHost,
48-
) {}
51+
) {
52+
this.evaluator = new PartialEvaluator(this.reflector, this.checker, null);
53+
}
4954

5055
/**
5156
* Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts
@@ -76,19 +81,16 @@ export class DtsMetadataReader implements MetadataReader {
7681
// Read the ModuleData out of the type arguments.
7782
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
7883

79-
const declarations = extractReferencesFromType(
80-
this.checker,
81-
declarationMetadata,
84+
const declarations = this.extractReferencesFromResolvedValue(
85+
this.evaluator.evaluateType(declarationMetadata, ref.bestGuessOwningModule),
8286
ref.bestGuessOwningModule,
8387
);
84-
const exports = extractReferencesFromType(
85-
this.checker,
86-
exportMetadata,
88+
const exports = this.extractReferencesFromResolvedValue(
89+
this.evaluator.evaluateType(exportMetadata, ref.bestGuessOwningModule),
8790
ref.bestGuessOwningModule,
8891
);
89-
const imports = extractReferencesFromType(
90-
this.checker,
91-
importMetadata,
92+
const imports = this.extractReferencesFromResolvedValue(
93+
this.evaluator.evaluateType(importMetadata, ref.bestGuessOwningModule),
9294
ref.bestGuessOwningModule,
9395
);
9496

@@ -116,6 +118,30 @@ export class DtsMetadataReader implements MetadataReader {
116118
};
117119
}
118120

121+
private extractReferencesFromResolvedValue(
122+
value: ResolvedValue,
123+
bestGuessOwningModule: OwningModule | null,
124+
): {result: Reference<ClassDeclaration>[]; isIncomplete: boolean} {
125+
const result: Reference<ClassDeclaration>[] = [];
126+
let isIncomplete = false;
127+
128+
if (Array.isArray(value)) {
129+
for (const element of value) {
130+
if (element instanceof Reference && this.reflector.isClass(element.node)) {
131+
result.push(element as Reference<ClassDeclaration>);
132+
} else {
133+
isIncomplete = true;
134+
}
135+
}
136+
} else if (value instanceof Reference && this.reflector.isClass(value.node)) {
137+
result.push(value as Reference<ClassDeclaration>);
138+
} else {
139+
isIncomplete = true;
140+
}
141+
142+
return {result, isIncomplete};
143+
}
144+
119145
/**
120146
* Read directive (or component) metadata from a referenced class in a .d.ts file.
121147
*/

packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import ts from 'typescript';
1010

11-
import {Reference} from '../../imports';
11+
import {OwningModule, Reference} from '../../imports';
1212
import {DependencyTracker} from '../../incremental/api';
1313
import {ReflectionHost} from '../../reflection';
1414

@@ -41,4 +41,20 @@ export class PartialEvaluator {
4141
foreignFunctionResolver,
4242
});
4343
}
44+
45+
evaluateType(
46+
typeNode: ts.TypeNode,
47+
owningModule: OwningModule | null = null,
48+
foreignFunctionResolver?: ForeignFunctionResolver,
49+
): ResolvedValue {
50+
const interpreter = new StaticInterpreter(this.host, this.checker, this.dependencyTracker);
51+
const sourceFile = typeNode.getSourceFile();
52+
return interpreter.visitType(typeNode, {
53+
originatingFile: sourceFile,
54+
absoluteModuleName: owningModule ? owningModule.specifier : null,
55+
resolutionContext: owningModule ? owningModule.resolutionContext : sourceFile.fileName,
56+
scope: new Map<ts.ParameterDeclaration, ResolvedValue>(),
57+
foreignFunctionResolver,
58+
});
59+
}
4460
}

packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import ts from 'typescript';
1111
import {Reference} from '../../imports';
1212
import {OwningModule} from '../../imports/src/references';
1313
import {DependencyTracker} from '../../incremental/api';
14-
import {Declaration, DeclarationNode, FunctionDefinition, ReflectionHost} from '../../reflection';
14+
import {
15+
Declaration,
16+
DeclarationNode,
17+
FunctionDefinition,
18+
ReflectionHost,
19+
reflectTypeEntityToDeclaration,
20+
} from '../../reflection';
21+
import {TypeEntityToDeclarationError} from '../../reflection/src/typescript';
1522
import {isDeclaration} from '../../util/src/typescript';
1623

1724
import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn, StringConcatBuiltinFn} from './builtin';
@@ -707,7 +714,7 @@ export class StaticInterpreter {
707714
return new Reference(node, owningModule(context));
708715
}
709716

710-
private visitType(node: ts.TypeNode, context: Context): ResolvedValue {
717+
public visitType(node: ts.TypeNode, context: Context): ResolvedValue {
711718
if (ts.isLiteralTypeNode(node)) {
712719
return this.visitExpression(node.literal, context);
713720
} else if (ts.isTupleTypeNode(node)) {
@@ -718,6 +725,8 @@ export class StaticInterpreter {
718725
return this.visitType(node.type, context);
719726
} else if (ts.isTypeQueryNode(node)) {
720727
return this.visitTypeQuery(node, context);
728+
} else if (ts.isTypeReferenceNode(node)) {
729+
return this.visitTypeReference(node, context);
721730
}
722731

723732
return DynamicValue.fromDynamicType(node);
@@ -734,18 +743,74 @@ export class StaticInterpreter {
734743
}
735744

736745
private visitTypeQuery(node: ts.TypeQueryNode, context: Context): ResolvedValue {
737-
const exprName = ts.isQualifiedName(node.exprName) ? node.exprName.right : node.exprName;
738-
if (!ts.isIdentifier(exprName)) {
746+
try {
747+
const result = reflectTypeEntityToDeclaration(node.exprName, this.checker);
748+
const declNode = result.node;
749+
const from = result.from;
750+
751+
let declContext = context;
752+
if (from !== null && !from.startsWith('.')) {
753+
declContext = {
754+
...context,
755+
absoluteModuleName: from,
756+
resolutionContext: node.getSourceFile().fileName,
757+
};
758+
}
759+
760+
return this.visitDeclaration(declNode, declContext);
761+
} catch (e) {
762+
if (e instanceof TypeEntityToDeclarationError) {
763+
return DynamicValue.fromUnknown(node);
764+
}
765+
throw e;
766+
}
767+
}
768+
769+
private visitTypeReference(node: ts.TypeReferenceNode, context: Context): ResolvedValue {
770+
const typeName = ts.isQualifiedName(node.typeName) ? node.typeName.right : node.typeName;
771+
if (!ts.isIdentifier(typeName)) {
739772
return DynamicValue.fromUnknown(node);
740773
}
741774

742-
const decl = this.host.getDeclarationOfIdentifier(exprName);
743-
if (decl === null) {
744-
return DynamicValue.fromUnknownIdentifier(exprName);
775+
if (
776+
typeName.text === 'ReturnType' &&
777+
node.typeArguments !== undefined &&
778+
node.typeArguments.length === 1
779+
) {
780+
const typeArg = node.typeArguments[0];
781+
const evaluatedTypeArg = this.visitType(typeArg, context);
782+
783+
if (evaluatedTypeArg instanceof Reference) {
784+
const decl = evaluatedTypeArg.node;
785+
if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) {
786+
const rawType = decl.type;
787+
if (rawType !== undefined) {
788+
if (ts.isTypeReferenceNode(rawType)) {
789+
const mwpTypeName = ts.isQualifiedName(rawType.typeName)
790+
? rawType.typeName.right
791+
: rawType.typeName;
792+
if (ts.isIdentifier(mwpTypeName) && mwpTypeName.text === 'ModuleWithProviders') {
793+
if (rawType.typeArguments !== undefined && rawType.typeArguments.length === 1) {
794+
const arg = rawType.typeArguments[0];
795+
return this.visitType(arg, context);
796+
}
797+
}
798+
}
799+
}
800+
}
801+
}
745802
}
746803

747-
const declContext: Context = {...context, ...joinModuleContext(context, node, decl)};
748-
return this.visitDeclaration(decl.node, declContext);
804+
// Handle general type reference to a class!
805+
const decl = this.host.getDeclarationOfIdentifier(typeName);
806+
if (decl !== null && this.host.isClass(decl.node)) {
807+
return this.getReference(decl.node, {
808+
...context,
809+
...joinModuleContext(context, typeName, decl),
810+
});
811+
}
812+
813+
return DynamicValue.fromDynamicType(node);
749814
}
750815
}
751816

0 commit comments

Comments
 (0)