8000 feat(compiler-cli): add experimental support for fast type declaratio… · angular/angular@e62fb35 · GitHub
[go: up one dir, main page]

Skip to content

Commit e62fb35

Browse files
Jonathan Meieralxhub
Jonathan Meier
authored andcommitted
feat(compiler-cli): add experimental support for fast type declaration emission (#61334)
In declaration-only emission mode, the compiler extracts the type declarations (.d.ts) files without full type-checking, which is possible with sufficient type annotations on exports that can be ensured by the `isolatedDeclarations` TS compiler option. This allows us to decouple type declaration emission from the actual full compilation doing the type-checking, thereby removing the edge between dependent TS files in the build action graph. In other words, the compilation of a TS file no longer indirectly depends on the compilation of all the TS files it imports through its dependency on their type declarations, because the type declarations themselves no longer depend on the compilation of their associated TS file. Without the coupling between type declaration emission and compilation, compilation time of a TS project is no longer bound dependent on the depth of the TS dependency tree as we can now build the entire project with just two entirely parallel phases: 1) emit the type declarations of all TS files in parallel and 2) compile all TS files in parallel. Since the Angular compiler adds static metadata fields to components, directives, modules, pipes and services based on their respective class annotations, it needs to actively partake in the type declaration emission in order to provide the types for these static fields in the declaration. In this change, we add experimental support for a declaration-only emission mode based on the local compilation mode, which already operates without type-checking and access to external type information, i.e. the same environment as is required for declaration-only emisssion. Apart from the same restrictions applied in local compilation mode, there are a few more restrictions imposed on code being compatible with this initial and experimental implementation: * No support for `@NgModule`s using external references. * No support for `hostDirectives` in `@Component`s and `@Directive`s using external references * No support for `@Input` annotations with `transform`. PR Close #61334
1 parent 68d774f commit e62fb35

File tree

37 files changed

+754
-101
lines changed

37 files changed

+754
-101
lines changed

