diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts index b4fb41f460..20d9ab0f76 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts @@ -90,18 +90,7 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr // Determine which transforms to apply. const getTransforms = []; - if (testWrapEnums(content)) { - getTransforms.push(getWrapEnumsTransformer); - } - - if (testImportTslib(content)) { - getTransforms.push(getImportTslibTransformer); - } - - if (testPrefixClasses(content)) { - getTransforms.push(getPrefixClassesTransformer); - } - + let typeCheck = false; if (options.isSideEffectFree || originalFilePath && isKnownSideEffectFree(originalFilePath)) { getTransforms.push( // getPrefixFunctionsTransformer is rather dangerous, apply only to known pure es5 modules. @@ -112,11 +101,29 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr getScrubFileTransformer, getFoldFileTransformer, ); + typeCheck = true; } else if (testScrubFile(content)) { + // Always test as these require the type checker getTransforms.push( getScrubFileTransformer, getFoldFileTransformer, ); + typeCheck = true; + } + + // tests are not needed for fast path + const ignoreTest = !options.emitSourceMap && !typeCheck; + + if (ignoreTest || testPrefixClasses(content)) { + getTransforms.unshift(getPrefixClassesTransformer); + } + + if (ignoreTest || testImportTslib(content)) { + getTransforms.unshift(getImportTslibTransformer); + } + + if (ignoreTest || testWrapEnums(content)) { + getTransforms.unshift(getWrapEnumsTransformer); } const transformJavascriptOpts: TransformJavascriptOptions = { @@ -126,6 +133,7 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr emitSourceMap: options.emitSourceMap, strict: options.strict, getTransforms, + typeCheck, }; return transformJavascript(transformJavascriptOpts); diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts index b73072d307..588f0c8b5d 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts @@ -88,17 +88,6 @@ describe('build-optimizer', () => { }); }); - it('doesn\'t process files without decorators/ctorParameters/outside Angular', () => { - const input = tags.oneLine` - var Clazz = (function () { function Clazz() { } return Clazz; }()); - ${staticProperty} - `; - - const boOutput = buildOptimizer({ content: input }); - expect(boOutput.content).toBeFalsy(); - expect(boOutput.emitSkipped).toEqual(true); - }); - it('supports es2015 modules', () => { // prefix-functions would add PURE_IMPORTS_START and PURE to the super call. // This test ensures it isn't applied to es2015 modules. diff --git a/packages/angular_devkit/build_optimizer/src/helpers/transform-javascript.ts b/packages/angular_devkit/build_optimizer/src/helpers/transform-javascript.ts index e7adc84a5b..8cb1ac8ae6 100644 --- a/packages/angular_devkit/build_optimizer/src/helpers/transform-javascript.ts +++ b/packages/angular_devkit/build_optimizer/src/helpers/transform-javascript.ts @@ -15,7 +15,8 @@ export interface TransformJavascriptOptions { outputFilePath?: string; emitSourceMap?: boolean; strict?: boolean; - getTransforms: Array<(program: ts.Program) => ts.TransformerFactory>; + typeCheck?: boolean; + getTransforms: Array<(program?: ts.Program) => ts.TransformerFactory>; } export interface TransformJavascriptOutput { @@ -24,6 +25,42 @@ export interface TransformJavascriptOutput { emitSkipped: boolean; } +interface DiagnosticSourceFile extends ts.SourceFile { + readonly parseDiagnostics?: ReadonlyArray; +} + +function validateDiagnostics(diagnostics: ReadonlyArray, strict?: boolean): boolean { + // Print error diagnostics. + const checkDiagnostics = (diagnostics: ReadonlyArray) => { + if (diagnostics && diagnostics.length > 0) { + let errors = ''; + errors = errors + '\n' + ts.formatDiagnostics(diagnostics, { + getCurrentDirectory: () => ts.sys.getCurrentDirectory(), + getNewLine: () => ts.sys.newLine, + getCanonicalFileName: (f: string) => f, + }); + + return errors; + } + }; + + const hasError = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); + if (hasError) { + // Throw only if we're in strict mode, otherwise return original content. + if (strict) { + throw new Error(` + TS failed with the following error messages: + + ${checkDiagnostics(diagnostics)} + `); + } else { + return false; + } + } + + return true; +} + export function transformJavascript( options: TransformJavascriptOptions, ): TransformJavascriptOutput { @@ -46,23 +83,68 @@ export function transformJavascript( }; } - // Print error diagnostics. - const checkDiagnostics = (diagnostics: ReadonlyArray) => { - if (diagnostics && diagnostics.length > 0) { - let errors = ''; - errors = errors + '\n' + ts.formatDiagnostics(diagnostics, { - getCurrentDirectory: () => ts.sys.getCurrentDirectory(), - getNewLine: () => ts.sys.newLine, - getCanonicalFileName: (f: string) => f, - }); + const allowFastPath = options.typeCheck === false && !emitSourceMap; + const outputs = new Map(); + const tempFilename = 'bo-default-file.js'; + const tempSourceFile = ts.createSourceFile( + tempFilename, + content, + ts.ScriptTarget.Latest, + allowFastPath, + ); + const parseDiagnostics = (tempSourceFile as DiagnosticSourceFile).parseDiagnostics; - return errors; - } + const tsOptions: ts.CompilerOptions = { + // We target latest so that there is no downleveling. + target: ts.ScriptTarget.Latest, + isolatedModules: true, + suppressOutputPathCheck: true, + allowNonTsExtensions: true, + noLib: true, + noResolve: true, + sourceMap: emitSourceMap, + inlineSources: emitSourceMap, + inlineSourceMap: false, }; - const outputs = new Map(); - const tempFilename = 'bo-default-file.js'; - const tempSourceFile = ts.createSourceFile(tempFilename, content, ts.ScriptTarget.Latest); + if (allowFastPath && parseDiagnostics) { + if (!validateDiagnostics(parseDiagnostics, strict)) { + return { + content: null, + sourceMap: null, + emitSkipped: true, + }; + } + + const transforms = getTransforms.map((getTf) => getTf(undefined)); + + const result = ts.transform(tempSourceFile, transforms, tsOptions); + if (result.transformed.length === 0 || result.transformed[0] === tempSourceFile) { + return { + content: null, + sourceMap: null, + emitSkipped: true, + }; + } + + const printer = ts.createPrinter( + undefined, + { + onEmitNode: result.emitNodeWithNotification, + substituteNode: result.substituteNode, + }, + ); + + const output = printer.printFile(result.transformed[0]); + + result.dispose(); + + return { + content: output, + sourceMap: null, + emitSkipped: false, + }; + } const host: ts.CompilerHost = { getSourceFile: (fileName) => { @@ -83,39 +165,15 @@ export function transformJavascript( writeFile: (fileName, text) => outputs.set(fileName, text), }; - const tsOptions: ts.CompilerOptions = { - // We target latest so that there is no downleveling. - target: ts.ScriptTarget.Latest, - isolatedModules: true, - suppressOutputPathCheck: true, - allowNonTsExtensions: true, - noLib: true, - noResolve: true, - sourceMap: emitSourceMap, - inlineSources: emitSourceMap, - inlineSourceMap: false, - }; - const program = ts.createProgram([tempFilename], tsOptions, host); const diagnostics = program.getSyntacticDiagnostics(tempSourceFile); - const hasError = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); - - if (hasError) { - // Throw only if we're in strict mode, otherwise return original content. - if (strict) { - throw new Error(` - TS failed with the following error messages: - - ${checkDiagnostics(diagnostics)} - `); - } else { - return { - content: null, - sourceMap: null, - emitSkipped: true, - }; - } + if (!validateDiagnostics(diagnostics, strict)) { + return { + content: null, + sourceMap: null, + emitSkipped: true, + }; } // We need the checker inside transforms. diff --git a/packages/angular_devkit/build_optimizer/src/transforms/class-fold_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/class-fold_spec.ts index 237c857ce9..8471e3630a 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/class-fold_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/class-fold_spec.ts @@ -11,7 +11,7 @@ import { getFoldFileTransformer } from './class-fold'; const transform = (content: string) => transformJavascript( - { content, getTransforms: [getFoldFileTransformer] }).content; + { content, getTransforms: [getFoldFileTransformer], typeCheck: true }).content; describe('class-fold', () => { it('folds static properties into class', () => { diff --git a/packages/angular_devkit/build_optimizer/src/transforms/prefix-functions.ts b/packages/angular_devkit/build_optimizer/src/transforms/prefix-functions.ts index d17331d375..a0c263f007 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/prefix-functions.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/prefix-functions.ts @@ -52,7 +52,6 @@ export function getPrefixFunctionsTransformer(): ts.TransformerFactorynode).expression.text - && (node as ts.CallExpression).expression.kind !== ts.SyntaxKind.PropertyAccessExpression; + ts.forEachChild(innerNode, cb); } ts.forEachChild(parentNode, cb); @@ -116,6 +108,9 @@ export function findPureImports(parentNode: ts.Node): string[] { } function hasPureComment(node: ts.Node) { + if (!node) { + return false; + } const leadingComment = ts.getSyntheticLeadingComments(node); return leadingComment && leadingComment.some((comment) => comment.text === pureFunctionComment); diff --git a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts index accfff1a56..d4d1295868 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts @@ -11,7 +11,7 @@ import { getScrubFileTransformer, testScrubFile } from './scrub-file'; const transform = (content: string) => transformJavascript( - { content, getTransforms: [getScrubFileTransformer] }).content; + { content, getTransforms: [getScrubFileTransformer], typeCheck: true }).content; describe('scrub-file', () => { const clazz = 'var Clazz = (function () { function Clazz() { } return Clazz; }());';