angular/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts
Paul Gschwendtner 8d7f1098d8 refactor: make all imports compatible with ESM/CJS output. (#43431)
As outlined in the previous commit which enabled the `esModuleInterop`
TypeScript compiler option, we need to update all namespace imports
for `typescript` to default imports. This is needed to allow for
TypeScript to be imported at runtime from an ES module.

Similar changes are needed for modules like `semver` where the types incorrectly
suggest named exports that will not exist at runtime when imported from ESM.

This commit refactors all imports to match with the lint rule we have
configured in the previous commit. See the previous commit for more
details on why certain imports have been changed.

A special case are the imports to `@babel/core` and `@babel/types`. For
these a special interop is needed as both default imports, or named
imports break the other module format. e.g default imports would work
well for ESM, but it breaks for CJS. For CJS, the named imports would
only work, but in ESM, only the default export exist. We work around
this for now until the devmode is using ESM as well (which would be
consistent with prodmode and gives us more valuable test results). More
details on the interop can be found in the `babel_core.ts` files (two
interops are needed for both localize/or the compiler-cli).

PR Close #43431
2021-10-01 18:28:45 +00:00

244 lines
9.9 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 ts from 'typescript';
import {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {DefaultMigrationHost} from '../../src/analysis/migration_host';
import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {createComponentDecorator} from '../../src/migrations/utils';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {makeTestEntryPointBundle} from '../helpers/utils';
import {getTraitDiagnostics} from '../host/util';
runInEachFileSystem(() => {
describe('DefaultMigrationHost', () => {
let _: typeof absoluteFrom;
let mockMetadata: any = {};
let mockEvaluator: any = {};
let mockClazz: any;
let injectedDecorator: any = {name: 'InjectedDecorator'};
beforeEach(() => {
_ = absoluteFrom;
const mockSourceFile: any = {
fileName: _('/node_modules/some-package/entry-point/test-file.js'),
};
mockClazz = {
name: {text: 'MockClazz'},
getSourceFile: () => mockSourceFile,
getStart: () => 0,
getWidth: () => 0,
};
});
function createMigrationHost({entryPoint, handlers}: {
entryPoint: EntryPointBundle;
handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[]
}) {
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
const compiler = new NgccTraitCompiler(handlers, reflectionHost);
const host = new DefaultMigrationHost(
reflectionHost, mockMetadata, mockEvaluator, compiler, entryPoint.entryPoint.path);
return {compiler, host};
}
describe('injectSyntheticDecorator()', () => {
it('should add the injected decorator into the compilation', () => {
const handler = new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
host.injectSyntheticDecorator(mockClazz, injectedDecorator);
const record = compiler.recordFor(mockClazz)!;
expect(record).toBeDefined();
expect(record.traits.length).toBe(1);
expect(record.traits[0].detected.decorator).toBe(injectedDecorator);
});
it('should mention the migration that failed in the diagnostics message', () => {
const handler = new DiagnosticProducingHandler();
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
host.injectSyntheticDecorator(mockClazz, decorator);
const record = compiler.recordFor(mockClazz)!;
const migratedTrait = record.traits[0];
const diagnostics = getTraitDiagnostics(migratedTrait);
if (diagnostics === null) {
return fail('Expected migrated class trait to be in an error state');
}
expect(diagnostics.length).toBe(1);
expect(ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n'))
.toEqual(
`test diagnostic\n` +
` Occurs for @Component decorator inserted by an automatic migration\n` +
` @Component({ template: "", selector: "comp" })`);
});
});
describe('getAllDecorators', () => {
it('should include injected decorators', () => {
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
loadTestFiles([{
name: _('/node_modules/test/index.js'),
contents: `
import {Directive} from '@angular/core';
export class MyClass {};
MyClass.decorators = [{ type: Directive }];
`
}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const {host, compiler} =
createMigrationHost({entryPoint, handlers: [directiveHandler, injectedHandler]});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
compiler.analyzeFile(entryPoint.src.file);
host.injectSyntheticDecorator(myClass, injectedDecorator);
const decorators = host.getAllDecorators(myClass)!;
expect(decorators.length).toBe(2);
expect(decorators[0].name).toBe('Directive');
expect(decorators[1].name).toBe('InjectedDecorator');
});
});
describe('isInScope', () => {
it('should be true for nodes within the entry-point', () => {
loadTestFiles([
{name: _('/node_modules/test/index.js'), contents: `export * from './internal';`},
{name: _('/node_modules/test/internal.js'), contents: `export class InternalClass {}`},
]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const {host} = createMigrationHost({entryPoint, handlers: []});
const internalClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/internal.js'), 'InternalClass',
isNamedClassDeclaration);
expect(host.isInScope(internalClass)).toBe(true);
});
it('should be false for nodes outside the entry-point (in sibling package)', () => {
loadTestFiles([
{name: _('/node_modules/external/index.js'), contents: `export class ExternalClass {}`},
{
name: _('/node_modules/test/index.js'),
contents: `
export {ExternalClass} from 'external';
export class InternalClass {}
`
},
]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const {host} = createMigrationHost({entryPoint, handlers: []});
const externalClass = getDeclaration(
entryPoint.src.program, _('/node_modules/external/index.js'), 'ExternalClass',
isNamedClassDeclaration);
expect(host.isInScope(externalClass)).toBe(false);
});
it('should be false for nodes outside the entry-point (in nested `node_modules/`)', () => {
loadTestFiles([
{
name: _('/node_modules/test/index.js'),
contents: `
export {NestedDependencyClass} from 'nested';
export class InternalClass {}
`,
},
{
name: _('/node_modules/test/node_modules/nested/index.js'),
contents: `export class NestedDependencyClass {}`,
},
]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const {host} = createMigrationHost({entryPoint, handlers: []});
const nestedDepClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/node_modules/nested/index.js'),
'NestedDependencyClass', isNamedClassDeclaration);
expect(host.isInScope(nestedDepClass)).toBe(false);
});
});
});
});
class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
readonly name = DetectDecoratorHandler.name;
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
if (decorators === null) {
return undefined;
}
const decorator = decorators.find(decorator => decorator.name === this.decorator);
if (decorator === undefined) {
return undefined;
}
return {trigger: node, decorator, metadata: {}};
}
analyze(node: ClassDeclaration): AnalysisOutput<unknown> {
return {};
}
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
return [];
}
}
class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
readonly name = DiagnosticProducingHandler.name;
readonly precedence = HandlerPrecedence.PRIMARY;
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
const decorator = decorators !== null ? decorators[0] : null;
return {trigger: node, decorator, metadata: {}};
}
analyze(node: ClassDeclaration): AnalysisOutput<any> {
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
}
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
return [];
}
}