From 045dcfbca0affcdaf1ea1d37e7b282c1f68528a8 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 17:07:59 -0800 Subject: [PATCH 01/10] continued progress --- package.json | 4 + src/debug.ts | 2 +- src/parsing/export-details.ts | 34 +- src/parsing/preserve-default-export.ts | 39 +++ src/parsing/preserve-named-constant-export.ts | 133 ++++++++ src/transformers/exports.ts | 314 +++++++++++------- src/types.ts | 3 +- .../fixtures/identifier-alias.esm.default.js | 3 +- .../fixtures/class.esm.default.js | 10 +- test/generator.js | 14 +- test/provided-externs/class.test.js | 6 +- .../fixtures/class.esm.advanced.js | 10 +- .../fixtures/class.esm.default.js | 10 +- .../fixtures/class.esm.es5.js | 8 +- 14 files changed, 446 insertions(+), 144 deletions(-) create mode 100644 src/parsing/preserve-default-export.ts create mode 100644 src/parsing/preserve-named-constant-export.ts diff --git a/package.json b/package.json index c67a1966..bc3726b5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,10 @@ "*.ts": [ "prettier --config .prettierrc --write", "git add" + ], + "*.js": [ + "prettier --config .prettierrc --write", + "git add" ] }, "husky": { diff --git a/src/debug.ts b/src/debug.ts index 3e54c051..5002d8cd 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -16,7 +16,7 @@ import { writeTempFile } from './temp-file'; -const DEBUG_ENABLED = false; +const DEBUG_ENABLED = true; /* c8 ignore next 12 */ export const logSource = async (preamble: string, source: string, code?: string): Promise => { diff --git a/src/parsing/export-details.ts b/src/parsing/export-details.ts index c1023d13..3d6fd7d9 100644 --- a/src/parsing/export-details.ts +++ b/src/parsing/export-details.ts @@ -14,7 +14,14 @@ * limitations under the License. */ -import { ExportNamedDeclaration, ExportDefaultDeclaration } from 'estree'; +import { + ExportNamedDeclaration, + ExportDefaultDeclaration, + Node, + ExpressionStatement, + MemberExpression, + Expression, +} from 'estree'; import { ExportDetails, Range, ExportClosureMapping } from '../types'; export function NamedDeclaration(declaration: ExportNamedDeclaration): Array { @@ -26,7 +33,6 @@ export function NamedDeclaration(declaration: ExportNamedDeclaration): Array export function foo(){} / function foo(){} + const assignmentExpression = ancestor.expression as AssignmentExpression; + const memberExpression = assignmentExpression.left as MemberExpression; + const functionExpression = assignmentExpression.right as FunctionExpression; + const [memberExpressionObjectStart] = memberExpression.object.range as Range; + + if (functionExpression.params.length > 0) { + const [paramsStart] = functionExpression.params[0].range as Range; + // FunctionExpression has parameters. + source.overwrite( + memberExpressionObjectStart, + paramsStart, + `${exportInline ? 'export ' : ''}function ${exportDetails.exported}(`, + ); + } else { + const [bodyStart] = functionExpression.body.range as Range; + source.overwrite( + memberExpressionObjectStart, + bodyStart, + `${exportInline ? 'export ' : ''}function ${exportDetails.exported}()`, + ); + } + + return !exportInline; +} + +function PreserveIdentifier( + code: string, + source: MagicString, + ancestor: ExpressionStatement, + exportDetails: ExportDetails, + exportInline: boolean, +): boolean { + const assignmentExpression = ancestor.expression as AssignmentExpression; + const left = assignmentExpression.left; + const right = assignmentExpression.right; + const [ancestorStart, ancestorEnd]: Range = ancestor.range as Range; + const [rightStart, rightEnd]: Range = right.range as Range; + + if (exportInline) { + source.overwrite( + ancestorStart, + ancestorEnd, + `export var ${exportDetails.exported}=${code.substring(rightStart, rightEnd)};`, + ); + } else if (exportDetails.source === null && 'name' in right) { + // This is a locally defined identifier with a name we can use. + exportDetails.local = right.name; + source.remove((left.range as Range)[0], rightEnd + 1); + return true; + } else { + // exportDetails.local = + source.overwrite( + ancestorStart, + ancestorEnd, + `var ${exportDetails.local}=${code.substring(rightStart, rightEnd)};`, + ); + } + + return !exportInline; +} + +/* +if (left.property.type === 'Identifier') { + // Identifiers are present when a complex object (class) has been saved as an export. + // In this case we currently opt out of inline exporting, since the identifier + // is a mangled name for the export. + exportDetails.local = right.name; + exportDetails.exported = left.property.name; + + source.remove( + (ancestor.expression.left.range as Range)[0], + (ancestor.expression.right.range as Range)[1] + 1, + ); + + // Since we're manually mapping the name back from the changes done by Closure + // Ensure the export isn't stored for insertion here and later on. + collectedExportsToAppend = ExportTransform.storeExportToAppend( + collectedExportsToAppend, + exportDetails, + ); + exportCollected = true; +} +*/ + +export function PreserveNamedConstant( + code: string, + source: MagicString, + ancestor: ExpressionStatement, + exportDetails: ExportDetails, + exportInline: boolean, +): boolean { + const assignmentExpression = ancestor.expression as AssignmentExpression; + switch (assignmentExpression.right.type) { + case 'FunctionExpression': + return PreserveFunction(code, source, ancestor, exportDetails, exportInline); + default: + return PreserveIdentifier(code, source, ancestor, exportDetails, exportInline); + } +} diff --git a/src/transformers/exports.ts b/src/transformers/exports.ts index d58b5f6d..8df0168f 100644 --- a/src/transformers/exports.ts +++ b/src/transformers/exports.ts @@ -20,9 +20,18 @@ import { ExportAllDeclaration, Identifier, Node, + AssignmentExpression, + MemberExpression, } from 'estree'; import { TransformSourceDescription } from 'rollup'; -import { NamedDeclaration, DefaultDeclaration } from '../parsing/export-details'; +import { + NamedDeclaration, + DefaultDeclaration, + NodeIsPreservedExport, + PreservedExportName, +} from '../parsing/export-details'; +import { PreserveNamedConstant } from '../parsing/preserve-named-constant-export'; +import { PreserveDefault } from '../parsing/preserve-default-export'; import { isESMFormat } from '../options'; import { Transform, @@ -61,7 +70,7 @@ export default class ExportTransform extends Transform implements TransformInter if (map.source === null) { this.currentSourceExportCount++; } - this.originalExports.set(map.closureName, map); + this.originalExports.set(map.local, map); }); private static storeExportToAppend( @@ -109,7 +118,7 @@ export default class ExportTransform extends Transform implements TransformInter for (const key of this.originalExports.keys()) { const value: ExportDetails = this.originalExports.get(key) as ExportDetails; if (value.source !== null) { - output += `function ${value.closureName}(){};\n`; + output += `function ${value.exported}(){};\n`; } } @@ -132,6 +141,7 @@ export default class ExportTransform extends Transform implements TransformInter await this.deriveExports(code); const source = new MagicString(code); + console.log('derived', this.originalExports); for (const key of this.originalExports.keys()) { const value: ExportDetails = this.originalExports.get(key) as ExportDetails; @@ -141,9 +151,9 @@ export default class ExportTransform extends Transform implements TransformInter source.remove(...value.range); // Window scoped references for each key are required to ensure Closure Compilre retains the code. if (value.source === null) { - source.append(`\nwindow['${value.closureName}'] = ${value.local};`); + source.append(`\nwindow['${value.local}'] = ${value.local};`); } else { - source.append(`\nwindow['${value.closureName}'] = ${value.exported};`); + source.append(`\nwindow['${value.exported}'] = ${value.exported};`); } } @@ -170,8 +180,8 @@ export default class ExportTransform extends Transform implements TransformInter if (isESMFormat(this.outputOptions.format)) { const source = new MagicString(code); const program = parse(code); - let collectedExportsToAppend: Map> = new Map(); const { originalExports, currentSourceExportCount } = this; + let collectedExportsToAppend: Map> = new Map(); source.trimEnd(); @@ -180,131 +190,179 @@ export default class ExportTransform extends Transform implements TransformInter // Now we need to find where Closure Compiler moved them, and restore the exports of their name. // ASTExporer Link: https://astexplorer.net/#/gist/94f185d06a4105d64828f1b8480bddc8/0fc5885ae5343f964d0cdd33c7d392a70cf5fcaf Identifier(node: Identifier, ancestors: Array) { - if (node.name === 'window') { - ancestors.forEach((ancestor: Node) => { - if ( - ancestor.type === 'ExpressionStatement' && - ancestor.expression.type === 'AssignmentExpression' && - ancestor.expression.left.type === 'MemberExpression' && - ancestor.expression.left.object.type === 'Identifier' && - ancestor.expression.left.object.name === 'window' - ) { - const { property: leftProperty } = ancestor.expression.left; - let exportName: string | null = null; - if (leftProperty.type === 'Identifier') { - exportName = leftProperty.name; - } else if ( - leftProperty.type === 'Literal' && - typeof leftProperty.value === 'string' - ) { - exportName = leftProperty.value; - } - - if (exportName !== null && originalExports.get(exportName)) { - const exportDetails: ExportDetails = originalExports.get( - exportName, - ) as ExportDetails; - switch (exportDetails.type) { - case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: - case ExportClosureMapping.DEFAULT: - if (ancestor.expression.left.range) { - source.overwrite( - ancestor.expression.left.range[0], - ancestor.expression.left.range[1] + ancestor.expression.operator.length, - 'export default ', - ); - } - break; - case ExportClosureMapping.NAMED_CONSTANT: - const exportFromCurrentSource: boolean = exportDetails.source === null; - const inlineExport: boolean = - exportFromCurrentSource && currentSourceExportCount === 1; - let exportCollected: boolean = false; - if (exportFromCurrentSource) { - const { object: leftObject } = ancestor.expression.left; - if (leftObject.range) { - const { left, right } = ancestor.expression; - switch (right.type) { - case 'FunctionExpression': - // Function Expressions can be inlined instead of preserved as variable references. - // window['foo'] = function(){}; => export function foo(){} / function foo(){} - if (right.params.length > 0) { - // FunctionExpression has parameters. - source.overwrite( - (leftObject.range as Range)[0], - (right.params[0].range as Range)[0], - `${inlineExport ? 'export ' : ''}function ${ - exportDetails.exported - }(`, - ); - } else { - source.overwrite( - (leftObject.range as Range)[0], - (right.body.range as Range)[0], - `${inlineExport ? 'export ' : ''}function ${ - exportDetails.exported - }()`, - ); - } - break; - case 'Identifier': - if (left.property.type === 'Identifier') { - // Identifiers are present when a complex object (class) has been saved as an export. - // In this case we currently opt out of inline exporting, since the identifier - // is a mangled name for the export. - exportDetails.local = right.name; - exportDetails.closureName = left.property.name; - - source.remove( - (ancestor.expression.left.range as Range)[0], - (ancestor.expression.right.range as Range)[1] + 1, - ); - - // Since we're manually mapping the name back from the changes done by Closure - // Ensure the export isn't stored for insertion here and later on. - collectedExportsToAppend = ExportTransform.storeExportToAppend( - collectedExportsToAppend, - exportDetails, - ); - exportCollected = true; - } - break; - default: - const statement = inlineExport ? 'export var ' : 'var '; - source.overwrite( - leftObject.range[0], - leftObject.range[1] + 1, - statement, - ); - break; - } - } - if (exportDetails.local !== exportDetails.exported) { - exportDetails.local = exportDetails.exported; - exportDetails.closureName = exportDetails.local; - } - } else if ( - ancestor.expression.left.range && - ancestor.expression.right.range - ) { - source.remove( - ancestor.expression.left.range[0], - ancestor.expression.right.range[1] + 1, - ); - } - - if (!inlineExport && !exportCollected) { - collectedExportsToAppend = ExportTransform.storeExportToAppend( - collectedExportsToAppend, - exportDetails, - ); - } - break; + if (node.name !== 'window') { + return; + } + + for (const ancestor of ancestors) { + if (!NodeIsPreservedExport(ancestor)) { + continue; + } + // Can cast these since they were validated with the `NodeIsPreservedExport` test. + const expression: AssignmentExpression = ancestor.expression as AssignmentExpression; + const left: MemberExpression = expression.left as MemberExpression; + const exportName: string | null = PreservedExportName(left); + + if (exportName !== null && originalExports.get(exportName)) { + const exportDetails: ExportDetails = originalExports.get(exportName) as ExportDetails; + const exportIsLocal: boolean = exportDetails.source === null; + const exportInline: boolean = + exportIsLocal && + currentSourceExportCount === 1 && + exportDetails.local === exportDetails.exported; + + switch (exportDetails.type) { + case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: + case ExportClosureMapping.DEFAULT: + if (PreserveDefault(code, source, ancestor, exportDetails, exportInline)) { + collectedExportsToAppend = ExportTransform.storeExportToAppend( + collectedExportsToAppend, + exportDetails, + ); + } + break; + case ExportClosureMapping.NAMED_CONSTANT: + if (PreserveNamedConstant(code, source, ancestor, exportDetails, exportInline)) { + collectedExportsToAppend = ExportTransform.storeExportToAppend( + collectedExportsToAppend, + exportDetails, + ); } - } + break; + default: + console.log('type', exportDetails.type); + break; } - }); + + if (!exportIsLocal) { + source.remove((left.range as Range)[0], (expression.right.range as Range)[1] + 1); + } + } } + + // ancestors.forEach((ancestor: Node) => { + // if ( + // ancestor.type === 'ExpressionStatement' && + // ancestor.expression.type === 'AssignmentExpression' && + // ancestor.expression.left.type === 'MemberExpression' && + // ancestor.expression.left.object.type === 'Identifier' && + // ancestor.expression.left.object.name === 'window' + // ) { + // const { property: leftProperty } = ancestor.expression.left; + // let exportName: string | null = null; + // if (leftProperty.type === 'Identifier') { + // exportName = leftProperty.name; + // } else if ( + // leftProperty.type === 'Literal' && + // typeof leftProperty.value === 'string' + // ) { + // exportName = leftProperty.value; + // } + + // if (exportName !== null && originalExports.get(exportName)) { + // const exportDetails: ExportDetails = originalExports.get( + // exportName, + // ) as ExportDetails; + // switch (exportDetails.type) { + // case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: + // case ExportClosureMapping.DEFAULT: + // if (ancestor.expression.left.range) { + // source.overwrite( + // ancestor.expression.left.range[0], + // ancestor.expression.left.range[1] + ancestor.expression.operator.length, + // 'export default ', + // ); + // } + // break; + // case ExportClosureMapping.NAMED_CONSTANT: + // const exportFromCurrentSource: boolean = exportDetails.source === null; + // const inlineExport: boolean = + // exportFromCurrentSource && currentSourceExportCount === 1; + // let exportCollected: boolean = false; + // if (exportFromCurrentSource) { + // const { object: leftObject } = ancestor.expression.left; + // if (leftObject.range) { + // const { left, right } = ancestor.expression; + // switch (right.type) { + // case 'FunctionExpression': + // // Function Expressions can be inlined instead of preserved as variable references. + // // window['foo'] = function(){}; => export function foo(){} / function foo(){} + // if (right.params.length > 0) { + // // FunctionExpression has parameters. + // source.overwrite( + // (leftObject.range as Range)[0], + // (right.params[0].range as Range)[0], + // `${inlineExport ? 'export ' : ''}function ${ + // exportDetails.exported + // }(`, + // ); + // } else { + // source.overwrite( + // (leftObject.range as Range)[0], + // (right.body.range as Range)[0], + // `${inlineExport ? 'export ' : ''}function ${ + // exportDetails.exported + // }()`, + // ); + // } + // break; + // case 'Identifier': + // if (left.property.type === 'Identifier') { + // // Identifiers are present when a complex object (class) has been saved as an export. + // // In this case we currently opt out of inline exporting, since the identifier + // // is a mangled name for the export. + // exportDetails.local = right.name; + // exportDetails.exported = left.property.name; + + // source.remove( + // (ancestor.expression.left.range as Range)[0], + // (ancestor.expression.right.range as Range)[1] + 1, + // ); + + // // Since we're manually mapping the name back from the changes done by Closure + // // Ensure the export isn't stored for insertion here and later on. + // collectedExportsToAppend = ExportTransform.storeExportToAppend( + // collectedExportsToAppend, + // exportDetails, + // ); + // exportCollected = true; + // } + // break; + // default: + // const statement = inlineExport ? 'export var ' : 'var '; + // source.overwrite( + // leftObject.range[0], + // leftObject.range[1] + 1, + // statement, + // ); + // break; + // } + // } + // if (exportDetails.local !== exportDetails.exported) { + // exportDetails.local = exportDetails.exported; + // exportDetails.exported = exportDetails.local; + // } + // } else if ( + // ancestor.expression.left.range && + // ancestor.expression.right.range + // ) { + // source.remove( + // ancestor.expression.left.range[0], + // ancestor.expression.right.range[1] + 1, + // ); + // } + + // if (!inlineExport && !exportCollected) { + // collectedExportsToAppend = ExportTransform.storeExportToAppend( + // collectedExportsToAppend, + // exportDetails, + // ); + // } + // break; + // } + // } + // } + // }); }, }); diff --git a/src/types.ts b/src/types.ts index 8dd4545a..305a9eb4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,8 @@ export const ALL_EXPORT_DECLARATIONS = [ export type Range = [number, number]; +export type CollectedExports = Map>; + export enum ExportClosureMapping { NAMED_FUNCTION = 0, NAMED_CLASS = 1, @@ -53,7 +55,6 @@ export enum ExportClosureMapping { export interface ExportDetails { local: string; exported: string; - closureName: string; type: ExportClosureMapping; range: Range; source: string | null; diff --git a/test/export-named/fixtures/identifier-alias.esm.default.js b/test/export-named/fixtures/identifier-alias.esm.default.js index 5fcd2ca9..1310abac 100644 --- a/test/export-named/fixtures/identifier-alias.esm.default.js +++ b/test/export-named/fixtures/identifier-alias.esm.default.js @@ -1 +1,2 @@ -export var bar=1; +var a = 1; +export { a as bar }; diff --git a/test/export-variables/fixtures/class.esm.default.js b/test/export-variables/fixtures/class.esm.default.js index 8a66c547..6c631527 100644 --- a/test/export-variables/fixtures/class.esm.default.js +++ b/test/export-variables/fixtures/class.esm.default.js @@ -1 +1,9 @@ -class a{constructor(b){this.name_=b}console(){console.log(this.name_)}}export{a as Exported} +class a { + constructor(b) { + this.name_ = b; + } + console() { + console.log(this.name_); + } +} +export var Exported = a; diff --git a/test/generator.js b/test/generator.js index 02353cf1..3053ef9c 100644 --- a/test/generator.js +++ b/test/generator.js @@ -34,8 +34,8 @@ const ES5_STRICT_CLOSURE_OPTIONS = { }; const defaultClosureFlags = { ...DEFAULT_CLOSURE_OPTIONS, - ...ADVANCED_CLOSURE_OPTIONS, - ...ES5_STRICT_CLOSURE_OPTIONS, + // ...ADVANCED_CLOSURE_OPTIONS, + // ...ES5_STRICT_CLOSURE_OPTIONS, }; const ES_OUTPUT = 'es'; @@ -111,7 +111,15 @@ function generate(shouldFail, category, name, codeSplit, formats, closureFlags, method( `${name} – ${format.padEnd(targetLength)} – ${optionKey.padEnd(optionLength)}`, async t => { - const output = await compile(category, name, codeSplit, closureFlags, optionKey, format, wrapper); + const output = await compile( + category, + name, + codeSplit, + closureFlags, + optionKey, + format, + wrapper, + ); t.plan(output.length); for (result of output) { diff --git a/test/provided-externs/class.test.js b/test/provided-externs/class.test.js index d4410eca..11e62252 100644 --- a/test/provided-externs/class.test.js +++ b/test/provided-externs/class.test.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {generator, ESM_OUTPUT} from '../generator'; +import { generator, ESM_OUTPUT } from '../generator'; const path = require('path'); const EXTERNS = path.resolve('test', 'provided-externs', 'fixtures', 'class.externs.js'); @@ -31,5 +31,5 @@ generator('provided-externs', 'class', false, [ESM_OUTPUT], { es5: { externs: EXTERNS, language_out: 'ECMASCRIPT5_STRICT', - } -}); \ No newline at end of file + }, +}); diff --git a/test/provided-externs/fixtures/class.esm.advanced.js b/test/provided-externs/fixtures/class.esm.advanced.js index 71bba236..4473af1f 100644 --- a/test/provided-externs/fixtures/class.esm.advanced.js +++ b/test/provided-externs/fixtures/class.esm.advanced.js @@ -1 +1,9 @@ -class a{constructor(b){this.a=b}console(){console.log(this.a)}}export{a as ExportThis} +class a { + constructor(b) { + this.a = b; + } + console() { + console.log(this.a); + } +} +export var ExportThis = a; diff --git a/test/provided-externs/fixtures/class.esm.default.js b/test/provided-externs/fixtures/class.esm.default.js index 74aeb2fc..a7afa837 100644 --- a/test/provided-externs/fixtures/class.esm.default.js +++ b/test/provided-externs/fixtures/class.esm.default.js @@ -1 +1,9 @@ -class a{constructor(b){this.name_=b}console(){console.log(this.name_)}}export{a as ExportThis} +class a { + constructor(b) { + this.name_ = b; + } + console() { + console.log(this.name_); + } +} +export var ExportThis = a; diff --git a/test/provided-externs/fixtures/class.esm.es5.js b/test/provided-externs/fixtures/class.esm.es5.js index f6538742..6cd525fb 100644 --- a/test/provided-externs/fixtures/class.esm.es5.js +++ b/test/provided-externs/fixtures/class.esm.es5.js @@ -1 +1,7 @@ -function a(b){this.name_=b}a.prototype.console=function(){console.log(this.name_)};export{a as ExportThis} +function a(b) { + this.name_ = b; +} +a.prototype.console = function() { + console.log(this.name_); +}; +export var ExportThis = a; From 7389fd75bbcf04672039251f7ae757ac7af3de05 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 17:40:12 -0800 Subject: [PATCH 02/10] js match was too broad --- package.json | 2 +- test/provided-externs/fixtures/class.esm.advanced.js | 10 +--------- test/provided-externs/fixtures/class.esm.default.js | 10 +--------- test/provided-externs/fixtures/class.esm.es5.js | 8 +------- 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index bc3726b5..c328748e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "prettier --config .prettierrc --write", "git add" ], - "*.js": [ + "*.test.js": [ "prettier --config .prettierrc --write", "git add" ] diff --git a/test/provided-externs/fixtures/class.esm.advanced.js b/test/provided-externs/fixtures/class.esm.advanced.js index 4473af1f..4ae1c575 100644 --- a/test/provided-externs/fixtures/class.esm.advanced.js +++ b/test/provided-externs/fixtures/class.esm.advanced.js @@ -1,9 +1 @@ -class a { - constructor(b) { - this.a = b; - } - console() { - console.log(this.a); - } -} -export var ExportThis = a; +class a{constructor(b){this.a=b}console(){console.log(this.a)}}export var ExportThis=a; diff --git a/test/provided-externs/fixtures/class.esm.default.js b/test/provided-externs/fixtures/class.esm.default.js index a7afa837..527965ca 100644 --- a/test/provided-externs/fixtures/class.esm.default.js +++ b/test/provided-externs/fixtures/class.esm.default.js @@ -1,9 +1 @@ -class a { - constructor(b) { - this.name_ = b; - } - console() { - console.log(this.name_); - } -} -export var ExportThis = a; +class a{constructor(b){this.name_=b}console(){console.log(this.name_)}}export var ExportThis=a; diff --git a/test/provided-externs/fixtures/class.esm.es5.js b/test/provided-externs/fixtures/class.esm.es5.js index 6cd525fb..e306655d 100644 --- a/test/provided-externs/fixtures/class.esm.es5.js +++ b/test/provided-externs/fixtures/class.esm.es5.js @@ -1,7 +1 @@ -function a(b) { - this.name_ = b; -} -a.prototype.console = function() { - console.log(this.name_); -}; -export var ExportThis = a; +function a(b){this.name_=b}a.prototype.console=function(){console.log(this.name_)};export var ExportThis=a; From 36e3d31a0e2722306cc5e4693454f9da5d261db1 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 17:40:36 -0800 Subject: [PATCH 03/10] husky hook --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c328748e..19ac6b78 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "husky": { "hooks": { - "pre-push": "yarn npm-run-all test build", + "pre-push": "npm-run-all test build", "pre-commit": "lint-staged" } }, From 70f19a0d0acb74eccf35856217ab8689fc5fcec8 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 17:41:18 -0800 Subject: [PATCH 04/10] husky hook --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19ac6b78..c328748e 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "husky": { "hooks": { - "pre-push": "npm-run-all test build", + "pre-push": "yarn npm-run-all test build", "pre-commit": "lint-staged" } }, From 40d467d477f94afa2f783bf599b734edbaac6eef Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 17:50:18 -0800 Subject: [PATCH 05/10] Continue cleaning up and getting closer --- package.json | 1 - src/options.ts | 3 ++- src/transforms.ts | 2 +- .../fixtures/identifier-alias.esm.default.js | 3 +-- test/export-variables/fixtures/class.esm.default.js | 10 +--------- yarn.lock | 7 ------- 6 files changed, 5 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index c328748e..d1b25fcc 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "@types/acorn": "4.0.5", "@types/estree": "0.0.41", "@types/node": "12.12.24", - "@types/temp-write": "3.3.0", "@types/uuid": "3.4.6", "ava": "2.4.0", "builtins": "3.0.0", diff --git a/src/options.ts b/src/options.ts index f1a78050..56a0ebde 100644 --- a/src/options.ts +++ b/src/options.ts @@ -69,7 +69,8 @@ export const defaults = async ( for (const transform of transformers || []) { const extern = transform.extern(options); if (extern !== null) { - transformerExterns.push(await writeTempFile(extern)); + const writtenExtern = await writeTempFile(extern); + transformerExterns.push(writtenExtern); } } diff --git a/src/transforms.ts b/src/transforms.ts index 5d6327b0..b791da65 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -65,7 +65,7 @@ export async function preCompilation( transform.outputOptions = outputOptions; const result = await transform.preCompilation(code); if (result && result.code) { - logSource(`after ${transform.name} preCompilation`, result && result.code); + await logSource(`after ${transform.name} preCompilation`, result && result.code); code = result.code; } } diff --git a/test/export-named/fixtures/identifier-alias.esm.default.js b/test/export-named/fixtures/identifier-alias.esm.default.js index 1310abac..57efc773 100644 --- a/test/export-named/fixtures/identifier-alias.esm.default.js +++ b/test/export-named/fixtures/identifier-alias.esm.default.js @@ -1,2 +1 @@ -var a = 1; -export { a as bar }; +var a=1;export{a as bar} diff --git a/test/export-variables/fixtures/class.esm.default.js b/test/export-variables/fixtures/class.esm.default.js index 6c631527..f0abc652 100644 --- a/test/export-variables/fixtures/class.esm.default.js +++ b/test/export-variables/fixtures/class.esm.default.js @@ -1,9 +1 @@ -class a { - constructor(b) { - this.name_ = b; - } - console() { - console.log(this.name_); - } -} -export var Exported = a; +class a{constructor(b){this.name_=b}console(){console.log(this.name_)}}export var Exported=a; diff --git a/yarn.lock b/yarn.lock index 785de5ca..872295d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -421,13 +421,6 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/temp-write@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@types/temp-write/-/temp-write-3.3.0.tgz#40fe3d1fae6e98a2e40a13abe83e7a1996ea51ee" - integrity sha512-RW+6TTQi6GVmOmpMoizl0Nfg8yhtPPGJQs8QtzW7eBH5XyoEM30GrUq4weYpEzITH2UrbGTd2Sn/5LRGlGPHrg== - dependencies: - "@types/node" "*" - "@types/uuid@3.4.6": version "3.4.6" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016" From 44876a382723e1e0e7cc4501f3353178af448906 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 21:56:19 -0800 Subject: [PATCH 06/10] Better debugging --- src/compiler.ts | 4 ++- src/debug.ts | 30 +++++++++++------- src/index.ts | 13 ++++++-- src/temp-file.ts | 4 +-- src/transforms.ts | 31 +++++++++++++------ .../fixtures/chunk-5275c9cc.esm.default.js | 1 + test/import/fixtures/utf8-common.js | 9 ++++++ test/import/fixtures/utf8-lazy.esm.default.js | 1 + test/import/fixtures/utf8-lazy.js | 2 ++ test/import/fixtures/utf8.esm.default.js | 1 + test/import/fixtures/utf8.js | 6 ++++ test/import/utf8.test.js | 22 +++++++++++++ 12 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 test/import/fixtures/chunk-5275c9cc.esm.default.js create mode 100644 test/import/fixtures/utf8-common.js create mode 100644 test/import/fixtures/utf8-lazy.esm.default.js create mode 100644 test/import/fixtures/utf8-lazy.js create mode 100644 test/import/fixtures/utf8.esm.default.js create mode 100644 test/import/fixtures/utf8.js create mode 100644 test/import/utf8.test.js diff --git a/src/compiler.ts b/src/compiler.ts index 4e8f37ba..10782c60 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -21,6 +21,7 @@ const { } = require('google-closure-compiler/lib/utils.js'); import { Transform } from './types'; import { postCompilation } from './transforms'; +import { RenderedChunk } from 'rollup'; enum Platform { NATIVE = 'native', @@ -71,6 +72,7 @@ function orderPlatforms(platformPreference: Platform | string): Array */ export default function( compileOptions: CompileOptions, + chunk: RenderedChunk, transforms: Array, ): Promise { return new Promise((resolve: (stdOut: string) => void, reject: (error: any) => void) => { @@ -95,7 +97,7 @@ export default function( } else if (exitCode !== 0) { reject(new Error(`Google Closure Compiler exit ${exitCode}: ${stdErr}`)); } else { - resolve(await postCompilation(code, transforms)); + resolve(await postCompilation(code, chunk, transforms)); } }); }); diff --git a/src/debug.ts b/src/debug.ts index 5002d8cd..b1bcb249 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -18,24 +18,30 @@ import { writeTempFile } from './temp-file'; const DEBUG_ENABLED = true; -/* c8 ignore next 12 */ -export const logSource = async (preamble: string, source: string, code?: string): Promise => { +/* c8 ignore next 8 */ +export async function logTransformChain( + file: string, + stage: string, + messages: Array<[string, string]>, +): Promise { if (DEBUG_ENABLED) { - const sourceLocation: string = await writeTempFile(source); - const codeLocation: string = code ? await writeTempFile(code) : ''; - - console.log(preamble); - console.log(sourceLocation); - if (code) { - console.log(codeLocation); + let output: string = `\n${file} - ${stage}`; + for (const [message, source] of messages) { + output += `\n${message.substr(0, 15).padEnd(18, '.')} - file://${await writeTempFile( + source, + '.js', + )}`; } + console.log(output); } -}; +} /* c8 ignore next 6 */ -export const log = (preamble: string, message: string | object): void | null => { +export const log = (preamble: string | undefined, message: string | object): void | null => { if (DEBUG_ENABLED) { - console.log(preamble); + if (preamble) { + console.log(preamble); + } console.log(message); } }; diff --git a/src/index.ts b/src/index.ts index ed7eb378..787966da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,8 +42,9 @@ const renderChunk = async ( requestedCompileOptions: CompileOptions = {}, sourceCode: string, outputOptions: OutputOptions, + chunk: RenderedChunk, ): Promise<{ code: string; map: SourceMapInput } | void> => { - const code = await preCompilation(sourceCode, outputOptions, transforms); + const code = await preCompilation(sourceCode, outputOptions, chunk, transforms); const [compileOptions, mapFile] = await options( requestedCompileOptions, outputOptions, @@ -53,7 +54,7 @@ const renderChunk = async ( try { return { - code: await compiler(compileOptions, transforms), + code: await compiler(compileOptions, chunk, transforms), map: JSON.parse(await fsPromises.readFile(mapFile, 'utf8')), }; } catch (error) { @@ -82,7 +83,13 @@ export default function closureCompiler(requestedCompileOptions: CompileOptions }, renderChunk: async (code: string, chunk: RenderedChunk, outputOptions: OutputOptions) => { const transforms = createTransforms(context, inputOptions); - const output = await renderChunk(transforms, requestedCompileOptions, code, outputOptions); + const output = await renderChunk( + transforms, + requestedCompileOptions, + code, + outputOptions, + chunk, + ); return output || null; }, }; diff --git a/src/temp-file.ts b/src/temp-file.ts index 4b43be8e..734f96d3 100644 --- a/src/temp-file.ts +++ b/src/temp-file.ts @@ -19,8 +19,8 @@ import { tmpdir } from 'os'; import { v4 } from 'uuid'; import { promises } from 'fs'; -export async function writeTempFile(content: string): Promise { - const path: string = join(tmpdir(), v4()); +export async function writeTempFile(content: string, extension: string = ''): Promise { + const path: string = join(tmpdir(), v4() + extension); await promises.mkdir(dirname(path), { recursive: true }); await promises.writeFile(path, content, 'utf-8'); diff --git a/src/transforms.ts b/src/transforms.ts index b791da65..ae8e6a45 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { OutputOptions, PluginContext, InputOptions } from 'rollup'; +import { OutputOptions, PluginContext, InputOptions, RenderedChunk } from 'rollup'; import { Transform } from './types'; import IifeTransform from './transformers/iife'; import CJSTransform from './transformers/cjs'; @@ -23,7 +23,7 @@ import ExportTransform from './transformers/exports'; import ImportTransform from './transformers/imports'; import StrictTransform from './transformers/strict'; import ConstTransform from './transformers/const'; -import { logSource } from './debug'; +import { logTransformChain } from './debug'; /** * Instantiate transform class instances for the plugin invocation. @@ -56,21 +56,26 @@ export const createTransforms = ( export async function preCompilation( code: string, outputOptions: OutputOptions, + chunk: RenderedChunk, transforms: Array, ): Promise { // Each transform has a 'preCompilation' step that must complete before passing // the resulting code to Closure Compiler. - await logSource('before preCompilation handlers', code); + const log: Array<[string, string]> = []; + + log.push(['before', code]); for (const transform of transforms) { transform.outputOptions = outputOptions; const result = await transform.preCompilation(code); if (result && result.code) { - await logSource(`after ${transform.name} preCompilation`, result && result.code); + log.push([transform.name, code]); code = result.code; } } - await logSource('after preCompilation handlers', code); + log.push(['after', code]); + await logTransformChain(chunk.fileName, 'PreCompilation', log); + return code; } @@ -80,18 +85,26 @@ export async function preCompilation( * @param transforms Transforms to execute. * @return source code following `postCompilation` */ -export async function postCompilation(code: string, transforms: Array): Promise { +export async function postCompilation( + code: string, + chunk: RenderedChunk, + transforms: Array, +): Promise { // Following successful Closure Compiler compilation, each transform needs an opportunity // to clean up work is performed in preCompilation via postCompilation. - await logSource('before postCompilation handlers', code); + const log: Array<[string, string]> = []; + + log.push(['before', code]); for (const transform of transforms) { const result = await transform.postCompilation(code); if (result && result.code) { - logSource(`after ${transform.name} postCompilation`, result && result.code); + log.push([transform.name, result.code]); code = result.code; } } - await logSource('after postCompilation handlers', code); + log.push(['after', code]); + await logTransformChain(chunk.fileName, 'PostCompilation', log); + return code; } diff --git a/test/import/fixtures/chunk-5275c9cc.esm.default.js b/test/import/fixtures/chunk-5275c9cc.esm.default.js new file mode 100644 index 00000000..31e45e44 --- /dev/null +++ b/test/import/fixtures/chunk-5275c9cc.esm.default.js @@ -0,0 +1 @@ +var a=function(){console.log("foo")};var b=function(){console.log("baz")};var c=function(){console.log("bar")};export{a,b,c}; diff --git a/test/import/fixtures/utf8-common.js b/test/import/fixtures/utf8-common.js new file mode 100644 index 00000000..e0432e0f --- /dev/null +++ b/test/import/fixtures/utf8-common.js @@ -0,0 +1,9 @@ +export function ɵɵfoo() { + console.log('foo'); +} +export function ɵɵbar() { + console.log('bar'); +} +export function baz() { + console.log('baz'); +} \ No newline at end of file diff --git a/test/import/fixtures/utf8-lazy.esm.default.js b/test/import/fixtures/utf8-lazy.esm.default.js new file mode 100644 index 00000000..ab0f2970 --- /dev/null +++ b/test/import/fixtures/utf8-lazy.esm.default.js @@ -0,0 +1 @@ +import { c as ɵɵbar } from './chunk-5275c9cc.js';\u0275\u0275bar(); \ No newline at end of file diff --git a/test/import/fixtures/utf8-lazy.js b/test/import/fixtures/utf8-lazy.js new file mode 100644 index 00000000..c3b14f10 --- /dev/null +++ b/test/import/fixtures/utf8-lazy.js @@ -0,0 +1,2 @@ +import { ɵɵbar } from './utf8-common.js'; +ɵɵbar(); \ No newline at end of file diff --git a/test/import/fixtures/utf8.esm.default.js b/test/import/fixtures/utf8.esm.default.js new file mode 100644 index 00000000..bb347ece --- /dev/null +++ b/test/import/fixtures/utf8.esm.default.js @@ -0,0 +1 @@ +import { a as ɵɵfoo, b as baz } from './chunk-5275c9cc.js';\u0275\u0275foo();baz();import("./utf8-lazy.js"); diff --git a/test/import/fixtures/utf8.js b/test/import/fixtures/utf8.js new file mode 100644 index 00000000..d71dab1e --- /dev/null +++ b/test/import/fixtures/utf8.js @@ -0,0 +1,6 @@ +import { ɵɵfoo, baz } from './utf8-common.js'; + +ɵɵfoo(); +baz(); + +import('./utf8-lazy.js'); \ No newline at end of file diff --git a/test/import/utf8.test.js b/test/import/utf8.test.js new file mode 100644 index 00000000..b6c4d6b5 --- /dev/null +++ b/test/import/utf8.test.js @@ -0,0 +1,22 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { generator, DEFAULT_CLOSURE_OPTIONS, ES5_STRICT_CLOSURE_OPTIONS } from '../generator'; + +generator('import', 'utf8', true, undefined, { + ...DEFAULT_CLOSURE_OPTIONS, + // ...ES5_STRICT_CLOSURE_OPTIONS, +}); From 9d8ce11a6826ffa944b83ecfb1ecbbbb49ff8da5 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 22:18:40 -0800 Subject: [PATCH 07/10] utf8 import tests --- src/parsing/preserve-named-constant-export.ts | 5 ++-- src/transformers/imports.ts | 2 +- src/transforms.ts | 23 +++++++++++-------- .../utf8-common-38fdc940.esm.default.js | 1 + .../utf8-lazy-d5bcdc27.esm.default.js | 1 + test/import/fixtures/utf8.esm.default.js | 2 +- 6 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 test/import/fixtures/utf8-common-38fdc940.esm.default.js create mode 100644 test/import/fixtures/utf8-lazy-d5bcdc27.esm.default.js diff --git a/src/parsing/preserve-named-constant-export.ts b/src/parsing/preserve-named-constant-export.ts index 766f825f..18c0e19b 100644 --- a/src/parsing/preserve-named-constant-export.ts +++ b/src/parsing/preserve-named-constant-export.ts @@ -36,6 +36,7 @@ function PreserveFunction( const memberExpression = assignmentExpression.left as MemberExpression; const functionExpression = assignmentExpression.right as FunctionExpression; const [memberExpressionObjectStart] = memberExpression.object.range as Range; + const functionName = exportInline ? exportDetails.exported : exportDetails.local; if (functionExpression.params.length > 0) { const [paramsStart] = functionExpression.params[0].range as Range; @@ -43,14 +44,14 @@ function PreserveFunction( source.overwrite( memberExpressionObjectStart, paramsStart, - `${exportInline ? 'export ' : ''}function ${exportDetails.exported}(`, + `${exportInline ? 'export ' : ''}function ${functionName}(`, ); } else { const [bodyStart] = functionExpression.body.range as Range; source.overwrite( memberExpressionObjectStart, bodyStart, - `${exportInline ? 'export ' : ''}function ${exportDetails.exported}()`, + `${exportInline ? 'export ' : ''}function ${functionName}()`, ); } diff --git a/src/transformers/imports.ts b/src/transformers/imports.ts index be23835d..b4b8d79c 100644 --- a/src/transformers/imports.ts +++ b/src/transformers/imports.ts @@ -84,7 +84,7 @@ window['${DYNAMIC_IMPORT_REPLACEMENT}'] = ${DYNAMIC_IMPORT_REPLACEMENT};`; const program = parse(code); walk.simple(program, { - async ImportDeclaration(node: ImportDeclaration) { + ImportDeclaration(node: ImportDeclaration) { const name = literalName(node.source); const range: Range = node.range as Range; const specifiers: Specifiers = Specifiers(node.specifiers); diff --git a/src/transforms.ts b/src/transforms.ts index ae8e6a45..956f9569 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -94,17 +94,22 @@ export async function postCompilation( // to clean up work is performed in preCompilation via postCompilation. const log: Array<[string, string]> = []; - log.push(['before', code]); - for (const transform of transforms) { - const result = await transform.postCompilation(code); - if (result && result.code) { - log.push([transform.name, result.code]); - code = result.code; + try { + log.push(['before', code]); + for (const transform of transforms) { + const result = await transform.postCompilation(code); + if (result && result.code) { + log.push([transform.name, result.code]); + code = result.code; + } } - } - log.push(['after', code]); - await logTransformChain(chunk.fileName, 'PostCompilation', log); + log.push(['after', code]); + await logTransformChain(chunk.fileName, 'PostCompilation', log); + } catch (e) { + await logTransformChain(chunk.fileName, 'PostCompilation', log); + throw e; + } return code; } diff --git a/test/import/fixtures/utf8-common-38fdc940.esm.default.js b/test/import/fixtures/utf8-common-38fdc940.esm.default.js new file mode 100644 index 00000000..d0757265 --- /dev/null +++ b/test/import/fixtures/utf8-common-38fdc940.esm.default.js @@ -0,0 +1 @@ +function ɵɵbar(){console.log("bar")};function baz(){console.log("baz")};function ɵɵfoo(){console.log("foo")};export{ɵɵbar as a,baz as b,ɵɵfoo as ɵ} diff --git a/test/import/fixtures/utf8-lazy-d5bcdc27.esm.default.js b/test/import/fixtures/utf8-lazy-d5bcdc27.esm.default.js new file mode 100644 index 00000000..6227a6c8 --- /dev/null +++ b/test/import/fixtures/utf8-lazy-d5bcdc27.esm.default.js @@ -0,0 +1 @@ +import {a as ɵɵbar} from './utf8-common-38fdc940.js';\u0275\u0275bar(); diff --git a/test/import/fixtures/utf8.esm.default.js b/test/import/fixtures/utf8.esm.default.js index bb347ece..bae75b51 100644 --- a/test/import/fixtures/utf8.esm.default.js +++ b/test/import/fixtures/utf8.esm.default.js @@ -1 +1 @@ -import { a as ɵɵfoo, b as baz } from './chunk-5275c9cc.js';\u0275\u0275foo();baz();import("./utf8-lazy.js"); +import {ɵ as ɵɵfoo,b as baz} from './utf8-common-38fdc940.js';\u0275\u0275foo();baz();import("./utf8-lazy-d5bcdc27.js"); From 3966fefcef7ebf4a0ba9087d5546ab826e378e06 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 22:45:01 -0800 Subject: [PATCH 08/10] Test now pass --- src/debug.ts | 6 +- src/options.ts | 9 +- src/transformers/exports.ts | 312 ++++++------------ src/transformers/strict.ts | 4 +- .../fixtures/identifier-alias.esm.advanced.js | 2 +- .../fixtures/identifier-alias.esm.es5.js | 2 +- .../fixtures/class.esm.advanced.js | 2 +- .../fixtures/class.esm.es5.js | 2 +- test/generator.js | 4 +- 9 files changed, 109 insertions(+), 234 deletions(-) diff --git a/src/debug.ts b/src/debug.ts index b1bcb249..6ad3c8dd 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -16,9 +16,9 @@ import { writeTempFile } from './temp-file'; -const DEBUG_ENABLED = true; +const DEBUG_ENABLED = false; -/* c8 ignore next 8 */ +/* c8 ignore next 16 */ export async function logTransformChain( file: string, stage: string, @@ -36,7 +36,7 @@ export async function logTransformChain( } } -/* c8 ignore next 6 */ +/* c8 ignore next 8 */ export const log = (preamble: string | undefined, message: string | object): void | null => { if (DEBUG_ENABLED) { if (preamble) { diff --git a/src/options.ts b/src/options.ts index 56a0ebde..4747b636 100644 --- a/src/options.ts +++ b/src/options.ts @@ -15,7 +15,7 @@ */ import { Transform } from './types'; -import { ModuleFormat, OutputOptions } from 'rollup'; +import { OutputOptions } from 'rollup'; import { CompileOptions } from 'google-closure-compiler'; import { writeTempFile } from './temp-file'; import { log } from './debug'; @@ -27,10 +27,11 @@ export const ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_INVALID = /** * Checks if output format is ESM - * @param format + * @param outputOptions * @return boolean */ -export const isESMFormat = (format?: ModuleFormat): boolean => format === 'esm' || format === 'es'; +export const isESMFormat = ({ format }: OutputOptions): boolean => + format === 'esm' || format === 'es'; /** * Throw Errors if compile options will result in unexpected behaviour. @@ -76,7 +77,7 @@ export const defaults = async ( return { language_out: 'NO_TRANSPILE', - assume_function_wrapper: isESMFormat(options.format), + assume_function_wrapper: isESMFormat(options), warning_level: 'QUIET', module_resolution: 'NODE', externs: transformerExterns.concat(providedExterns), diff --git a/src/transformers/exports.ts b/src/transformers/exports.ts index 8df0168f..ea2ca8fe 100644 --- a/src/transformers/exports.ts +++ b/src/transformers/exports.ts @@ -69,8 +69,10 @@ export default class ExportTransform extends Transform implements TransformInter mapping.forEach(map => { if (map.source === null) { this.currentSourceExportCount++; + this.originalExports.set(map.local, map); + } else { + this.originalExports.set(map.exported, map); } - this.originalExports.set(map.local, map); }); private static storeExportToAppend( @@ -137,34 +139,33 @@ export default class ExportTransform extends Transform implements TransformInter * @return modified input source with window scoped references. */ public async preCompilation(code: string): Promise { - if (isESMFormat(this.outputOptions.format)) { - await this.deriveExports(code); - const source = new MagicString(code); - - console.log('derived', this.originalExports); - for (const key of this.originalExports.keys()) { - const value: ExportDetails = this.originalExports.get(key) as ExportDetails; - - // Remove export statements before Closure Compiler sees the code - // This prevents CC from transpiling `export` statements when the language_out is set to a value - // where exports were not part of the language. - source.remove(...value.range); - // Window scoped references for each key are required to ensure Closure Compilre retains the code. - if (value.source === null) { - source.append(`\nwindow['${value.local}'] = ${value.local};`); - } else { - source.append(`\nwindow['${value.exported}'] = ${value.exported};`); - } - } - + if (!isESMFormat(this.outputOptions)) { return { - code: source.toString(), - map: source.generateMap().mappings, + code, }; } + await this.deriveExports(code); + const source = new MagicString(code); + + for (const key of this.originalExports.keys()) { + const value: ExportDetails = this.originalExports.get(key) as ExportDetails; + + // Remove export statements before Closure Compiler sees the code + // This prevents CC from transpiling `export` statements when the language_out is set to a value + // where exports were not part of the language. + source.remove(...value.range); + // Window scoped references for each key are required to ensure Closure Compilre retains the code. + if (value.source === null) { + source.append(`\nwindow['${value.local}'] = ${value.local};`); + } else { + source.append(`\nwindow['${value.exported}'] = ${value.exported};`); + } + } + return { - code, + code: source.toString(), + map: source.generateMap().mappings, }; } @@ -177,214 +178,87 @@ export default class ExportTransform extends Transform implements TransformInter * @return Promise containing the repaired source */ public async postCompilation(code: string): Promise { - if (isESMFormat(this.outputOptions.format)) { - const source = new MagicString(code); - const program = parse(code); - const { originalExports, currentSourceExportCount } = this; - let collectedExportsToAppend: Map> = new Map(); + if (!isESMFormat(this.outputOptions)) { + return { + code, + }; + } - source.trimEnd(); + const source = new MagicString(code); + const program = parse(code); + const { originalExports, currentSourceExportCount } = this; + let collectedExportsToAppend: Map> = new Map(); + + source.trimEnd(); + + walk.ancestor(program, { + // We inserted window scoped assignments for all the export statements during `preCompilation` + // Now we need to find where Closure Compiler moved them, and restore the exports of their name. + // ASTExporer Link: https://astexplorer.net/#/gist/94f185d06a4105d64828f1b8480bddc8/0fc5885ae5343f964d0cdd33c7d392a70cf5fcaf + Identifier(node: Identifier, ancestors: Array) { + if (node.name !== 'window') { + return; + } - walk.ancestor(program, { - // We inserted window scoped assignments for all the export statements during `preCompilation` - // Now we need to find where Closure Compiler moved them, and restore the exports of their name. - // ASTExporer Link: https://astexplorer.net/#/gist/94f185d06a4105d64828f1b8480bddc8/0fc5885ae5343f964d0cdd33c7d392a70cf5fcaf - Identifier(node: Identifier, ancestors: Array) { - if (node.name !== 'window') { - return; + for (const ancestor of ancestors) { + if (!NodeIsPreservedExport(ancestor)) { + continue; } - - for (const ancestor of ancestors) { - if (!NodeIsPreservedExport(ancestor)) { - continue; + // Can cast these since they were validated with the `NodeIsPreservedExport` test. + const expression: AssignmentExpression = ancestor.expression as AssignmentExpression; + const left: MemberExpression = expression.left as MemberExpression; + const exportName: string | null = PreservedExportName(left); + + if (exportName !== null && originalExports.get(exportName)) { + const exportDetails: ExportDetails = originalExports.get(exportName) as ExportDetails; + const exportIsLocal: boolean = exportDetails.source === null; + const exportInline: boolean = + exportIsLocal && + currentSourceExportCount === 1 && + exportDetails.local === exportDetails.exported; + + switch (exportDetails.type) { + case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: + case ExportClosureMapping.DEFAULT: + if (PreserveDefault(code, source, ancestor, exportDetails, exportInline)) { + collectedExportsToAppend = ExportTransform.storeExportToAppend( + collectedExportsToAppend, + exportDetails, + ); + } + break; + case ExportClosureMapping.NAMED_CONSTANT: + if (PreserveNamedConstant(code, source, ancestor, exportDetails, exportInline)) { + collectedExportsToAppend = ExportTransform.storeExportToAppend( + collectedExportsToAppend, + exportDetails, + ); + } + break; } - // Can cast these since they were validated with the `NodeIsPreservedExport` test. - const expression: AssignmentExpression = ancestor.expression as AssignmentExpression; - const left: MemberExpression = expression.left as MemberExpression; - const exportName: string | null = PreservedExportName(left); - - if (exportName !== null && originalExports.get(exportName)) { - const exportDetails: ExportDetails = originalExports.get(exportName) as ExportDetails; - const exportIsLocal: boolean = exportDetails.source === null; - const exportInline: boolean = - exportIsLocal && - currentSourceExportCount === 1 && - exportDetails.local === exportDetails.exported; - switch (exportDetails.type) { - case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: - case ExportClosureMapping.DEFAULT: - if (PreserveDefault(code, source, ancestor, exportDetails, exportInline)) { - collectedExportsToAppend = ExportTransform.storeExportToAppend( - collectedExportsToAppend, - exportDetails, - ); - } - break; - case ExportClosureMapping.NAMED_CONSTANT: - if (PreserveNamedConstant(code, source, ancestor, exportDetails, exportInline)) { - collectedExportsToAppend = ExportTransform.storeExportToAppend( - collectedExportsToAppend, - exportDetails, - ); - } - break; - default: - console.log('type', exportDetails.type); - break; - } - - if (!exportIsLocal) { - source.remove((left.range as Range)[0], (expression.right.range as Range)[1] + 1); - } + if (!exportIsLocal) { + source.remove((left.range as Range)[0], (expression.right.range as Range)[1] + 1); } } + } + }, + }); - // ancestors.forEach((ancestor: Node) => { - // if ( - // ancestor.type === 'ExpressionStatement' && - // ancestor.expression.type === 'AssignmentExpression' && - // ancestor.expression.left.type === 'MemberExpression' && - // ancestor.expression.left.object.type === 'Identifier' && - // ancestor.expression.left.object.name === 'window' - // ) { - // const { property: leftProperty } = ancestor.expression.left; - // let exportName: string | null = null; - // if (leftProperty.type === 'Identifier') { - // exportName = leftProperty.name; - // } else if ( - // leftProperty.type === 'Literal' && - // typeof leftProperty.value === 'string' - // ) { - // exportName = leftProperty.value; - // } - - // if (exportName !== null && originalExports.get(exportName)) { - // const exportDetails: ExportDetails = originalExports.get( - // exportName, - // ) as ExportDetails; - // switch (exportDetails.type) { - // case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: - // case ExportClosureMapping.DEFAULT: - // if (ancestor.expression.left.range) { - // source.overwrite( - // ancestor.expression.left.range[0], - // ancestor.expression.left.range[1] + ancestor.expression.operator.length, - // 'export default ', - // ); - // } - // break; - // case ExportClosureMapping.NAMED_CONSTANT: - // const exportFromCurrentSource: boolean = exportDetails.source === null; - // const inlineExport: boolean = - // exportFromCurrentSource && currentSourceExportCount === 1; - // let exportCollected: boolean = false; - // if (exportFromCurrentSource) { - // const { object: leftObject } = ancestor.expression.left; - // if (leftObject.range) { - // const { left, right } = ancestor.expression; - // switch (right.type) { - // case 'FunctionExpression': - // // Function Expressions can be inlined instead of preserved as variable references. - // // window['foo'] = function(){}; => export function foo(){} / function foo(){} - // if (right.params.length > 0) { - // // FunctionExpression has parameters. - // source.overwrite( - // (leftObject.range as Range)[0], - // (right.params[0].range as Range)[0], - // `${inlineExport ? 'export ' : ''}function ${ - // exportDetails.exported - // }(`, - // ); - // } else { - // source.overwrite( - // (leftObject.range as Range)[0], - // (right.body.range as Range)[0], - // `${inlineExport ? 'export ' : ''}function ${ - // exportDetails.exported - // }()`, - // ); - // } - // break; - // case 'Identifier': - // if (left.property.type === 'Identifier') { - // // Identifiers are present when a complex object (class) has been saved as an export. - // // In this case we currently opt out of inline exporting, since the identifier - // // is a mangled name for the export. - // exportDetails.local = right.name; - // exportDetails.exported = left.property.name; - - // source.remove( - // (ancestor.expression.left.range as Range)[0], - // (ancestor.expression.right.range as Range)[1] + 1, - // ); - - // // Since we're manually mapping the name back from the changes done by Closure - // // Ensure the export isn't stored for insertion here and later on. - // collectedExportsToAppend = ExportTransform.storeExportToAppend( - // collectedExportsToAppend, - // exportDetails, - // ); - // exportCollected = true; - // } - // break; - // default: - // const statement = inlineExport ? 'export var ' : 'var '; - // source.overwrite( - // leftObject.range[0], - // leftObject.range[1] + 1, - // statement, - // ); - // break; - // } - // } - // if (exportDetails.local !== exportDetails.exported) { - // exportDetails.local = exportDetails.exported; - // exportDetails.exported = exportDetails.local; - // } - // } else if ( - // ancestor.expression.left.range && - // ancestor.expression.right.range - // ) { - // source.remove( - // ancestor.expression.left.range[0], - // ancestor.expression.right.range[1] + 1, - // ); - // } - - // if (!inlineExport && !exportCollected) { - // collectedExportsToAppend = ExportTransform.storeExportToAppend( - // collectedExportsToAppend, - // exportDetails, - // ); - // } - // break; - // } - // } - // } - // }); - }, - }); - - for (const exportSource of collectedExportsToAppend.keys()) { - const toAppend = collectedExportsToAppend.get(exportSource); - if (toAppend && toAppend.length > 0) { - if (exportSource === null) { - source.append(`export{${toAppend.join(',')}}`); - } else { - source.prepend(`export{${toAppend.join(',')}}from'${exportSource}';`); - } + for (const exportSource of collectedExportsToAppend.keys()) { + const toAppend = collectedExportsToAppend.get(exportSource); + if (toAppend && toAppend.length > 0) { + if (exportSource === null) { + source.append(`export{${toAppend.join(',')}}`); + } else { + source.prepend(`export{${toAppend.join(',')}}from'${exportSource}';`); } } - - return { - code: source.toString(), - map: source.generateMap().mappings, - }; } return { - code, + code: source.toString(), + map: source.generateMap().mappings, }; } } diff --git a/src/transformers/strict.ts b/src/transformers/strict.ts index f4363f60..c37fb0e4 100644 --- a/src/transformers/strict.ts +++ b/src/transformers/strict.ts @@ -32,9 +32,9 @@ export default class StrictTransform extends Transform { * @return code after removing the strict mode declaration (when safe to do so) */ public async postCompilation(code: string): Promise { - const { format, file } = this.outputOptions; + const { file } = this.outputOptions; - if (isESMFormat(format) || (file && extname(file) === '.mjs')) { + if (isESMFormat(this.outputOptions) || (file && extname(file) === '.mjs')) { const source = new MagicString(code); const program = parse(code); diff --git a/test/export-named/fixtures/identifier-alias.esm.advanced.js b/test/export-named/fixtures/identifier-alias.esm.advanced.js index 5fcd2ca9..57efc773 100644 --- a/test/export-named/fixtures/identifier-alias.esm.advanced.js +++ b/test/export-named/fixtures/identifier-alias.esm.advanced.js @@ -1 +1 @@ -export var bar=1; +var a=1;export{a as bar} diff --git a/test/export-named/fixtures/identifier-alias.esm.es5.js b/test/export-named/fixtures/identifier-alias.esm.es5.js index 5fcd2ca9..57efc773 100644 --- a/test/export-named/fixtures/identifier-alias.esm.es5.js +++ b/test/export-named/fixtures/identifier-alias.esm.es5.js @@ -1 +1 @@ -export var bar=1; +var a=1;export{a as bar} diff --git a/test/export-variables/fixtures/class.esm.advanced.js b/test/export-variables/fixtures/class.esm.advanced.js index 9ac1d495..9f2a3470 100644 --- a/test/export-variables/fixtures/class.esm.advanced.js +++ b/test/export-variables/fixtures/class.esm.advanced.js @@ -1 +1 @@ -class a{constructor(b){this.a=b}console(){console.log(this.a)}}export{a as Exported} +class a{constructor(b){this.a=b}console(){console.log(this.a)}}export var Exported=a; diff --git a/test/export-variables/fixtures/class.esm.es5.js b/test/export-variables/fixtures/class.esm.es5.js index 00b57ee3..a94cd2e3 100644 --- a/test/export-variables/fixtures/class.esm.es5.js +++ b/test/export-variables/fixtures/class.esm.es5.js @@ -1 +1 @@ -function a(b){this.name_=b}a.prototype.console=function(){console.log(this.name_)};export{a as Exported} +function a(b){this.name_=b}a.prototype.console=function(){console.log(this.name_)};export var Exported=a; diff --git a/test/generator.js b/test/generator.js index 3053ef9c..8be6a931 100644 --- a/test/generator.js +++ b/test/generator.js @@ -34,8 +34,8 @@ const ES5_STRICT_CLOSURE_OPTIONS = { }; const defaultClosureFlags = { ...DEFAULT_CLOSURE_OPTIONS, - // ...ADVANCED_CLOSURE_OPTIONS, - // ...ES5_STRICT_CLOSURE_OPTIONS, + ...ADVANCED_CLOSURE_OPTIONS, + ...ES5_STRICT_CLOSURE_OPTIONS, }; const ES_OUTPUT = 'es'; From 423312a4eb6b0de3425147add7aea88ae1db0919 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 22:55:38 -0800 Subject: [PATCH 09/10] Further cleanups --- src/parsing/import-specifiers.ts | 2 +- src/parsing/literal-name.ts | 3 +-- src/parsing/preserve-named-constant-export.ts | 23 ------------------- src/transformers/exports.ts | 6 +++-- src/transformers/imports.ts | 2 +- 5 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/parsing/import-specifiers.ts b/src/parsing/import-specifiers.ts index d85e71c9..5ae6d19e 100644 --- a/src/parsing/import-specifiers.ts +++ b/src/parsing/import-specifiers.ts @@ -17,7 +17,7 @@ import { IMPORT_SPECIFIER, IMPORT_NAMESPACE_SPECIFIER, IMPORT_DEFAULT_SPECIFIER } from '../types'; import { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier } from 'estree'; -export interface Specifiers { +interface Specifiers { default: string | null; specific: Array; local: Array; diff --git a/src/parsing/literal-name.ts b/src/parsing/literal-name.ts index c51babc4..6f06e736 100644 --- a/src/parsing/literal-name.ts +++ b/src/parsing/literal-name.ts @@ -17,6 +17,5 @@ import { Literal, SimpleLiteral } from 'estree'; export function literalName(literal: Literal): string { - const literalValue = (literal as SimpleLiteral).value; - return typeof literalValue === 'string' ? literalValue : ''; + return (literal as SimpleLiteral).value as string; } diff --git a/src/parsing/preserve-named-constant-export.ts b/src/parsing/preserve-named-constant-export.ts index 18c0e19b..8c67b3c7 100644 --- a/src/parsing/preserve-named-constant-export.ts +++ b/src/parsing/preserve-named-constant-export.ts @@ -94,29 +94,6 @@ function PreserveIdentifier( return !exportInline; } -/* -if (left.property.type === 'Identifier') { - // Identifiers are present when a complex object (class) has been saved as an export. - // In this case we currently opt out of inline exporting, since the identifier - // is a mangled name for the export. - exportDetails.local = right.name; - exportDetails.exported = left.property.name; - - source.remove( - (ancestor.expression.left.range as Range)[0], - (ancestor.expression.right.range as Range)[1] + 1, - ); - - // Since we're manually mapping the name back from the changes done by Closure - // Ensure the export isn't stored for insertion here and later on. - collectedExportsToAppend = ExportTransform.storeExportToAppend( - collectedExportsToAppend, - exportDetails, - ); - exportCollected = true; -} -*/ - export function PreserveNamedConstant( code: string, source: MagicString, diff --git a/src/transformers/exports.ts b/src/transformers/exports.ts index ea2ca8fe..6f1a743d 100644 --- a/src/transformers/exports.ts +++ b/src/transformers/exports.ts @@ -248,10 +248,12 @@ export default class ExportTransform extends Transform implements TransformInter for (const exportSource of collectedExportsToAppend.keys()) { const toAppend = collectedExportsToAppend.get(exportSource); if (toAppend && toAppend.length > 0) { + const names = toAppend.join(','); + if (exportSource === null) { - source.append(`export{${toAppend.join(',')}}`); + source.append(`export{${names}}`); } else { - source.prepend(`export{${toAppend.join(',')}}from'${exportSource}';`); + source.prepend(`export{${names}}from'${exportSource}';`); } } } diff --git a/src/transformers/imports.ts b/src/transformers/imports.ts index b4b8d79c..fcd22708 100644 --- a/src/transformers/imports.ts +++ b/src/transformers/imports.ts @@ -87,7 +87,7 @@ window['${DYNAMIC_IMPORT_REPLACEMENT}'] = ${DYNAMIC_IMPORT_REPLACEMENT};`; ImportDeclaration(node: ImportDeclaration) { const name = literalName(node.source); const range: Range = node.range as Range; - const specifiers: Specifiers = Specifiers(node.specifiers); + const specifiers = Specifiers(node.specifiers); self.importedExternalsSyntax[name] = FormatSpecifiers(specifiers, name); self.importedExternalsLocalNames.push(...specifiers.local); From 34d93890b63137133c7a3f1b0735ee48bef75dc7 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 23:06:00 -0800 Subject: [PATCH 10/10] reenable utf8 test for es5 --- test/import/fixtures/utf8-common-38fdc940.esm.es5.js | 1 + test/import/fixtures/utf8-lazy-d5bcdc27.esm.es5.js | 1 + test/import/fixtures/utf8.esm.es5.js | 1 + test/import/utf8.test.js | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 test/import/fixtures/utf8-common-38fdc940.esm.es5.js create mode 100644 test/import/fixtures/utf8-lazy-d5bcdc27.esm.es5.js create mode 100644 test/import/fixtures/utf8.esm.es5.js diff --git a/test/import/fixtures/utf8-common-38fdc940.esm.es5.js b/test/import/fixtures/utf8-common-38fdc940.esm.es5.js new file mode 100644 index 00000000..d0757265 --- /dev/null +++ b/test/import/fixtures/utf8-common-38fdc940.esm.es5.js @@ -0,0 +1 @@ +function ɵɵbar(){console.log("bar")};function baz(){console.log("baz")};function ɵɵfoo(){console.log("foo")};export{ɵɵbar as a,baz as b,ɵɵfoo as ɵ} diff --git a/test/import/fixtures/utf8-lazy-d5bcdc27.esm.es5.js b/test/import/fixtures/utf8-lazy-d5bcdc27.esm.es5.js new file mode 100644 index 00000000..6227a6c8 --- /dev/null +++ b/test/import/fixtures/utf8-lazy-d5bcdc27.esm.es5.js @@ -0,0 +1 @@ +import {a as ɵɵbar} from './utf8-common-38fdc940.js';\u0275\u0275bar(); diff --git a/test/import/fixtures/utf8.esm.es5.js b/test/import/fixtures/utf8.esm.es5.js new file mode 100644 index 00000000..bae75b51 --- /dev/null +++ b/test/import/fixtures/utf8.esm.es5.js @@ -0,0 +1 @@ +import {ɵ as ɵɵfoo,b as baz} from './utf8-common-38fdc940.js';\u0275\u0275foo();baz();import("./utf8-lazy-d5bcdc27.js"); diff --git a/test/import/utf8.test.js b/test/import/utf8.test.js index b6c4d6b5..228e7a8f 100644 --- a/test/import/utf8.test.js +++ b/test/import/utf8.test.js @@ -18,5 +18,5 @@ import { generator, DEFAULT_CLOSURE_OPTIONS, ES5_STRICT_CLOSURE_OPTIONS } from ' generator('import', 'utf8', true, undefined, { ...DEFAULT_CLOSURE_OPTIONS, - // ...ES5_STRICT_CLOSURE_OPTIONS, + ...ES5_STRICT_CLOSURE_OPTIONS, });