Skip to content

Commit

Permalink
[bundler] Preserve correctness of import.meta.url in bundled module…
Browse files Browse the repository at this point in the history
…s. (#300)

* Add acorn-import-meta and rewrite import.meta when necessary to provide corrected URL.
* Updated package-lock.json files for other packages because thats what lerna bootstrap needed to do after I added acorn-import-meta.
* [bundler] Updated CHANGELOG to include `import.meta.url` preservation support.
* [bundler] Remove unused variable from unit test file.
* [bundler] Use explicit types for NodePath<babel.Something> instead after-the-fact casting.
* Add TODO about getting some typechecking code in place for Document<ParsedJavascriptDocument> verification and add typings to its ast.
* import.met.url = new URL().href not new URL().
* [bundler] Use path.replaceWith() instead of spooky rewriteObject()
  • Loading branch information
usergenic authored May 3, 2018
1 parent 3418741 commit f313b16
Show file tree
Hide file tree
Showing 17 changed files with 9,864 additions and 9,620 deletions.
12 changes: 6 additions & 6 deletions packages/analyzer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

200 changes: 100 additions & 100 deletions packages/build/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/bundler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
* Dropped support for node v6. This is a soft break, as we aren't
making any changes that are known to break node v6, but we're no longer testing against it. See our [node version support policy](https://www.polymer-project.org/2.0/docs/tools/node-support)
for details.
* Added support for `import.meta.url` preservation in bundled modules.
<!-- Add new, unreleased changes here. -->

## 4.0.0-pre.6 - 2018-04-25
Expand Down
16 changes: 12 additions & 4 deletions packages/bundler/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@types/babel-generator": "^6.25.1",
"@types/babel-traverse": "^6.25.3",
"@types/rollup": "^0.54.0",
"acorn-import-meta": "^0.2.1",
"babel-generator": "^6.26.1",
"babel-traverse": "^6.26.0",
"clone": "^2.1.0",
Expand Down
117 changes: 103 additions & 14 deletions packages/bundler/src/es6-rewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {getOrSetBundleModuleExportName} from './es6-module-utils';
import {appendUrlPath, ensureLeadingDot, getFileExtension} from './url-utils';
import {rewriteObject} from './utils';

const acornImportMetaInject = require('acorn-import-meta/inject');

/**
* Utility class to rollup/merge ES6 modules code using rollup and rewrite
* import statements to point to appropriate bundles.
Expand Down Expand Up @@ -59,7 +61,15 @@ export class Es6Rewriter {
input,
external,
onwarn: (warning: string) => {},
experimentalDynamicImport: true,
treeshake: false,
acorn: {
plugins: {
dynamicImport: true,
importMeta: true,
},
},
acornInjectPlugins: [acornImportMetaInject],
plugins: [
{
name: 'analyzerPlugin',
Expand All @@ -83,9 +93,13 @@ export class Es6Rewriter {
importee as PackageRelativeUrl)! as string;
},
load: (id: ResolvedUrl) => {
// 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 = getAnalysisDocument(analysis, id);
if (!jsImportResolvedUrls.has(id)) {
Expand All @@ -106,20 +120,43 @@ export class Es6Rewriter {
}
}
}
return document.parsedDocument.contents;

// 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 document.parsedDocument.contents;
}

// 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 =
ensureLeadingDot(this.bundler.analyzer.urlResolver.relative(
this.bundle.url, id));

// TODO(usergenic): This code makes assumptions about the type of
// the ast and the document contents that are potentially not true
// if there are coding or resolution errors. Need to add some kind
// of type checking or assertion that we're dealing with at least
// a `Document<ParsedJavascriptDocument>` here.
const newAst = this._rewriteImportMetaToBundleMeta(
document.parsedDocument.ast as babel.File, relativeUrl);
const newCode = serialize(newAst).code;
return newCode;
}
},
},
],
experimentalDynamicImport: true,
});
const {code: rolledUpCode} = await rollupBundle.generate({
format: 'es',
freeze: false,
});
// 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.
// 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 = getFileExtension(url) === '.js' ?
url :
appendUrlPath(url, '_inline_es6_module.js');
Expand Down Expand Up @@ -151,11 +188,8 @@ export class Es6Rewriter {
traverse(node, {
noScope: true,
ImportDeclaration: {
enter(path: NodePath) {
enter(path: NodePath<babel.ImportDeclaration>) {
const importDeclaration = path.node;
if (!babel.isImportDeclaration(importDeclaration)) {
return;
}
const source = babel.isStringLiteral(importDeclaration.source) &&
importDeclaration.source.value;
if (!source) {
Expand Down Expand Up @@ -189,9 +223,8 @@ export class Es6Rewriter {
traverse(node, {
noScope: true,
ExportNamedDeclaration: {
enter(path: NodePath) {
const exportNamedDeclaration =
path.node as babel.ExportNamedDeclaration;
enter(path: NodePath<babel.ExportNamedDeclaration>) {
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
Expand Down Expand Up @@ -239,8 +272,8 @@ export class Es6Rewriter {
traverse(node, {
noScope: true,
ImportDeclaration: {
enter(path: NodePath) {
const importDeclaration = path.node as babel.ImportDeclaration;
enter(path: NodePath<babel.ImportDeclaration>) {
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.
Expand Down Expand Up @@ -432,4 +465,60 @@ export class Es6Rewriter {
importSpecifier,
{type: 'ImportSpecifier', imported: babel.identifier(exportName)});
}

private _rewriteImportMetaToBundleMeta(
moduleFile: babel.File,
relativeUrl: FileRelativeUrl): babel.File {
// Generate a stand-in for any local references to import.meta...
// const __bundledImportMeta = {...import.meta, url: __bundledImportURL};
// 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 bundledImportMetaName = '__bundledImportMeta';
const bundledImportMetaDeclaration = babel.variableDeclaration(
//
'const',
[
//
babel.variableDeclarator(
babel.identifier(bundledImportMetaName), 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);
traverse(newModuleFile, {
noScope: true,
MetaProperty: {
enter(path: NodePath<babel.MetaProperty>) {
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(bundledImportMetaName);
path.replaceWith(bundledImportMeta);
},
},
});
newModuleFile.program.body.unshift(bundledImportMetaDeclaration);
return newModuleFile;
}
}
107 changes: 107 additions & 0 deletions packages/bundler/src/test/import-meta-url_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @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
*/
import * as babel from 'babel-types';
import {assert} from 'chai';
import {Bundle} from 'magic-string';
import {PackageRelativeUrl} from 'polymer-analyzer';

import {Bundler} from '../bundler';

import {heredoc, inMemoryAnalyzer} from './test-utils';

suite('import.meta.url', () => {

const analyzer = inMemoryAnalyzer({
'a.js': `
const myUrl = import.meta.url;
export {myUrl};
`,
'subfolder/b.js': `
import {myUrl as aUrl} from '../a.js';
const myUrl = import.meta.url;
export {myUrl, aUrl};
`,
'c.js': `
import {aUrl, myUrl as bUrl} from './subfolder/b.js';
const myMeta = import.meta;
export {aUrl, bUrl, myMeta};
`,
});

const aUrl = analyzer.resolveUrl('a.js')!;
const bUrl = analyzer.resolveUrl('subfolder/b.js')!;
const cUrl = analyzer.resolveUrl('c.js')!;

test('bundled module with same URL as bundle', async () => {
const bundler = new Bundler({analyzer});
const aDoc = (await analyzer.analyze([aUrl])).getDocument(aUrl)!;
const {documents} =
await bundler.bundle(await bundler.generateManifest([aUrl]));
assert.deepEqual(documents.get(aUrl)!.content, heredoc`
const myUrl = import.meta.url;
var a = {
myUrl: myUrl
};
export { a as $a, myUrl };`);
});

test('corrected import.meta.url for bundled import', async () => {
const bundler = new Bundler({analyzer});
const {documents} =
await bundler.bundle(await bundler.generateManifest([bUrl]));
assert.deepEqual(documents.get(bUrl)!.content, heredoc`
const __bundledImportMeta = { ...import.meta,
url: new URL('../a.js', import.meta.url).href
};
const myUrl = __bundledImportMeta.url;
var a = {
myUrl: myUrl
};
const myUrl$1 = import.meta.url;
var b = {
myUrl: myUrl$1,
aUrl: myUrl
};
export { a as $a, b as $b, myUrl as myUrl$1, myUrl$1 as myUrl, myUrl as aUrl };`);
});

test('multiple corrected import.meta.url values', async () => {
const bundler = new Bundler({analyzer});
const {documents} =
await bundler.bundle(await bundler.generateManifest([cUrl]));
assert.deepEqual(documents.get(cUrl)!.content, heredoc`
const __bundledImportMeta = { ...import.meta,
url: new URL('./a.js', import.meta.url).href
};
const myUrl = __bundledImportMeta.url;
var a = {
myUrl: myUrl
};
const __bundledImportMeta$1 = { ...import.meta,
url: new URL('./subfolder/b.js', import.meta.url).href
};
const myUrl$1 = __bundledImportMeta$1.url;
var b = {
myUrl: myUrl$1,
aUrl: myUrl
};
const myMeta = import.meta;
var c = {
aUrl: myUrl,
bUrl: myUrl$1,
myMeta: myMeta
};
export { a as $a, c as $c, b as $b, myUrl, myUrl as aUrl, myUrl$1 as bUrl, myMeta, myUrl$1, myUrl as aUrl$1 };`);
});
});
Loading

0 comments on commit f313b16

Please sign in to comment.