From 1ff3e2fb13945b83492eaabf97c11126423a7ac1 Mon Sep 17 00:00:00 2001 From: Kristofer Baxter Date: Wed, 8 Jan 2020 23:13:53 -0800 Subject: [PATCH] Refactor Exports, Add UTF-8 Tests (#277) * Better debugging * utf8 import tests * Test now pass * reenable utf8 test for es5 --- package.json | 5 +- src/compiler.ts | 4 +- src/debug.ts | 32 +- src/index.ts | 13 +- src/options.ts | 12 +- src/parsing/export-details.ts | 34 ++- src/parsing/import-specifiers.ts | 2 +- src/parsing/literal-name.ts | 3 +- src/parsing/preserve-default-export.ts | 39 +++ src/parsing/preserve-named-constant-export.ts | 111 +++++++ src/temp-file.ts | 4 +- src/transformers/exports.ts | 274 +++++++----------- src/transformers/imports.ts | 4 +- src/transformers/strict.ts | 4 +- src/transforms.ts | 44 ++- src/types.ts | 3 +- .../fixtures/identifier-alias.esm.advanced.js | 2 +- .../fixtures/identifier-alias.esm.default.js | 2 +- .../fixtures/identifier-alias.esm.es5.js | 2 +- .../fixtures/class.esm.advanced.js | 2 +- .../fixtures/class.esm.default.js | 2 +- .../fixtures/class.esm.es5.js | 2 +- test/generator.js | 10 +- .../fixtures/chunk-5275c9cc.esm.default.js | 1 + .../utf8-common-38fdc940.esm.default.js | 1 + .../fixtures/utf8-common-38fdc940.esm.es5.js | 1 + test/import/fixtures/utf8-common.js | 9 + .../utf8-lazy-d5bcdc27.esm.default.js | 1 + .../fixtures/utf8-lazy-d5bcdc27.esm.es5.js | 1 + 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.esm.es5.js | 1 + test/import/fixtures/utf8.js | 6 + test/import/utf8.test.js | 22 ++ test/provided-externs/class.test.js | 6 +- .../fixtures/class.esm.advanced.js | 2 +- .../fixtures/class.esm.default.js | 2 +- .../fixtures/class.esm.es5.js | 2 +- yarn.lock | 7 - 40 files changed, 437 insertions(+), 239 deletions(-) create mode 100644 src/parsing/preserve-default-export.ts create mode 100644 src/parsing/preserve-named-constant-export.ts create mode 100644 test/import/fixtures/chunk-5275c9cc.esm.default.js create mode 100644 test/import/fixtures/utf8-common-38fdc940.esm.default.js create mode 100644 test/import/fixtures/utf8-common-38fdc940.esm.es5.js create mode 100644 test/import/fixtures/utf8-common.js create mode 100644 test/import/fixtures/utf8-lazy-d5bcdc27.esm.default.js create mode 100644 test/import/fixtures/utf8-lazy-d5bcdc27.esm.es5.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.esm.es5.js create mode 100644 test/import/fixtures/utf8.js create mode 100644 test/import/utf8.test.js diff --git a/package.json b/package.json index de01c70a..34c0a871 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", @@ -65,6 +64,10 @@ "*.ts": [ "prettier --config .prettierrc --write", "git add" + ], + "*.test.js": [ + "prettier --config .prettierrc --write", + "git add" ] }, "husky": { 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 3e54c051..6ad3c8dd 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -18,24 +18,30 @@ import { writeTempFile } from './temp-file'; const DEBUG_ENABLED = false; -/* c8 ignore next 12 */ -export const logSource = async (preamble: string, source: string, code?: string): Promise => { +/* c8 ignore next 16 */ +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 => { +/* c8 ignore next 8 */ +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/options.ts b/src/options.ts index f1a78050..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. @@ -69,13 +70,14 @@ 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); } } 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/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; 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-default-export.ts b/src/parsing/preserve-default-export.ts new file mode 100644 index 00000000..fa3b594d --- /dev/null +++ b/src/parsing/preserve-default-export.ts @@ -0,0 +1,39 @@ +/** + * 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 { ExpressionStatement, AssignmentExpression, MemberExpression } from 'estree'; +import { ExportDetails, Range } from '../types'; +import MagicString from 'magic-string'; + +export function PreserveDefault( + code: string, + source: MagicString, + ancestor: ExpressionStatement, + exportDetails: ExportDetails, + exportInline: boolean, +): boolean { + const assignmentExpression = ancestor.expression as AssignmentExpression; + const memberExpression = assignmentExpression.left as MemberExpression; + const [memberExpressionStart, memberExpressionEnd]: Range = memberExpression.range as Range; + + source.overwrite( + memberExpressionStart, + memberExpressionEnd + assignmentExpression.operator.length, + 'export default ', + ); + + return false; +} diff --git a/src/parsing/preserve-named-constant-export.ts b/src/parsing/preserve-named-constant-export.ts new file mode 100644 index 00000000..8c67b3c7 --- /dev/null +++ b/src/parsing/preserve-named-constant-export.ts @@ -0,0 +1,111 @@ +/** + * 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 { + ExpressionStatement, + AssignmentExpression, + FunctionExpression, + MemberExpression, +} from 'estree'; +import { ExportDetails, Range } from '../types'; +import MagicString from 'magic-string'; + +function PreserveFunction( + code: string, + source: MagicString, + ancestor: ExpressionStatement, + exportDetails: ExportDetails, + exportInline: boolean, +): boolean { + // Function Expressions can be inlined instead of preserved as variable references. + // window['foo'] = function(){}; => 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; + const functionName = exportInline ? exportDetails.exported : exportDetails.local; + + if (functionExpression.params.length > 0) { + const [paramsStart] = functionExpression.params[0].range as Range; + // FunctionExpression has parameters. + source.overwrite( + memberExpressionObjectStart, + paramsStart, + `${exportInline ? 'export ' : ''}function ${functionName}(`, + ); + } else { + const [bodyStart] = functionExpression.body.range as Range; + source.overwrite( + memberExpressionObjectStart, + bodyStart, + `${exportInline ? 'export ' : ''}function ${functionName}()`, + ); + } + + 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; +} + +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/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/transformers/exports.ts b/src/transformers/exports.ts index d58b5f6d..6f1a743d 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, @@ -60,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.closureName, map); }); private static storeExportToAppend( @@ -109,7 +120,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`; } } @@ -128,33 +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); + if (!isESMFormat(this.outputOptions)) { + return { + code, + }; + } - for (const key of this.originalExports.keys()) { - const value: ExportDetails = this.originalExports.get(key) as ExportDetails; + await this.deriveExports(code); + const source = new MagicString(code); - // 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.closureName}'] = ${value.local};`); - } else { - source.append(`\nwindow['${value.closureName}'] = ${value.exported};`); - } - } + for (const key of this.originalExports.keys()) { + const value: ExportDetails = this.originalExports.get(key) as ExportDetails; - return { - code: source.toString(), - map: source.generateMap().mappings, - }; + // 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, }; } @@ -167,166 +178,89 @@ 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); - let collectedExportsToAppend: Map> = new Map(); - const { originalExports, currentSourceExportCount } = this; + 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(); - 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') { - 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; - } + source.trimEnd(); - 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; + 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; + } - source.remove( - (ancestor.expression.left.range as Range)[0], - (ancestor.expression.right.range as Range)[1] + 1, - ); + 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); - // 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 (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; - if (!inlineExport && !exportCollected) { - collectedExportsToAppend = ExportTransform.storeExportToAppend( - collectedExportsToAppend, - exportDetails, - ); - } - break; - } + 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; + } - 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}';`); + if (!exportIsLocal) { + source.remove((left.range as Range)[0], (expression.right.range as Range)[1] + 1); + } } } - } + }, + }); - return { - code: source.toString(), - map: source.generateMap().mappings, - }; + 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{${names}}`); + } else { + source.prepend(`export{${names}}from'${exportSource}';`); + } + } } return { - code, + code: source.toString(), + map: source.generateMap().mappings, }; } } diff --git a/src/transformers/imports.ts b/src/transformers/imports.ts index be23835d..fcd22708 100644 --- a/src/transformers/imports.ts +++ b/src/transformers/imports.ts @@ -84,10 +84,10 @@ 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); + const specifiers = Specifiers(node.specifiers); self.importedExternalsSyntax[name] = FormatSpecifiers(specifiers, name); self.importedExternalsLocalNames.push(...specifiers.local); 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/src/transforms.ts b/src/transforms.ts index 5d6327b0..956f9569 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) { - 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,31 @@ 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); - for (const transform of transforms) { - const result = await transform.postCompilation(code); - if (result && result.code) { - logSource(`after ${transform.name} postCompilation`, result && result.code); - code = result.code; + const log: Array<[string, string]> = []; + + 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); + } catch (e) { + await logTransformChain(chunk.fileName, 'PostCompilation', log); + throw e; } - await logSource('after postCompilation handlers', code); return code; } 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.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.default.js b/test/export-named/fixtures/identifier-alias.esm.default.js index 5fcd2ca9..57efc773 100644 --- a/test/export-named/fixtures/identifier-alias.esm.default.js +++ b/test/export-named/fixtures/identifier-alias.esm.default.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.default.js b/test/export-variables/fixtures/class.esm.default.js index 8a66c547..f0abc652 100644 --- a/test/export-variables/fixtures/class.esm.default.js +++ b/test/export-variables/fixtures/class.esm.default.js @@ -1 +1 @@ -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/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 02353cf1..8be6a931 100644 --- a/test/generator.js +++ b/test/generator.js @@ -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/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-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-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-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-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-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-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..bae75b51 --- /dev/null +++ b/test/import/fixtures/utf8.esm.default.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/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/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..228e7a8f --- /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, +}); 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..4ae1c575 100644 --- a/test/provided-externs/fixtures/class.esm.advanced.js +++ b/test/provided-externs/fixtures/class.esm.advanced.js @@ -1 +1 @@ -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..527965ca 100644 --- a/test/provided-externs/fixtures/class.esm.default.js +++ b/test/provided-externs/fixtures/class.esm.default.js @@ -1 +1 @@ -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..e306655d 100644 --- a/test/provided-externs/fixtures/class.esm.es5.js +++ b/test/provided-externs/fixtures/class.esm.es5.js @@ -1 +1 @@ -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; diff --git a/yarn.lock b/yarn.lock index f06740da..5043c872 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"