Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/compiler_options.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface DiagnosticOptions {
[Name in ExtendedTemplateDiagnosticName]?: DiagnosticCategoryLabel;
};
};
strictStandalone?: boolean;
}

// @public
Expand Down
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/error_code.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export enum ErrorCode {
NGMODULE_INVALID_REEXPORT = 6004,
NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC = 6005,
NGMODULE_REEXPORT_NAME_COLLISION = 6006,
NON_STANDALONE_NOT_ALLOWED = 2023,
NULLISH_COALESCING_NOT_NULLABLE = 8102,
OPTIONAL_CHAIN_NOT_NULLABLE = 8107,
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export class ComponentDecoratorHandler
private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null,
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
private readonly i18nPreserveSignificantWhitespace: boolean,
private readonly strictStandalone: boolean,
) {
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
Expand Down Expand Up @@ -431,6 +432,7 @@ export class ComponentDecoratorHandler
this.annotateForClosureCompiler,
this.compilationMode,
this.elementSchemaRegistry.getDefaultComponentElementName(),
this.strictStandalone,
);
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Component` has
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ function setup(
/* localCompilationExtraImportsTracker */ null,
jitDeclarationRegistry,
/* i18nPreserveSignificantWhitespace */ true,
/* strictStandalone */ false,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export class DirectiveDecoratorHandler
private includeClassMetadata: boolean,
private readonly compilationMode: CompilationMode,
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
private readonly strictStandalone: boolean,
) {}

readonly precedence = HandlerPrecedence.PRIMARY;
Expand Down Expand Up @@ -190,6 +191,7 @@ export class DirectiveDecoratorHandler
this.annotateForClosureCompiler,
this.compilationMode,
/* defaultSelector */ null,
this.strictStandalone,
);
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Directive` has
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function extractDirectiveMetadata(
annotateForClosureCompiler: boolean,
compilationMode: CompilationMode,
defaultSelector: string | null,
strictStandalone: boolean,
):
| {
jitForced: false;
Expand Down Expand Up @@ -342,6 +343,14 @@ export function extractDirectiveMetadata(
throw createValueHasWrongTypeError(expr, resolved, `standalone flag must be a boolean`);
}
isStandalone = resolved;

if (!isStandalone && strictStandalone) {
throw new FatalDiagnosticError(
ErrorCode.NON_STANDALONE_NOT_ALLOWED,
expr,
`Only standalone components/directives are allowed when 'strictStandalone' is enabled.`,
);
}
}
let isSignal = false;
if (directive.has('signals')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ runInEachFileSystem(() => {
/*includeClassMetadata*/ true,
/*compilationMode */ CompilationMode.FULL,
jitDeclarationRegistry,
/* strictStandalone */ false,
);

const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export class PipeDecoratorHandler
private includeClassMetadata: boolean,
private readonly compilationMode: CompilationMode,
private readonly generateExtraImportsInLocalMode: boolean,
private readonly strictStandalone: boolean,
) {}

readonly precedence = HandlerPrecedence.PRIMARY;
Expand Down Expand Up @@ -183,6 +184,14 @@ export class PipeDecoratorHandler
throw createValueHasWrongTypeError(expr, resolved, `standalone flag must be a boolean`);
}
isStandalone = resolved;

if (!isStandalone && this.strictStandalone) {
throw new FatalDiagnosticError(
ErrorCode.NON_STANDALONE_NOT_ALLOWED,
expr,
`Only standalone pipes are allowed when 'strictStandalone' is enabled.`,
);
}
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ export interface DiagnosticOptions {
*/
checks?: {[Name in ExtendedTemplateDiagnosticName]?: DiagnosticCategoryLabel};
};

/**
* If enabled, non-standalone declarations are prohibited and result in build errors.
*/
strictStandalone?: boolean;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,7 @@ export class NgCompiler {
localCompilationExtraImportsTracker,
jitDeclarationRegistry,
this.options.i18nPreserveWhitespaceForLegacyExtraction ?? true,
!!this.options.strictStandalone,
),

// TODO(alxhub): understand why the cast here is necessary (something to do with `null`
Expand All @@ -1482,6 +1483,7 @@ export class NgCompiler {
supportTestBed,
compilationMode,
jitDeclarationRegistry,
!!this.options.strictStandalone,
) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null, unknown>>,
// Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them)
Expand All @@ -1496,6 +1498,7 @@ export class NgCompiler {
supportTestBed,
compilationMode,
!!this.options.generateExtraImportsInLocalMode,
!!this.options.strictStandalone,
),
new InjectableDecoratorHandler(
reflector,
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ export enum ErrorCode {
*/
COMPONENT_UNKNOWN_DEFERRED_IMPORT = 2022,

/**
* Raised when a `standalone: false` component is declared but `strictStandalone` is set.
*/
NON_STANDALONE_NOT_ALLOWED = 2023,

SYMBOL_NOT_EXPORTED = 3001,
/**
* Raised when a relationship between directives and/or pipes would cause a cyclic import to be
Expand Down
75 changes: 75 additions & 0 deletions packages/compiler-cli/test/ngtsc/standalone_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,4 +1208,79 @@ runInEachFileSystem(() => {
});
});
});

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');
});
});
});