refactor(@ngtools/webpack): use ES modules imports instead of require for resources

With this change we update the replace_resources transformer for styles and templates to use ES6 module import syntax instead of the CommonJs require syntax.
This commit is contained in:
Alan Agius 2020-08-17 12:09:28 +02:00
parent 439385fef6
commit 7e61677d0b
2 changed files with 108 additions and 127 deletions

View File

@ -7,18 +7,6 @@
*/
import * as ts from 'typescript';
// emit helper for `import Name from "foo"`
// importName is marked as an internal property but is needed for the tslib import.
const importDefaultHelper: ts.UnscopedEmitHelper & { importName?: string } = {
name: 'typescript:commonjsimportdefault',
importName: '__importDefault',
scoped: false,
text: `
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};`,
};
export function replaceResources(
shouldTransform: (fileName: string) => boolean,
getTypeChecker: () => ts.TypeChecker,
@ -26,12 +14,13 @@ export function replaceResources(
): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
const typeChecker = getTypeChecker();
const resourceImportDeclarations: ts.ImportDeclaration[] = [];
const visitNode: ts.Visitor = (node: ts.Node) => {
if (ts.isClassDeclaration(node)) {
const decorators = ts.visitNodes(node.decorators, (node) =>
const decorators = ts.visitNodes(node.decorators, node =>
ts.isDecorator(node)
? visitDecorator(context, node, typeChecker, directTemplateLoading)
? visitDecorator(node, typeChecker, directTemplateLoading, resourceImportDeclarations)
: node,
);
@ -50,20 +39,35 @@ export function replaceResources(
};
return (sourceFile: ts.SourceFile) => {
if (shouldTransform(sourceFile.fileName)) {
return ts.visitNode(sourceFile, visitNode);
if (!shouldTransform(sourceFile.fileName)) {
return sourceFile;
}
return sourceFile;
const updatedSourceFile = ts.visitNode(sourceFile, visitNode);
if (resourceImportDeclarations.length) {
// Add resource imports
return ts.updateSourceFileNode(
updatedSourceFile,
ts.setTextRange(
ts.createNodeArray([
...resourceImportDeclarations,
...updatedSourceFile.statements,
]),
updatedSourceFile.statements,
),
);
}
return updatedSourceFile;
};
};
}
function visitDecorator(
context: ts.TransformationContext,
node: ts.Decorator,
typeChecker: ts.TypeChecker,
directTemplateLoading: boolean,
resourceImportDeclarations: ts.ImportDeclaration[],
): ts.Decorator {
if (!isComponentDecorator(node, typeChecker)) {
return node;
@ -84,9 +88,9 @@ function visitDecorator(
const styleReplacements: ts.Expression[] = [];
// visit all properties
let properties = ts.visitNodes(objectExpression.properties, (node) =>
let properties = ts.visitNodes(objectExpression.properties, node =>
ts.isObjectLiteralElementLike(node)
? visitComponentMetadata(context, node, styleReplacements, directTemplateLoading)
? visitComponentMetadata(node, styleReplacements, directTemplateLoading, resourceImportDeclarations)
: node,
);
@ -109,10 +113,10 @@ function visitDecorator(
}
function visitComponentMetadata(
context: ts.TransformationContext,
node: ts.ObjectLiteralElementLike,
styleReplacements: ts.Expression[],
directTemplateLoading: boolean,
resourceImportDeclarations: ts.ImportDeclaration[],
): ts.ObjectLiteralElementLike | undefined {
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
return node;
@ -124,16 +128,16 @@ function visitComponentMetadata(
return undefined;
case 'templateUrl':
const importName = createResourceImport(node.initializer, directTemplateLoading ? '!raw-loader!' : '', resourceImportDeclarations);
if (!importName) {
return node;
}
return ts.updatePropertyAssignment(
node,
ts.createIdentifier('template'),
createRequireExpression(
context,
node.initializer,
directTemplateLoading ? '!raw-loader!' : '',
),
importName,
);
case 'styles':
case 'styleUrls':
if (!ts.isArrayLiteralExpression(node.initializer)) {
@ -141,14 +145,16 @@ function visitComponentMetadata(
}
const isInlineStyles = name === 'styles';
const styles = ts.visitNodes(node.initializer.elements, (node) => {
const styles = ts.visitNodes(node.initializer.elements, node => {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
return node;
}
return isInlineStyles
? ts.createLiteral(node.text)
: createRequireExpression(context, node);
if (isInlineStyles) {
return ts.createLiteral(node.text);
}
return createResourceImport(node, undefined, resourceImportDeclarations) || node;
});
// Styles should be placed first
@ -159,12 +165,33 @@ function visitComponentMetadata(
}
return undefined;
default:
return node;
}
}
export function createResourceImport(
node: ts.Node,
loader: string | undefined,
resourceImportDeclarations: ts.ImportDeclaration[],
): ts.Identifier | null {
const url = getResourceUrl(node, loader);
if (!url) {
return null;
}
const importName = ts.createIdentifier(`__NG_CLI_RESOURCE__${resourceImportDeclarations.length}`);
resourceImportDeclarations.push(ts.createImportDeclaration(
undefined,
undefined,
ts.createImportClause(importName, undefined),
ts.createLiteral(url),
));
return importName;
}
export function getResourceUrl(node: ts.Node, loader = ''): string | null {
// only analyze strings
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
@ -187,35 +214,6 @@ function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node
return false;
}
function createRequireExpression(
context: ts.TransformationContext,
node: ts.Expression,
loader?: string,
): ts.Expression {
const url = getResourceUrl(node, loader);
if (!url) {
return node;
}
context.requestEmitHelper(importDefaultHelper);
const callExpression = ts.createCall(ts.createIdentifier('require'), undefined, [
ts.createLiteral(url),
]);
return ts.createPropertyAccess(
ts.createCall(
ts.setEmitFlags(
ts.createIdentifier('__importDefault'),
ts.EmitFlags.HelperName | ts.EmitFlags.AdviseOnEmitNode,
),
undefined,
[callExpression],
),
'default',
);
}
interface DecoratorOrigin {
name: string;
module: string;

View File

@ -42,8 +42,12 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import __NG_CLI_RESOURCE__2 from "./app.component.2.css";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
this.title = 'app';
@ -52,8 +56,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
})
], AppComponent);
export { AppComponent };
@ -63,49 +67,6 @@ describe('@ngtools/webpack transformers', () => {
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});
it(`should replace resources and add helper when 'importHelpers' is false`, () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css', './app.component.2.css']
})
export class AppComponent {
title = 'app';
}
`;
const output = tags.stripIndent`
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null
? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object"
&& typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--)
if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; };
var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; };
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
this.title = 'app';
}
};
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default]
})
], AppComponent);
export { AppComponent };
`;
const result = transform(input, undefined, undefined, false);
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});
it('should not replace resources when directTemplateLoading is false', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';
@ -123,7 +84,10 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import __NG_CLI_RESOURCE__2 from "./app.component.2.css";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
@ -133,8 +97,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
})
], AppComponent);
export { AppComponent };
@ -158,7 +122,8 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.svg";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
@ -168,7 +133,7 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.svg")).default
template: __NG_CLI_RESOURCE__0
})
], AppComponent);
export { AppComponent };
@ -193,8 +158,11 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
this.title = 'app';
@ -203,8 +171,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: ["a { color: red }", __importDefault(require("./app.component.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: ["a { color: red }", __NG_CLI_RESOURCE__1]
})
], AppComponent);
export { AppComponent };
@ -228,7 +196,11 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = `
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import __NG_CLI_RESOURCE__2 from "./app.component.2.css";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
@ -238,8 +210,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
})
], AppComponent);
export { AppComponent };
@ -263,8 +235,12 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import __NG_CLI_RESOURCE__2 from "./app.component.2.css";
import { Component as NgComponent } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
this.title = 'app';
@ -273,8 +249,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
NgComponent({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
})
], AppComponent);
export { AppComponent };
@ -302,7 +278,11 @@ describe('@ngtools/webpack transformers', () => {
}
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import __NG_CLI_RESOURCE__2 from "./app.component.2.css";
import * as ng from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
@ -312,8 +292,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
ng.Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
})
], AppComponent);
export { AppComponent };
@ -343,7 +323,10 @@ describe('@ngtools/webpack transformers', () => {
`;
const output = tags.stripIndent`
import { __decorate, __importDefault } from "tslib";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "./app.component.css";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
@ -360,8 +343,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default,
styles: [__importDefault(require("./app.component.css")).default]
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1]
})
], AppComponent);
export { AppComponent };