Skip to content

Commit 6f8c336

Browse files
filipesilvamgechev
authored andcommitted
fix(@angular-devkit/build-optimizer): identify relative imports in angular core
Build optimizer was broken for non-FESM files inside @angular/core because it couldn't identify relative imports were still inside core. This change adds a known list of angular core files as a default, and also allows passing in a override.
1 parent 07ceb05 commit 6f8c336

File tree

6 files changed

+127
-20
lines changed

6 files changed

+127
-20
lines changed

etc/api/angular_devkit/build_optimizer/src/_golden-api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export declare function getPrefixFunctionsTransformer(): ts.TransformerFactory<t
1212

1313
export declare function getScrubFileTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile>;
1414

15+
export declare function getScrubFileTransformerForCore(program: ts.Program): ts.TransformerFactory<ts.SourceFile>;
16+
1517
export declare function getWrapEnumsTransformer(): ts.TransformerFactory<ts.SourceFile>;
1618

1719
export declare function testImportTslib(content: string): boolean;

packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import { getFoldFileTransformer } from '../transforms/class-fold';
1515
import { getImportTslibTransformer, testImportTslib } from '../transforms/import-tslib';
1616
import { getPrefixClassesTransformer, testPrefixClasses } from '../transforms/prefix-classes';
1717
import { getPrefixFunctionsTransformer } from '../transforms/prefix-functions';
18-
import { getScrubFileTransformer, testScrubFile } from '../transforms/scrub-file';
18+
import {
19+
getScrubFileTransformer,
20+
getScrubFileTransformerForCore,
21+
testScrubFile,
22+
} from '../transforms/scrub-file';
1923
import { getWrapEnumsTransformer } from '../transforms/wrap-enums';
2024

2125

@@ -44,6 +48,18 @@ const ngFactories = [
4448
/\.ngstyle\.[jt]s/,
4549
];
4650

