Skip to content

Commit 9acd2ac

Browse files
crisbetoalxhub
authored andcommitted
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
1 parent e9c3790 commit 9acd2ac

File tree

6 files changed

+71
-14
lines changed

6 files changed

+71
-14
lines changed

packages/compiler-cli/linker/src/file_linker/file_linker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
6969
const minVersion = metaObj.getString('minVersion');
7070
const version = metaObj.getString('version');
7171
const linker = this.linkerSelector.getLinker(declarationFn, minVersion, version);
72-
const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj);
72+
const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj, version);
7373

7474
return emitScope.translateDefinition(definition);
7575
}

packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
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';
9+
import semver from 'semver';
910

1011
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
1112
import {Range} from '../../ast/ast_host';
@@ -15,7 +16,7 @@ import {GetSourceFileFn} from '../get_source_file';
1516

1617
import {toR3DirectiveMeta} from './partial_directive_linker_1';
1718
import {LinkedDefinition, PartialLinker} from './partial_linker';
18-
import {extractForwardRef} from './util';
19+
import {extractForwardRef, PLACEHOLDER_VERSION} from './util';
1920

2021
function makeDirectiveMetadata<TExpression>(
2122
directiveExpr: AstObject<R3DeclareDirectiveDependencyMetadata, TExpression>,
@@ -49,22 +50,27 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
4950
private code: string) {}
5051

5152
linkPartialDeclaration(
52-
constantPool: ConstantPool,
53-
metaObj: AstObject<R3PartialDeclaration, TExpression>): LinkedDefinition {
54-
const meta = this.toR3ComponentMeta(metaObj);
53+
constantPool: ConstantPool, metaObj: AstObject<R3PartialDeclaration, TExpression>,
54+
version: string): LinkedDefinition {
55+
const meta = this.toR3ComponentMeta(metaObj, version);
5556
return compileComponentFromMetadata(meta, constantPool, makeBindingParser());
5657
}
5758

