Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Build Optimizer Lightweight Transformations #346

Merged
merged 2 commits into from
Jan 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 = {
Expand All @@ -126,6 +133,7 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr
emitSourceMap: options.emitSourceMap,
strict: options.strict,
getTransforms,
typeCheck,
};

return transformJavascript(transformJavascriptOpts);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export interface TransformJavascriptOptions {
outputFilePath?: string;
emitSourceMap?: boolean;
strict?: boolean;
getTransforms: Array<(program: ts.Program) => ts.TransformerFactory<ts.SourceFile>>;
typeCheck?: boolean;
getTransforms: Array<(program?: ts.Program) => ts.TransformerFactory<ts.SourceFile>>;
}

export interface TransformJavascriptOutput {
Expand All @@ -24,6 +25,42 @@ export interface TransformJavascriptOutput {
emitSkipped: boolean;
}

interface DiagnosticSourceFile extends ts.SourceFile {
readonly parseDiagnostics?: ReadonlyArray<ts.Diagnostic>;
}

function validateDiagnostics(diagnostics: ReadonlyArray<ts.Diagnostic>, strict?: boolean): boolean {
// Print error diagnostics.
const checkDiagnostics = (diagnostics: ReadonlyArray<ts.Diagnostic>) => {
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 {
Expand All @@ -46,23 +83,68 @@ export function transformJavascript(
};
}

// Print error diagnostics.
const checkDiagnostics = (diagnostics: ReadonlyArray<ts.Diagnostic>) => {
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<string, string>();
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<string, string>();
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) => {
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,40 +52,32 @@ export function getPrefixFunctionsTransformer(): ts.TransformerFactory<ts.Source
export function findTopLevelFunctions(parentNode: ts.Node): ts.Node[] {
const topLevelFunctions: ts.Node[] = [];

let previousNode: ts.Node;
function cb(node: ts.Node) {
// Stop recursing into this branch if it's a function expression or declaration
if (node.kind === ts.SyntaxKind.FunctionDeclaration
|| node.kind === ts.SyntaxKind.FunctionExpression) {
return;
}

// We need to check specially for IIFEs formatted as call expressions inside parenthesized
// expressions: `(function() {}())` Their start pos doesn't include the opening paren
// and must be adjusted.
if (isIIFE(node)
&& previousNode.kind === ts.SyntaxKind.ParenthesizedExpression
&& node.parent
&& !hasPureComment(node.parent)) {
topLevelFunctions.push(node.parent);
} else if ((node.kind === ts.SyntaxKind.CallExpression
|| node.kind === ts.SyntaxKind.NewExpression)
&& !hasPureComment(node)
) {
topLevelFunctions.push(node);
let noPureComment = !hasPureComment(node);
let innerNode = node;
while (innerNode && ts.isParenthesizedExpression(innerNode)) {
innerNode = innerNode.expression;
noPureComment = noPureComment && !hasPureComment(innerNode);
}

previousNode = node;
if (!innerNode) {
return;
}

ts.forEachChild(node, cb);
}
if (noPureComment) {
if (innerNode.kind === ts.SyntaxKind.CallExpression
|| innerNode.kind === ts.SyntaxKind.NewExpression) {
topLevelFunctions.push(node);
}
}

function isIIFE(node: ts.Node): boolean {
return node.kind === ts.SyntaxKind.CallExpression
// This check was in the old ngo but it doesn't seem to make sense with the typings.
// TODO(filipesilva): ask Alex Rickabaugh about it.
// && !(<ts.CallExpression>node).expression.text
&& (node as ts.CallExpression).expression.kind !== ts.SyntaxKind.PropertyAccessExpression;
ts.forEachChild(innerNode, cb);
}

ts.forEachChild(parentNode, cb);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }());';
Expand Down