Previously, it was not possible to block a partial-linker from trying to process a declaration that was defined in a newer version of Angular than that of the partial-linker. For example, if a partial-linker was published as part of version 12.0.0, there was no way for a partially-compiled declaration compiled via version 13.0.0 to tell the 12.0.0 linker that it would be invalid to attempt to process it. This commit adds a new `minVersion` property to partial-declarations, which is interpreted as the "minimum partial-linker version" that can process this declaration. When selecting a partial-linker for such a declaration, the known linker version ranges are checked to find the most recent linker whose version range has an overlap with the interpreted declaration range. This approach allows us to set a minimum version for a declaration, which can inform an old partial-linker that will it not be able to accurately process the declaration. Note that any pre-release part to versions are ignored in this selection process. The file-linker can be configured, via the `unknownDeclarationVersionHandling` property of `LinkerOptions`, to handle such a situation in one of three ways: - `error` - the version mismatch is a fatal error - `warn` - a warning is sent to the logger but the most recent partial-linker will attempt to process the declaration anyway. - `ignore` - the most recent partial-linker will, silently, attempt to process the declaration. The default is to throw an error. Closes #41497 PR Close #41578
		
			
				
	
	
		
			231 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * @license
 | 
						|
 * Copyright Google LLC All Rights Reserved.
 | 
						|
 *
 | 
						|
 * 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 * as o from '@angular/compiler/src/output/output_ast';
 | 
						|
import * as ts from 'typescript';
 | 
						|
 | 
						|
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
 | 
						|
import {MockLogger} from '../../../src/ngtsc/logging/testing';
 | 
						|
import {TypeScriptAstFactory} from '../../../src/ngtsc/translator';
 | 
						|
import {AstHost} from '../../src/ast/ast_host';
 | 
						|
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
 | 
						|
import {DeclarationScope} from '../../src/file_linker/declaration_scope';
 | 
						|
import {FileLinker} from '../../src/file_linker/file_linker';
 | 
						|
import {LinkerEnvironment} from '../../src/file_linker/linker_environment';
 | 
						|
import {DEFAULT_LINKER_OPTIONS} from '../../src/file_linker/linker_options';
 | 
						|
import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1';
 | 
						|
 | 
						|
import {generate} from './helpers';
 | 
						|
 | 
						|