5859
/**
5960
* This function derives the `R3ComponentMetadata` from the provided AST object.
6061
*/
61-
private toR3ComponentMeta(metaObj: AstObject<R3DeclareComponentMetadata, TExpression>):
62-
R3ComponentMetadata<R3TemplateDependencyMetadata> {
62+
private toR3ComponentMeta(
63+
metaObj: AstObject<R3DeclareComponentMetadata, TExpression>,
64+
version: string): R3ComponentMetadata<R3TemplateDependencyMetadata> {
6365
const interpolation = parseInterpolationConfig(metaObj);
6466
const templateSource = metaObj.getValue('template');
6567
const isInline = metaObj.has('isInline') ? metaObj.getBoolean('isInline') : false;
6668
const templateInfo = this.getTemplateInfo(templateSource, isInline);
6769

70+
// Enable the new block syntax if compiled with v17 and
71+
// above, or when using the local placeholder version.
72+
const supportsBlockSyntax = semver.major(version) >= 17 || version === PLACEHOLDER_VERSION;
73+
6874
const template = parseTemplate(templateInfo.code, templateInfo.sourceUrl, {
6975
escapedString: templateInfo.isEscaped,
7076
interpolationConfig: interpolation,
@@ -74,6 +80,12 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
7480
metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false,
7581
// We normalize line endings if the template is was inline.
7682
i18nNormalizeLineEndingsInICUs: isInline,
83+
84+
// TODO(crisbeto): hardcode the supported blocks for now. Before the final release
85+
// `enabledBlockTypes` will be replaced with a boolean, at which point `supportsBlockSyntax`
86+
// can be passed in directly here.
87+
enabledBlockTypes: supportsBlockSyntax ? new Set(['if', 'switch', 'for', 'defer']) :
88+
undefined,
7789
});
7890
if (template.errors !== null) {
7991
const errors = template.errors.map(err => err.toString()).join('\n');

packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ export interface PartialLinker<TExpression> {
2828
* `R3DeclareComponentMetadata` interfaces.
2929
*/
3030
linkPartialDeclaration(
31-
constantPool: ConstantPool,
32-
metaObj: AstObject<R3PartialDeclaration, TExpression>): LinkedDefinition;
31+
constantPool: ConstantPool, metaObj: AstObject<R3PartialDeclaration, TExpression>,
32+
version: string): LinkedDefinition;
3333
}

packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {PartialInjectorLinkerVersion1} from './partial_injector_linker_1';
2121
import {PartialLinker} from './partial_linker';
2222
import {PartialNgModuleLinkerVersion1} from './partial_ng_module_linker_1';
2323
import {PartialPipeLinkerVersion1} from './partial_pipe_linker_1';
24+
import {PLACEHOLDER_VERSION} from './util';
2425

2526
export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
2627
export const ɵɵngDeclareClassMetadata = 'ɵɵngDeclareClassMetadata';
@@ -68,7 +69,7 @@ export function createLinkerMap<TStatement, TExpression>(
6869
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
6970
code: string): Map<string, LinkerRange<TExpression>[]> {
7071
const linkers = new Map<string, LinkerRange<TExpression>[]>();
71-
const LATEST_VERSION_RANGE = getRange('<=', '0.0.0-PLACEHOLDER');
72+
const LATEST_VERSION_RANGE = getRange('<=', PLACEHOLDER_VERSION);
7273

7374
linkers.set(ɵɵngDeclareDirective, [
7475
{range: LATEST_VERSION_RANGE, linker: new PartialDirectiveLinkerVersion1(sourceUrl, code)},
@@ -143,7 +144,7 @@ export class PartialLinkerSelector<TExpression> {
143144
}
144145
const linkerRanges = this.linkers.get(functionName)!;
145146

146-
if (version === '0.0.0-PLACEHOLDER') {
147+
if (version === PLACEHOLDER_VERSION) {
147148
// Special case if the `version` is the same as the current compiler version.
148149
// This helps with compliance tests where the version placeholders have not been replaced.
149150
return linkerRanges[linkerRanges.length - 1].linker;

packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {createMayBeForwardRefExpression, ForwardRefHandling, MaybeForwardRefExpr
1010
import {AstObject, AstValue} from '../../ast/ast_value';
1111
import {FatalLinkerError} from '../../fatal_linker_error';
1212

13+
export const PLACEHOLDER_VERSION = '0.0.0-PLACEHOLDER';
14+
1315
export function wrapReference<TExpression>(wrapped: o.WrappedNodeExpr<TExpression>): R3Reference {
1416
return {value: wrapped, type: wrapped};
1517
}

packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,48 @@ describe('FileLinker', () => {
120120
});
121121
});
122122

123+
describe('block syntax support', () => {
124+
function linkComponentWithTemplate(version: string, template: string): string {
125+
// Note that the `minVersion` is set to the placeholder,
126+
// because that's what we have in the source code as well.
127+
const source = `
128+
ɵɵngDeclareComponent({
129+
minVersion: "0.0.0-PLACEHOLDER",
130+
version: "${version}",
131+
ngImport: core,
132+
template: \`${template}\`,
133+
isInline: true,
134+
type: SomeComp
135+
});
136+
`;
137+
138+
// We need to create a new source file here, because template parsing requires
139+
// the template string to have offsets which synthetic nodes do not.
140+
const {fileLinker} = createFileLinker(source);
141+
const sourceFile = ts.createSourceFile('', source, ts.ScriptTarget.Latest, true);
142+
const call =
143+
(sourceFile.statements[0] as ts.ExpressionStatement).expression as ts.CallExpression;
144+
const result = fileLinker.linkPartialDeclaration(
145+
'ɵɵngDeclareComponent', [call.arguments[0]], new MockDeclarationScope());
146+
return ts.createPrinter().printNode(ts.EmitHint.Unspecified, result, sourceFile);
147+
}
148+
149+
it('should enable block syntax if compiled with version 17 or above', () => {
150+
for (const version of ['17.0.0', '17.0.1', '17.1.0', '17.0.0-next.0', '18.0.0']) {
151+
expect(linkComponentWithTemplate(version, '@defer {}')).toContain('ɵɵdefer(');
152+
}
153+
});
154+
155+
it('should enable block syntax if compiled with a local version', () => {
156+
expect(linkComponentWithTemplate('0.0.0-PLACEHOLDER', '@defer {}')).toContain('ɵɵdefer(');
157+
});
158+
159+
it('should not enable block syntax if compiled with a version older than 17', () => {
160+
expect(linkComponentWithTemplate('16.2.0', '@Input() is a decorator. This is a brace }'))
161+
.toContain('@Input() is a decorator. This is a brace }');
162+
});
163+
});
164+
123165
describe('getConstantStatements()', () => {
124166
it('should capture shared constant values', () => {
125167
const {fileLinker} = createFileLinker();
@@ -179,17 +221,17 @@ describe('FileLinker', () => {
179221
});
180222
});
181223

182-
function createFileLinker(): {
224+
function createFileLinker(code = '// test code'): {
183225
host: AstHost<ts.Expression>,
184-
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>
226+
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>,
185227
} {
186228
const fs = new MockFileSystemNative();
187229
const logger = new MockLogger();
188230
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
189231
fs, logger, new TypeScriptAstHost(),
190232
new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), DEFAULT_LINKER_OPTIONS);
191233
const fileLinker = new FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>(
192-
linkerEnvironment, fs.resolve('/test.js'), '// test code');
234+
linkerEnvironment, fs.resolve('/test.js'), code);
193235
return {host: linkerEnvironment.host, fileLinker};
194236
}
195237
});

0 commit comments

Comments
 (0)