From 9acd2ac98bc3b6ffc5a8d6c19f7290d05fe1f896 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 2 Oct 2023 12:59:36 +0200 Subject: [PATCH] fix(compiler): enable block syntax in the linker (#51979) Adds some logic to enable parsing of block syntax in the linker. Note that the syntax is only enabled on code compiled with Angular v17 or later. PR Close #51979 --- .../linker/src/file_linker/file_linker.ts | 2 +- .../partial_component_linker_1.ts | 24 +++++++--- .../partial_linkers/partial_linker.ts | 4 +- .../partial_linker_selector.ts | 5 +- .../src/file_linker/partial_linkers/util.ts | 2 + .../test/file_linker/file_linker_spec.ts | 48 +++++++++++++++++-- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/compiler-cli/linker/src/file_linker/file_linker.ts b/packages/compiler-cli/linker/src/file_linker/file_linker.ts index d30f8f88855..14c55d41e93 100644 --- a/packages/compiler-cli/linker/src/file_linker/file_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/file_linker.ts @@ -69,7 +69,7 @@ export class FileLinker { const minVersion = metaObj.getString('minVersion'); const version = metaObj.getString('version'); const linker = this.linkerSelector.getLinker(declarationFn, minVersion, version); - const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj); + const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj, version); return emitScope.translateDefinition(definition); } 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 680f87ce376..7bee7d19642 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 @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation} from '@angular/compiler'; +import semver from 'semver'; import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; import {Range} from '../../ast/ast_host'; @@ -15,7 +16,7 @@ import {GetSourceFileFn} from '../get_source_file'; import {toR3DirectiveMeta} from './partial_directive_linker_1'; import {LinkedDefinition, PartialLinker} from './partial_linker'; -import {extractForwardRef} from './util'; +import {extractForwardRef, PLACEHOLDER_VERSION} from './util'; function makeDirectiveMetadata( directiveExpr: AstObject, @@ -49,22 +50,27 @@ export class PartialComponentLinkerVersion1 implements private code: string) {} linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { - const meta = this.toR3ComponentMeta(metaObj); + constantPool: ConstantPool, metaObj: AstObject, + version: string): LinkedDefinition { + const meta = this.toR3ComponentMeta(metaObj, version); return compileComponentFromMetadata(meta, constantPool, makeBindingParser()); } /** * This function derives the `R3ComponentMetadata` from the provided AST object. */ - private toR3ComponentMeta(metaObj: AstObject): - R3ComponentMetadata { + private toR3ComponentMeta( + metaObj: AstObject, + version: string): R3ComponentMetadata { const interpolation = parseInterpolationConfig(metaObj); const templateSource = metaObj.getValue('template'); const isInline = metaObj.has('isInline') ? metaObj.getBoolean('isInline') : false; const templateInfo = this.getTemplateInfo(templateSource, isInline); + // Enable the new block syntax if compiled with v17 and + // above, or when using the local placeholder version. + const supportsBlockSyntax = semver.major(version) >= 17 || version === PLACEHOLDER_VERSION; + const template = parseTemplate(templateInfo.code, templateInfo.sourceUrl, { escapedString: templateInfo.isEscaped, interpolationConfig: interpolation, @@ -74,6 +80,12 @@ export class PartialComponentLinkerVersion1 implements metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false, // We normalize line endings if the template is was inline. i18nNormalizeLineEndingsInICUs: isInline, + + // TODO(crisbeto): hardcode the supported blocks for now. Before the final release + // `enabledBlockTypes` will be replaced with a boolean, at which point `supportsBlockSyntax` + // can be passed in directly here. + enabledBlockTypes: supportsBlockSyntax ? new Set(['if', 'switch', 'for', 'defer']) : + undefined, }); if (template.errors !== null) { const errors = template.errors.map(err => err.toString()).join('\n'); diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts index 8baf54a1988..680db8a33ec 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts @@ -28,6 +28,6 @@ export interface PartialLinker { * `R3DeclareComponentMetadata` interfaces. */ linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition; + constantPool: ConstantPool, metaObj: AstObject, + version: string): LinkedDefinition; } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts index 8bfa59796fd..03e0cbf9d33 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts @@ -21,6 +21,7 @@ import {PartialInjectorLinkerVersion1} from './partial_injector_linker_1'; import {PartialLinker} from './partial_linker'; import {PartialNgModuleLinkerVersion1} from './partial_ng_module_linker_1'; import {PartialPipeLinkerVersion1} from './partial_pipe_linker_1'; +import {PLACEHOLDER_VERSION} from './util'; export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective'; export const ɵɵngDeclareClassMetadata = 'ɵɵngDeclareClassMetadata'; @@ -68,7 +69,7 @@ export function createLinkerMap( environment: LinkerEnvironment, sourceUrl: AbsoluteFsPath, code: string): Map[]> { const linkers = new Map[]>(); - const LATEST_VERSION_RANGE = getRange('<=', '0.0.0-PLACEHOLDER'); + const LATEST_VERSION_RANGE = getRange('<=', PLACEHOLDER_VERSION); linkers.set(ɵɵngDeclareDirective, [ {range: LATEST_VERSION_RANGE, linker: new PartialDirectiveLinkerVersion1(sourceUrl, code)}, @@ -143,7 +144,7 @@ export class PartialLinkerSelector { } const linkerRanges = this.linkers.get(functionName)!; - if (version === '0.0.0-PLACEHOLDER') { + if (version === PLACEHOLDER_VERSION) { // Special case if the `version` is the same as the current compiler version. // This helps with compliance tests where the version placeholders have not been replaced. return linkerRanges[linkerRanges.length - 1].linker; diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts index c2e4e30c1b9..c7ae16836ad 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts @@ -10,6 +10,8 @@ import {createMayBeForwardRefExpression, ForwardRefHandling, MaybeForwardRefExpr import {AstObject, AstValue} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; +export const PLACEHOLDER_VERSION = '0.0.0-PLACEHOLDER'; + export function wrapReference(wrapped: o.WrappedNodeExpr): R3Reference { return {value: wrapped, type: wrapped}; } diff --git a/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts b/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts index 4d761cb6306..3effd9c02af 100644 --- a/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts @@ -120,6 +120,48 @@ describe('FileLinker', () => { }); }); + describe('block syntax support', () => { + function linkComponentWithTemplate(version: string, template: string): string { + // Note that the `minVersion` is set to the placeholder, + // because that's what we have in the source code as well. + const source = ` + ɵɵngDeclareComponent({ + minVersion: "0.0.0-PLACEHOLDER", + version: "${version}", + ngImport: core, + template: \`${template}\`, + isInline: true, + type: SomeComp + }); + `; + + // We need to create a new source file here, because template parsing requires + // the template string to have offsets which synthetic nodes do not. + const {fileLinker} = createFileLinker(source); + const sourceFile = ts.createSourceFile('', source, ts.ScriptTarget.Latest, true); + const call = + (sourceFile.statements[0] as ts.ExpressionStatement).expression as ts.CallExpression; + const result = fileLinker.linkPartialDeclaration( + 'ɵɵngDeclareComponent', [call.arguments[0]], new MockDeclarationScope()); + return ts.createPrinter().printNode(ts.EmitHint.Unspecified, result, sourceFile); + } + + it('should enable block syntax if compiled with version 17 or above', () => { + for (const version of ['17.0.0', '17.0.1', '17.1.0', '17.0.0-next.0', '18.0.0']) { + expect(linkComponentWithTemplate(version, '@defer {}')).toContain('ɵɵdefer('); + } + }); + + it('should enable block syntax if compiled with a local version', () => { + expect(linkComponentWithTemplate('0.0.0-PLACEHOLDER', '@defer {}')).toContain('ɵɵdefer('); + }); + + it('should not enable block syntax if compiled with a version older than 17', () => { + expect(linkComponentWithTemplate('16.2.0', '@Input() is a decorator. This is a brace }')) + .toContain('@Input() is a decorator. This is a brace }'); + }); + }); + describe('getConstantStatements()', () => { it('should capture shared constant values', () => { const {fileLinker} = createFileLinker(); @@ -179,9 +221,9 @@ describe('FileLinker', () => { }); }); - function createFileLinker(): { + function createFileLinker(code = '// test code'): { host: AstHost, - fileLinker: FileLinker + fileLinker: FileLinker, } { const fs = new MockFileSystemNative(); const logger = new MockLogger(); @@ -189,7 +231,7 @@ describe('FileLinker', () => { fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), DEFAULT_LINKER_OPTIONS); const fileLinker = new FileLinker( - linkerEnvironment, fs.resolve('/test.js'), '// test code'); + linkerEnvironment, fs.resolve('/test.js'), code); return {host: linkerEnvironment.host, fileLinker}; } });