@ampproject/rollup-plugin-closure-compiler
Version:
Rollup + Google Closure Compiler
1,265 lines (1,243 loc) • 73.1 kB
JavaScript
'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