UNPKG

@ampproject/rollup-plugin-closure-compiler

Version:

Rollup + Google Closure Compiler

1,265 lines (1,243 loc) 73.1 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var fs = require('fs'); var googleClosureCompiler = require('google-closure-compiler'); var path = require('path'); var os = require('os'); var crypto = require('crypto'); var uuid = require('uuid'); var MagicString = _interopDefault(require('magic-string')); var remapping = _interopDefault(require('@ampproject/remapping')); var acorn = require('acorn'); var estreeWalker = require('estree-walker'); /** * 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. */ async function writeTempFile(content, extension = '', stableName = true) { let hash; if (stableName) { hash = crypto.createHash('sha1') .update(content) .digest('hex'); } else { hash = uuid.v4(); } const fullpath = path.join(os.tmpdir(), hash + extension); await fs.promises.mkdir(path.dirname(fullpath), { recursive: true }); await fs.promises.writeFile(fullpath, content, 'utf-8'); return fullpath; } /** * Copyright 2018 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. */ /* c8 ignore next 15 */ async function logTransformChain(file, stage, messages) { return; } /* c8 ignore next 7 */ const log = (preamble, message) => { return; }; /** * Copyright 2018 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. */ function createDecodedSourceMap(magicstring, source) { return { ...magicstring.generateDecodedMap({ hires: true, source }), version: 3, }; } function createExistingRawSourceMap(maps, file) { var _a; const remappedSourceMap = remapping(maps, () => null); return { ...remappedSourceMap, sources: remappedSourceMap.sources.map(source => source || ''), sourcesContent: ((_a = remappedSourceMap.sourcesContent) === null || _a === void 0 ? void 0 : _a.map(content => content || '')) || undefined, file, }; } /** * 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. */ class Transform { constructor(context, pluginOptions, mangler, memory, inputOptions, outputOptions) { this.name = 'Transform'; this.context = context; this.pluginOptions = pluginOptions; this.mangler = mangler; this.memory = memory; this.inputOptions = inputOptions; this.outputOptions = outputOptions; } } class SourceTransform extends Transform { constructor() { super(...arguments); this.name = 'SourceTransform'; } async transform(id, source) { return source; } } class ChunkTransform extends Transform { constructor() { super(...arguments); this.name = 'ChunkTransform'; } extern(options) { return null; } async pre(fileName, source) { return source; } async post(fileName, source) { return source; } } async function chunkLifecycle(fileName, printableName, method, code, transforms) { const log = []; const sourcemaps = []; let source = new MagicString(code); let finalSource = ''; log.push(['before', code]); try { for (const transform of transforms) { const transformed = await transform[method](fileName, source); const transformedSource = transformed.toString(); sourcemaps.push(createDecodedSourceMap(transformed, fileName)); source = new MagicString(transformedSource); log.push([transform.name, transformedSource]); } finalSource = source.toString(); } catch (e) { log.push(['after', finalSource]); await logTransformChain(); throw e; } log.push(['after', finalSource]); await logTransformChain(); return { code: finalSource, map: createExistingRawSourceMap(sourcemaps, fileName), }; } async function sourceLifecycle(id, printableName, code, transforms) { const fileName = path.basename(id); const log = []; const sourcemaps = []; let source = new MagicString(code); log.push(['before', code]); for (const transform of transforms) { const transformed = await transform.transform(id, source); const transformedSource = transformed.toString(); sourcemaps.push(createDecodedSourceMap(transformed, id)); source = new MagicString(transformedSource); log.push([transform.name, transformedSource]); } const finalSource = source.toString(); log.push(['after', finalSource]); await logTransformChain(); return { code: finalSource, map: createExistingRawSourceMap(sourcemaps, fileName), }; } /** * 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. */ /** * Closure Compiler will not compile code that is prefixed with a hashbang (common to rollup output for CLIs). * * This transform will remove the hashbang (if present) and ask Ebbinghaus to remember if for after compilation. */ class HashbangRemoveTransform extends ChunkTransform { constructor() { super(...arguments); this.name = 'HashbangRemoveTransform'; } /** * @param source MagicString of source to process post Closure Compilation. */ async pre(fileName, source) { const stringified = source.trim().toString(); const match = /^#!.*/.exec(stringified); if (!match) { return source; } this.memory.hashbang = match[0]; source.remove(0, match[0].length); return source; } } /** * 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. */ /** * Closure Compiler will not compile code that is prefixed with a hashbang (common to rollup output for CLIs). * * This transform will restore the hashbang if Ebbinghaus knows it exists. */ class HashbangApplyTransform extends ChunkTransform { constructor() { super(...arguments); this.name = 'HashbangApplyTransform'; } /** * @param source MagicString of source to process post Closure Compilation. */ async post(fileName, source) { if (this.memory.hashbang === null) { return source; } source.prepend(this.memory.hashbang + '\n'); return source; } } /** * Copyright 2018 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. */ const HEADER = `/** * @fileoverview Externs built via derived configuration from Rollup or input code. * This extern contains the iife name so it does not get mangled at the top level. * @externs */ `; /** * This Transform will apply only if the Rollup configuration is for a iife output with a defined name. * * In order to preserve the name of the iife output, derive an extern definition for Closure Compiler. * This preserves the name after compilation since Closure now believes it to be a well known global. */ class IifeTransform extends ChunkTransform { constructor() { super(...arguments); this.name = 'IifeTransform'; } extern(options) { if (options.format === 'iife' && options.name) { return HEADER + `window['${options.name}'] = ${options.name};\n`; } return null; } } /** * 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. */ const HEADER$1 = `/** * @fileoverview Externs built via derived configuration from Rollup or input code. * This extern contains the cjs typing info for modules. * @externs */ /** * @typedef {{ * __esModule: boolean, * }} */ var exports;`; /** * This Transform will apply only if the Rollup configuration is for a cjs output. * * In order to preserve the __esModules boolean on an Object, this typedef needs to be present. */ class CJSTransform extends ChunkTransform { constructor() { super(...arguments); this.name = 'CJSTransform'; } extern(options) { if (options.format === 'cjs') { return HEADER$1; } return null; } } /** * Copyright 2018 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. */ const acornWalk = require('acorn-walk'); const walk = { simple: acornWalk.simple, ancestor: acornWalk.ancestor, }; const DEFAULT_ACORN_OPTIONS = { ecmaVersion: 2020, sourceType: 'module', preserveParens: false, ranges: true, }; async function parse(fileName, source) { try { return acorn.parse(source, DEFAULT_ACORN_OPTIONS); } catch (e) { log(`parse exception in ${fileName}`, `file://${await writeTempFile(source, '.js')}`); throw e; } } function isIdentifier(node) { return node.type === 'Identifier'; } function isImportDeclaration(node) { return node.type === 'ImportDeclaration'; } function isImportExpression(node) { // @types/estree does not yet support 2020 addons to ECMA. // This includes ImportExpression ... import("thing") return node.type === 'ImportExpression'; } function isVariableDeclarator(node) { return node.type === 'VariableDeclarator'; } function isBlockStatement(node) { return node.type === 'BlockStatement'; } function isExportNamedDeclaration(node) { return node.type === 'ExportNamedDeclaration'; } function isFunctionDeclaration(node) { return node.type === 'FunctionDeclaration'; } function isVariableDeclaration(node) { return node.type === 'VariableDeclaration'; } function isClassDeclaration(node) { return node.type === 'ClassDeclaration'; } function isProperty(node) { return node.type === 'Property'; } /** * Copyright 2018 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. */ /** * Closure Compiler will not transform computed keys with literal values back to the literal value. * e.g {[0]: 'value'} => {0: 'value'} * * This transform does so only if a computed key is a Literal, and thus easily known to be static. * @see https://astexplorer.net/#/gist/d2414b45a81db3a41ee6902bfd09947a/d7176ac33a2733e1a4b1f65ec3ac626e24f7b60d */ class LiteralComputedKeys extends ChunkTransform { constructor() { super(...arguments); this.name = 'LiteralComputedKeysTransform'; } /** * @param code source to parse, and modify * @return modified input source with computed literal keys */ async post(fileName, source) { const program = await parse(fileName, source.toString()); walk.simple(program, { ObjectExpression(node) { for (const property of node.properties) { if (isProperty(property) && property.computed && property.key.type === 'Literal') { const [propertyStart] = property.range; const [valueStart] = property.value.range; source.overwrite(propertyStart, valueStart, `${property.key.value}${property.value.type !== 'FunctionExpression' ? ':' : ''}`); } } }, }); return source; } } /** * Copyright 2018 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. */ const IMPORT_SPECIFIER = 'ImportSpecifier'; const IMPORT_DEFAULT_SPECIFIER = 'ImportDefaultSpecifier'; const IMPORT_NAMESPACE_SPECIFIER = 'ImportNamespaceSpecifier'; var ExportClosureMapping; (function (ExportClosureMapping) { ExportClosureMapping[ExportClosureMapping["NAMED_FUNCTION"] = 0] = "NAMED_FUNCTION"; ExportClosureMapping[ExportClosureMapping["NAMED_CLASS"] = 1] = "NAMED_CLASS"; ExportClosureMapping[ExportClosureMapping["NAMED_DEFAULT_FUNCTION"] = 2] = "NAMED_DEFAULT_FUNCTION"; ExportClosureMapping[ExportClosureMapping["DEFAULT_FUNCTION"] = 3] = "DEFAULT_FUNCTION"; ExportClosureMapping[ExportClosureMapping["NAMED_DEFAULT_CLASS"] = 4] = "NAMED_DEFAULT_CLASS"; ExportClosureMapping[ExportClosureMapping["DEFAULT_CLASS"] = 5] = "DEFAULT_CLASS"; ExportClosureMapping[ExportClosureMapping["NAMED_CONSTANT"] = 6] = "NAMED_CONSTANT"; ExportClosureMapping[ExportClosureMapping["DEFAULT"] = 7] = "DEFAULT"; ExportClosureMapping[ExportClosureMapping["DEFAULT_VALUE"] = 8] = "DEFAULT_VALUE"; ExportClosureMapping[ExportClosureMapping["DEFAULT_OBJECT"] = 9] = "DEFAULT_OBJECT"; })(ExportClosureMapping || (ExportClosureMapping = {})); /** * 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. */ /** * Locally within exporting we always need a name * If value passed is not present in the mangler then use original name. * @param input * @param unmangle */ function getName(input, unmangle) { if (unmangle) { return unmangle(input) || input; } return input; } function NamedDeclaration(node, unmangle) { var _a; const exportDetails = []; const range = node.range; const source = typeof ((_a = node.source) === null || _a === void 0 ? void 0 : _a.value) === 'string' ? node.source.value : null; const { specifiers, declaration } = node; // NamedDeclarations either have specifiers or declarations. if (specifiers.length > 0) { for (const specifier of specifiers) { const exported = getName(specifier.exported.name, unmangle); exportDetails.push({ local: getName(specifier.local.name, unmangle), exported, type: ExportClosureMapping.NAMED_CONSTANT, range, source, }); } return exportDetails; } if (declaration) { if (isFunctionDeclaration(declaration)) { // Only default exports can be missing an identifier. exportDetails.push({ local: getName(declaration.id.name, unmangle), exported: getName(declaration.id.name, unmangle), type: ExportClosureMapping.NAMED_FUNCTION, range, source, }); } if (isVariableDeclaration(declaration)) { for (const eachDeclaration of declaration.declarations) { if (isIdentifier(eachDeclaration.id)) { exportDetails.push({ local: getName(eachDeclaration.id.name, unmangle), exported: getName(eachDeclaration.id.name, unmangle), type: ExportClosureMapping.NAMED_CONSTANT, range, source, }); } } } if (isClassDeclaration(declaration)) { // Only default exports can be missing an identifier. exportDetails.push({ local: getName(declaration.id.name, unmangle), exported: getName(declaration.id.name, unmangle), type: ExportClosureMapping.NAMED_CLASS, range, source, }); } } return exportDetails; } function DefaultDeclaration(defaultDeclaration, unmangle) { const { declaration } = defaultDeclaration; if (declaration.type === 'Identifier' && declaration.name) { return [ { local: getName(declaration.name, unmangle), exported: getName(declaration.name, unmangle), type: ExportClosureMapping.NAMED_DEFAULT_FUNCTION, range: defaultDeclaration.range, source: null, }, ]; } return []; } function NodeIsPreservedExport(node) { return (node.type === 'ExpressionStatement' && node.expression.type === 'AssignmentExpression' && node.expression.left.type === 'MemberExpression' && node.expression.left.object.type === 'Identifier' && node.expression.left.object.name === 'window'); } function PreservedExportName(node) { const { property } = node; if (property.type === 'Identifier') { return property.name; } if (property.type === 'Literal' && typeof property.value === 'string') { return property.value; } return null; } /** * 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. */ function PreserveFunction(code, source, ancestor, exportDetails, exportInline) { // Function Expressions can be inlined instead of preserved as variable references. // window['foo'] = function(){}; => export function foo(){} / function foo(){} const assignmentExpression = ancestor.expression; const memberExpression = assignmentExpression.left; const functionExpression = assignmentExpression.right; const [memberExpressionObjectStart] = memberExpression.object.range; const functionName = exportInline ? exportDetails.exported : exportDetails.local; if (functionExpression.params.length > 0) { const [paramsStart] = functionExpression.params[0].range; // FunctionExpression has parameters. source.overwrite(memberExpressionObjectStart, paramsStart, `${exportInline ? 'export ' : ''}function ${functionName}(`); } else { const [bodyStart] = functionExpression.body.range; source.overwrite(memberExpressionObjectStart, bodyStart, `${exportInline ? 'export ' : ''}function ${functionName}()`); } return !exportInline; } function PreserveIdentifier(code, source, ancestor, exportDetails, exportInline) { const assignmentExpression = ancestor.expression; const left = assignmentExpression.left; const right = assignmentExpression.right; const [ancestorStart, ancestorEnd] = ancestor.range; const [leftStart] = left.range; const [rightStart, rightEnd] = right.range; if (exportInline) { const output = (exportDetails.exported === 'default' ? `export default ` : `export var ${exportDetails.exported}=`) + `${code.substring(rightStart, rightEnd)};`; source.overwrite(ancestorStart, ancestorEnd, output); } 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(leftStart, ancestorEnd); return true; } else { source.overwrite(ancestorStart, ancestorEnd, `var ${exportDetails.local}=${code.substring(rightStart, rightEnd)};`); } return !exportInline; } function PreserveNamedConstant(code, source, ancestor, exportDetails, exportInline) { const assignmentExpression = ancestor.expression; switch (assignmentExpression.right.type) { case 'FunctionExpression': return PreserveFunction(code, source, ancestor, exportDetails, exportInline); default: return PreserveIdentifier(code, source, ancestor, exportDetails, exportInline); } } /** * 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. */ function PreserveDefault(code, source, ancestor, exportDetails, exportInline) { const assignmentExpression = ancestor.expression; const [leftStart] = assignmentExpression.left.range; const [rightStart] = assignmentExpression.right.range; source.overwrite(leftStart, rightStart, 'export default '); return false; } /** * Copyright 2018 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. */ const ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_UNSPECIFIED = 'Providing the warning_level=VERBOSE compile option also requires a valid language_out compile option.'; const ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_INVALID = 'Providing the warning_level=VERBOSE and language_out=NO_TRANSPILE compile options will remove warnings.'; const OPTIONS_TO_REMOVE_FOR_CLOSURE = ['remove_strict_directive']; /** * Checks if output format is ESM * @param outputOptions * @return boolean */ const isESMFormat = ({ format }) => format === 'esm' || format === 'es'; /** * Throw Errors if compile options will result in unexpected behaviour. * @param compileOptions */ function validateCompileOptions(compileOptions) { if ('warning_level' in compileOptions && compileOptions.warning_level === 'VERBOSE') { if (!('language_out' in compileOptions)) { throw new Error(ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_UNSPECIFIED); } if (compileOptions.language_out === 'NO_TRANSPILE') { throw new Error(ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_INVALID); } } } /** * Normalize the compile options given by the user into something usable. * @param compileOptions */ function normalizeExternOptions(compileOptions) { validateCompileOptions(compileOptions); let externs = []; if ('externs' in compileOptions) { switch (typeof compileOptions.externs) { case 'boolean': externs = []; break; case 'string': externs = [compileOptions.externs]; break; default: externs = compileOptions.externs; break; } delete compileOptions.externs; } if (compileOptions) { for (const optionToDelete of OPTIONS_TO_REMOVE_FOR_CLOSURE) { if (optionToDelete in compileOptions) { // @ts-ignore delete compileOptions[optionToDelete]; } } } return [externs, compileOptions]; } /** * Pluck the PluginOptions from the CompileOptions * @param compileOptions */ function pluckPluginOptions(compileOptions) { const pluginOptions = {}; if (!compileOptions) { return pluginOptions; } for (const optionToDelete of OPTIONS_TO_REMOVE_FOR_CLOSURE) { if (optionToDelete in compileOptions) { // @ts-ignore pluginOptions[optionToDelete] = compileOptions[optionToDelete]; } } return pluginOptions; } /** * Generate default Closure Compiler CompileOptions an author can override if they wish. * These must be derived from configuration or input sources. * @param transformers * @param options * @return derived CompileOptions for Closure Compiler */ const defaults = async (options, providedExterns, transformers) => { // Defaults for Rollup Projects are slightly different than Closure Compiler defaults. // - Users of Rollup tend to transpile their code before handing it to a minifier, // so no transpile is default. // - When Rollup output is set to "es|esm" it is expected the code will live in a ES Module, // so safely be more aggressive in minification. const transformerExterns = []; for (const transform of transformers || []) { const extern = transform.extern(options); if (extern !== null) { const writtenExtern = await writeTempFile(extern); transformerExterns.push(writtenExtern); } } return { language_out: 'NO_TRANSPILE', assume_function_wrapper: isESMFormat(options), warning_level: 'QUIET', module_resolution: 'NODE', externs: transformerExterns.concat(providedExterns), }; }; /** * Compile Options is the final configuration to pass into Closure Compiler. * defaultCompileOptions are overrideable by ones passed in directly to the plugin * but the js source and sourcemap are not overrideable, since this would break the output if passed. * @param compileOptions * @param outputOptions * @param code * @param transforms */ async function options (incomingCompileOptions, outputOptions, code, transforms) { const mapFile = await writeTempFile('', '', false); const [externs, compileOptions] = normalizeExternOptions({ ...incomingCompileOptions }); const options = { ...(await defaults(outputOptions, externs, transforms)), ...compileOptions, js: await writeTempFile(code), create_source_map: mapFile, }; return [options, mapFile]; } /** * Copyright 2018 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. */ const EXTERN_OVERVIEW = `/** * @fileoverview Externs built via derived configuration from Rollup or input code. * @externs */`; /** * This Transform will apply only if the Rollup configuration is for 'esm' output. * * In order to preserve the export statements: * 1. Create extern definitions for them (to keep them their names from being mangled). * 2. Insert additional JS referencing the exported names on the window scope * 3. After Closure Compilation is complete, replace the window scope references with the original export statements. */ class ExportTransform extends ChunkTransform { constructor() { super(...arguments); this.name = 'ExportTransform'; this.originalExports = new Map(); this.currentSourceExportCount = 0; /** * Store an export from a source into the originalExports Map. * @param mapping mapping of details from this declaration. */ this.storeExport = (mapping) => mapping.forEach(map => { if (map.source === null) { this.currentSourceExportCount++; this.originalExports.set(map.local, map); } else { this.originalExports.set(map.exported, map); } }); } static storeExportToAppend(collected, exportDetails) { const update = collected.get(exportDetails.source) || []; if (exportDetails.exported === exportDetails.local) { update.push(exportDetails.exported); } else { update.push(`${exportDetails.local} as ${exportDetails.exported}`); } collected.set(exportDetails.source, update); return collected; } async deriveExports(fileName, code) { const program = await parse(fileName, code); walk.simple(program, { ExportNamedDeclaration: (node) => { this.storeExport(NamedDeclaration(node, this.mangler.getName)); }, ExportDefaultDeclaration: (node) => { this.storeExport(DefaultDeclaration(node, this.mangler.getName)); }, ExportAllDeclaration: () => { // TODO(KB): This case `export * from "./import"` is not currently supported. this.context.error(new Error(`Rollup Plugin Closure Compiler does not support export all syntax for externals.`)); }, }); } extern() { if (Array.from(this.originalExports.keys()).length > 0) { let output = EXTERN_OVERVIEW; for (const key of this.originalExports.keys()) { const value = this.originalExports.get(key); if (value.source !== null) { output += `function ${value.exported}(){};\n`; } } return output; } return null; } /** * Before Closure Compiler modifies the source, we need to ensure it has window scoped * references to the named exports. This prevents Closure from mangling their names. * @param code source to parse, and modify * @param chunk OutputChunk from Rollup for this code. * @param id Rollup id reference to the source * @return modified input source with window scoped references. */ async pre(fileName, source) { if (!isESMFormat(this.outputOptions)) { return super.pre(fileName, source); } const code = source.toString(); await this.deriveExports(fileName, code); for (const key of this.originalExports.keys()) { const value = this.originalExports.get(key); // 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 source; } /** * After Closure Compiler has modified the source, we need to replace the window scoped * references we added with the intended export statements * @param code source post Closure Compiler Compilation * @return Promise containing the repaired source */ async post(fileName, source) { if (!isESMFormat(this.outputOptions)) { return super.post(fileName, source); } const code = source.toString(); const program = await parse(fileName, code); let collectedExportsToAppend = 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, ancestors) => { 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 = ancestor.expression; const left = expression.left; const exportName = PreservedExportName(left); if (exportName !== null && this.originalExports.get(exportName)) { const exportDetails = this.originalExports.get(exportName); const exportIsLocal = exportDetails.source === null; const exportInline = (exportIsLocal && this.currentSourceExportCount === 1 && exportDetails.local === exportDetails.exported) || exportDetails.exported === 'default'; switch (exportDetails.type) { case ExportClosureMapping.NAMED_DEFAULT_FUNCTION: case ExportClosureMapping.DEFAULT: if (PreserveDefault(code, source, ancestor)) ; break; case ExportClosureMapping.NAMED_CONSTANT: if (PreserveNamedConstant(code, source, ancestor, exportDetails, exportInline)) { collectedExportsToAppend = ExportTransform.storeExportToAppend(collectedExportsToAppend, exportDetails); } break; } if (!exportIsLocal) { const [leftStart] = left.range; const { 1: ancestorEnd } = ancestor.range; source.remove(leftStart, ancestorEnd); } // An Export can only be processed once. this.originalExports.delete(exportName); } } }, }); 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 source; } } /** * 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. */ function literalName(literal) { return literal.value; } /** * 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. */ function Specifiers(specifiers) { var _a; const returnable = { default: null, specific: [], local: [], namespace: false, }; for (const specifier of specifiers) { returnable.local.push(specifier.local.name); switch (specifier.type) { case IMPORT_SPECIFIER: const { name: local } = specifier.local; const { name: imported } = ((_a = specifier) === null || _a === void 0 ? void 0 : _a.imported) || { name: specifier.local, }; if (local === imported) { returnable.specific.push(local); } else { returnable.specific.push(`${imported} as ${local}`); } break; case IMPORT_NAMESPACE_SPECIFIER: const { name: namespace } = specifier.local; returnable.specific.push(namespace); returnable.namespace = true; break; case IMPORT_DEFAULT_SPECIFIER: returnable.default = specifier.local.name; break; } } return returnable; } function FormatSpecifiers(specifiers, name) { const hasDefault = specifiers.default !== null; const hasNamespace = specifiers.namespace === true; const hasSpecifics = !hasNamespace && specifiers.specific.length > 0; const hasLocals = specifiers.local.length > 0; const includesFrom = hasNamespace || hasNamespace || hasSpecifics || hasLocals; let formatted = 'import'; let values = []; if (hasDefault) { values.push(`${specifiers.default}`); } if (hasNamespace) { values.push(`* as ${specifiers.specific[0]}`); } if (hasSpecifics) { values.push(`{${specifiers.specific.join(',')}}`); } formatted += `${hasDefault || hasNamespace ? ' ' : ''}${values.join(',')}${hasSpecifics ? '' : ' '}${includesFrom ? 'from' : ''}'${name}';`; return formatted; } /** * Copyright 2018 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. */ const DYNAMIC_IMPORT_KEYWORD = 'import'; const DYNAMIC_IMPORT_REPLACEMENT = `import_${new Date().getMilliseconds()}`; const HEADER$2 = `/** * @fileoverview Externs built via derived configuration from Rollup or input code. * This extern contains the external import names, to prevent compilation failures. * @externs */ `; /** * Locally within imports we always need a name * If value passed is not present in the mangler then use original name. * @param input * @param unmangle */ function getName$1(input, unmangle) { if (unmangle) { return unmangle(input) || input; } return input; } class ImportTransform extends ChunkTransform { constructor() { super(...arguments); this.importedExternalsSyntax = {}; this.importedExternalsLocalNames = []; this.dynamicImportPresent = false; this.name = 'ImportTransform'; /** * Before Closure Compiler modifies the source, we need to ensure external imports have been removed * since Closure will error out when it encounters them. * @param code source to parse, and modify * @return modified input source with external imports removed. */ this.pre = async (fileName, source) => { const code = source.toString(); let program = await parse(fileName, code); let dynamicImportPresent = false; let { mangler, importedExternalsSyntax, importedExternalsLocalNames } = this; await estreeWalker.asyncWalk(program, { enter: async function (node) { if (isImportDeclaration(node)) { const [importDeclarationStart, importDeclarationEnd] = node.range; const originalName = literalName(node.source); let specifiers = Specifiers(node.specifiers); specifiers = { ...specifiers, default: mangler.getName(specifiers.default || '') || specifiers.default, specific: specifiers.specific.map(specific => { if (specific.includes(' as ')) { const split = specific.split(' as '); return `${getName$1(split[0])} as ${getName$1(split[1])}`; } return getName$1(specific); }), local: specifiers.local.map(local => getName$1(local)), }; const unmangledName = getName$1(originalName); importedExternalsSyntax[unmangledName] = FormatSpecifiers(specifiers, unmangledName); importedExternalsLocalNames.push(...specifiers.local); source.remove(importDeclarationStart, importDeclarationEnd); this.skip(); } if (isIdentifier(node)) { const unmangled = mangler.getName(node.name); if (unmangled) { const [identifierStart, identifierEnd] = node.range; source.overwrite(identifierStart, identifierEnd, unmangled); } } if (isImportExpression(node)) { const [dynamicImportStart, dynamicImportEnd] = node.range; dynamicImportPresent = true; // Rename the `import` method to something we can put in externs. // CC doesn't understand dynamic import yet. source.overwrite(dynamicImportStart, dynamicImportEnd, code .substring(dynamicImportStart, dynamicImportEnd) .replace(DYNAMIC_IMPORT_KEYWORD, DYNAMIC_IMPORT_REPLACEMENT)); } }, }); this.dynamicImportPresent = dynamicImportPresent; return source; }; } /** * Generate externs for local names of external imports. * Otherwise, advanced mode compilation will fail since the reference is unknown. * @return string representing content of generated extern. */ extern() { let extern = HEADER$2; if (this.importedExternalsLocalNames.length > 0) { for (const name of this.importedExternalsLocalNames) { extern += `function ${name}(){};\n`; } } if (this.dynamicImportPresent) { extern += ` /** * @param {string} path * @return {!Promise<?>} */ function ${DYNAMIC_IMPORT_REPLACEMENT}(path) { return Promise.resolve(path) }; window['${DYNAMIC_IMPORT_REPLACEMENT}'] = ${DYNAMIC_IMPORT_REPLACEMENT};`; } return extern === HEADER$2 ? null : extern; } /** * After Closure Compiler has modified the source, we need to re-add the external imports * @param code source post Closure Compiler Compilation * @return Promise containing the repaired source */ async post(fileName, source