51+
// Known locations for the source files of @angular/core.
52+
const coreFilesRegex = [
53+
/[\\/]node_modules[\\/]@angular[\\/]core[\\/]esm5[\\/]/,
54+
/[\\/]node_modules[\\/]@angular[\\/]core[\\/]fesm5[\\/]/,
55+
/[\\/]node_modules[\\/]@angular[\\/]core[\\/]esm2015[\\/]/,
56+
/[\\/]node_modules[\\/]@angular[\\/]core[\\/]fesm2015[\\/]/,
57+
];
58+
59+
function isKnownCoreFile(filePath: string) {
60+
return coreFilesRegex.some(re => re.test(filePath));
61+
}
62+
4763
function isKnownSideEffectFree(filePath: string) {
4864
return ngFactories.some((re) => re.test(filePath)) ||
4965
whitelistedAngularModules.some((re) => re.test(filePath));
@@ -57,11 +73,12 @@ export interface BuildOptimizerOptions {
5773
emitSourceMap?: boolean;
5874
strict?: boolean;
5975
isSideEffectFree?: boolean;
76+
isAngularCoreFile?: boolean;
6077
}
6178

6279
export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascriptOutput {
6380

64-
const { inputFilePath } = options;
81+
const { inputFilePath, isAngularCoreFile } = options;
6582
let { originalFilePath, content } = options;
6683

6784
if (!originalFilePath && inputFilePath) {
@@ -84,6 +101,15 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr
84101
};
85102
}
86103

104+
let selectedGetScrubFileTransformer = getScrubFileTransformer;
105+
106+
if (
107+
isAngularCoreFile === true ||
108+
(isAngularCoreFile === undefined && originalFilePath && isKnownCoreFile(originalFilePath))
109+
) {
110+
selectedGetScrubFileTransformer = getScrubFileTransformerForCore;
111+
}
112+
87113
const isWebpackBundle = content.indexOf('__webpack_require__') !== -1;
88114

89115
// Determine which transforms to apply.
@@ -97,14 +123,14 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr
97123
// We only apply it to whitelisted modules, since we know they are safe.
98124
// getPrefixFunctionsTransformer needs to be before getFoldFileTransformer.
99125
getPrefixFunctionsTransformer,
100-
getScrubFileTransformer,
126+
selectedGetScrubFileTransformer,
101127
getFoldFileTransformer,
102128
);
103129
typeCheck = true;
104130
} else if (testScrubFile(content)) {
105131
// Always test as these require the type checker
106132
getTransforms.push(
107-
getScrubFileTransformer,
133+
selectedGetScrubFileTransformer,
108134
getFoldFileTransformer,
109135
);
110136
typeCheck = true;

packages/angular_devkit/build_optimizer/src/build-optimizer/rollup-plugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const DEBUG = false;
2020

2121
export interface Options {
2222
sideEffectFreeModules?: string[];
23+
angularCoreModules?: string[];
2324
}
2425

2526
export default function optimizer(options: Options) {
@@ -34,8 +35,10 @@ export default function optimizer(options: Options) {
3435
const normalizedId = id.replace(/\\/g, '/');
3536
const isSideEffectFree = options.sideEffectFreeModules &&
3637
options.sideEffectFreeModules.some(m => normalizedId.indexOf(m) >= 0);
38+
const isAngularCoreFile = options.angularCoreModules &&
39+
options.angularCoreModules.some(m => normalizedId.indexOf(m) >= 0);
3740
const { content: code, sourceMap: map } = buildOptimizer({
38-
content, inputFilePath: id, emitSourceMap: true, isSideEffectFree,
41+
content, inputFilePath: id, emitSourceMap: true, isSideEffectFree, isAngularCoreFile,
3942
});
4043
if (!code) {
4144
if (DEBUG) {

packages/angular_devkit/build_optimizer/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ export { getFoldFileTransformer } from './transforms/class-fold';
1414
export { getImportTslibTransformer, testImportTslib } from './transforms/import-tslib';
1515
export { getPrefixClassesTransformer, testPrefixClasses } from './transforms/prefix-classes';
1616
export { getPrefixFunctionsTransformer } from './transforms/prefix-functions';
17-
export { getScrubFileTransformer, testScrubFile } from './transforms/scrub-file';
17+
export {
18+
getScrubFileTransformer,
19+
getScrubFileTransformerForCore,
20+
testScrubFile,
21+
} from './transforms/scrub-file';
1822
export { getWrapEnumsTransformer } from './transforms/wrap-enums';

packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,21 @@ const angularSpecifiers = [
5050
];
5151

5252
export function getScrubFileTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
53-
const checker = program.getTypeChecker();
53+
return scrubFileTransformer(program.getTypeChecker(), false);
54+
}
55+
56+
export function getScrubFileTransformerForCore(
57+
program: ts.Program,
58+
): ts.TransformerFactory<ts.SourceFile> {
59+
return scrubFileTransformer(program.getTypeChecker(), true);
60+
}
5461

62+
function scrubFileTransformer(checker: ts.TypeChecker, isAngularCoreFile: boolean) {
5563
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
5664

5765
const transformer: ts.Transformer<ts.SourceFile> = (sf: ts.SourceFile) => {
5866

59-
const ngMetadata = findAngularMetadata(sf);
67+
const ngMetadata = findAngularMetadata(sf, isAngularCoreFile);
6068
const tslibImports = findTslibImports(sf);
6169

6270
const nodes: ts.Node[] = [];
@@ -116,22 +124,27 @@ function nameOfSpecifier(node: ts.ImportSpecifier): string {
116124
return node.name && node.name.text || '<unknown>';
117125
}
118126

119-
function findAngularMetadata(node: ts.Node): ts.Node[] {
127+
function findAngularMetadata(node: ts.Node, isAngularCoreFile: boolean): ts.Node[] {
120128
let specs: ts.Node[] = [];
129+
// Find all specifiers from imports of `@angular/core`.
121130
ts.forEachChild(node, (child) => {
122131
if (child.kind === ts.SyntaxKind.ImportDeclaration) {
123132
const importDecl = child as ts.ImportDeclaration;
124-
if (isAngularCoreImport(importDecl)) {
133+
if (isAngularCoreImport(importDecl, isAngularCoreFile)) {
125134
specs.push(...collectDeepNodes<ts.ImportSpecifier>(node, ts.SyntaxKind.ImportSpecifier)
126135
.filter((spec) => isAngularCoreSpecifier(spec)));
127136
}
128137
}
129138
});
130139

131-
const localDecl = findAllDeclarations(node)
132-
.filter((decl) => angularSpecifiers.indexOf((decl.name as ts.Identifier).text) !== -1);
133-
if (localDecl.length === angularSpecifiers.length) {
134-
specs = specs.concat(localDecl);
140+
// Check if the current module contains all know `@angular/core` specifiers.
141+
// If it does, we assume it's a `@angular/core` FESM.
142+
if (isAngularCoreFile) {
143+
const localDecl = findAllDeclarations(node)
144+
.filter((decl) => angularSpecifiers.indexOf((decl.name as ts.Identifier).text) !== -1);
145+
if (localDecl.length === angularSpecifiers.length) {
146+
specs = specs.concat(localDecl);
147+
}
135148
}
136149

137150
return specs;
@@ -154,11 +167,23 @@ function findAllDeclarations(node: ts.Node): ts.VariableDeclaration[] {
154167
return nodes;
155168
}
156169

157-
function isAngularCoreImport(node: ts.ImportDeclaration): boolean {
158-
return true &&
159-
node.moduleSpecifier &&
160-
node.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral &&
161-
(node.moduleSpecifier as ts.StringLiteral).text === '@angular/core';
170+
function isAngularCoreImport(node: ts.ImportDeclaration, isAngularCoreFile: boolean): boolean {
171+
if (!(node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier))) {
172+
return false;
173+
}
174+
const importText = node.moduleSpecifier.text;
175+
176+
// Imports to `@angular/core` are always core imports.
177+
if (importText === '@angular/core') {
178+
return true;
179+
}
180+
181+
// Relative imports from a Angular core file are also core imports.
182+
if (isAngularCoreFile && (importText.startsWith('./') || importText.startsWith('../'))) {
183+
return true;
184+
}
185+
186+
return false;
162187
}
163188

164189
function isAngularCoreSpecifier(node: ts.ImportSpecifier): boolean {

packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99
// tslint:disable-next-line:no-implicit-dependencies
1010
import { tags } from '@angular-devkit/core';
1111
import { transformJavascript } from '../helpers/transform-javascript';
12-
import { getScrubFileTransformer, testScrubFile } from './scrub-file';
12+
import {
13+
getScrubFileTransformer,
14+
getScrubFileTransformerForCore,
15+
testScrubFile,
16+
} from './scrub-file';
1317

1418

1519
const transform = (content: string) => transformJavascript(
1620
{ content, getTransforms: [getScrubFileTransformer], typeCheck: true }).content;
21+
const transformCore = (content: string) => transformJavascript(
22+
{ content, getTransforms: [getScrubFileTransformerForCore], typeCheck: true }).content;
1723

1824
describe('scrub-file', () => {
1925
const clazz = 'var Clazz = (function () { function Clazz() { } return Clazz; }());';
@@ -289,6 +295,47 @@ describe('scrub-file', () => {
289295
expect(testScrubFile(input)).toBeTruthy();
290296
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
291297
});
298+
299+
it('recognizes decorator imports in Angular core', () => {
300+
const input = tags.stripIndent`
301+
import * as tslib_1 from "tslib";
302+
import { Injectable } from './di';
303+
var Console = /** @class */ (function () {
304+
function Console() {
305+
}
306+
Console.prototype.log = function (message) {
307+
console.log(message);
308+
};
309+
Console.prototype.warn = function (message) {
310+
console.warn(message);
311+
};
312+
Console = tslib_1.__decorate([
313+
Injectable()
314+
], Console);
315+
return Console;
316+
}());
317+
export { Console };
318+
`;
319+
const output = tags.stripIndent`
320+
import * as tslib_1 from "tslib";
321+
import { Injectable } from './di';
322+
var Console = /** @class */ (function () {
323+
function Console() {
324+
}
325+
Console.prototype.log = function (message) {
326+
console.log(message);
327+
};
328+
Console.prototype.warn = function (message) {
329+
console.warn(message);
330+
};
331+
return Console;
332+
}());
333+
export { Console };
334+
`;
335+
336+
expect(testScrubFile(input)).toBeTruthy();
337+
expect(tags.oneLine`${transformCore(input)}`).toEqual(tags.oneLine`${output}`);
338+
});
292339
});
293340

294341
describe('__metadata', () => {

0 commit comments

Comments
 (0)