refactor(compiler-cli): add flag to enable selectorless (#60977)

Adds a private flag that we can use to enable selectorless as it's being developed.

PR Close #60977
This commit is contained in:
Kristiyan Kostadinov 2025-04-23 11:29:21 +02:00 committed by Miles Malerba
parent c2987d8402
commit bc9a067ef4
12 changed files with 61 additions and 9 deletions

View file

@ -120,6 +120,8 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression>
i18nNormalizeLineEndingsInICUs: isInline,
enableBlockSyntax,
enableLetSyntax,
// TODO(crisbeto): figure out how this is enabled.
enableSelectorless: false,
});
if (template.errors !== null) {
const errors = template.errors.map((err) => err.toString()).join('\n');

View file

@ -272,6 +272,7 @@ export class ComponentDecoratorHandler
private readonly enableHmr: boolean,
private readonly implicitStandaloneValue: boolean,
private readonly typeCheckHostBindings: boolean,
private readonly enableSelectorless: boolean,
) {
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
@ -279,6 +280,7 @@ export class ComponentDecoratorHandler
usePoisonedData: this.usePoisonedData,
enableBlockSyntax: this.enableBlockSyntax,
enableLetSyntax: this.enableLetSyntax,
enableSelectorless: this.enableSelectorless,
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
};
@ -308,6 +310,7 @@ export class ComponentDecoratorHandler
usePoisonedData: boolean;
enableBlockSyntax: boolean;
enableLetSyntax: boolean;
enableSelectorless: boolean;
preserveSignificantWhitespace?: boolean;
};
@ -705,6 +708,7 @@ export class ComponentDecoratorHandler
usePoisonedData: this.usePoisonedData,
enableBlockSyntax: this.enableBlockSyntax,
enableLetSyntax: this.enableLetSyntax,
enableSelectorless: this.enableSelectorless,
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
},
this.compilationMode,

View file

