Skip to content

Commit

Permalink
feat(compiler-cli): 'strictStandalone' flag enforces standalone (angu…
Browse files Browse the repository at this point in the history
…lar#57935)

Add the `strictStandalone` flag to `angularCompilerOptions`. When set to
true, the compiler will require that all declarations of components,
directive, and pipes be standalone. When `standalone: false` is provided,
an error is raised.

Note that until the default value of the standalone flag is flipped, this
does not catch the case where a declaration does not specify a value for
`standalone`.

The default value of the `strictStandalone` flag is `false`.

PR Close angular#57935
  • Loading branch information
alxhub committed Sep 26, 2024
1 parent 3240598 commit d9687f4
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 0 deletions.
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');
});
});
});

0 comments on commit d9687f4

Please sign in to comment.