mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Since standalone is the default, we can dropn the `standalone: true` flags from our tests. PR Close #63963
1195 lines
36 KiB
TypeScript
1195 lines
36 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 {
|
|
diagnosticToNode,
|
|
getSourceCodeForDiagnostic,
|
|
loadStandardTestFiles,
|
|
} from '../../src/ngtsc/testing';
|
|
|
|
import {NgtscTestEnvironment} from './env';
|
|
|
|
const testFiles = loadStandardTestFiles();
|
|
|
|
runInEachFileSystem(() => {
|
|
describe('ngtsc compilation of standalone types', () => {
|
|
let env!: NgtscTestEnvironment;
|
|
|
|
beforeEach(() => {
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
env.tsconfig({strictTemplates: true});
|
|
});
|
|
|
|
describe('component-side', () => {
|
|
it('should compile a basic standalone component', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
})
|
|
export class TestDir {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [TestDir],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('dependencies: [TestDir]');
|
|
|
|
const dtsCode = env.getContents('test.d.ts');
|
|
expect(dtsCode).toContain(
|
|
'i0.ɵɵDirectiveDeclaration<TestDir, "[dir]", never, {}, {}, never, never, true, never>;',
|
|
);
|
|
expect(dtsCode).toContain(
|
|
'i0.ɵɵComponentDeclaration<TestCmp, "test-cmp", never, {}, {}, never, never, true, never>;',
|
|
);
|
|
});
|
|
|
|
it('should compile a recursive standalone component', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<test-cmp></test-cmp>',
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('dependencies: [TestCmp]');
|
|
});
|
|
|
|
it('should compile a basic standalone pipe', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Pipe} from '@angular/core';
|
|
|
|
@Pipe({name: 'test'})
|
|
export class TestPipe {
|
|
transform(value: any): any {}
|
|
}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
const jsCode = env.getContents('test.js');
|
|
|
|
const dtsCode = env.getContents('test.d.ts');
|
|
expect(dtsCode).toContain('i0.ɵɵPipeDeclaration<TestPipe, "test", true>');
|
|
});
|
|
|
|
it('should use existing imports for dependencies', () => {
|
|
env.write(
|
|
'dep.ts',
|
|
`
|
|
import {Directive} from '@angular/core';
|
|
|
|
@Directive({selector: '[dir]'})
|
|
export class TestDir {}
|
|
`,
|
|
);
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {TestDir} from './dep';
|
|
|
|
@Component({
|
|
imports: [TestDir],
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
expect(jsContents).toContain('dependencies: [TestDir]');
|
|
});
|
|
|
|
it('should compile a standalone component even in the presence of cycles', () => {
|
|
env.write(
|
|
'dep.ts',
|
|
`
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
// This import creates a cycle, since 'test.ts' imports 'dir.ts'.
|
|
import {TestType} from './test';
|
|
|
|
@Directive({selector: '[dir]'})
|
|
export class TestDir {
|
|
@Input() value?: TestType;
|
|
}
|
|
`,
|
|
);
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {TestDir} from './dep';
|
|
|
|
export interface TestType {
|
|
value: string;
|
|
}
|
|
|
|
@Component({
|
|
imports: [TestDir],
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
expect(jsContents).toContain('dependencies: [TestDir]');
|
|
});
|
|
|
|
it('should compile a standalone component in a cycle with its module', () => {
|
|
env.write(
|
|
'module.ts',
|
|
`
|
|
import {Component, NgModule, forwardRef} from '@angular/core';
|
|
|
|
import {StandaloneCmp} from './component';
|
|
|
|
@Component({
|
|
selector: 'module-cmp',
|
|
template: '<standalone-cmp></standalone-cmp>',
|
|
standalone: false,
|
|
})
|
|
export class ModuleCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [ModuleCmp],
|
|
exports: [ModuleCmp],
|
|
imports: [forwardRef(() => StandaloneCmp)],
|
|
})
|
|
export class Module {}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'component.ts',
|
|
`
|
|
import {Component, forwardRef} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
@Component({
|
|
imports: [forwardRef(() => Module)],
|
|
selector: 'standalone-cmp',
|
|
template: '<module-cmp></module-cmp>',
|
|
})
|
|
export class StandaloneCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const moduleJs = env.getContents('module.js');
|
|
expect(moduleJs).toContain(
|
|
'i0.ɵɵsetComponentScope(ModuleCmp, function () { return [i1.StandaloneCmp]; }, []);',
|
|
);
|
|
|
|
const cmpJs = env.getContents('component.js');
|
|
expect(cmpJs).toContain('dependencies: () => [Module, i1.ModuleCmp]');
|
|
});
|
|
|
|
it('should error when a non-standalone component tries to use imports', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive} from '@angular/core';
|
|
|
|
@Directive({selector: '[dir]'})
|
|
export class TestDir {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [TestDir],
|
|
standalone: false,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE));
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[TestDir]');
|
|
});
|
|
|
|
it('should compile a standalone component with schema support', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, NO_ERRORS_SCHEMA} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<is-unknown></is-unknown>',
|
|
schemas: [NO_ERRORS_SCHEMA],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
// verify that there are no compilation errors
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(0);
|
|
|
|
// verify generated code for the unknown element
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('i0.ɵɵdomElement(0, "is-unknown");');
|
|
|
|
// verify schemas are not included in the generated code
|
|
const jsCodeAoT = jsCode.slice(
|
|
0,
|
|
jsCode.indexOf('(() => { (typeof ngDevMode === "undefined" || ngDevMode)'),
|
|
);
|
|
expect(jsCodeAoT).not.toContain('schemas: [NO_ERRORS_SCHEMA]');
|
|
});
|
|
|
|
it('should error when a non-standalone component tries to use schemas', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, NO_ERRORS_SCHEMA} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div></div>',
|
|
schemas: [NO_ERRORS_SCHEMA],
|
|
standalone: false,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE));
|
|
expect(diags[0].messageText).toBe(
|
|
`'schemas' is only valid on a component that is standalone.`,
|
|
);
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[NO_ERRORS_SCHEMA]');
|
|
});
|
|
|
|
it('should compile a standalone component that imports an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
standalone: false,
|
|
})
|
|
export class TestDir {}
|
|
|
|
@NgModule({
|
|
declarations: [TestDir],
|
|
exports: [TestDir],
|
|
})
|
|
export class TestModule {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [TestModule],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain('dependencies: [TestModule, TestDir]');
|
|
});
|
|
|
|
it('should allow nested arrays in standalone component imports', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
})
|
|
export class TestDir {}
|
|
|
|
export const DIRECTIVES = [TestDir];
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [DIRECTIVES],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain('dependencies: [TestDir]');
|
|
});
|
|
|
|
it('should deduplicate standalone component imports', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive} from '@angular/core';
|
|
|
|
@Directive({selector: '[dir]'})
|
|
export class TestDir {}
|
|
|
|
export const DIRECTIVES = [TestDir];
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [TestDir, DIRECTIVES],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain('dependencies: [TestDir]');
|
|
});
|
|
|
|
it('should error when a standalone component imports a non-standalone entity', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
standalone: false,
|
|
})
|
|
export class TestDir {}
|
|
|
|
@NgModule({
|
|
declarations: [TestDir],
|
|
exports: [TestDir],
|
|
})
|
|
export class TestModule {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [TestDir],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_IMPORT_NOT_STANDALONE));
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('TestDir');
|
|
|
|
// The diagnostic produced here should suggest that the directive be imported via its
|
|
// NgModule instead.
|
|
expect(diags[0].relatedInformation).not.toBeUndefined();
|
|
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation![0])).toEqual('TestModule');
|
|
expect(diags[0].relatedInformation![0].messageText).toMatch(
|
|
/It can be imported using its '.*' NgModule instead./,
|
|
);
|
|
});
|
|
|
|
it('should error when a standalone component imports a ModuleWithProviders using a foreign function', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, ModuleWithProviders, NgModule} from '@angular/core';
|
|
|
|
@NgModule({})
|
|
export class TestModule {}
|
|
|
|
declare function moduleWithProviders(): ModuleWithProviders<TestModule>;
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div></div>',
|
|
// @ts-ignore
|
|
imports: [moduleWithProviders()],
|
|
})
|
|
export class TestCmpWithLiteralImports {}
|
|
|
|
const IMPORTS = [moduleWithProviders()];
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div></div>',
|
|
// @ts-ignore
|
|
imports: IMPORTS,
|
|
})
|
|
export class TestCmpWithReferencedImports {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(2);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_IMPORT));
|
|
// The import occurs within the array literal, such that the error can be reported for
|
|
// the specific import that is rejected.
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('moduleWithProviders()');
|
|
|
|
expect(diags[1].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_IMPORT));
|
|
// The import occurs in a referenced variable, which reports the error on the full
|
|
// `imports` expression.
|
|
expect(getSourceCodeForDiagnostic(diags[1])).toEqual('IMPORTS');
|
|
});
|
|
|
|
it('should error when a standalone component imports a ModuleWithProviders', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, ModuleWithProviders, NgModule} from '@angular/core';
|
|
|
|
@NgModule({})
|
|
export class TestModule {
|
|
static forRoot(): ModuleWithProviders<TestModule> {
|
|
return {ngModule: TestModule};
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div></div>',
|
|
// @ts-ignore
|
|
imports: [TestModule.forRoot()],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_IMPORT));
|
|
// The static interpreter does not track source locations for locally evaluated functions,
|
|
// so the error is reported on the full `imports` expression.
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[TestModule.forRoot()]');
|
|
});
|
|
|
|
it('should error when a standalone component imports a non-standalone entity, with a specific error when that entity is not exported', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
standalone: false,
|
|
})
|
|
export class TestDir {}
|
|
|
|
@NgModule({
|
|
declarations: [TestDir],
|
|
})
|
|
export class TestModule {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [TestDir],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_IMPORT_NOT_STANDALONE));
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('TestDir');
|
|
|
|
expect(diags[0].relatedInformation).not.toBeUndefined();
|
|
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation![0])).toEqual('TestModule');
|
|
expect(diags[0].relatedInformation![0].messageText).toEqual(
|
|
`It's declared in the 'TestModule' NgModule, but is not exported. Consider exporting it and importing the NgModule instead.`,
|
|
);
|
|
});
|
|
|
|
it('should type-check standalone component templates', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Input, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'not-standalone',
|
|
template: '',
|
|
standalone: false,
|
|
})
|
|
export class NotStandaloneCmp {
|
|
@Input() value!: string;
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [NotStandaloneCmp],
|
|
exports: [NotStandaloneCmp],
|
|
})
|
|
export class NotStandaloneModule {}
|
|
|
|
@Component({
|
|
selector: 'is-standalone',
|
|
template: '',
|
|
})
|
|
export class IsStandaloneCmp {
|
|
@Input() value!: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [NotStandaloneModule, IsStandaloneCmp],
|
|
template: '<not-standalone [value]="3"></not-standalone><is-standalone [value]="true"></is-standalone>',
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics().map((diag) => diag.messageText);
|
|
expect(diags.length).toBe(2);
|
|
expect(diags).toContain(`Type 'number' is not assignable to type 'string'.`);
|
|
expect(diags).toContain(`Type 'boolean' is not assignable to type 'string'.`);
|
|
});
|
|
|
|
it('should not spam errors if imports is misconfigured', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'dep-cmp',
|
|
template: '',
|
|
})
|
|
export class DepCmp {}
|
|
|
|
@Component({
|
|
// standalone: false, would ordinarily cause template type-checking errors
|
|
// as well as an error about imports on a non-standalone component.
|
|
imports: [DepCmp],
|
|
selector: 'test-cmp',
|
|
template: '<dep-cmp></dep-cmp>',
|
|
standalone: false,
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toContain('imports');
|
|
});
|
|
|
|
it('should handle a forwardRef used inside `imports`', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, forwardRef} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test',
|
|
imports: [forwardRef(() => StandaloneComponent)],
|
|
template: "<other-standalone></other-standalone>"
|
|
})
|
|
class TestComponent {
|
|
}
|
|
|
|
@Component({selector: 'other-standalone', template: ""})
|
|
class StandaloneComponent {
|
|
}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
const jsCode = env.getContents('test.js');
|
|
|
|
expect(diags.length).toBe(0);
|
|
expect(jsCode).toContain('dependencies: () => [StandaloneComponent]');
|
|
});
|
|
});
|
|
|
|
describe('NgModule-side', () => {
|
|
it('should not allow a standalone component to be declared in an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: 'Test',
|
|
})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [TestCmp],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NGMODULE_DECLARATION_IS_STANDALONE));
|
|
expect(diags[0].messageText).toContain('Component TestCmp is standalone');
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('TestCmp');
|
|
});
|
|
|
|
it('should not allow a standalone pipe to be declared in an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Pipe, NgModule} from '@angular/core';
|
|
|
|
@Pipe({name: 'test'})
|
|
export class TestPipe {}
|
|
|
|
@NgModule({
|
|
declarations: [TestPipe],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NGMODULE_DECLARATION_IS_STANDALONE));
|
|
expect(diags[0].messageText).toContain('Pipe TestPipe is standalone');
|
|
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('TestPipe');
|
|
});
|
|
|
|
it('should allow a standalone component to be imported by an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'st-cmp',
|
|
template: 'Test',
|
|
})
|
|
export class StandaloneCmp {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<st-cmp></st-cmp>',
|
|
standalone: false,
|
|
})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [TestCmp],
|
|
imports: [StandaloneCmp],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain('dependencies: [StandaloneCmp]');
|
|
});
|
|
|
|
it('should allow a standalone directive to be imported by an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
|
|
@Directive({selector: '[st-dir]'})
|
|
export class StandaloneDir {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div st-dir></div>',
|
|
standalone: false,
|
|
})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [TestCmp],
|
|
imports: [StandaloneDir],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain('dependencies: [StandaloneDir]');
|
|
});
|
|
|
|
it('should allow a standalone pipe to be imported by an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Pipe, NgModule} from '@angular/core';
|
|
|
|
@Pipe({name: 'stpipe'})
|
|
export class StandalonePipe {
|
|
transform(value: any): any {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '{{data | stpipe}}',
|
|
standalone: false,
|
|
})
|
|
export class TestCmp {
|
|
data = 'test';
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [TestCmp],
|
|
imports: [StandalonePipe],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain('dependencies: [StandalonePipe]');
|
|
});
|
|
|
|
it('should error when a standalone entity is exported by an NgModule without importing it first', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
|
|
@Directive({selector: '[dir]'})
|
|
export class TestDir {}
|
|
|
|
@NgModule({
|
|
exports: [TestDir],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NGMODULE_INVALID_REEXPORT));
|
|
expect(diags[0].messageText).toContain('it must be imported first');
|
|
expect(diagnosticToNode(diags[0], ts.isIdentifier).parent.parent.getText()).toEqual(
|
|
'exports: [TestDir]',
|
|
);
|
|
});
|
|
|
|
it('should error when a non-standalone entity is imported into an NgModule', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
standalone: false,
|
|
})
|
|
export class TestDir {}
|
|
|
|
@NgModule({
|
|
imports: [TestDir],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toContain('is not standalone');
|
|
expect(diagnosticToNode(diags[0], ts.isIdentifier).parent.parent.getText()).toEqual(
|
|
'imports: [TestDir]',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('from libraries', () => {
|
|
it('should consume standalone directives from libraries', () => {
|
|
env.write(
|
|
'lib.d.ts',
|
|
`
|
|
import {ɵɵDirectiveDeclaration} from '@angular/core';
|
|
|
|
export declare class StandaloneDir {
|
|
static ɵdir: ɵɵDirectiveDeclaration<StandaloneDir, "[dir]", never, {}, {}, never, never, true>;
|
|
}
|
|
`,
|
|
);
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {StandaloneDir} from './lib';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [StandaloneDir],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('dependencies: [StandaloneDir]');
|
|
});
|
|
|
|
it('should consume standalone components from libraries', () => {
|
|
env.write(
|
|
'lib.d.ts',
|
|
`
|
|
import {ɵɵComponentDeclaration} from '@angular/core';
|
|
|
|
export declare class StandaloneCmp {
|
|
static ɵcmp: ɵɵComponentDeclaration<StandaloneCmp, "standalone-cmp", never, {}, {}, never, never, true>;
|
|
}
|
|
`,
|
|
);
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {StandaloneCmp} from './lib';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<standalone-cmp></standalone-cmp>',
|
|
imports: [StandaloneCmp],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('dependencies: [StandaloneCmp]');
|
|
});
|
|
|
|
it('should consume standalone pipes from libraries', () => {
|
|
env.write(
|
|
'lib.d.ts',
|
|
`
|
|
import {ɵɵPipeDeclaration} from '@angular/core';
|
|
|
|
export declare class StandalonePipe {
|
|
transform(value: any): any;
|
|
static ɵpipe: ɵɵPipeDeclaration<StandalonePipe, "standalone", true>;
|
|
}
|
|
`,
|
|
);
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
import {StandalonePipe} from './lib';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '{{value | standalone}}',
|
|
imports: [StandalonePipe],
|
|
})
|
|
export class StandaloneComponent {
|
|
value!: string;
|
|
}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('dependencies: [StandalonePipe]');
|
|
});
|
|
|
|
it('should compile imports using a const tuple in external library', () => {
|
|
env.write(
|
|
'node_modules/external/index.d.ts',
|
|
`
|
|
import {ɵɵDirectiveDeclaration} from '@angular/core';
|
|
|
|
export declare class StandaloneDir {
|
|
static ɵdir: ɵɵDirectiveDeclaration<StandaloneDir, "[dir]", never, {}, {}, never, never, true>;
|
|
}
|
|
|
|
export declare const DECLARATIONS: readonly [typeof StandaloneDir];
|
|
`,
|
|
);
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Directive} from '@angular/core';
|
|
import {DECLARATIONS} from 'external';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div dir></div>',
|
|
imports: [DECLARATIONS],
|
|
})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('import * as i1 from "external";');
|
|
expect(jsCode).toContain('dependencies: [i1.StandaloneDir]');
|
|
});
|
|
});
|
|
|
|
describe('optimizations', () => {
|
|
it('does emit standalone components in injector imports if they contain providers', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Injectable, NgModule} from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class Service {}
|
|
|
|
@NgModule({
|
|
providers: [Service],
|
|
})
|
|
export class DepModule {}
|
|
|
|
@Component({
|
|
selector: 'standalone-cmp',
|
|
imports: [DepModule],
|
|
template: '',
|
|
})
|
|
export class StandaloneCmp {}
|
|
|
|
@NgModule({
|
|
imports: [StandaloneCmp],
|
|
exports: [StandaloneCmp],
|
|
})
|
|
export class Module {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('i0.ɵɵdefineInjector({ imports: [StandaloneCmp] });');
|
|
});
|
|
|
|
it('does emit standalone components in injector imports if they import a NgModule from .d.ts', () => {
|
|
env.write(
|
|
'dep.d.ts',
|
|
`
|
|
import {ɵɵNgModuleDeclaration} from '@angular/core';
|
|
|
|
declare class DepModule {
|
|
static ɵmod: ɵɵNgModuleDeclaration<DepModule, never, never, never>;
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Injectable, NgModule} from '@angular/core';
|
|
import {DepModule} from './dep';
|
|
|
|
@Component({
|
|
selector: 'standalone-cmp',
|
|
imports: [DepModule],
|
|
template: '',
|
|
})
|
|
export class StandaloneCmp {}
|
|
|
|
@NgModule({
|
|
imports: [StandaloneCmp],
|
|
exports: [StandaloneCmp],
|
|
})
|
|
export class Module {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('i0.ɵɵdefineInjector({ imports: [StandaloneCmp] });');
|
|
});
|
|
|
|
it('does emit standalone components in injector imports if they import a component from .d.ts', () => {
|
|
env.write(
|
|
'dep.d.ts',
|
|
`
|
|
import {ɵɵComponentDeclaration} from '@angular/core';
|
|
|
|
export declare class DepCmp {
|
|
static ɵcmp: ɵɵComponentDeclaration<DepCmp, "dep-cmp", never, {}, {}, never, never, true>
|
|
}
|
|
`,
|
|
);
|
|
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, Injectable, NgModule} from '@angular/core';
|
|
import {DepCmp} from './dep';
|
|
|
|
@Component({
|
|
selector: 'standalone-cmp',
|
|
imports: [DepCmp],
|
|
template: '<dep-cmp/>',
|
|
})
|
|
export class StandaloneCmp {}
|
|
|
|
@NgModule({
|
|
imports: [StandaloneCmp],
|
|
exports: [StandaloneCmp],
|
|
})
|
|
export class Module {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('i0.ɵɵdefineInjector({ imports: [StandaloneCmp] });');
|
|
});
|
|
|
|
it('does not emit standalone directives or pipes in injector imports', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Directive, NgModule, Pipe} from '@angular/core';
|
|
|
|
@Directive({selector: '[dir]'})
|
|
export class StandaloneDir {}
|
|
|
|
@Pipe({name: 'standalone'})
|
|
export class StandalonePipe {
|
|
transform(value: any): any {}
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [StandaloneDir, StandalonePipe],
|
|
exports: [StandaloneDir, StandalonePipe],
|
|
})
|
|
export class Module {}
|
|
`,
|
|
);
|
|
env.driveMain();
|
|
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('i0.ɵɵdefineInjector({});');
|
|
});
|
|
|
|
it('should exclude directives from NgModule imports if they expose no providers', () => {
|
|
env.write(
|
|
'test.ts',
|
|
`
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@NgModule({})
|
|
export class DepModule {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
imports: [DepModule],
|
|
template: '',
|
|
})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({
|
|
imports: [TestCmp],
|
|
exports: [TestCmp],
|
|
})
|
|
export class TestModule {}
|
|
`,
|
|
);
|
|
|
|
env.driveMain();
|
|
const jsCode = env.getContents('test.js');
|
|
expect(jsCode).toContain('i0.ɵɵdefineInjector({});');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('strictStandalone flag', () => {
|
|
let env!: NgtscTestEnvironment;
|
|
|
|
beforeEach(() => {
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
env.tsconfig({strictTemplates: true, strictStandalone: true});
|
|
});
|
|
|
|
it('should not allow a non-standalone component', () => {
|
|
env.write(
|
|
'app.ts',
|
|
`
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({standalone: false, template: ''})
|
|
export class TestCmp {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toEqual(ngErrorCode(ErrorCode.NON_STANDALONE_NOT_ALLOWED));
|
|
expect(diags[0].messageText).toContain('component');
|
|
});
|
|
|
|
it('should not allow a non-standalone directive', () => {
|
|
env.write(
|
|
'app.ts',
|
|
`
|
|
import {Directive} from '@angular/core';
|
|
|
|
@Directive({standalone: false})
|
|
export class TestDir {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toEqual(ngErrorCode(ErrorCode.NON_STANDALONE_NOT_ALLOWED));
|
|
expect(diags[0].messageText).toContain('directive');
|
|
});
|
|
|
|
it('should allow a no-arg directive', () => {
|
|
env.write(
|
|
'app.ts',
|
|
`
|
|
import {Directive} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(0);
|
|
});
|
|
|
|
it('should not allow a non-standalone pipe', () => {
|
|
env.write(
|
|
'app.ts',
|
|
`
|
|
import {Pipe} from '@angular/core';
|
|
|
|
@Pipe({name: 'test', standalone: false})
|
|
export class TestPipe {}
|
|
`,
|
|
);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].code).toEqual(ngErrorCode(ErrorCode.NON_STANDALONE_NOT_ALLOWED));
|
|
expect(diags[0].messageText).toContain('pipe');
|
|
});
|
|
});
|
|
});
|