diff --git a/goldens/public-api/compiler-cli/compiler_options.api.md b/goldens/public-api/compiler-cli/compiler_options.api.md index 1e28ddd272c..e9d45462f1e 100644 --- a/goldens/public-api/compiler-cli/compiler_options.api.md +++ b/goldens/public-api/compiler-cli/compiler_options.api.md @@ -7,6 +7,7 @@ // @public export interface BazelAndG3Options { annotateForClosureCompiler?: boolean; + _geminiAllowEmitDeclarationOnly?: boolean; generateDeepReexports?: boolean; generateExtraImportsInLocalMode?: boolean; onlyExplicitDeferDependencyImports?: boolean; diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index d9ea9bb162e..401b13e9849 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -279,6 +279,7 @@ export class ComponentDecoratorHandler private readonly implicitStandaloneValue: boolean, private readonly typeCheckHostBindings: boolean, private readonly enableSelectorless: boolean, + private readonly emitDeclarationOnly: boolean, ) { this.extractTemplateOptions = { enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, @@ -488,6 +489,7 @@ export class ComponentDecoratorHandler this.elementSchemaRegistry.getDefaultComponentElementName(), this.strictStandalone, this.implicitStandaloneValue, + this.emitDeclarationOnly, ); // `extractDirectiveMetadata` returns `jitForced = true` when the `@Component` has // set `jit: true`. In this case, compilation of the decorator is skipped. Returning diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts index 6bc0265511e..de4146a0276 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts @@ -162,6 +162,7 @@ function setup( /* implicitStandaloneValue */ true, /* typeCheckHostBindings */ true, /* enableSelectorless */ false, + /* emitDeclarationOnly */ false, ); return {reflectionHost, handler, resourceLoader, metaRegistry}; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index 9833c255e8b..8d12bca7c46 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -153,6 +153,7 @@ export class DirectiveDecoratorHandler private readonly implicitStandaloneValue: boolean, private readonly usePoisonedData: boolean, private readonly typeCheckHostBindings: boolean, + private readonly emitDeclarationOnly: boolean, ) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -209,6 +210,7 @@ export class DirectiveDecoratorHandler /* defaultSelector */ null, this.strictStandalone, this.implicitStandaloneValue, + this.emitDeclarationOnly, ); // `extractDirectiveMetadata` returns `jitForced = true` when the `@Directive` has // set `jit: true`. In this case, compilation of the decorator is skipped. Returning diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts index 67a242c884c..185492dd00d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts @@ -126,6 +126,7 @@ export function extractDirectiveMetadata( defaultSelector: string | null, strictStandalone: boolean, implicitStandaloneValue: boolean, + emitDeclarationOnly: boolean, ): | { jitForced: false; @@ -185,6 +186,7 @@ export function extractDirectiveMetadata( reflector, refEmitter, compilationMode, + emitDeclarationOnly, ); const inputsFromFields = parseInputFields( clazz, @@ -197,6 +199,7 @@ export function extractDirectiveMetadata( compilationMode, inputsFromMeta, decorator, + emitDeclarationOnly, ); const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields}); @@ -394,6 +397,7 @@ export function extractDirectiveMetadata( evaluator, compilationMode, createForwardRefResolver(isCore), + emitDeclarationOnly, ); if (compilationMode !== CompilationMode.LOCAL && hostDirectives !== null) { @@ -965,6 +969,7 @@ function parseInputsArray( reflector: ReflectionHost, refEmitter: ReferenceEmitter, compilationMode: CompilationMode, + emitDeclarationOnly: boolean, ): Record { const inputsField = decoratorMetadata.get('inputs'); @@ -1030,6 +1035,7 @@ function parseInputsArray( reflector, refEmitter, compilationMode, + emitDeclarationOnly, ); } @@ -1080,6 +1086,7 @@ function tryParseInputFieldMapping( isCore: boolean, refEmitter: ReferenceEmitter, compilationMode: CompilationMode, + emitDeclarationOnly: boolean, ): InputMapping | null { const classPropertyName = member.name; @@ -1156,6 +1163,7 @@ function tryParseInputFieldMapping( reflector, refEmitter, compilationMode, + emitDeclarationOnly, ); } @@ -1192,6 +1200,7 @@ function parseInputFields( compilationMode: CompilationMode, inputsFromClassDecorator: Record, classDecorator: Decorator, + emitDeclarationOnly: boolean, ): Record { const inputs = {} as Record; @@ -1206,6 +1215,7 @@ function parseInputFields( isCore, refEmitter, compilationMode, + emitDeclarationOnly, ); if (inputMapping === null) { continue; @@ -1252,7 +1262,24 @@ export function parseDecoratorInputTransformFunction( reflector: ReflectionHost, refEmitter: ReferenceEmitter, compilationMode: CompilationMode, + emitDeclarationOnly: boolean, ): DecoratorInputTransform { + if (emitDeclarationOnly) { + const chain: ts.DiagnosticMessageChain = { + messageText: + '@Input decorators with a transform function are not supported in experimental declaration-only emission mode', + category: ts.DiagnosticCategory.Error, + code: 0, + next: [ + { + messageText: `Consider converting '${clazz.name.text}.${classPropertyName}' to an input signal`, + category: ts.DiagnosticCategory.Message, + code: 0, + }, + ], + }; + throw new FatalDiagnosticError(ErrorCode.DECORATOR_UNEXPECTED, value.node, chain); + } // In local compilation mode we can skip type checking the function args. This is because usually // the type check is done in a separate build which runs in full compilation mode. So here we skip // all the diagnostics. @@ -1708,6 +1735,7 @@ function extractHostDirectives( evaluator: PartialEvaluator, compilationMode: CompilationMode, forwardRefResolver: ForeignFunctionResolver, + emitDeclarationOnly: boolean, ): HostDirectiveMeta[] { const resolved = evaluator.evaluate(rawHostDirectives, forwardRefResolver); if (!Array.isArray(resolved)) { @@ -1759,6 +1787,14 @@ function extractHostDirectives( ); } + if (emitDeclarationOnly) { + throw new FatalDiagnosticError( + ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION, + hostReference.node, + 'External references in host directives are not supported in experimental declaration-only emission mode', + ); + } + directive = new WrappedNodeExpr(hostReference.node); } else if (hostReference instanceof Reference) { directive = hostReference as Reference; diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts index 8941c4b2cd2..41b8d0c00ad 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts @@ -237,6 +237,7 @@ runInEachFileSystem(() => { /* implicitStandaloneValue */ true, /* usePoisonedData */ false, /* typeCheckHostBindings */ true, + /* emitDeclarationOnly */ false, ); const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration); diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts index 673038301ff..e200a4815f2 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts @@ -287,6 +287,7 @@ export class NgModuleDecoratorHandler private readonly compilationMode: CompilationMode, private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null, private readonly jitDeclarationRegistry: JitDeclarationRegistry, + private readonly emitDeclarationOnly: boolean, ) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -354,6 +355,8 @@ export class NgModuleDecoratorHandler forwardRefResolver, ]); + const allowUnresolvedReferences = + this.compilationMode === CompilationMode.LOCAL && !this.emitDeclarationOnly; const diagnostics: ts.Diagnostic[] = []; // Resolving declarations @@ -367,7 +370,7 @@ export class NgModuleDecoratorHandler name, 'declarations', 0, - this.compilationMode === CompilationMode.LOCAL, + allowUnresolvedReferences, ).references; // Look through the declarations to make sure they're all a part of the current compilation. @@ -403,7 +406,7 @@ export class NgModuleDecoratorHandler name, 'imports', 0, - this.compilationMode === CompilationMode.LOCAL, + allowUnresolvedReferences, ); if ( @@ -438,7 +441,7 @@ export class NgModuleDecoratorHandler name, 'exports', 0, - this.compilationMode === CompilationMode.LOCAL, + allowUnresolvedReferences, ).references; this.referencesRegistry.add(node, ...exportRefs); } @@ -446,7 +449,7 @@ export class NgModuleDecoratorHandler // Resolving bootstrap let bootstrapRefs: Reference[] = []; const rawBootstrap: ts.Expression | null = ngModule.get('bootstrap') ?? null; - if (this.compilationMode !== CompilationMode.LOCAL && rawBootstrap !== null) { + if (!allowUnresolvedReferences && rawBootstrap !== null) { const bootstrapMeta = this.evaluator.evaluate(rawBootstrap, forwardRefResolver); bootstrapRefs = this.resolveTypeList( rawBootstrap, @@ -546,7 +549,7 @@ export class NgModuleDecoratorHandler const type = wrapTypeReference(this.reflector, node); let ngModuleMetadata: R3NgModuleMetadata; - if (this.compilationMode === CompilationMode.LOCAL) { + if (allowUnresolvedReferences) { ngModuleMetadata = { kind: R3NgModuleMetadataKind.Local, type, @@ -602,7 +605,7 @@ export class NgModuleDecoratorHandler } const topLevelImports: TopLevelImportedExpression[] = []; - if (this.compilationMode !== CompilationMode.LOCAL && ngModule.has('imports')) { + if (!allowUnresolvedReferences && ngModule.has('imports')) { const rawImports = unwrapExpression(ngModule.get('imports')!); let topLevelExpressions: ts.Expression[] = []; @@ -650,7 +653,7 @@ export class NgModuleDecoratorHandler imports: [], }; - if (this.compilationMode === CompilationMode.LOCAL) { + if (allowUnresolvedReferences) { // Adding NgModule's raw imports/exports to the injector's imports field in local compilation // mode. for (const exp of [rawImports, rawExports]) { @@ -1170,6 +1173,17 @@ export class NgModuleDecoratorHandler } else if (entry instanceof DynamicValue && allowUnresolvedReferences) { dynamicValueSet.add(entry); continue; + } else if ( + this.emitDeclarationOnly && + entry instanceof DynamicValue && + entry.isFromUnknownIdentifier() + ) { + throw createValueHasWrongTypeError( + entry.node, + entry, + `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is an external reference. ` + + 'External references in @NgModule declarations are not supported in experimental declaration-only emission mode', + ); } else { // TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array. throw createValueHasWrongTypeError( diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts index 8ee71d759da..1d611c1726c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts @@ -77,6 +77,7 @@ function setup(program: ts.Program, compilationMode = CompilationMode.FULL) { compilationMode, /* localCompilationExtraImportsTracker */ null, jitDeclarationRegistry, + /* emitDeclarationOnly */ false, ); return {handler, reflectionHost}; diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts b/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts index 29ebdbb0fe5..bad6c20aebc 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts @@ -342,6 +342,20 @@ export interface BazelAndG3Options { * extra imports are needed for bundling purposes in g3. */ generateExtraImportsInLocalMode?: boolean; + + /** + * Whether to allow the experimental declaration-only emission mode when the `emitDeclarationOnly` + * TS compiler option is enabled. + * + * The declaration-only emission mode relies on the local compilation mode for fast type + * declaration emission, i.e. emitting `.d.ts` files without type-checking. Certain restrictions + * on supported code constructs apply due to the absence of type information for external + * references. + * + * The mode is experimental and specifically tailored to support fast type declaration emission + * for the Gemini app in g3. + */ + _geminiAllowEmitDeclarationOnly?: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index ba09feb870b..cc5a375dbce 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -393,6 +393,7 @@ export class NgCompiler { private readonly enableHmr: boolean; private readonly implicitStandaloneValue: boolean; private readonly enableSelectorless: boolean; + private readonly emitDeclarationOnly: boolean; /** * `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each @@ -466,6 +467,8 @@ export class NgCompiler { this.enableBlockSyntax = options['_enableBlockSyntax'] ?? true; this.enableLetSyntax = options['_enableLetSyntax'] ?? true; this.enableSelectorless = options['_enableSelectorless'] ?? false; + this.emitDeclarationOnly = + !!options.emitDeclarationOnly && !!options._geminiAllowEmitDeclarationOnly; // Standalone by default is enabled since v19. We need to toggle it here, // because the language service extension may be running with the latest // version of the compiler against an older version of Angular. @@ -826,6 +829,7 @@ export class NgCompiler { this.delegatingPerfRecorder, compilation.isCore, this.closureCompilerEnabled, + this.emitDeclarationOnly, ), aliasTransformFactory(compilation.traitCompiler.exportStatements), defaultImportTracker.importPreservingTransformer(), @@ -869,9 +873,15 @@ export class NgCompiler { // In local compilation mode we don't make use of .d.ts files for Angular compilation, so their // transformation can be ditched. if ( - this.options.compilationMode !== 'experimental-local' && + (this.options.compilationMode !== 'experimental-local' || this.emitDeclarationOnly) && compilation.dtsTransforms !== null ) { + // If we are emitting declarations only, the script transformations are skipped by the TS + // compiler, so we have to add them to the afterDeclarations transforms to run their analysis + // because the declaration transform depends on their metadata output. + if (this.emitDeclarationOnly) { + afterDeclarations.push(...before); + } afterDeclarations.push( declarationTransformFactory( compilation.dtsTransforms, @@ -1286,6 +1296,9 @@ export class NgCompiler { break; } } + if (this.emitDeclarationOnly) { + compilationMode = CompilationMode.LOCAL; + } const checker = this.inputProgram.getTypeChecker(); @@ -1513,6 +1526,7 @@ export class NgCompiler { this.implicitStandaloneValue, typeCheckHostBindings, this.enableSelectorless, + this.emitDeclarationOnly, ), // TODO(alxhub): understand why the cast here is necessary (something to do with `null` @@ -1541,6 +1555,7 @@ export class NgCompiler { this.implicitStandaloneValue, this.usePoisonedData, typeCheckHostBindings, + this.emitDeclarationOnly, ) as Readonly>, // Pipe handler must be before injectable handler in list so pipe factories are printed // before injectable factories (so injectable factories can delegate to them) @@ -1588,6 +1603,7 @@ export class NgCompiler { compilationMode, localCompilationExtraImportsTracker, jitDeclarationRegistry, + this.emitDeclarationOnly, ), ]; @@ -1601,6 +1617,7 @@ export class NgCompiler { dtsTransforms, semanticDepGraphUpdater, this.adapter, + this.emitDeclarationOnly, ); // Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index b9d3a2f72ec..d0616d55cba 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -118,6 +118,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { private dtsTransforms: DtsTransformRegistry, private semanticDepGraphUpdater: SemanticDepGraphUpdater | null, private sourceFileTypeIdentifier: SourceFileTypeIdentifier, + private emitDeclarationOnly: boolean, ) { for (const handler of handlers) { this.handlersByName.set(handler.name, handler); @@ -374,14 +375,16 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { ) { // Custom decorators found in local compilation mode! In this mode we don't support custom // decorators yet. But will eventually do (b/320536434). For now a temporary error is thrown. + const compilationModeName = this.emitDeclarationOnly + ? 'experimental declaration-only emission' + : 'local compilation'; record.metaDiagnostics = [...nonNgDecoratorsInLocalMode].map((decorator) => ({ category: ts.DiagnosticCategory.Error, code: Number('-99' + ErrorCode.DECORATOR_UNEXPECTED), file: getSourceFile(clazz), start: decorator.node.getStart(), length: decorator.node.getWidth(), - messageText: - 'In local compilation mode, Angular does not support custom decorators. Ensure all class decorators are from Angular.', + messageText: `In ${compilationModeName} mode, Angular does not support custom decorators. Ensure all class decorators are from Angular.`, })); record.traits = foundTraits = []; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 64f6ac38668..ab1dd4d095f 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -52,6 +52,7 @@ export function ivyTransformFactory( perf: PerfRecorder, isCore: boolean, isClosureCompilerEnabled: boolean, + emitDeclarationOnly: boolean, ): ts.TransformerFactory { const recordWrappedNode = createRecorderFn(defaultImportTracker); return (context: ts.TransformationContext): ts.Transformer => { @@ -66,6 +67,7 @@ export function ivyTransformFactory( file, isCore, isClosureCompilerEnabled, + emitDeclarationOnly, recordWrappedNode, ), ); @@ -368,6 +370,7 @@ function transformIvySourceFile( file: ts.SourceFile, isCore: boolean, isClosureCompilerEnabled: boolean, + emitDeclarationOnly: boolean, recordWrappedNode: RecordWrappedNodeFn, ): ts.SourceFile { const constantPool = new ConstantPool(isClosureCompilerEnabled); @@ -390,6 +393,11 @@ function transformIvySourceFile( const compilationVisitor = new IvyCompilationVisitor(compilation, constantPool); visit(file, compilationVisitor, context); + // If we are emitting declarations only, we can skip the script transforms. + if (emitDeclarationOnly) { + return file; + } + // Step 2. Scan through the AST again and perform transformations based on Ivy compilation // results obtained at Step 1. const transformationVisitor = new IvyTransformationVisitor( diff --git a/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts b/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts index c9d6b0c822e..a5136660784 100644 --- a/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts +++ b/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts @@ -62,6 +62,7 @@ runInEachFileSystem(() => { new DtsTransformRegistry(), null, fakeSfTypeIdentifier, + /* emitDeclarationOnly */ false, ); const sourceFile = program.getSourceFile(filename)!; diff --git a/packages/compiler-cli/test/compliance/README.md b/packages/compiler-cli/test/compliance/README.md index f9b03d18950..335fef79188 100644 --- a/packages/compiler-cli/test/compliance/README.md +++ b/packages/compiler-cli/test/compliance/README.md @@ -2,13 +2,17 @@ This directory contains rules, helpers and test-cases for the Angular compiler compliance tests. -There are three different types of tests that are run based on file-based "test-cases". +There are five different types of tests that are run based on file-based "test-cases". * **Full compile** - in this test the source files defined by the test-case are fully compiled by Angular. The generated files are compared to "expected files" via a matching algorithm that is tolerant to whitespace and variable name changes. * **Local compile** - in this test the source files defined by the test-case are compiled in local mode by Angular. The generated files are compared to "expected files" via a matching algorithm similar to the case of "full compile" +* **Declaration-only emit** - in this test the source files defined by the test-case are compiled in full mode by + Angular. The same source files are then compiled again in declaration-only emission mode and the resulting type + declarations (.d.ts files) are compared to the type declarations produced by the full compilation checking for exact + matches. * **Partial compile** - in this test the source files defined by the test-case are "partially" compiled by Angular to produce files that can be published. These partially compiled files are compared directly against "golden files" to ensure that we do not inadvertently break the public API of partial diff --git a/packages/compiler-cli/test/compliance/declaration-only/BUILD.bazel b/packages/compiler-cli/test/compliance/declaration-only/BUILD.bazel new file mode 100644 index 00000000000..fb5082975c3 --- /dev/null +++ b/packages/compiler-cli/test/compliance/declaration-only/BUILD.bazel @@ -0,0 +1,24 @@ +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") + +ts_library( + name = "test_lib", + testonly = True, + srcs = ["declaration_only_emit_spec.ts"], + deps = [ + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/test/compliance/test_helpers", + ], +) + +jasmine_node_test( + name = "declaration-only", + bootstrap = ["//tools/testing:node_no_angular"], + data = [ + "//packages/compiler-cli/test/compliance/test_cases", + "//packages/core:npm_package", + ], + shard_count = 2, + deps = [ + ":test_lib", + ], +) diff --git a/packages/compiler-cli/test/compliance/declaration-only/declaration_only_emit_spec.ts b/packages/compiler-cli/test/compliance/declaration-only/declaration_only_emit_spec.ts new file mode 100644 index 00000000000..19f6144b95e --- /dev/null +++ b/packages/compiler-cli/test/compliance/declaration-only/declaration_only_emit_spec.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import {FileSystem} from '../../../src/ngtsc/file_system'; +import {getReferenceFileForTypeDeclaration} from '../test_helpers/check_type_declarations'; +import {CompileResult, compileTest} from '../test_helpers/compile_test'; +import {ComplianceTest} from '../test_helpers/get_compliance_tests'; +import {runTests} from '../test_helpers/test_runner'; + +runTests('declaration-only emit', emitDeclarationOnlyTest, {emitDeclarationOnly: true}); + +/** + * Compile all the input files in the given `test` in full compilation mode and compare the emitted + * type declarations with the declarations produced by declaration-only emission. + * + * @param fs The mock file-system where the input files can be found. + * @param test The compliance test whose input files should be compiled. + */ +function emitDeclarationOnlyTest(fs: FileSystem, test: ComplianceTest): CompileResult { + const {emittedFiles} = compileTest( + fs, + test.inputFiles, + test.compilerOptions, + test.angularCompilerOptions, + ); + const emittedTypeDeclarations = emittedFiles.filter((file) => file.endsWith('.d.ts')); + for (const emittedTypeDeclaration of emittedTypeDeclarations) { + fs.moveFile( + emittedTypeDeclaration, + getReferenceFileForTypeDeclaration(fs, emittedTypeDeclaration), + ); + } + return compileTest( + fs, + test.inputFiles, + { + ...test.compilerOptions, + emitDeclarationOnly: true, + }, + { + ...test.angularCompilerOptions, + _geminiAllowEmitDeclarationOnly: true, + }, + ); +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/class_metadata/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/class_metadata/TEST_CASES.json index 7d1344cddf6..adcad424b2f 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/class_metadata/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/class_metadata/TEST_CASES.json @@ -13,6 +13,10 @@ "class_decorators.js" ] } + ], + "compilationModeFilter": [ + "full compile", + "linked compile" ] }, { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json index 09262616546..6be669eeead 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json @@ -213,7 +213,8 @@ ], "compilationModeFilter": [ "full compile", - "local compile" + "local compile", + "declaration-only emit" ] } ] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json index 1ba426e1adc..81bd37b7813 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json @@ -16,7 +16,8 @@ ], "compilationModeFilter": [ "full compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json index a445b132d49..b6f45d6899e 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json @@ -60,7 +60,11 @@ ] } ], - "compilationModeFilter": ["full compile", "local compile"] + "compilationModeFilter": [ + "full compile", + "local compile", + "declaration-only emit" + ] }, { "description": "should support complex selectors", diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/TEST_CASES.json index c72512a363b..52b134b7405 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/TEST_CASES.json @@ -15,7 +15,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ] }, { @@ -33,7 +34,8 @@ ], "compilationModeFilter": [ "full compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { @@ -64,7 +66,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "angularCompilerOptions": { "linkerJitMode": true @@ -86,7 +89,8 @@ "compilationModeFilter": [ "full compile", "linked compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { @@ -104,7 +108,8 @@ ], "compilationModeFilter": [ "full compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { @@ -121,7 +126,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "angularCompilerOptions": { "linkerJitMode": true @@ -143,7 +149,8 @@ "compilationModeFilter": [ "full compile", "linked compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { @@ -162,7 +169,8 @@ "compilationModeFilter": [ "full compile", "linked compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { @@ -179,7 +187,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "angularCompilerOptions": { "linkerJitMode": true @@ -201,7 +210,8 @@ "compilationModeFilter": [ "full compile", "linked compile", - "local compile" + "local compile", + "declaration-only emit" ] }, { @@ -220,7 +230,8 @@ "compilationModeFilter": [ "full compile", "linked compile", - "local compile" + "local compile", + "declaration-only emit" ] } ] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/es5_support/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/es5_support/TEST_CASES.json index b55b5610880..84cc1a23d2d 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/es5_support/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/es5_support/TEST_CASES.json @@ -7,7 +7,8 @@ "target": "ES5" }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/line_ending_normalization/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/line_ending_normalization/TEST_CASES.json index d91df4e2f65..3e5c45518af 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/line_ending_normalization/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/line_ending_normalization/TEST_CASES.json @@ -107,7 +107,8 @@ "enableI18nLegacyMessageIdFormat": true }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { @@ -134,7 +135,8 @@ "enableI18nLegacyMessageIdFormat": true }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { @@ -161,7 +163,8 @@ "enableI18nLegacyMessageIdFormat": true }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { @@ -182,7 +185,8 @@ "enableI18nLegacyMessageIdFormat": true }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/localize_legacy_message_ids/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/localize_legacy_message_ids/TEST_CASES.json index 4e88947a539..3201f03eba0 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/localize_legacy_message_ids/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/localize_legacy_message_ids/TEST_CASES.json @@ -10,7 +10,8 @@ "enableI18nLegacyMessageIdFormat": true }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/ng-container_ng-template/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/ng-container_ng-template/TEST_CASES.json index accc9d30ca7..eafeffed8a1 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/ng-container_ng-template/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/ng-container_ng-template/TEST_CASES.json @@ -115,6 +115,10 @@ "verifyUniqueConsts" ] } + ], + "compilationModeFilter": [ + "full compile", + "linked compile" ] }, { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_input_outputs/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_input_outputs/TEST_CASES.json index 51ccf85bc7e..73d514d206b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_input_outputs/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_input_outputs/TEST_CASES.json @@ -50,6 +50,10 @@ ], "failureMessage": "Incorrect directive definition" } + ], + "compilationModeFilter": [ + "full compile", + "linked compile" ] } ] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/TEST_CASES.json index 94eff3b4f91..d533fb25dfe 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/TEST_CASES.json @@ -66,7 +66,8 @@ "externalRuntimeStyles": true }, "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "expectations": [ { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/invalid/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/invalid/TEST_CASES.json index e8308fdefc7..87f2d5bc161 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/invalid/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/invalid/TEST_CASES.json @@ -13,7 +13,10 @@ ] } ], - "compilationModeFilter": ["full compile"] + "compilationModeFilter": [ + "full compile", + "declaration-only emit" + ] } ] } diff --git a/packages/compiler-cli/test/compliance/test_cases/signal_inputs/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/signal_inputs/TEST_CASES.json index 4080bbfb66d..86df9bd374a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/signal_inputs/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/signal_inputs/TEST_CASES.json @@ -44,6 +44,10 @@ ], "failureMessage": "Incorrect definition" } + ], + "compilationModeFilter": [ + "full compile", + "linked compile" ] }, { diff --git a/packages/compiler-cli/test/compliance/test_cases/source_mapping/external_templates/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/source_mapping/external_templates/TEST_CASES.json index 8e03cd94c2b..3aeabea9643 100644 --- a/packages/compiler-cli/test/compliance/test_cases/source_mapping/external_templates/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/source_mapping/external_templates/TEST_CASES.json @@ -7,7 +7,8 @@ "external_template.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true, @@ -30,7 +31,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true, @@ -43,7 +45,8 @@ "extra_root_dir.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true, @@ -60,7 +63,8 @@ "extra_root_dir.ts" ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "expectations": [ { @@ -87,7 +91,8 @@ "escaped_chars.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true, @@ -110,7 +115,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true, diff --git a/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/TEST_CASES.json index 37a0a2071b6..d715c116c84 100644 --- a/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/TEST_CASES.json @@ -7,7 +7,8 @@ "simple_element.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -29,7 +30,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -41,7 +43,8 @@ "void_element.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -63,7 +66,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -75,7 +79,8 @@ "interpolation_basic.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -97,7 +102,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -109,7 +115,8 @@ "interpolation_complex.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -131,7 +138,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -143,7 +151,8 @@ "interpolation_properties.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -165,7 +174,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -177,7 +187,8 @@ "interpolation_with_pipe.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -199,7 +210,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -211,7 +223,8 @@ "input_binding_simple.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -233,7 +246,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -245,7 +259,8 @@ "input_binding_complex.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -267,7 +282,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -279,7 +295,8 @@ "input_binding_longhand.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -301,7 +318,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -313,7 +331,8 @@ "output_binding_simple.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -335,7 +354,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -347,7 +367,8 @@ "output_binding_complex.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -369,7 +390,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -381,7 +403,8 @@ "output_binding_longhand.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -403,7 +426,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -415,7 +439,8 @@ "two_way_binding_simple.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -437,7 +462,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -449,7 +475,8 @@ "two_way_binding_longhand.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -471,7 +498,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -483,7 +511,8 @@ "input_binding_class.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -505,7 +534,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -517,7 +547,8 @@ "ng_if_simple.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -539,7 +570,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -551,7 +583,8 @@ "ng_if_templated.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -573,7 +606,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -585,7 +619,8 @@ "ng_for_simple.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -608,7 +643,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -620,7 +656,8 @@ "ng_for_templated.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -642,7 +679,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -654,7 +692,8 @@ "projection.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -676,7 +715,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -688,7 +728,8 @@ "i18n_message_simple.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -710,7 +751,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -722,7 +764,8 @@ "i18n_message_placeholder.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -744,7 +787,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -756,7 +800,8 @@ "i18n_message_placeholder_entities.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -778,7 +823,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -800,7 +846,8 @@ } ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -823,7 +870,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -835,7 +883,8 @@ "i18n_message_element_whitespace.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -857,7 +906,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -869,7 +919,8 @@ "i18n_message_container_tag.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -891,7 +942,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -903,7 +955,8 @@ "update_mode.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -925,7 +978,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -937,7 +991,8 @@ "escape_sequences.ts" ], "compilationModeFilter": [ - "full compile" + "full compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true @@ -959,7 +1014,8 @@ } ], "compilationModeFilter": [ - "linked compile" + "linked compile", + "declaration-only emit" ], "compilerOptions": { "sourceMap": true diff --git a/packages/compiler-cli/test/compliance/test_cases/test_case_schema.json b/packages/compiler-cli/test/compliance/test_cases/test_case_schema.json index 04f0ca988a1..a9a8a7e8587 100644 --- a/packages/compiler-cli/test/compliance/test_cases/test_case_schema.json +++ b/packages/compiler-cli/test/compliance/test_cases/test_case_schema.json @@ -23,9 +23,18 @@ "type": "array", "items": { "type": "string", - "enum": ["full compile", "linked compile", "local compile"] + "enum": [ + "full compile", + "linked compile", + "local compile", + "declaration-only emit" + ] }, - "default": ["full compile", "linked compile"] + "default": [ + "full compile", + "linked compile", + "declaration-only emit" + ] }, "inputFiles": { "title": "A collection of source files to compile", diff --git a/packages/compiler-cli/test/compliance/test_helpers/check_type_declarations.ts b/packages/compiler-cli/test/compliance/test_helpers/check_type_declarations.ts new file mode 100644 index 00000000000..aab68a045fa --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_helpers/check_type_declarations.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; + +/** + * Check that the content of each emitted type declaration file matches the content of their + * associated reference file. The content of the files is in the filesystem. The reference file is + * defined by the {@link getReferenceFileForTypeDeclaration} function. + * + * @param fs The mock file-system where the files can be found. + * @param emittedFiles The list of type declaration files to check. + */ +export function checkTypeDeclarations(fs: FileSystem, emittedFiles: AbsoluteFsPath[]) { + const emittedDeclarationFiles = emittedFiles.filter((file) => file.endsWith('.d.ts')); + if (!emittedDeclarationFiles.length) { + throw new Error('No type declarations emitted.'); + } + const diff = emittedDeclarationFiles + .map((file) => ({ + expectedFilename: getReferenceFileForTypeDeclaration(fs, file), + generatedFilename: file, + })) + .map(({expectedFilename, generatedFilename}) => ({ + expectedFile: {name: expectedFilename, content: fs.readFile(expectedFilename)}, + generatedFile: {name: generatedFilename, content: fs.readFile(generatedFilename)}, + })) + .find(({expectedFile, generatedFile}) => expectedFile.content !== generatedFile.content); + + if (diff) { + throw new Error( + [ + `Type declarations don't match between full compilation and declaration-only emission\n\n`, + `/** FULL COMPILATION: ${diff.expectedFile.name} **/\n`, + `${diff.expectedFile.content}\n\n`, + `/** DECLARATION-ONLY EMISSION: ${diff.generatedFile.name} */\n`, + `${diff.generatedFile.content}\n`, + ].join(''), + ); + } +} + +/** + * Adds a '.ref' pre-extension to a type declaration file representing its reference file. + * + * Example: my_declarations.d.ts -> my_declarations.ref.d.ts + * + * @param fs The file-system used for file name and path manipulation. + * @param file The path of the original type declaration file. + * @returns The path of the reference type declaration file. + */ +export function getReferenceFileForTypeDeclaration(fs: FileSystem, file: AbsoluteFsPath) { + return fs.join(fs.dirname(file), fs.basename(file, 'd.ts') + '.ref.d.ts'); +} diff --git a/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts b/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts index 7890fd72555..49c4eebc844 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts @@ -49,7 +49,7 @@ export function* getComplianceTests(absTestConfigPath: AbsoluteFsPath): Generato test, 'compilationModeFilter', realTestPath, - ['linked compile', 'full compile'], + ['linked compile', 'full compile', 'declaration-only emit'], ) as CompilationMode[]; yield { @@ -292,7 +292,11 @@ export interface ComplianceTest { excludeTest?: boolean; } -export type CompilationMode = 'linked compile' | 'full compile' | 'local compile'; +export type CompilationMode = + | 'linked compile' + | 'full compile' + | 'local compile' + | 'declaration-only emit'; export interface Expectation { /** The message to display if this expectation fails. */ diff --git a/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts b/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts index aa57976af7c..5cb37b573be 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts @@ -9,6 +9,7 @@ import {FileSystem} from '../../../src/ngtsc/file_system'; import {checkErrors, checkNoUnexpectedErrors} from './check_errors'; import {checkExpectations} from './check_expectations'; +import {checkTypeDeclarations} from './check_type_declarations'; import {CompileResult, initMockTestFileSystem} from './compile_test'; import { CompilationMode, @@ -47,7 +48,11 @@ function getFilenameForLocalCompilation(fileName: string): string { export function runTests( type: CompilationMode, compileFn: (fs: FileSystem, test: ComplianceTest) => CompileResult, - options: {isLocalCompilation?: boolean; skipMappingChecks?: boolean} = {}, + options: { + isLocalCompilation?: boolean; + emitDeclarationOnly?: boolean; + skipMappingChecks?: boolean; + } = {}, ) { describe(`compliance tests (${type})`, () => { for (const test of getAllComplianceTests()) { @@ -69,7 +74,7 @@ export function runTests( } const fs = initMockTestFileSystem(test.realTestPath); - const {errors} = compileFn(fs, test); + const {errors, emittedFiles} = compileFn(fs, test); for (const expectation of test.expectations) { transformExpectation(expectation, !!options.isLocalCompilation); if (expectation.expectedErrors.length > 0) { @@ -79,6 +84,9 @@ export function runTests( expectation.expectedErrors, errors, ); + } else if (!!options.emitDeclarationOnly) { + checkNoUnexpectedErrors(test.relativePath, errors); + checkTypeDeclarations(fs, emittedFiles); } else { checkNoUnexpectedErrors(test.relativePath, errors); checkExpectations( diff --git a/packages/compiler-cli/test/ngtsc/declaration_only_emission_spec.ts b/packages/compiler-cli/test/ngtsc/declaration_only_emission_spec.ts new file mode 100644 index 00000000000..6aabb0e50fd --- /dev/null +++ b/packages/compiler-cli/test/ngtsc/declaration_only_emission_spec.ts @@ -0,0 +1,289 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import ts from 'typescript'; + +import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; +import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; +import {loadStandardTestFiles} from '../../src/ngtsc/testing'; + +import {NgtscTestEnvironment} from './env'; + +const testFiles = loadStandardTestFiles(); + +runInEachFileSystem(() => { + describe('declaration-only emission', () => { + let env!: NgtscTestEnvironment; + + beforeEach(() => { + env = NgtscTestEnvironment.setup(testFiles); + const tsconfig: {[key: string]: any} = { + extends: '../tsconfig-base.json', + compilerOptions: { + baseUrl: '.', + rootDirs: ['/app'], + emitDeclarationOnly: true, + noCheck: true, + }, + angularCompilerOptions: { + _geminiAllowEmitDeclarationOnly: true, + }, + }; + env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2)); + }); + + it('should show correct error message when using an @NgModule with an external reference in declarations', () => { + env.write( + 'test.ts', + ` + import {NgModule} from '@angular/core'; + import {Comp} from './comp'; + + @NgModule({ + declarations: [Comp], + }) + export class CompModule {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toContain( + 'Value at position 0 in the NgModule.declarations of CompModule is an external reference. ' + + 'External references in @NgModule declarations are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an @NgModule with an external reference in imports', () => { + env.write( + 'test.ts', + ` + import {NgModule} from '@angular/core'; + import {Comp} from './comp'; + + @NgModule({ + imports: [Comp], + }) + export class CompModule {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toContain( + 'Value at position 0 in the NgModule.imports of CompModule is an external reference. ' + + 'External references in @NgModule declarations are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an @NgModule with an external reference in exports', () => { + env.write( + 'test.ts', + ` + import {NgModule} from '@angular/core'; + import {Comp} from './comp'; + + @NgModule({ + exports: [Comp], + }) + export class CompModule {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toContain( + 'Value at position 0 in the NgModule.exports of CompModule is an external reference. ' + + 'External references in @NgModule declarations are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an @NgModule with an external reference in bootstrap', () => { + env.write( + 'test.ts', + ` + import {NgModule} from '@angular/core'; + import {Comp} from './comp'; + + @NgModule({ + bootstrap: [Comp], + }) + export class CompModule {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toContain( + 'Value at position 0 in the NgModule.bootstrap of CompModule is an external reference. ' + + 'External references in @NgModule declarations are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an external reference in simple host directive on a component', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + import {Dir} from './dir'; + + @Component({ + template: '', + selector: 'host-comp', + hostDirectives: [Dir], + }) + export class HostComp {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + 'External references in host directives are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an external reference in host directive object on a component', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + import {Dir} from './dir'; + + @Component({ + template: '', + selector: 'host-comp', + hostDirectives: [{ + directive: Dir, + }], + }) + export class HostComp {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + 'External references in host directives are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an external reference in simple host directive on a directive', () => { + env.write( + 'test.ts', + ` + import {Directive} from '@angular/core'; + import {Dir} from './dir'; + + @Directive({ + selector: '[host-dir]', + hostDirectives: [Dir], + }) + export class HostDir {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + 'External references in host directives are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an external reference in host directive object on a directive', () => { + env.write( + 'test.ts', + ` + import {Directive} from '@angular/core'; + import {Dir} from './dir'; + + @Directive({ + selector: '[host-dir]', + hostDirectives: [{ + directive: Dir, + }], + }) + export class HostDir {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + 'External references in host directives are not supported in experimental declaration-only emission mode', + ); + }); + + it('should show correct error message when using an @Input decorator with a transform function', () => { + env.write( + 'test.ts', + ` + import {booleanAttribute, Component, Input} from '@angular/core'; + + @Component({template: '', selector: 'comp'}) + export class Comp { + @Input({ transform: booleanAttribute }) decoratedInput!: boolean; + } + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED)); + const errorMessage = ts.flattenDiagnosticMessageText(errors[0].messageText, '\n'); + expect(errorMessage).toContain( + '@Input decorators with a transform function are not supported in experimental declaration-only emission mode', + ); + expect(errorMessage).toContain( + `Consider converting 'Comp.decoratedInput' to an input signal`, + ); + }); + + it('should show correct error message when using custom decorators', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + + export function Custom() { + return function(target: any) {}; + } + + @Custom() + @Component({template: '', selector: 'comp'}) + export class Comp {} + `, + ); + + const errors = env.driveDiagnostics(); + + expect(errors.length).toBe(1); + expect(errors[0].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED)); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + 'In experimental declaration-only emission mode, Angular does not support custom decorators. Ensure all class decorators are from Angular.', + ); + }); + }); +}); diff --git a/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts b/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts index e653dd38c04..eb702b003c8 100644 --- a/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts +++ b/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts @@ -210,6 +210,7 @@ function parseTransformOfInput( reflector, noopRefEmitter, CompilationMode.FULL, + /* emitDeclarationOnly */ false, ); } catch (e: unknown) { if (!(e instanceof FatalDiagnosticError)) {