angular/packages/compiler-cli/test/transformers/inline_resources_spec.ts
Kristiyan Kostadinov ea61ec2562 feat(core): support TypeScript 4.4 (#43281)
Adds support for TypeScript 4.4. High-level overview of the changes made in this PR:

* Bumps the various packages to `typescript@4.4.2` and `tslib@2.3.0`.
* The `useUnknownInCatchVariables` compiler option has been disabled so that we don't have to cast error objects explicitly everywhere.
* TS now passes in a third argument to the `__spreadArray` call inside child class constructors. I had to update a couple of places in the runtime and ngcc to be able to pick up the calls correctly.
* TS now generates code like `(0, foo)(arg1, arg2)` for imported function calls. I had to update a few of our tests to account for it. See https://github.com/microsoft/TypeScript/pull/44624.
* Our `ngtsc` test setup calls the private `matchFiles` function from TS. I had to update our usage, because a new parameter was added.
* There was one place where we were setting the readonly `hasTrailingComma` property. I updated the usage to pass in the value when constructing the object instead.
* Some browser types were updated which meant that I had to resolve some trivial type errors.
* The downlevel decorators tranform was running into an issue where the Closure synthetic comments were being emitted twice. I've worked around it by recreating the class declaration node instead of cloning it.

PR Close #43281
2021-09-23 14:49:19 -07:00

194 lines
7.4 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 ts from 'typescript';
import {isClassMetadata, MetadataCollector} from '../../src/metadata/index';
import {getInlineResourcesTransformFactory, InlineResourcesMetadataTransformer} from '../../src/transformers/inline_resources';
import {MetadataCache} from '../../src/transformers/metadata_cache';
import {MockAotContext, MockCompilerHost} from '../mocks';
describe('inline resources transformer', () => {
describe('decorator input', () => {
describe('should not touch unrecognized decorators', () => {
it('Not from @angular/core', () => {
expect(convert(`declare const Component: Function;
@Component({templateUrl: './thing.html'}) class Foo {}`))
.toContain('templateUrl');
});
it('missing @ sign', () => {
expect(convert(`import {Component} from '@angular/core';
Component({templateUrl: './thing.html'}) class Foo {}`))
.toContain('templateUrl');
});
it('too many arguments to @Component', () => {
expect(convert(`import {Component} from '@angular/core';
@Component(1, {templateUrl: './thing.html'}) class Foo {}`))
.toContain('templateUrl');
});
it('wrong argument type to @Component', () => {
expect(convert(`import {Component} from '@angular/core';
@Component([{templateUrl: './thing.html'}]) class Foo {}`))
.toContain('templateUrl');
});
});
it('should replace templateUrl', () => {
const actual = convert(`import {Component} from '@angular/core';
@Component({
templateUrl: './thing.html',
otherProp: 3,
}) export class Foo {}`);
expect(actual).not.toContain('templateUrl:');
expect(actual.replace(/\s+/g, ' '))
.toContain(
'Foo = __decorate([ (0, core_1.Component)({ template: "Some template", otherProp: 3 }) ], Foo)');
});
it('should allow different quotes', () => {
const actual = convert(`import {Component} from '@angular/core';
@Component({"templateUrl": \`./thing.html\`}) export class Foo {}`);
expect(actual).not.toContain('templateUrl:');
expect(actual).toContain('{ template: "Some template" }');
});
it('should replace styleUrls', () => {
const actual = convert(`import {Component} from '@angular/core';
@Component({
styleUrls: ['./thing1.css', './thing2.css'],
})
export class Foo {}`);
expect(actual).not.toContain('styleUrls:');
expect(actual).toContain('styles: [".some_style {}", ".some_other_style {}"]');
});
it('should preserve existing styles', () => {
const actual = convert(`import {Component} from '@angular/core';
@Component({
styles: ['h1 { color: blue }'],
styleUrls: ['./thing1.css'],
})
export class Foo {}`);
expect(actual).not.toContain('styleUrls:');
expect(actual).toContain(`styles: ['h1 { color: blue }', ".some_style {}"]`);
});
it('should handle empty styleUrls', () => {
const actual = convert(`import {Component} from '@angular/core';
@Component({styleUrls: [], styles: []}) export class Foo {}`);
expect(actual).not.toContain('styleUrls:');
expect(actual).not.toContain('styles:');
});
});
describe('annotation input', () => {
it('should replace templateUrl', () => {
const actual = convert(`import {Component} from '@angular/core';
declare const NotComponent: Function;
export class Foo {
static decorators: {type: Function, args?: any[]}[] = [
{
type: NotComponent,
args: [],
},{
type: Component,
args: [{
templateUrl: './thing.html'
}],
}];
}
`);
expect(actual).not.toContain('templateUrl:');
expect(actual.replace(/\s+/g, ' '))
.toMatch(
/Foo\.decorators = [{ .*type: core_1\.Component, args: [{ template: "Some template" }]/);
});
it('should replace styleUrls', () => {
const actual = convert(`import {Component} from '@angular/core';
declare const NotComponent: Function;
export class Foo {
static decorators: {type: Function, args?: any[]}[] = [{
type: Component,
args: [{
styleUrls: ['./thing1.css', './thing2.css'],
}],
}];
}
`);
expect(actual).not.toContain('styleUrls:');
expect(actual.replace(/\s+/g, ' '))
.toMatch(
/Foo\.decorators = [{ .*type: core_1\.Component, args: [{ style: "Some template" }]/);
});
});
});
describe('metadata transformer', () => {
it('should transform decorators', () => {
const source = `import {Component} from '@angular/core';
@Component({
templateUrl: './thing.html',
styleUrls: ['./thing1.css', './thing2.css'],
styles: ['h1 { color: red }'],
})
export class Foo {}
`;
const sourceFile = ts.createSourceFile(
'someFile.ts', source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
const cache = new MetadataCache(
new MetadataCollector(), /* strict */ true,
[new InlineResourcesMetadataTransformer(
{loadResource, resourceNameToFileName: (u: string) => u})]);
const metadata = cache.getMetadata(sourceFile);
expect(metadata).toBeDefined('Expected metadata from test source file');
if (metadata) {
const classData = metadata.metadata['Foo'];
expect(classData && isClassMetadata(classData))
.toBeDefined(`Expected metadata to contain data for Foo`);
if (classData && isClassMetadata(classData)) {
expect(JSON.stringify(classData)).not.toContain('templateUrl');
expect(JSON.stringify(classData)).toContain('"template":"Some template"');
expect(JSON.stringify(classData)).not.toContain('styleUrls');
expect(JSON.stringify(classData))
.toContain('"styles":["h1 { color: red }",".some_style {}",".some_other_style {}"]');
}
}
});
});
function loadResource(path: string): Promise<string>|string {
if (path === './thing.html') return 'Some template';
if (path === './thing1.css') return '.some_style {}';
if (path === './thing2.css') return '.some_other_style {}';
throw new Error('No fake data for path ' + path);
}
function convert(source: string) {
const baseFileName = 'someFile';
const moduleName = '/' + baseFileName;
const fileName = moduleName + '.ts';
const context = new MockAotContext('/', {[baseFileName + '.ts']: source});
const host = new MockCompilerHost(context);
const program = ts.createProgram(
[fileName], {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2017,
},
host);
const moduleSourceFile = program.getSourceFile(fileName);
const transformers: ts.CustomTransformers = {
before: [getInlineResourcesTransformFactory(
program, {loadResource, resourceNameToFileName: (u: string) => u})]
};
let result = '';
program.emit(
moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => {
if (fileName.startsWith(moduleName)) {
result = data;
}
}, undefined, undefined, transformers);
return result;
}