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
This commit is contained in:
Kristiyan Kostadinov 2023-10-02 12:59:36 +02:00 committed by Alex Rickabaugh
parent e9c3790b4e
commit 9acd2ac98b
6 changed files with 71 additions and 14 deletions

View file

@ -69,7 +69,7 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
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);
}

View file

@ -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<TExpression>(
directiveExpr: AstObject<R3DeclareDirectiveDependencyMetadata, TExpression>,
@ -49,22 +50,27 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
private code: string) {}
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): LinkedDefinition {
const meta = this.toR3ComponentMeta(metaObj);
constantPool: ConstantPool, metaObj: AstObject<R3PartialDeclaration, TExpression>,
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<R3DeclareComponentMetadata, TExpression>):
R3ComponentMetadata<R3TemplateDependencyMetadata> {
private toR3ComponentMeta(
metaObj: AstObject<R3DeclareComponentMetadata, TExpression>,
version: string): R3ComponentMetadata<R3TemplateDependencyMetadata> {
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<TStatement, TExpression> 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');

View file

@ -28,6 +28,6 @@ export interface PartialLinker<TExpression> {
* `R3DeclareComponentMetadata` interfaces.
*/
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): LinkedDefinition;
constantPool: ConstantPool, metaObj: AstObject<R3PartialDeclaration, TExpression>,
version: string): LinkedDefinition;
}

View file

@ -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<TStatement, TExpression>(
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
code: string): Map<string, LinkerRange<TExpression>[]> {
const linkers = new Map<string, LinkerRange<TExpression>[]>();
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<TExpression> {
}
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;

View file

@ -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<TExpression>(wrapped: o.WrappedNodeExpr<TExpression>): R3Reference {
return {value: wrapped, type: wrapped};
}

View file

@ -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<ts.Expression>,
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>,
} {
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<MockConstantScopeRef, ts.Statement, ts.Expression>(
linkerEnvironment, fs.resolve('/test.js'), '// test code');
linkerEnvironment, fs.resolve('/test.js'), code);
return {host: linkerEnvironment.host, fileLinker};
}
});