From bc9a067ef4e9ce9d3c14e191efe6d79ded72caeb Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 23 Apr 2025 11:29:21 +0200 Subject: [PATCH] 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 --- .../partial_component_linker_1.ts | 2 ++ .../annotations/component/src/handler.ts | 4 +++ .../annotations/component/src/resources.ts | 2 ++ .../component/test/component_spec.ts | 1 + .../src/ngtsc/core/api/src/options.ts | 7 ++++ .../src/ngtsc/core/src/compiler.ts | 5 +++ .../src/ngtsc/typecheck/api/api.ts | 5 +++ .../src/ngtsc/typecheck/testing/index.ts | 32 ++++++++++++++----- .../src/template/pipeline/src/ingest.ts | 2 ++ .../identify_template_references.ts | 1 + packages/language-service/api.ts | 5 +++ .../language-service/src/language_service.ts | 4 ++- 12 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index 116f6f1076e..b4ef389017b 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -120,6 +120,8 @@ export class PartialComponentLinkerVersion1 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'); diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 55a7192dacf..70f1b0e6bde 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -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, diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts index f1b7dac7b2d..0b56ef5ccf6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts @@ -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 ?? '', { diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts index 012ddbb6443..6bc0265511e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts @@ -161,6 +161,7 @@ function setup( /* enableHmr */ false, /* implicitStandaloneValue */ true, /* typeCheckHostBindings */ true, + /* enableSelectorless */ false, ); return {reflectionHost, handler, resourceLoader, metaRegistry}; } diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/options.ts b/packages/compiler-cli/src/ngtsc/core/api/src/options.ts index 60fb8de0654..cb3b0c54d29 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/options.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/options.ts @@ -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. diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index cd2f6595822..bc7ea8f8ed5 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -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` diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index 78ec9bf9150..fe2704dfabf 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -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 = diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index 9beb5ce0c42..7b875a7d7ad 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -15,6 +15,7 @@ import { PropertyRead, PropertyWrite, R3TargetBinder, + SelectorlessMatcher, SelectorMatcher, TmplAstElement, TmplAstLetDeclaration, @@ -279,6 +280,7 @@ export const ALL_ENABLED_CONFIG: Readonly = { 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(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(matcher); const classRef = new Reference(classDecl); @@ -776,8 +782,8 @@ function prepareDeclarations( declarations: TestDeclaration[], resolveDeclaration: DeclarationResolver, metadataRegistry: Map, + selectorlessEnabled: boolean, ) { - const matcher = new SelectorMatcher(); const pipes = new Map(); const hostDirectiveResolder = new HostDirectivesResolver( getFakeMetadataReader(metadataRegistry as Map), @@ -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(); + for (const meta of directives) { + registry.set(meta.name, [meta, ...hostDirectiveResolder.resolve(meta)]); + } + return {matcher: new SelectorlessMatcher(registry), pipes}; + } else { + const matcher = new SelectorMatcher(); + 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 { diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 1d75ea916d6..2a71e7a524d 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -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}`); } diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/identify_template_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/identify_template_references.ts index f2711cce477..d5d7ba786aa 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/identify_template_references.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/identify_template_references.ts @@ -135,6 +135,7 @@ function extractTemplateWithoutCompilerAnalysis( usePoisonedData: true, enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false, i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true, + enableSelectorless: false, }, CompilationMode.FULL, ).nodes; diff --git a/packages/language-service/api.ts b/packages/language-service/api.ts index 8b54f3a9636..ebc7aa116cd 100644 --- a/packages/language-service/api.ts +++ b/packages/language-service/api.ts @@ -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. */ diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index fb7b730ff43..a60ad23c630 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -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'];