@ -135,6 +135,7 @@ export interface ExtractTemplateOptions {
i18nNormalizeLineEndingsInICUs: boolean;
enableBlockSyntax: boolean;
enableLetSyntax: boolean;
enableSelectorless: boolean;
preserveSignificantWhitespace?: boolean;
}
@ -319,6 +320,7 @@ function parseExtractedTemplate(
escapedString,
enableBlockSyntax: options.enableBlockSyntax,
enableLetSyntax: options.enableLetSyntax,
enableSelectorless: options.enableSelectorless,
};
const parsedTemplate = parseTemplate(sourceStr, sourceMapUrl ?? '', {

View file

@ -161,6 +161,7 @@ function setup(
/* enableHmr */ false,
/* implicitStandaloneValue */ true,
/* typeCheckHostBindings */ true,
/* enableSelectorless */ false,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
}

View file

@ -121,6 +121,13 @@ export interface InternalOptions {
*/
_enableHmr?: boolean;
/**
* Whether selectorless is enabled.
*
* @internal
*/
_enableSelectorless?: boolean;
// TODO(crisbeto): this is a temporary flag that will be removed in v20.
/**
* Whether to check the event side of two-way bindings.

View file

@ -391,6 +391,7 @@ export class NgCompiler {
private readonly angularCoreVersion: string | null;
private readonly enableHmr: boolean;
private readonly implicitStandaloneValue: boolean;
private readonly enableSelectorless: boolean;
/**
* `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each
@ -463,6 +464,7 @@ export class NgCompiler {
// TODO(crisbeto): remove this flag and base `enableBlockSyntax` on the `angularCoreVersion`.
this.enableBlockSyntax = options['_enableBlockSyntax'] ?? true;
this.enableLetSyntax = options['_enableLetSyntax'] ?? true;
this.enableSelectorless = options['_enableSelectorless'] ?? false;
// 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.
@ -1084,6 +1086,7 @@ export class NgCompiler {
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
checkTwoWayBoundEvents,
selectorlessEnabled: this.enableSelectorless,
};
} else {
typeCheckingConfig = {
@ -1119,6 +1122,7 @@ export class NgCompiler {
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
checkTwoWayBoundEvents,
selectorlessEnabled: this.enableSelectorless,
};
}
@ -1507,6 +1511,7 @@ export class NgCompiler {
this.enableHmr,
this.implicitStandaloneValue,
typeCheckHostBindings,
this.enableSelectorless,
),
// TODO(alxhub): understand why the cast here is necessary (something to do with `null`

View file

@ -362,6 +362,11 @@ export interface TypeCheckingConfig {
* Whether the event side of a two-way binding should be type checked.
*/
checkTwoWayBoundEvents: boolean;
/**
* Whether selectorless syntax is enabled.
*/
selectorlessEnabled: boolean;
}
export type SourceMapping =

View file

@ -15,6 +15,7 @@ import {
PropertyRead,
PropertyWrite,
R3TargetBinder,
SelectorlessMatcher,
SelectorMatcher,
TmplAstElement,
TmplAstLetDeclaration,
@ -279,6 +280,7 @@ export const ALL_ENABLED_CONFIG: Readonly<TypeCheckingConfig> = {
unusedStandaloneImports: 'warning',
allowSignalsInTwoWayBindings: true,
checkTwoWayBoundEvents: true,
selectorlessEnabled: false,
};
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
@ -299,7 +301,7 @@ export interface TestDirective
>
>
> {
selector: string;
selector: string | null;
name: string;
file?: AbsoluteFsPath;
type: 'directive';
@ -369,6 +371,7 @@ export function tcb(
const clazz = getClass(sf, 'Test');
const templateUrl = 'synthetic.html';
const {nodes, errors} = parseTemplate(template, templateUrl, templateParserOptions);
const selectorlessEnabled = templateParserOptions?.enableSelectorless ?? false;
if (errors !== null) {
throw new Error('Template parse errors: \n' + errors.join('\n'));
@ -378,6 +381,7 @@ export function tcb(
declarations,
(decl) => getClass(sf, decl.name),
new Map(),
selectorlessEnabled,
);
const binder = new R3TargetBinder<DirectiveMeta>(matcher);
const boundTarget = binder.bind({template: nodes});
@ -419,6 +423,7 @@ export function tcb(
suggestionsForSuboptimalTypeInference: false,
allowSignalsInTwoWayBindings: true,
checkTwoWayBoundEvents: true,
selectorlessEnabled,
...config,
};
options = options || {emitSpans: false};
@ -608,6 +613,7 @@ export function setup(
return getClass(declFile, decl.name);
},
fakeMetadataRegistry,
overrides.parseOptions?.enableSelectorless ?? false,
);
const binder = new R3TargetBinder<DirectiveMeta>(matcher);
const classRef = new Reference(classDecl);
@ -776,8 +782,8 @@ function prepareDeclarations(
declarations: TestDeclaration[],
resolveDeclaration: DeclarationResolver,
metadataRegistry: Map<string, TypeCheckableDirectiveMeta>,
selectorlessEnabled: boolean,
) {
const matcher = new SelectorMatcher<DirectiveMeta[]>();
const pipes = new Map<string, PipeMeta>();
const hostDirectiveResolder = new HostDirectivesResolver(
getFakeMetadataReader(metadataRegistry as Map<string, DirectiveMeta>),
@ -809,13 +815,23 @@ function prepareDeclarations(
// We need to make two passes over the directives so that all declarations
// have been registered by the time we resolve the host directives.
for (const meta of directives) {
const selector = CssSelector.parse(meta.selector || '');
const matches = [...hostDirectiveResolder.resolve(meta), meta] as DirectiveMeta[];
matcher.addSelectables(selector, matches);
}
return {matcher, pipes};
if (selectorlessEnabled) {
const registry = new Map<string, DirectiveMeta[]>();
for (const meta of directives) {
registry.set(meta.name, [meta, ...hostDirectiveResolder.resolve(meta)]);
}
return {matcher: new SelectorlessMatcher<DirectiveMeta[]>(registry), pipes};
} else {
const matcher = new SelectorMatcher<DirectiveMeta[]>();
for (const meta of directives) {
const selector = CssSelector.parse(meta.selector || '');
const matches = [...hostDirectiveResolder.resolve(meta), meta] as DirectiveMeta[];
matcher.addSelectables(selector, matches);
}
return {matcher, pipes};
}
}
export function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDeclaration> {

View file

@ -231,6 +231,8 @@ function ingestNodes(unit: ViewCompilationUnit, template: t.Node[]): void {
ingestForBlock(unit, node);
} else if (node instanceof t.LetDeclaration) {
ingestLetDeclaration(unit, node);
} else if (node instanceof t.Component) {
// TODO(crisbeto): account for selectorless nodes.
} else {
throw new Error(`Unsupported template node: ${node.constructor.name}`);
}

View file

@ -135,6 +135,7 @@ function extractTemplateWithoutCompilerAnalysis(
usePoisonedData: true,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
enableSelectorless: false,
},
CompilationMode.FULL,
).nodes;

View file

@ -42,6 +42,11 @@ export interface PluginConfig {
*/
enableLetSyntax?: false;
/**
* Whether selectorless is enabled.
*/
enableSelectorless?: true;
/**
* A list of diagnostic codes that should be supressed in the language service.
*/

View file

@ -728,10 +728,12 @@ function parseNgCompilerOptions(
if (config['enableBlockSyntax'] === false) {
options['_enableBlockSyntax'] = false;
}
if (config['enableLetSyntax'] === false) {
options['_enableLetSyntax'] = false;
}
if (config['enableSelectorless'] === true) {
options['_enableSelectorless'] = true;
}
options['_angularCoreVersion'] = config['angularCoreVersion'];