angular/packages/compiler-cli/test/ngtsc/declaration_only_emission_spec.ts
Jonathan Meier be7110342b fix(compiler-cli): disallow compiling with the emitDeclarationOnly TS compiler option enabled (#61609)
Running the Angular compiler with declaration-only emission is dangerous
because Angular does not yet support this mode as it relies on the Ivy
compilation which does not run in this mode

In the best case, everything works fine as incidentally there's no
difference in the emitted type declarations (e.g. this is the case for
TS files containing no Angular annotations or only `@Injectable`
annotations).

In the worst case, compilation silently fails in that the compilation
succeeds but the resulting type declarations are missing the Angular
type information and are therefore incomplete. This happens for all
components, directives and modules.

BREAKING CHANGE: The Angular compiler now produces an error when the
the `emitDeclarationOnly` TS compiler option is enabled as this mode is
not supported.

PR Close #61609
2025-08-14 13:03:54 +02:00

659 lines
21 KiB
TypeScript

/**
* @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();
const tsconfigBase = {
extends: '../tsconfig-base.json',
compilerOptions: {
baseUrl: '.',
rootDirs: ['/app'],
emitDeclarationOnly: true,
noCheck: true,
},
};
runInEachFileSystem(() => {
describe('declaration-only emission', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
const tsconfig: {[key: string]: any} = {
...tsconfigBase,
angularCompilerOptions: {
_experimentalAllowEmitDeclarationOnly: true,
},
};
env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
});
it('fails with config diagnostic if experimental flag is not provided', () => {
env.write('tsconfig.json', JSON.stringify(tsconfigBase, null, 2));
env.write('test.ts', '');
const errors = env.driveDiagnostics();
expect(errors.length).toBe(1);
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.CONFIG_EMIT_DECLARATION_ONLY_UNSUPPORTED));
expect(errors[0].messageText).toBe(
'TS compiler option "emitDeclarationOnly" is not supported.',
);
});
it('fails with config diagnostic if experimental flag is disabled', () => {
const tsconfig = {
...tsconfigBase,
angularCompilerOptions: {
_experimentalAllowEmitDeclarationOnly: false,
},
};
env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
env.write('test.ts', '');
const errors = env.driveDiagnostics();
expect(errors.length).toBe(1);
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.CONFIG_EMIT_DECLARATION_ONLY_UNSUPPORTED));
expect(errors[0].messageText).toBe(
'TS compiler option "emitDeclarationOnly" is not supported.',
);
});
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 emit type declarations containing 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 {}
`,
);
env.driveMain();
const dtsContent = env.getContents('test.d.ts');
expect(dtsContent).toContain(
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof i1.Dir; inputs: {}; outputs: {}; }]>;',
);
});
it('should emit type declarations containing 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 {}
`,
);
env.driveMain();
const dtsContent = env.getContents('test.d.ts');
expect(dtsContent).toContain(
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof i1.Dir; inputs: {}; outputs: {}; }]>;',
);
});
it('should emit type declarations containing 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 {}
`,
);
env.driveMain();
const dtsContent = env.getContents('test.d.ts');
expect(dtsContent).toContain(
'static ɵdir: i0.ɵɵDirectiveDeclaration<HostDir, "[host-dir]", never, {}, {}, never, never, true, [{ directive: typeof i1.Dir; inputs: {}; outputs: {}; }]>;',
);
});
it('should emit type declarations containing 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 {}
`,
);
env.driveMain();
const dtsContent = env.getContents('test.d.ts');
expect(dtsContent).toContain(
'static ɵdir: i0.ɵɵDirectiveDeclaration<HostDir, "[host-dir]", never, {}, {}, never, never, true, [{ directive: typeof i1.Dir; inputs: {}; outputs: {}; }]>;',
);
});
it('should show correct error message when using an indirect external reference in a simple host directive on a component', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {Dir} from './dir';
const DirIndirect = Dir;
@Component({
template: '',
selector: 'host-comp',
hostDirectives: [DirIndirect],
})
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(
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
);
});
it('should show correct error message when using an indirect external reference in host directive object on a component', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {Dir} from './dir';
const DirIndirect = Dir;
@Component({
template: '',
selector: 'host-comp',
hostDirectives: [{
directive: DirIndirect,
}],
})
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(
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
);
});
it('should show correct error message when using an indirect external reference in a simple host directive on a directive', () => {
env.write(
'test.ts',
`
import {Directive} from '@angular/core';
import {Dir} from './dir';
const DirIndirect = Dir;
@Directive({
selector: '[host-dir]',
hostDirectives: [DirIndirect],
})
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(
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
);
});
it('should show correct error message when using an indirect external reference in host directive object on a directive', () => {
env.write(
'test.ts',
`
import {Directive} from '@angular/core';
import {Dir} from './dir';
const DirIndirect = Dir;
@Directive({
selector: '[host-dir]',
hostDirectives: [{
directive: DirIndirect,
}],
})
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(
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
);
});
it('should show correct error message when using a property access expression resolving to an indirect external reference in a simple host directive on a component', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {Dir} from './dir';
const DIR = {
Dir
};
@Component({
template: '',
selector: 'host-comp',
hostDirectives: [DIR.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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using a property access expression resolving to an indirect external reference in host directive object on a component', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {Dir} from './dir';
const DIR = {
Dir
};
@Component({
template: '',
selector: 'host-comp',
hostDirectives: [{
directive: DIR.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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using a property access expression resolving to an indirect external reference in a simple host directive on a directive', () => {
env.write(
'test.ts',
`
import {Directive} from '@angular/core';
import {Dir} from './dir';
const DIR = {
Dir
};
@Directive({
selector: '[host-dir]',
hostDirectives: [DIR.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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using a property access expression resolving to an indirect external reference in host directive object on a directive', () => {
env.write(
'test.ts',
`
import {Directive} from '@angular/core';
import {Dir} from './dir';
const DIR = {
Dir
};
@Directive({
selector: '[host-dir]',
hostDirectives: [{
directive: DIR.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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using an expression resovling to an external reference in a simple host directive on a component', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {Dir} from './dir';
const DirArray = [Dir];
@Component({
template: '',
selector: 'host-comp',
hostDirectives: [DirArray[0]],
})
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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using an expression resovling to an external reference in host directive object on a component', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {Dir} from './dir';
const DirArray = [Dir];
@Component({
template: '',
selector: 'host-comp',
hostDirectives: [{
directive: DirArray[0],
}],
})
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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using an expression resovling to an external reference in a simple host directive on a directive', () => {
env.write(
'test.ts',
`
import {Directive} from '@angular/core';
import {Dir} from './dir';
const DirArray = [Dir];
@Directive({
selector: '[host-dir]',
hostDirectives: [DirArray[0]],
})
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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
it('should show correct error message when using an expression resovling to an external reference in host directive object on a directive', () => {
env.write(
'test.ts',
`
import {Directive} from '@angular/core';
import {Dir} from './dir';
const DirArray = [Dir];
@Directive({
selector: '[host-dir]',
hostDirectives: [{
directive: DirArray[0],
}],
})
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(
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
);
});
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.',
);
});
});
});