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'; 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( export function replaceResources(
shouldTransform: (fileName: string) => boolean, shouldTransform: (fileName: string) => boolean,
getTypeChecker: () => ts.TypeChecker, getTypeChecker: () => ts.TypeChecker,
@ -26,12 +14,13 @@ export function replaceResources(
): ts.TransformerFactory<ts.SourceFile> { ): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => { return (context: ts.TransformationContext) => {
const typeChecker = getTypeChecker(); const typeChecker = getTypeChecker();
const resourceImportDeclarations: ts.ImportDeclaration[] = [];
const visitNode: ts.Visitor = (node: ts.Node) => { const visitNode: ts.Visitor = (node: ts.Node) => {
if (ts.isClassDeclaration(node)) { if (ts.isClassDeclaration(node)) {
const decorators = ts.visitNodes(node.decorators, (node) => const decorators = ts.visitNodes(node.decorators, node =>
ts.isDecorator(node) ts.isDecorator(node)
? visitDecorator(context, node, typeChecker, directTemplateLoading) ? visitDecorator(node, typeChecker, directTemplateLoading, resourceImportDeclarations)
: node, : node,
); );
@ -50,20 +39,35 @@ export function replaceResources(
}; };
return (sourceFile: ts.SourceFile) => { return (sourceFile: ts.SourceFile) => {
if (shouldTransform(sourceFile.fileName)) { if (!shouldTransform(sourceFile.fileName)) {
return ts.visitNode(sourceFile, visitNode); 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( function visitDecorator(
context: ts.TransformationContext,
node: ts.Decorator, node: ts.Decorator,
typeChecker: ts.TypeChecker, typeChecker: ts.TypeChecker,
directTemplateLoading: boolean, directTemplateLoading: boolean,
resourceImportDeclarations: ts.ImportDeclaration[],
): ts.Decorator { ): ts.Decorator {
if (!isComponentDecorator(node, typeChecker)) { if (!isComponentDecorator(node, typeChecker)) {
return node; return node;
@ -84,9 +88,9 @@ function visitDecorator(
const styleReplacements: ts.Expression[] = []; const styleReplacements: ts.Expression[] = [];
// visit all properties // visit all properties
let properties = ts.visitNodes(objectExpression.properties, (node) => let properties = ts.visitNodes(objectExpression.properties, node =>
ts.isObjectLiteralElementLike(node) ts.isObjectLiteralElementLike(node)
? visitComponentMetadata(context, node, styleReplacements, directTemplateLoading) ? visitComponentMetadata(node, styleReplacements, directTemplateLoading, resourceImportDeclarations)
: node, : node,
); );
@ -109,10 +113,10 @@ function visitDecorator(
} }
function visitComponentMetadata( function visitComponentMetadata(
context: ts.TransformationContext,
node: ts.ObjectLiteralElementLike, node: ts.ObjectLiteralElementLike,
styleReplacements: ts.Expression[], styleReplacements: ts.Expression[],
directTemplateLoading: boolean, directTemplateLoading: boolean,
resourceImportDeclarations: ts.ImportDeclaration[],
): ts.ObjectLiteralElementLike | undefined { ): ts.ObjectLiteralElementLike | undefined {
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) { if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
return node; return node;
@ -124,16 +128,16 @@ function visitComponentMetadata(
return undefined; return undefined;
case 'templateUrl': case 'templateUrl':
const importName = createResourceImport(node.initializer, directTemplateLoading ? '!raw-loader!' : '', resourceImportDeclarations);
if (!importName) {
return node;
}
return ts.updatePropertyAssignment( return ts.updatePropertyAssignment(
node, node,
ts.createIdentifier('template'), ts.createIdentifier('template'),
createRequireExpression( importName,
context,
node.initializer,
directTemplateLoading ? '!raw-loader!' : '',
),
); );
case 'styles': case 'styles':
case 'styleUrls': case 'styleUrls':
if (!ts.isArrayLiteralExpression(node.initializer)) { if (!ts.isArrayLiteralExpression(node.initializer)) {
@ -141,14 +145,16 @@ function visitComponentMetadata(
} }
const isInlineStyles = name === 'styles'; 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)) { if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
return node; return node;
} }
return isInlineStyles if (isInlineStyles) {
? ts.createLiteral(node.text) return ts.createLiteral(node.text);
: createRequireExpression(context, node); }
return createResourceImport(node, undefined, resourceImportDeclarations) || node;
}); });
// Styles should be placed first // Styles should be placed first
@ -159,12 +165,33 @@ function visitComponentMetadata(
} }
return undefined; return undefined;
default: default:
return node; 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 { export function getResourceUrl(node: ts.Node, loader = ''): string | null {
// only analyze strings // only analyze strings
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) { if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
@ -187,35 +214,6 @@ function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node
return false; 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 { interface DecoratorOrigin {
name: string; name: string;
module: string; module: string;

View File

@ -42,8 +42,12 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = tags.stripIndent` 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'; import { Component } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
this.title = 'app'; this.title = 'app';
@ -52,8 +56,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
Component({ Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default] styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -63,49 +67,6 @@ describe('@ngtools/webpack transformers', () => {
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); 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', () => { it('should not replace resources when directTemplateLoading is false', () => {
const input = tags.stripIndent` const input = tags.stripIndent`
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@ -123,7 +84,10 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = tags.stripIndent` 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'; import { Component } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
@ -133,8 +97,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
Component({ Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default] styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -158,7 +122,8 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = tags.stripIndent` 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'; import { Component } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
@ -168,7 +133,7 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
Component({ Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.svg")).default template: __NG_CLI_RESOURCE__0
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -193,8 +158,11 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = tags.stripIndent` 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'; import { Component } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
this.title = 'app'; this.title = 'app';
@ -203,8 +171,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
Component({ Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: ["a { color: red }", __importDefault(require("./app.component.css")).default] styles: ["a { color: red }", __NG_CLI_RESOURCE__1]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -228,7 +196,11 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = ` 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'; import { Component } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
@ -238,8 +210,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
Component({ Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default] styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -263,8 +235,12 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = tags.stripIndent` 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'; import { Component as NgComponent } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
this.title = 'app'; this.title = 'app';
@ -273,8 +249,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
NgComponent({ NgComponent({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default] styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -302,7 +278,11 @@ describe('@ngtools/webpack transformers', () => {
} }
`; `;
const output = tags.stripIndent` 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'; import * as ng from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
constructor() { constructor() {
@ -312,8 +292,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
ng.Component({ ng.Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: [__importDefault(require("./app.component.css")).default, __importDefault(require("./app.component.2.css")).default] styles: [__NG_CLI_RESOURCE__1, __NG_CLI_RESOURCE__2]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };
@ -343,7 +323,10 @@ describe('@ngtools/webpack transformers', () => {
`; `;
const output = tags.stripIndent` 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'; import { Component } from '@angular/core';
let AppComponent = class AppComponent { let AppComponent = class AppComponent {
@ -360,8 +343,8 @@ describe('@ngtools/webpack transformers', () => {
AppComponent = __decorate([ AppComponent = __decorate([
Component({ Component({
selector: 'app-root', selector: 'app-root',
template: __importDefault(require("!raw-loader!./app.component.html")).default, template: __NG_CLI_RESOURCE__0,
styles: [__importDefault(require("./app.component.css")).default] styles: [__NG_CLI_RESOURCE__1]
}) })
], AppComponent); ], AppComponent);
export { AppComponent }; export { AppComponent };