polymer-bundler
Version:
Process Web Components into one output file
547 lines • 27.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* @license
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
const babel_traverse_1 = require("babel-traverse");
const babel = require("babel-types");
const clone = require("clone");
const rollup_1 = require("rollup");
const analyzer_utils_1 = require("./analyzer-utils");
const babel_utils_1 = require("./babel-utils");
const es6_module_utils_1 = require("./es6-module-utils");
const url_utils_1 = require("./url-utils");
const utils_1 = require("./utils");
/**
* Utility class to rollup/merge ES6 modules code using rollup and rewrite
* import statements to point to appropriate bundles.
*/
class Es6Rewriter {
constructor(bundler, manifest, bundle) {
this.bundler = bundler;
this.manifest = manifest;
this.bundle = bundle;
}
/**
* Produces a module bundle from given source code string and from analysis of
* imported ES6 modules.
* @param url The base URL of the module to roll-up.
* @param code The source code to roll-up.
* @param document The optional Document which contains the source code to
* rollup; this is used to get access to the Analyzer's resolutions of module
* specifiers encountered in the source code if available.
* TODO(usergenic): Return a valid source-map along with the code.
*/
rollup(url, code, document) {
return __awaiter(this, void 0, void 0, function* () {
// This is a synthetic module specifier used to identify the code to rollup
// and differentiate it from the a request to contents of the document at
// the actual given url which should load from the analyzer.
const input = '*bundle*';
const analysis = yield this.bundler.analyzer.analyze([...this.bundle.bundle.files]);
const external = [];
for (const [url, bundle] of this.manifest.bundles) {
if (url !== this.bundle.url) {
external.push(...[...bundle.files, url]);
}
}
external.push(...this.bundler.excludes);
// For each document loaded from the analyzer, we build a map of the
// original specifiers to the resolved URLs since we want to use analyzer
// resolutions for such things as bare module specifiers.
const jsImportResolvedUrls = new Map();
if (document) {
jsImportResolvedUrls.set(input, this.getEs6ImportResolutions(document));
}
const rollupBundle = yield rollup_1.rollup({
input,
external,
onwarn: (warning) => { },
treeshake: this.bundler.treeshake,
plugins: [
{
name: 'analyzerPlugin',
resolveId: (importee, importer) => importee,
load: (id) => {
// When requesting the main document, just return it as-is.
if (id === input) {
return code;
}
// When the requested document is part of the bundle, get it
// from the analysis.
if (this.bundle.bundle.files.has(id)) {
const document = analyzer_utils_1.assertIsJsDocument(analyzer_utils_1.getAnalysisDocument(analysis, id));
const ast = clone(document.parsedDocument.ast);
if (!jsImportResolvedUrls.has(id)) {
jsImportResolvedUrls.set(id, this.getEs6ImportResolutions(document));
}
// When Rollup encounters module IDs that are not
// 'absolute', it tries to be clever by creating a relative
// path it can use, but it uses `process.cwd()` which is
// nearly never what we want, and so we have to work around
// this by converting all module IDs in the returned
// document to fully resolved URLs.
this.rewriteEs6SourceUrlsToResolved(ast, jsImportResolvedUrls.get(id));
// Rollup has difficulties dealing with `export * from` cases
// expecially where the source is an external module. As a
// means to work around this, we will rewrite `export * from`
// as explicit named exports of all the identified exports of
// the external module, since we should be able to get them
// from `polymer-analyzer`.
// See https://github.com/rollup/rollup/issues/2165 for
// status of Rollup's handling of this case.
this.rewriteExportAllToNamedExports(ast, analysis);
const serialized = babel_utils_1.serialize(ast);
// If the URL of the requested document is the same as the
// bundle URL or the requested file doesn't use
// `import.meta` anywhere, we can return it as-is.
if (this.bundle.url === id ||
!document.parsedDocument.contents.includes('import.meta')) {
return serialized.code;
}
// We need to rewrite instances of `import.meta` in the
// document to preserve its location because `import.meta`
// is used and the URL has changed as a result of bundling.
const relativeUrl = url_utils_1.ensureLeadingDot(this.bundler.analyzer.urlResolver.relative(this.bundle.url, id));
const newAst = this._rewriteImportMetaToBundleMeta(utils_1.generateUniqueIdentifierName('bundledImportMeta', serialized.code), ast, relativeUrl);
const newCode = babel_utils_1.serialize(newAst).code;
return newCode;
}
return null;
}
},
],
});
const { output } = yield rollupBundle.generate({
format: 'es',
freeze: false,
});
if (output.length !== 1) {
throw new Error(`Failed to bundle. Rollup generated ${output.length} chunks or assets. Expected 1.`);
}
const chunkOrAsset = output[0];
const rolledUpCode = chunkOrAsset.code || '';
// We have to force the extension of the URL to analyze here because
// inline es6 module document url is going to end in `.html` and the file
// would be incorrectly analyzed as an HTML document.
const rolledUpUrl = url_utils_1.getFileExtension(url) === '.js' ?
url :
url_utils_1.appendUrlPath(url, '_inline_es6_module.js');
const rolledUpDocument = yield this.bundler.analyzeContents(rolledUpUrl, rolledUpCode);
const babelFile = analyzer_utils_1.assertIsJsDocument(rolledUpDocument).parsedDocument.ast;
this._rewriteExportStatements(url, babelFile);
this._rewriteImportStatements(url, babelFile);
this._deduplicateImportStatements(babelFile);
const { code: rewrittenCode } = babel_utils_1.serialize(babelFile);
return { code: rewrittenCode, map: undefined };
});
}
getEs6ImportResolutions(document) {
const jsImports = document.getFeatures({
kind: 'js-import',
imported: false,
externalPackages: true,
excludeBackreferences: true,
});
const resolutions = new Map();
for (const jsImport of jsImports) {
const node = jsImport.astNode.node;
if ('source' in node) {
if (node.source && jsImport.document !== undefined) {
resolutions.set(node.source.value, jsImport.document.url);
}
}
else if (node.callee && node.callee.type + '' === 'Import' &&
jsImport.document !== undefined) {
const source = node.arguments[0];
if (source) {
if (babel.isStringLiteral(source)) {
resolutions.set(source.value, jsImport.document.url);
}
}
}
}
return resolutions;
}
rewriteEs6SourceUrlsToResolved(node, jsImportResolvedUrls) {
const rewriteDeclarationSource = {
enter(path) {
const declaration = path.node;
const source = declaration.source &&
babel.isStringLiteral(declaration.source) &&
declaration.source.value;
if (!source) {
return;
}
const resolution = jsImportResolvedUrls.get(source);
if (resolution) {
declaration.source.value = resolution;
}
}
};
babel_traverse_1.default(node, {
noScope: true,
ExportAllDeclaration: rewriteDeclarationSource,
ExportNamedDeclaration: rewriteDeclarationSource,
ImportDeclaration: rewriteDeclarationSource,
CallExpression: {
enter(path) {
const callExpression = path.node;
const callee = callExpression['callee'];
const callArguments = callExpression['arguments'];
if (!callee || !callArguments || callArguments.length < 1 ||
!babel.isStringLiteral(callArguments[0])) {
return;
}
if (callee.type === 'Import') {
callArguments[0].value =
jsImportResolvedUrls.get(callArguments[0].value);
}
}
}
});
}
rewriteExportAllToNamedExports(node, analysis) {
babel_traverse_1.default(node, {
noScope: true,
ExportAllDeclaration: {
enter(path) {
const exportAllDeclaration = path.node;
const sourceUrl = babel.isStringLiteral(exportAllDeclaration.source) &&
exportAllDeclaration.source.value;
if (!sourceUrl) {
return;
}
const sourceDocument = analyzer_utils_1.getAnalysisDocument(analysis, sourceUrl);
const documentExports = sourceDocument.getFeatures({ kind: 'export' });
const specifiers = [];
for (const documentExport of documentExports) {
for (const exportIdentifier of documentExport.identifiers) {
const identifierValue = exportIdentifier.valueOf();
// It does not appear that `export * from` should re-export
// the default module export of a module.
if (identifierValue !== 'default') {
specifiers.push(babel.exportSpecifier(babel.identifier(identifierValue), babel.identifier(identifierValue)));
}
}
}
const namedExportDeclaration = babel.exportNamedDeclaration(undefined, specifiers, babel.stringLiteral(sourceUrl));
utils_1.rewriteObject(exportAllDeclaration, namedExportDeclaration);
}
}
});
}
/**
* Attempts to reduce the number of distinct import declarations by
* combining those referencing the same source into the same declaration.
* Results in deduplication of imports of the same item as well. It
* should NOT touch dynamic imports at all.
*
* Before:
* import {a} from './module-1.js';
* import {b} from './module-1.js';
* import {c} from './module-2.js';
* import('./module-3.js');
* import('./module-3.js');
* After:
* import {a,b} from './module-1.js';
* import {c} from './module-2.js';
* import('./module-3.js');
* import('./module-3.js');
*/
_deduplicateImportStatements(node) {
const importDeclarations = new Map();
babel_traverse_1.default(node, {
noScope: true,
ImportDeclaration: {
enter(path) {
const importDeclaration = path.node;
const source = babel.isStringLiteral(importDeclaration.source) &&
importDeclaration.source.value;
if (!source) {
return;
}
const hasNamespaceSpecifier = importDeclaration.specifiers.some((s) => babel.isImportNamespaceSpecifier(s));
const hasDefaultSpecifier = importDeclaration.specifiers.some((s) => babel.isImportDefaultSpecifier(s));
if (!importDeclarations.has(source) && !hasNamespaceSpecifier &&
!hasDefaultSpecifier) {
importDeclarations.set(source, importDeclaration);
}
else if (importDeclarations.has(source)) {
const existingDeclaration = importDeclarations.get(source);
for (const specifier of importDeclaration.specifiers) {
existingDeclaration.specifiers.push(specifier);
}
path.remove();
}
}
}
});
}
/**
* Rewrite export declarations source URLs to reference the bundle URL for
* bundled files.
*/
_rewriteExportStatements(baseUrl, node) {
const this_ = this;
babel_traverse_1.default(node, {
noScope: true,
ExportNamedDeclaration: {
enter(path) {
const exportNamedDeclaration = path.node;
if (!exportNamedDeclaration.source ||
!babel.isStringLiteral(exportNamedDeclaration.source)) {
// We can't rewrite a source if there isn't one or if it isn't a
// string literal in the first place.
return;
}
const source = exportNamedDeclaration.source.value;
const sourceBundle = this_.manifest.getBundleForFile(source);
// If there is no bundle associated with the export from statement
// then the URL is not bundled (perhaps it was excluded) so we will
// just ensure the URL is converted back to a relative URL.
if (!sourceBundle) {
exportNamedDeclaration.source.value = url_utils_1.ensureLeadingDot(this_.bundler.analyzer.urlResolver.relative(baseUrl, source));
return;
}
exportNamedDeclaration.source.value =
url_utils_1.ensureLeadingDot(this_.bundler.analyzer.urlResolver.relative(baseUrl, sourceBundle.url));
}
}
});
}
/**
* Rewrite import declarations source URLs to reference the bundle URL for
* bundled files and import names to correspond to names as exported by
* bundles.
*/
_rewriteImportStatements(baseUrl, node) {
const this_ = this;
babel_traverse_1.default(node, {
noScope: true,
// Dynamic import() syntax doesn't have full type support yet, so we
// have to use generic `enter` and walk all nodes until that's fixed.
// TODO(usergenic): Switch this to the `Import: { enter }` style
// after dynamic imports fully supported.
enter(path) {
if (path.node.type === 'Import') {
this_._rewriteDynamicImport(baseUrl, node, path);
}
},
});
babel_traverse_1.default(node, {
noScope: true,
ImportDeclaration: {
enter(path) {
const importDeclaration = path.node;
if (!babel.isStringLiteral(importDeclaration.source)) {
// We can't actually handle values which are not string literals,
// so we'll skip them.
return;
}
const source = importDeclaration.source.value;
const sourceBundle = this_.manifest.getBundleForFile(source);
// If there is no import bundle, then this URL is not bundled (maybe
// excluded or something) so we should just ensure the URL is
// converted back to a relative URL.
if (!sourceBundle) {
importDeclaration.source.value = url_utils_1.ensureLeadingDot(this_.bundler.analyzer.urlResolver.relative(baseUrl, source));
return;
}
for (const specifier of importDeclaration.specifiers) {
if (babel.isImportSpecifier(specifier)) {
this_._rewriteImportSpecifierName(specifier, source, sourceBundle);
}
if (babel.isImportDefaultSpecifier(specifier)) {
this_._rewriteImportDefaultSpecifier(specifier, source, sourceBundle);
}
if (babel.isImportNamespaceSpecifier(specifier)) {
this_._rewriteImportNamespaceSpecifier(specifier, source, sourceBundle);
}
}
importDeclaration.source.value =
url_utils_1.ensureLeadingDot(this_.bundler.analyzer.urlResolver.relative(baseUrl, sourceBundle.url));
}
}
});
}
/**
* Extends dynamic import statements to extract the explicitly namespace
* export for the imported module.
*
* Before:
* import('./module-a.js')
* .then((moduleA) => moduleA.doSomething());
*
* After:
* import('./bundle_1.js')
* .then(bundle => bundle && bundle.$moduleA || {})
* .then((moduleA) => moduleA.doSomething());
*/
_rewriteDynamicImport(baseUrl, root, importNodePath) {
if (!importNodePath) {
return;
}
const importCallExpression = importNodePath.parent;
if (!importCallExpression ||
!babel.isCallExpression(importCallExpression)) {
return;
}
const importCallArgument = importCallExpression.arguments[0];
if (!babel.isStringLiteral(importCallArgument)) {
return;
}
const sourceUrl = importCallArgument.value;
const resolvedSourceUrl = this.bundler.analyzer.urlResolver.resolve(baseUrl, sourceUrl);
if (!resolvedSourceUrl) {
return;
}
const sourceBundle = this.manifest.getBundleForFile(resolvedSourceUrl);
// TODO(usergenic): To support *skipping* the rewrite, we need a way to
// identify whether a bundle contains a single top-level module or is a
// merged bundle with multiple top-level modules.
let exportName;
if (sourceBundle) {
exportName =
es6_module_utils_1.getOrSetBundleModuleExportName(sourceBundle, resolvedSourceUrl, '*');
}
// If there's no source bundle or the namespace export name of the bundle
// is just '*', then we don't need to append a .then() to transform the
// return value of the import(). Lets just rewrite the URL to be a
// relative path and exit.
if (!sourceBundle || exportName === '*') {
const relativeSourceUrl = url_utils_1.ensureLeadingDot(this.bundler.analyzer.urlResolver.relative(baseUrl, resolvedSourceUrl));
importCallArgument.value = relativeSourceUrl;
return;
}
// Rewrite the URL to be a relative path to the bundle.
const relativeSourceUrl = url_utils_1.ensureLeadingDot(this.bundler.analyzer.urlResolver.relative(baseUrl, sourceBundle.url));
importCallArgument.value = relativeSourceUrl;
const importCallExpressionParent = importNodePath.parentPath.parent;
if (!importCallExpressionParent) {
return;
}
const thenifiedCallExpression = babel.callExpression(babel.memberExpression(clone(importCallExpression), babel.identifier('then')), [babel.arrowFunctionExpression([babel.identifier('bundle')], babel.logicalExpression('||', babel.logicalExpression('&&', babel.identifier('bundle'), babel.memberExpression(babel.identifier('bundle'), babel.identifier(exportName))), babel.objectExpression([])))]);
utils_1.rewriteObject(importCallExpression, thenifiedCallExpression);
}
/**
* Changes an import specifier to use the exported name defined in the bundle.
*
* Before:
* import {something} from './module-a.js';
*
* After:
* import {something_1} from './bundle_1.js';
*/
_rewriteImportSpecifierName(specifier, source, sourceBundle) {
const originalExportName = specifier.imported.name;
const exportName = es6_module_utils_1.getOrSetBundleModuleExportName(sourceBundle, source, originalExportName);
specifier.imported.name = exportName;
}
/**
* Changes an import specifier to use the exported name for original module's
* default as defined in the bundle.
*
* Before:
* import moduleA from './module-a.js';
*
* After:
* import {$moduleADefault} from './bundle_1.js';
*/
_rewriteImportDefaultSpecifier(specifier, source, sourceBundle) {
const exportName = es6_module_utils_1.getOrSetBundleModuleExportName(sourceBundle, source, 'default');
// No rewrite necessary if default is the name, since this indicates there
// was no rewriting or bundling of the default export.
if (exportName === 'default') {
return;
}
// tslint:disable-next-line: no-any Look into NodePath.replace instead.
const importSpecifier = specifier;
Object.assign(importSpecifier, { type: 'ImportSpecifier', imported: babel.identifier(exportName) });
}
/**
* Changes an import specifier to use the exported name for original module's
* namespace as defined in the bundle.
*
* Before:
* import * as moduleA from './module-a.js';
*
* After:
* import {$moduleA} from './bundle_1.js';
*/
_rewriteImportNamespaceSpecifier(specifier, source, sourceBundle) {
const exportName = es6_module_utils_1.getOrSetBundleModuleExportName(sourceBundle, source, '*');
// No rewrite necessary if * is the name, since this indicates there was
// no bundling of the namespace.
if (exportName === '*') {
return;
}
// tslint:disable-next-line: no-any Look into NodePath.replace instead.
const importSpecifier = specifier;
Object.assign(importSpecifier, { type: 'ImportSpecifier', imported: babel.identifier(exportName) });
}
_rewriteImportMetaToBundleMeta(bundledImportMetaIdentifierName, moduleFile, relativeUrl) {
// Generate a stand-in for any local references to import.meta...
// ```javascript
// const bundledImportMeta = {
// ...import.meta,
// url: new URL(${ relativeUrl }, import.meta.url).href
// };
// ```
// TODO(usergenic): Consider migrating this AST production mishmash into
// the `ast` tagged template literal available like this:
// https://github.com/Polymer/tools/blob/master/packages/build/src/babel-plugin-dynamic-import-amd.ts#L64
const bundledImportMetaDeclaration = babel.variableDeclaration(
//
'const', [
//
babel.variableDeclarator(babel.identifier(bundledImportMetaIdentifierName), babel.objectExpression([
babel.spreadProperty(babel.memberExpression(babel.identifier('import'), babel.identifier('meta'))),
babel.objectProperty(babel.identifier('url'), babel.memberExpression(babel.newExpression(babel.identifier('URL'), [
//
babel.stringLiteral(relativeUrl),
babel.memberExpression(babel.memberExpression(babel.identifier('import'), babel.identifier('meta')), babel.identifier('url'))
]), babel.identifier('href')))
]))
]);
const newModuleFile = clone(moduleFile);
babel_traverse_1.default(newModuleFile, {
noScope: true,
MetaProperty: {
enter(path) {
const metaProperty = path.node;
if (metaProperty.meta.name !== 'import' &&
metaProperty.property.name !== 'meta') {
// We're specifically looking for instances of `import.meta` so
// ignore any other meta properties.
return;
}
const bundledImportMeta = babel.identifier(bundledImportMetaIdentifierName);
path.replaceWith(bundledImportMeta);
},
},
});
newModuleFile.program.body.unshift(bundledImportMetaDeclaration);
return newModuleFile;
}
}
exports.Es6Rewriter = Es6Rewriter;
//# sourceMappingURL=es6-rewriter.js.map