goldens/public-api/compiler-cli/compiler_options.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// @public
88
export interface BazelAndG3Options {
99
annotateForClosureCompiler?: boolean;
10+
_geminiAllowEmitDeclarationOnly?: boolean;
1011
generateDeepReexports?: boolean;
1112
generateExtraImportsInLocalMode?: boolean;
1213
onlyExplicitDeferDependencyImports?: boolean;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export class ComponentDecoratorHandler
279279
private readonly implicitStandaloneValue: boolean,
280280
private readonly typeCheckHostBindings: boolean,
281281
private readonly enableSelectorless: boolean,
282+
private readonly emitDeclarationOnly: boolean,
282283
) {
283284
this.extractTemplateOptions = {
284285
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
@@ -488,6 +489,7 @@ export class ComponentDecoratorHandler
488489
this.elementSchemaRegistry.getDefaultComponentElementName(),
489490
this.strictStandalone,
490491
this.implicitStandaloneValue,
492+
this.emitDeclarationOnly,
491493
);
492494
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Component` has
493495
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning

packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ function setup(
162162
/* implicitStandaloneValue */ true,
163163
/* typeCheckHostBindings */ true,
164164
/* enableSelectorless */ false,
165+
/* emitDeclarationOnly */ false,
165166
);
166167
return {reflectionHost, handler, resourceLoader, metaRegistry};
167168
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export class DirectiveDecoratorHandler
153153
private readonly implicitStandaloneValue: boolean,
154154
private readonly usePoisonedData: boolean,
155155
private readonly typeCheckHostBindings: boolean,
156+
private readonly emitDeclarationOnly: boolean,
156157
) {}
157158

158159
readonly precedence = HandlerPrecedence.PRIMARY;
@@ -209,6 +210,7 @@ export class DirectiveDecoratorHandler
209210
/* defaultSelector */ null,
210211
this.strictStandalone,
211212
this.implicitStandaloneValue,
213+
this.emitDeclarationOnly,
212214
);
213215
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Directive` has
214216
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning

packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export function extractDirectiveMetadata(
126126
defaultSelector: string | null,
127127
strictStandalone: boolean,
128128
implicitStandaloneValue: boolean,
129+
emitDeclarationOnly: boolean,
129130
):
130131
| {
131132
jitForced: false;
@@ -185,6 +186,7 @@ export function extractDirectiveMetadata(
185186
reflector,
186187
refEmitter,
187188
compilationMode,
189+
emitDeclarationOnly,
188190
);
189191
const inputsFromFields = parseInputFields(
190192
clazz,
@@ -197,6 +199,7 @@ export function extractDirectiveMetadata(
197199
compilationMode,
198200
inputsFromMeta,
199201
decorator,
202+
emitDeclarationOnly,
200203
);
201204
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
202205

@@ -394,6 +397,7 @@ export function extractDirectiveMetadata(
394397
evaluator,
395398
compilationMode,
396399
createForwardRefResolver(isCore),
400+
emitDeclarationOnly,
397401
);
398402

399403
if (compilationMode !== CompilationMode.LOCAL && hostDirectives !== null) {
@@ -965,6 +969,7 @@ function parseInputsArray(
965969
reflector: ReflectionHost,
966970
refEmitter: ReferenceEmitter,
967971
compilationMode: CompilationMode,
972+
emitDeclarationOnly: boolean,
968973
): Record<string, InputMapping> {
969974
const inputsField = decoratorMetadata.get('inputs');
970975

@@ -1030,6 +1035,7 @@ function parseInputsArray(
10301035
reflector,
10311036
refEmitter,
10321037
compilationMode,
1038+
emitDeclarationOnly,
10331039
);
10341040
}
10351041

@@ -1080,6 +1086,7 @@ function tryParseInputFieldMapping(
10801086
isCore: boolean,
10811087
refEmitter: ReferenceEmitter,
10821088
compilationMode: CompilationMode,
1089+
emitDeclarationOnly: boolean,
10831090
): InputMapping | null {
10841091
const classPropertyName = member.name;
10851092

@@ -1156,6 +1163,7 @@ function tryParseInputFieldMapping(
11561163
reflector,
11571164
refEmitter,
11581165
compilationMode,
1166+
emitDeclarationOnly,
11591167
);
11601168
}
11611169

@@ -1192,6 +1200,7 @@ function parseInputFields(
11921200
compilationMode: CompilationMode,
11931201
inputsFromClassDecorator: Record<string, InputMapping>,
11941202
classDecorator: Decorator,
1203+
emitDeclarationOnly: boolean,
11951204
): Record<string, InputMapping> {
11961205
const inputs = {} as Record<string, InputMapping>;
11971206

@@ -1206,6 +1215,7 @@ function parseInputFields(
12061215
isCore,
12071216
refEmitter,
12081217
compilationMode,
1218+
emitDeclarationOnly,
12091219
);
12101220
if (inputMapping === null) {
12111221
continue;
@@ -1252,7 +1262,24 @@ export function parseDecoratorInputTransformFunction(
12521262
reflector: ReflectionHost,
12531263
refEmitter: ReferenceEmitter,
12541264
compilationMode: CompilationMode,
1265+
emitDeclarationOnly: boolean,
12551266
): DecoratorInputTransform {
1267+
if (emitDeclarationOnly) {
1268+
const chain: ts.DiagnosticMessageChain = {
1269+
messageText:
1270+
'@Input decorators with a transform function are not supported in experimental declaration-only emission mode',
1271+
category: ts.DiagnosticCategory.Error,
1272+
code: 0,
1273+
next: [
1274+
{
1275+
messageText: `Consider converting '${clazz.name.text}.${classPropertyName}' to an input signal`,
1276+
category: ts.DiagnosticCategory.Message,
1277+
code: 0,
1278+
},
1279+
],
1280+
};
1281+
throw new FatalDiagnosticError(ErrorCode.DECORATOR_UNEXPECTED, value.node, chain);
1282+
}
12561283
// In local compilation mode we can skip type checking the function args. This is because usually
12571284
// the type check is done in a separate build which runs in full compilation mode. So here we skip
12581285
// all the diagnostics.
@@ -1708,6 +1735,7 @@ function extractHostDirectives(
17081735
evaluator: PartialEvaluator,
17091736
compilationMode: CompilationMode,
17101737
forwardRefResolver: ForeignFunctionResolver,
1738+
emitDeclarationOnly: boolean,
17111739
): HostDirectiveMeta[] {
17121740
const resolved = evaluator.evaluate(rawHostDirectives, forwardRefResolver);
17131741
if (!Array.isArray(resolved)) {
@@ -1759,6 +1787,14 @@ function extractHostDirectives(
17591787
);
17601788
}
17611789

1790+
if (emitDeclarationOnly) {
1791+
throw new FatalDiagnosticError(
1792+
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
1793+
hostReference.node,
1794+
'External references in host directives are not supported in experimental declaration-only emission mode',
1795+
);
1796+
}
1797+
17621798
directive = new WrappedNodeExpr(hostReference.node);
17631799
} else if (hostReference instanceof Reference) {
17641800
directive = hostReference as Reference<ClassDeclaration>;

packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ runInEachFileSystem(() => {
237237
/* implicitStandaloneValue */ true,
238238
/* usePoisonedData */ false,
239239
/* typeCheckHostBindings */ true,
240+
/* emitDeclarationOnly */ false,
240241
);
241242

242243
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export class NgModuleDecoratorHandler
287287
private readonly compilationMode: CompilationMode,
288288
private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null,
289289
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
290+
private readonly emitDeclarationOnly: boolean,
290291
) {}
291292

292293
readonly precedence = HandlerPrecedence.PRIMARY;
@@ -354,6 +355,8 @@ export class NgModuleDecoratorHandler
354355
forwardRefResolver,
355356
]);
356357

358+
const allowUnresolvedReferences =
359+
this.compilationMode === CompilationMode.LOCAL && !this.emitDeclarationOnly;
357360
const diagnostics: ts.Diagnostic[] = [];
358361

359362
// Resolving declarations
@@ -367,7 +370,7 @@ export class NgModuleDecoratorHandler
367370
name,
368371
'declarations',
369372
0,
370-
this.compilationMode === CompilationMode.LOCAL,
373+
allowUnresolvedReferences,
371374
).references;
372375

373376
// Look through the declarations to make sure they're all a part of the current compilation.
@@ -403,7 +406,7 @@ export class NgModuleDecoratorHandler
403406
name,
404407
'imports',
405408
0,
406-
this.compilationMode === CompilationMode.LOCAL,
409+
allowUnresolvedReferences,
407410
);
408411

409412
if (
@@ -438,15 +441,15 @@ export class NgModuleDecoratorHandler
438441
name,
439442
'exports',
440443
0,
441-
this.compilationMode === CompilationMode.LOCAL,
444+
allowUnresolvedReferences,
442445
).references;
443446
this.referencesRegistry.add(node, ...exportRefs);
444447
}
445448

446449
// Resolving bootstrap
447450
let bootstrapRefs: Reference<ClassDeclaration>[] = [];
448451
const rawBootstrap: ts.Expression | null = ngModule.get('bootstrap') ?? null;
449-
if (this.compilationMode !== CompilationMode.LOCAL && rawBootstrap !== null) {
452+
if (!allowUnresolvedReferences && rawBootstrap !== null) {
450453
const bootstrapMeta = this.evaluator.evaluate(rawBootstrap, forwardRefResolver);
451454
bootstrapRefs = this.resolveTypeList(
452455
rawBootstrap,
@@ -546,7 +549,7 @@ export class NgModuleDecoratorHandler
546549
const type = wrapTypeReference(this.reflector, node);
547550

548551
let ngModuleMetadata: R3NgModuleMetadata;
549-
if (this.compilationMode === CompilationMode.LOCAL) {
552+
if (allowUnresolvedReferences) {
550553
ngModuleMetadata = {
551554
kind: R3NgModuleMetadataKind.Local,
552555
type,
@@ -602,7 +605,7 @@ export class NgModuleDecoratorHandler
602605
}
603606

604607
const topLevelImports: TopLevelImportedExpression[] = [];
605-
if (this.compilationMode !== CompilationMode.LOCAL && ngModule.has('imports')) {
608+
if (!allowUnresolvedReferences && ngModule.has('imports')) {
606609
const rawImports = unwrapExpression(ngModule.get('imports')!);
607610

608611
let topLevelExpressions: ts.Expression[] = [];
@@ -650,7 +653,7 @@ export class NgModuleDecoratorHandler
650653
imports: [],
651654
};
652655

653-
if (this.compilationMode === CompilationMode.LOCAL) {
656+
if (allowUnresolvedReferences) {
654657
// Adding NgModule's raw imports/exports to the injector's imports field in local compilation
655658
// mode.
656659
for (const exp of [rawImports, rawExports]) {
@@ -1170,6 +1173,17 @@ export class NgModuleDecoratorHandler
11701173
} else if (entry instanceof DynamicValue && allowUnresolvedReferences) {
11711174
dynamicValueSet.add(entry);
11721175
continue;
1176+
} else if (
1177+
this.emitDeclarationOnly &&
1178+
entry instanceof DynamicValue &&
1179+
entry.isFromUnknownIdentifier()
1180+
) {
1181+
throw createValueHasWrongTypeError(
1182+
entry.node,
1183+
entry,
1184+
`Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is an external reference. ` +
1185+
'External references in @NgModule declarations are not supported in experimental declaration-only emission mode',
1186+
);
11731187
} else {
11741188
// TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
11751189
throw createValueHasWrongTypeError(

packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function setup(program: ts.Program, compilationMode = CompilationMode.FULL) {
7777
compilationMode,
7878
/* localCompilationExtraImportsTracker */ null,
7979
jitDeclarationRegistry,
80+
/* emitDeclarationOnly */ false,
8081
);
8182

8283
return {handler, reflectionHost};

packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,20 @@ export interface BazelAndG3Options {
342342
* extra imports are needed for bundling purposes in g3.
343343
*/
344344
generateExtraImportsInLocalMode?: boolean;
345+
346+
/**
347+
* Whether to allow the experimental declaration-only emission mode when the `emitDeclarationOnly`
348+
* TS compiler option is enabled.
349+
*
350+
* The declaration-only emission mode relies on the local compilation mode for fast type
351+
* declaration emission, i.e. emitting `.d.ts` files without type-checking. Certain restrictions
352+
* on supported code constructs apply due to the absence of type information for external
353+
* references.
354+
*
355+
* The mode is experimental and specifically tailored to support fast type declaration emission
356+
* for the Gemini app in g3.
357+
*/
358+
_geminiAllowEmitDeclarationOnly?: boolean;
345359
}
346360

347361
/**

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export class NgCompiler {
393393
private readonly enableHmr: boolean;
394394
private readonly implicitStandaloneValue: boolean;
395395
private readonly enableSelectorless: boolean;
396+
private readonly emitDeclarationOnly: boolean;
396397

397398
/**
398399
* `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each
@@ -466,6 +467,8 @@ export class NgCompiler {
466467
this.enableBlockSyntax = options['_enableBlockSyntax'] ?? true;
467468
this.enableLetSyntax = options['_enableLetSyntax'] ?? true;
468469
this.enableSelectorless = options['_enableSelectorless'] ?? false;
470+
this.emitDeclarationOnly =
471+
!!options.emitDeclarationOnly && !!options._geminiAllowEmitDeclarationOnly;
469472
// Standalone by default is enabled since v19. We need to toggle it here,
470473
// because the language service extension may be running with the latest
471474
// version of the compiler against an older version of Angular.
@@ -826,6 +829,7 @@ export class NgCompiler {
826829
this.delegatingPerfRecorder,
827830
compilation.isCore,
828831
this.closureCompilerEnabled,
832+
this.emitDeclarationOnly,
829833
),
830834
aliasTransformFactory(compilation.traitCompiler.exportStatements),
831835
defaultImportTracker.importPreservingTransformer(),
@@ -869,9 +873,15 @@ export class NgCompiler {
869873
// In local compilation mode we don't make use of .d.ts files for Angular compilation, so their
870874
// transformation can be ditched.
871875
if (
872-
this.options.compilationMode !== 'experimental-local' &&
876+
(this.options.compilationMode !== 'experimental-local' || this.emitDeclarationOnly) &&
873877
compilation.dtsTransforms !== null
874878
) {
879+
// If we are emitting declarations only, the script transformations are skipped by the TS
880+
// compiler, so we have to add them to the afterDeclarations transforms to run their analysis
881+
// because the declaration transform depends on their metadata output.
882+
if (this.emitDeclarationOnly) {
883+
afterDeclarations.push(...before);
884+
}
875885
afterDeclarations.push(
876886
declarationTransformFactory(
877887
compilation.dtsTransforms,
@@ -1286,6 +1296,9 @@ export class NgCompiler {
12861296
break;
12871297
}
12881298
}
1299+
if (this.emitDeclarationOnly) {
1300+
compilationMode = CompilationMode.LOCAL;
1301+
}
12891302

12901303
const checker = this.inputProgram.getTypeChecker();
12911304

@@ -1513,6 +1526,7 @@ export class NgCompiler {
15131526
this.implicitStandaloneValue,
15141527
typeCheckHostBindings,
15151528
this.enableSelectorless,
1529+
this.emitDeclarationOnly,
15161530
),
15171531

15181532
// TODO(alxhub): understand why the cast here is necessary (something to do with `null`
@@ -1541,6 +1555,7 @@ export class NgCompiler {
15411555
this.implicitStandaloneValue,
15421556
this.usePoisonedData,
15431557
typeCheckHostBindings,
1558+
this.emit A4F7 DeclarationOnly,
15441559
) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null, unknown>>,
15451560
// Pipe handler must be before injectable handler in list so pipe factories are printed
15461561
// before injectable factories (so injectable factories can delegate to them)
@@ -1588,6 +1603,7 @@ export class NgCompiler {
15881603
compilationMode,
15891604
localCompilationExtraImportsTracker,
15901605
jitDeclarationRegistry,
1606+
this.emitDeclarationOnly,
15911607
),
15921608
];
15931609

@@ -1601,6 +1617,7 @@ export class NgCompiler {
16011617
dtsTransforms,
16021618
semanticDepGraphUpdater,
16031619
this.adapter,
1620+
this.emitDeclarationOnly,
16041621
);
16051622

16061623
// Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this

0 commit comments

Comments
 (0)
0