refactor(compiler-cli): use semver range checking for partial versions (#39847)

The partial compiler will add a version number to the objects that are
generated so that the linker can select the appropriate partial linker
class to process the metadata.

Previously this version matching was a simple number check. Now
the partial compilation writes the current Angular compiler version
into the generated metadata, and semantic version ranges are used
to select the appropriate partial linker.

PR Close #39847
This commit is contained in:
Pete Bacon Darwin
2020-11-25 21:03:02 +00:00
committed by Misko Hevery
parent 8b0cccca45
commit d2042a0da2
54 changed files with 463 additions and 373 deletions
+2
View File
@@ -10,6 +10,8 @@ ts_library(
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@types/semver",
"@npm//semver",
"@npm//typescript",
],
)
@@ -59,9 +59,9 @@ describe('createEs2015LinkerPlugin()', () => {
transformSync(
[
'var core;',
`ɵɵngDeclareDirective({version: 1, ngImport: core, x: 1});`,
`ɵɵngDeclareComponent({version: 1, ngImport: core, foo: () => ɵɵngDeclareDirective({version: 1, ngImport: core, x: 2})});`,
`x.qux(() => ɵɵngDeclareDirective({version: 1, ngImport: core, x: 3}));`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 1});`,
`ɵɵngDeclareComponent({version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 2})});`,
`x.qux(() => ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 3}));`,
'spread(...x);',
].join('\n'),
{
@@ -71,13 +71,13 @@ describe('createEs2015LinkerPlugin()', () => {
});
expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([
['ɵɵngDeclareDirective', '{version:1,ngImport:core,x:1}'],
['ɵɵngDeclareDirective', '{version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:1}'],
[
'ɵɵngDeclareComponent',
'{version:1,ngImport:core,foo:()=>ɵɵngDeclareDirective({version:1,ngImport:core,x:2})}'
'{version:\'0.0.0-PLACEHOLDER\',ngImport:core,foo:()=>ɵɵngDeclareDirective({version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:2})}'
],
// Note we do not process `x:2` declaration since it is nested within another declaration
['ɵɵngDeclareDirective', '{version:1,ngImport:core,x:3}']
['ɵɵngDeclareDirective', '{version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:3}']
]);
});
@@ -89,8 +89,8 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'var core;',
'ɵɵngDeclareDirective({version: 1, ngImport: core});',
'ɵɵngDeclareDirective({version: 1, ngImport: core, foo: () => bar({})});',
'ɵɵngDeclareDirective({version: \'0.0.0-PLACEHOLDER\', ngImport: core});',
'ɵɵngDeclareDirective({version: \'0.0.0-PLACEHOLDER\', ngImport: core, foo: () => bar({})});',
'x.qux();',
'spread(...x);',
].join('\n'),
@@ -109,9 +109,9 @@ describe('createEs2015LinkerPlugin()', () => {
[
'import * as core from \'some-module\';',
'import {id} from \'other-module\';',
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
@@ -130,9 +130,9 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'var core;',
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
@@ -151,9 +151,10 @@ describe('createEs2015LinkerPlugin()', () => {
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
const result = transformSync(
[
'function run(core) {', ` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, ngImport: core})`, '}'
'function run(core) {',
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`, '}'
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
@@ -172,9 +173,9 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'function run() {',
` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
'}',
].join('\n'),
{
@@ -197,7 +198,7 @@ describe('createEs2015LinkerPlugin()', () => {
spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO'));
const result = transformSync(
[
`ɵɵngDeclareDirective({version: 1, ngImport: core}); FOO;`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`,
].join('\n'),
{
plugins: [
@@ -235,7 +236,7 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'import * as core from \'some-module\';',
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
@@ -40,7 +40,8 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
*
* @param declarationFn the name of the function used to declare the partial declaration - e.g.
* `ɵɵngDeclareDirective`.
* @param args the arguments passed to the declaration function.
* @param args the arguments passed to the declaration function, should be a single object that
* corresponds to the `R3DeclareDirectiveMetadata` or `R3DeclareComponentMetadata` interfaces.
* @param declarationScope the scope that contains this call to the declaration function.
*/
linkPartialDeclaration(
@@ -56,7 +57,7 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
const ngImport = metaObj.getNode('ngImport');
const emitScope = this.getEmitScope(ngImport, declarationScope);
const version = metaObj.getNumber('version');
const version = metaObj.getString('version');
const linker = this.linkerSelector.getLinker(declarationFn, version);
const definition =
linker.linkPartialDeclaration(this.sourceUrl, this.code, emitScope.constantPool, metaObj);
@@ -15,6 +15,9 @@ import {AstObject} from '../../ast/ast_value';
export interface PartialLinker<TExpression> {
/**
* Link the partial declaration `metaObj` information to generate a full definition expression.
*
* @param metaObj An object that fits one of the `R3DeclareDirectiveMetadata` or
* `R3DeclareComponentMetadata` interfaces.
*/
linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool,
@@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {satisfies} from 'semver';
import {LinkerOptions} from '../linker_options';
import {PartialComponentLinkerVersion1} from './partial_component_linker_1';
@@ -12,13 +13,29 @@ import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
import {PartialLinker} from './partial_linker';
export class PartialLinkerSelector<TExpression> {
private linkers: Record<string, Record<number, PartialLinker<TExpression>>> = {
'ɵɵngDeclareDirective': {
1: new PartialDirectiveLinkerVersion1(),
},
'ɵɵngDeclareComponent': {
1: new PartialComponentLinkerVersion1(this.options),
},
/**
* A database of linker instances that should be used if their given semver range satisfies the
* version found in the code to be linked.
*
* Note that the ranges are checked in order, and the first matching range will be selected, so
* ranges should be most restrictive first.
*
* Also, ranges are matched to include "pre-releases", therefore if the range is `>=11.1.0-next.1`
* then this includes `11.1.0-next.2` and also `12.0.0-next.1`.
*
* Finally, note that we always start with the current version (i.e. `0.0.0-PLACEHOLDER`). This
* allows the linker to work on local builds effectively.
*/
private linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]> = {
'ɵɵngDeclareDirective': [
{range: '0.0.0-PLACEHOLDER', linker: new PartialDirectiveLinkerVersion1()},
{range: '>=11.1.0-next.1', linker: new PartialDirectiveLinkerVersion1()},
],
'ɵɵngDeclareComponent':
[
{range: '0.0.0-PLACEHOLDER', linker: new PartialComponentLinkerVersion1(this.options)},
{range: '>=11.1.0-next.1', linker: new PartialComponentLinkerVersion1(this.options)},
],
};
constructor(private options: LinkerOptions) {}
@@ -34,15 +51,18 @@ export class PartialLinkerSelector<TExpression> {
* Returns the `PartialLinker` that can handle functions with the given name and version.
* Throws an error if there is none.
*/
getLinker(functionName: string, version: number): PartialLinker<TExpression> {
getLinker(functionName: string, version: string): PartialLinker<TExpression> {
const versions = this.linkers[functionName];
if (versions === undefined) {
throw new Error(`Unknown partial declaration function ${functionName}.`);
}
const linker = versions[version];
if (linker === undefined) {
throw new Error(`Unsupported partial declaration version ${version} for ${functionName}.`);
for (const {range, linker} of versions) {
if (satisfies(version, range, {includePrerelease: true})) {
return linker;
}
}
return linker;
throw new Error(
`Unsupported partial declaration version ${version} for ${functionName}.\n` +
'Valid version ranges are:\n' + versions.map(v => ` - ${v.range}`).join('\n'));
}
}
@@ -38,7 +38,7 @@ describe('FileLinker', () => {
describe('linkPartialDeclaration()', () => {
it('should throw an error if the function name is not recognised', () => {
const {fileLinker} = createFileLinker();
const version = factory.createLiteral(1);
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'version', quoted: false, value: version},
@@ -80,7 +80,7 @@ describe('FileLinker', () => {
.and.returnValue(o.literal('compilation result'));
const ngImport = factory.createIdentifier('core');
const version = factory.createLiteral(1);
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: ngImport},
{propertyName: 'version', quoted: false, value: version},
@@ -104,7 +104,7 @@ describe('FileLinker', () => {
// constant statements.
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
{propertyName: 'version', quoted: false, value: factory.createLiteral(1)},
{propertyName: 'version', quoted: false, value: factory.createLiteral('0.0.0-PLACEHOLDER')},
]);
const replacement = fileLinker.linkPartialDeclaration(
@@ -127,7 +127,11 @@ describe('FileLinker', () => {
// statements to be emitted in an IIFE rather than added to the shared constant scope.
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')},
{propertyName: 'version', quoted: false, value: factory.createLiteral(1)},
{
propertyName: 'version',
quoted: false,
value: factory.createLiteral('0.0.0-PLACEHOLDER')
},
]);
const replacement = fileLinker.linkPartialDeclaration(
@@ -29,20 +29,62 @@ describe('PartialLinkerSelector', () => {
});
describe('getLinker()', () => {
it('should return the linker that matches the name and version number', () => {
it('should return the latest linker if the version is "0.0.0-PLACEHOLDER"', () => {
const selector = new PartialLinkerSelector(options);
expect(selector.getLinker('ɵɵngDeclareDirective', 1))
expect(selector.getLinker('ɵɵngDeclareDirective', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareComponent', 1))
expect(selector.getLinker('ɵɵngDeclareComponent', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialComponentLinkerVersion1);
});
it('should return the linker that matches the name and valid full version', () => {
const selector = new PartialLinkerSelector(options);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.2'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.2.5'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '12.0.0'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
});
it('should return the linker that matches the name and valid pre-release versions', () => {
const selector = new PartialLinkerSelector(options);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.1'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.7'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '12.0.0-next.7'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
});
it('should throw an error if there is no linker that matches the given name or version', () => {
const selector = new PartialLinkerSelector(options);
expect(() => selector.getLinker('$foo', 1))
// `$foo` is not a valid name, even though `0.0.0-PLACEHOLDER` is a valid version
expect(() => selector.getLinker('$foo', '0.0.0-PLACEHOLDER'))
.toThrowError('Unknown partial declaration function $foo.');
expect(() => selector.getLinker('ɵɵngDeclareDirective', 2))
.toThrowError('Unsupported partial declaration version 2 for ɵɵngDeclareDirective.');
// `$foo` is not a valid name, even though `11.1.0` is a valid version
expect(() => selector.getLinker('$foo', '11.1.0'))
.toThrowError('Unknown partial declaration function $foo.');
// There are no linkers earlier than 11.1.0-next.1
expect(() => selector.getLinker('ɵɵngDeclareDirective', '10.2.1'))
.toThrowError(
'Unsupported partial declaration version 10.2.1 for ɵɵngDeclareDirective.\n' +
'Valid version ranges are:\n' +
' - 0.0.0-PLACEHOLDER\n' +
' - >=11.1.0-next.1');
expect(() => selector.getLinker('ɵɵngDeclareDirective', '11.0.2'))
.toThrowError(
'Unsupported partial declaration version 11.0.2 for ɵɵngDeclareDirective.\n' +
'Valid version ranges are:\n' +
' - 0.0.0-PLACEHOLDER\n' +
' - >=11.1.0-next.1');
expect(() => selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.0'))
.toThrowError(
'Unsupported partial declaration version 11.1.0-next.0 for ɵɵngDeclareDirective.\n' +
'Valid version ranges are:\n' +
' - 0.0.0-PLACEHOLDER\n' +
' - >=11.1.0-next.1');
});
});
});