describe('FileLinker', () => {
 | 
						|
  let factory: TypeScriptAstFactory;
 | 
						|
  beforeEach(() => factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false));
 | 
						|
 | 
						|
  describe('isPartialDeclaration()', () => {
 | 
						|
    it('should return true if the callee is recognized', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      expect(fileLinker.isPartialDeclaration('ɵɵngDeclareDirective')).toBe(true);
 | 
						|
      expect(fileLinker.isPartialDeclaration('ɵɵngDeclareComponent')).toBe(true);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return false if the callee is not recognized', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      expect(fileLinker.isPartialDeclaration('$foo')).toBe(false);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('linkPartialDeclaration()', () => {
 | 
						|
    it('should throw an error if the function name is not recognised', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      const version = factory.createLiteral('0.0.0-PLACEHOLDER');
 | 
						|
      const ngImport = factory.createIdentifier('core');
 | 
						|
      const declarationArg = factory.createObjectLiteral([
 | 
						|
        {propertyName: 'minVersion', quoted: false, value: version},
 | 
						|
        {propertyName: 'version', quoted: false, value: version},
 | 
						|
        {propertyName: 'ngImport', quoted: false, value: ngImport},
 | 
						|
      ]);
 | 
						|
      expect(
 | 
						|
          () => fileLinker.linkPartialDeclaration(
 | 
						|
              'foo', [declarationArg], new MockDeclarationScope()))
 | 
						|
          .toThrowError('Unknown partial declaration function foo.');
 | 
						|
    });
 | 
						|
 | 
						|
    it('should throw an error if the metadata object does not have a `minVersion` property', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      const version = factory.createLiteral('0.0.0-PLACEHOLDER');
 | 
						|
      const ngImport = factory.createIdentifier('core');
 | 
						|
      const declarationArg = factory.createObjectLiteral([
 | 
						|
        {propertyName: 'version', quoted: false, value: version},
 | 
						|
        {propertyName: 'ngImport', quoted: false, value: ngImport},
 | 
						|
      ]);
 | 
						|
      expect(
 | 
						|
          () => fileLinker.linkPartialDeclaration(
 | 
						|
              'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()))
 | 
						|
          .toThrowError(`Expected property 'minVersion' to be present.`);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should throw an error if the metadata object does not have a `version` property', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      const version = factory.createLiteral('0.0.0-PLACEHOLDER');
 | 
						|
      const ngImport = factory.createIdentifier('core');
 | 
						|
      const declarationArg = factory.createObjectLiteral([
 | 
						|
        {propertyName: 'minVersion', quoted: false, value: version},
 | 
						|
        {propertyName: 'ngImport', quoted: false, value: ngImport},
 | 
						|
      ]);
 | 
						|
      expect(
 | 
						|
          () => fileLinker.linkPartialDeclaration(
 | 
						|
              'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()))
 | 
						|
          .toThrowError(`Expected property 'version' to be present.`);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should throw an error if the metadata object does not have a `ngImport` property', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      const version = factory.createLiteral('0.0.0-PLACEHOLDER');
 | 
						|
      const declarationArg = factory.createObjectLiteral([
 | 
						|
        {propertyName: 'minVersion', quoted: false, value: version},
 | 
						|
        {propertyName: 'version', quoted: false, value: version},
 | 
						|
      ]);
 | 
						|
      expect(
 | 
						|
          () => fileLinker.linkPartialDeclaration(
 | 
						|
              'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()))
 | 
						|
          .toThrowError(`Expected property 'ngImport' to be present.`);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should call `linkPartialDeclaration()` on the appropriate partial compiler', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      const compileSpy = spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
 | 
						|
                             .and.returnValue(o.literal('compilation result'));
 | 
						|
 | 
						|
      const ngImport = factory.createIdentifier('core');
 | 
						|
      const version = factory.createLiteral('0.0.0-PLACEHOLDER');
 | 
						|
      const declarationArg = factory.createObjectLiteral([
 | 
						|
        {propertyName: 'ngImport', quoted: false, value: ngImport},
 | 
						|
        {propertyName: 'minVersion', quoted: false, value: version},
 | 
						|
        {propertyName: 'version', quoted: false, value: version},
 | 
						|
      ]);
 | 
						|
 | 
						|
      const compilationResult = fileLinker.linkPartialDeclaration(
 | 
						|
          'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope());
 | 
						|
 | 
						|
      expect(compilationResult).toEqual(factory.createLiteral('compilation result'));
 | 
						|
      expect(compileSpy).toHaveBeenCalled();
 | 
						|
      expect(compileSpy.calls.mostRecent().args[1].getNode('ngImport')).toBe(ngImport);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('getConstantStatements()', () => {
 | 
						|
    it('should capture shared constant values', () => {
 | 
						|
      const {fileLinker} = createFileLinker();
 | 
						|
      spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
 | 
						|
 | 
						|
      // Here we use the `core` idenfifier for `ngImport` to trigger the use of a shared scope for
 | 
						|
      // constant statements.
 | 
						|
      const declarationArg = factory.createObjectLiteral([
 | 
						|
        {propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
 | 
						|
        {
 | 
						|
          propertyName: 'minVersion',
 | 
						|
          quoted: false,
 | 
						|
          value: factory.createLiteral('0.0.0-PLACEHOLDER')
 | 
						|
        },
 | 
						|
        {propertyName: 'version', quoted: false, value: factory.createLiteral('0.0.0-PLACEHOLDER')},
 | 
						|
      ]);
 | 
						|
 | 
						|
      const replacement = fileLinker.linkPartialDeclaration(
 | 
						|
          'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope());
 | 
						|
      expect(generate(replacement)).toEqual('"REPLACEMENT"');
 | 
						|
 | 
						|
      const results = fileLinker.getConstantStatements();
 | 
						|
      expect(results.length).toEqual(1);
 | 
						|
      const {constantScope, statements} = results[0];
 | 
						|
      expect(constantScope).toBe(MockConstantScopeRef.singleton);
 | 
						|
      expect(statements.map(generate)).toEqual(['const _c0 = [1];']);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should be no shared constant statements to capture when they are emitted into the replacement IIFE',
 | 
						|
       () => {
 | 
						|
         const {fileLinker} = createFileLinker();
 | 
						|
         spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
 | 
						|
 | 
						|
         // Here we use a string literal `"not-a-module"` for `ngImport` to cause constant
 | 
						|
         // 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: 'minVersion',
 | 
						|
             quoted: false,
 | 
						|
             value: factory.createLiteral('0.0.0-PLACEHOLDER')
 | 
						|
           },
 | 
						|
           {
 | 
						|
             propertyName: 'version',
 | 
						|
             quoted: false,
 | 
						|
             value: factory.createLiteral('0.0.0-PLACEHOLDER')
 | 
						|
           },
 | 
						|
         ]);
 | 
						|
 | 
						|
         const replacement = fileLinker.linkPartialDeclaration(
 | 
						|
             'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope());
 | 
						|
         expect(generate(replacement))
 | 
						|
             .toEqual('function () { const _c0 = [1]; return "REPLACEMENT"; }()');
 | 
						|
 | 
						|
         const results = fileLinker.getConstantStatements();
 | 
						|
         expect(results.length).toEqual(0);
 | 
						|
       });
 | 
						|
  });
 | 
						|
 | 
						|
  function createFileLinker(): {
 | 
						|
    host: AstHost<ts.Expression>,
 | 
						|
    fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>
 | 
						|
  } {
 | 
						|
    const fs = new MockFileSystemNative();
 | 
						|
    const logger = new MockLogger();
 | 
						|
    const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
 | 
						|
        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');
 | 
						|
    return {host: linkerEnvironment.host, fileLinker};
 | 
						|
  }
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 * This mock implementation of `DeclarationScope` will return a singleton instance of
 | 
						|
 * `MockConstantScopeRef` if the expression is an identifier, or `null` otherwise.
 | 
						|
 *
 | 
						|
 * This way we can simulate whether the constants will be shared or inlined into an IIFE.
 | 
						|
 */
 | 
						|
class MockDeclarationScope implements DeclarationScope<MockConstantScopeRef, ts.Expression> {
 | 
						|
  getConstantScopeRef(expression: ts.Expression): MockConstantScopeRef|null {
 | 
						|
    if (ts.isIdentifier(expression)) {
 | 
						|
      return MockConstantScopeRef.singleton;
 | 
						|
    } else {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class MockConstantScopeRef {
 | 
						|
  private constructor() {}
 | 
						|
  static singleton = new MockDeclarationScope();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Spy on the `PartialDirectiveLinkerVersion1.linkPartialDeclaration()` method, triggering
 | 
						|
 * shared constants to be created.
 | 
						|
 */
 | 
						|
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
 | 
						|
  let callCount = 0;
 | 
						|
  spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
 | 
						|
      .and.callFake((constantPool => {
 | 
						|
                      const constArray = o.literalArr([o.literal(++callCount)]);
 | 
						|
                      // We have to add the constant twice or it will not create a shared statement
 | 
						|
                      constantPool.getConstLiteral(constArray);
 | 
						|
                      constantPool.getConstLiteral(constArray);
 | 
						|
                      return replacement;
 | 
						|
                    }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration);
 | 
						|
}
 |