diff --git a/packages/bundler/.clang-format b/packages/bundler/.clang-format
new file mode 100644
index 000000000..cfe46005b
--- /dev/null
+++ b/packages/bundler/.clang-format
@@ -0,0 +1,10 @@
+BasedOnStyle: Google
+AlignAfterOpenBracket: AlwaysBreak
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+BinPackArguments: false
+BinPackParameters: false
diff --git a/packages/bundler/.editorconfig b/packages/bundler/.editorconfig
new file mode 100644
index 000000000..99b853347
--- /dev/null
+++ b/packages/bundler/.editorconfig
@@ -0,0 +1,19 @@
+# Copyright (c) 2014 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
+
+# Polymer EditorConfig
+
+root = true
+
+[*]
+charset = utf-8
+indent_size = 2
+indent_style = space
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
\ No newline at end of file
diff --git a/packages/bundler/.gitattributes b/packages/bundler/.gitattributes
new file mode 100644
index 000000000..176a458f9
--- /dev/null
+++ b/packages/bundler/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/packages/bundler/.github/PULL_REQUEST_TEMPLATE b/packages/bundler/.github/PULL_REQUEST_TEMPLATE
new file mode 100644
index 000000000..dc2b53802
--- /dev/null
+++ b/packages/bundler/.github/PULL_REQUEST_TEMPLATE
@@ -0,0 +1,16 @@
+
+
+ - [ ] CHANGELOG.md has been updated
diff --git a/packages/bundler/.gitignore b/packages/bundler/.gitignore
new file mode 100644
index 000000000..0be6d6916
--- /dev/null
+++ b/packages/bundler/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+example/bower_components
+**/.DS_Store
+typings
+lib
diff --git a/packages/bundler/.npmignore b/packages/bundler/.npmignore
new file mode 100644
index 000000000..999d6ad2a
--- /dev/null
+++ b/packages/bundler/.npmignore
@@ -0,0 +1,7 @@
+node_modules
+# npmignore
+example
+CHANGELOG.md
+README.md
+util
+test
diff --git a/packages/bundler/.travis.yml b/packages/bundler/.travis.yml
new file mode 100644
index 000000000..10ae1c064
--- /dev/null
+++ b/packages/bundler/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - "6"
+ - "node"
+sudo: false
diff --git a/packages/bundler/.vscode/settings.json b/packages/bundler/.vscode/settings.json
new file mode 100644
index 000000000..317a7d506
--- /dev/null
+++ b/packages/bundler/.vscode/settings.json
@@ -0,0 +1,14 @@
+{
+ "clang-format.style": "file",
+ "editor.detectIndentation": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true,
+ "editor.tabSize": 2,
+ "files.insertFinalNewline": true,
+ "search.exclude": {
+ "node_modules/": true,
+ "lib/": true
+ },
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
diff --git a/packages/bundler/CHANGELOG.md b/packages/bundler/CHANGELOG.md
new file mode 100644
index 000000000..3294f9a50
--- /dev/null
+++ b/packages/bundler/CHANGELOG.md
@@ -0,0 +1,774 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+
+
+
+## 4.0.0-pre.4 - 2018-04-02
+- Fix issue where external script tags referencing bundled ES modules were not updated.
+
+## 4.0.0-pre.3 - 2018-03-28
+- [BREAKING] The `--in-html` and `--out-html` arguments for `bin/polymer-bundler` are now `--in-file` and `--out-file` because input filetypes are no longer limited to HTML.
+- ES6 Module Bundling! Bundler can simultaneously process HTML imports and ES6 module imports, including inline ``
+ moduleScriptImports: Map,
+};
+
+/**
+ * These are the options included in every `Document#getFeatures` call, DRY'd up
+ * here for brevity and consistency.
+ */
+const getFeaturesOptions = {
+ imported: false,
+ externalPackages: true,
+ excludeBackreferences: true,
+};
+
+/**
+ * For a given document, return a set of transitive dependencies, including
+ * all eagerly-loaded dependencies and lazy html imports encountered.
+ */
+function getDependencies(
+ analyzer: Analyzer, document: Document): DependencyMapEntry {
+ const deps = new Set();
+ const eagerDeps = new Set();
+ const lazyImports = new Set();
+ const moduleScriptImports = new Map();
+ _getDependencies(document, true);
+ return {deps, eagerDeps, lazyImports, moduleScriptImports};
+
+ function _getDependencies(document: Document, viaEager: boolean) {
+ // HTML document dependencies include external modules referenced by script
+ // src attribute, external modules imported by inline module import
+ // statements, and HTML imports (recursively).
+ if (document.kinds.has('html-document')) {
+ _getHtmlExternalModuleDependencies(document);
+ _getHtmlInlineModuleDependencies(document);
+ _getImportDependencies(
+ document.getFeatures({kind: 'html-import', ...getFeaturesOptions}),
+ viaEager);
+ }
+
+ // JavaScript documents, when parsed as modules, have dependencies defined
+ // by their import statements.
+ if (document.kinds.has('js-document')) {
+ _getImportDependencies(
+ // TODO(usergenic): We should be able to filter here on:
+ // `.filter((d) => d.parsedAsSourceType === 'module')`
+ // here, but Analyzer wont report that if there are no
+ // import/export statements in the imported file.
+ document.getFeatures({kind: 'js-import', ...getFeaturesOptions}) as
+ Set,
+ viaEager);
+ }
+ }
+
+ function _getHtmlExternalModuleDependencies(document: Document) {
+ let externalModuleCount = 0;
+ const htmlScripts =
+ [...document.getFeatures({kind: 'html-script', ...getFeaturesOptions})]
+ .filter(
+ (i) => i.document !== undefined &&
+ (i.document.parsedDocument as JavaScriptDocument)
+ .parsedAsSourceType === 'module');
+ for (const htmlScript of htmlScripts) {
+ const relativeUrl =
+ analyzer.urlResolver.relative(document.url, htmlScript.document!.url);
+ moduleScriptImports.set(
+ `external#${++externalModuleCount}>${relativeUrl}>es6-module`,
+ htmlScript.document!);
+ }
+ }
+
+ function _getHtmlInlineModuleDependencies(document: Document) {
+ let jsDocumentCount = 0;
+ const jsDocuments =
+ [...document.getFeatures({kind: 'js-document', ...getFeaturesOptions})]
+ .filter(
+ (d) => d.kinds.has('inline-document') &&
+ d.parsedDocument.parsedAsSourceType === 'module');
+ for (const jsDocument of jsDocuments) {
+ moduleScriptImports.set(
+ `inline#${++jsDocumentCount}>es6-module`, jsDocument);
+ }
+ }
+
+ function _getImportDependencies(
+ imports: Iterable, viaEager: boolean) {
+ for (const imprt of imports) {
+ if (imprt.document === undefined) {
+ continue;
+ }
+ const importUrl = imprt.document.url;
+ if (imprt.lazy) {
+ lazyImports.add(importUrl);
+ }
+ if (eagerDeps.has(importUrl)) {
+ continue;
+ }
+ const isEager = viaEager && !lazyImports.has(importUrl);
+ if (isEager) {
+ // In this case we've visited a node eagerly for the first time,
+ // so recurse.
+ eagerDeps.add(importUrl);
+ } else if (deps.has(importUrl)) {
+ // In this case we're seeing a node lazily again, so don't recurse
+ continue;
+ }
+ deps.add(importUrl);
+ _getDependencies(imprt.document, isEager);
+ }
+ }
+}
diff --git a/packages/bundler/src/document-collection.ts b/packages/bundler/src/document-collection.ts
new file mode 100644
index 000000000..964e24c9b
--- /dev/null
+++ b/packages/bundler/src/document-collection.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright (c) 2016 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 {ASTNode} from 'parse5';
+import {ResolvedUrl} from 'polymer-analyzer';
+
+export interface BundledDocument {
+ ast: ASTNode;
+ content: string;
+ files: ResolvedUrl[];
+}
+
+/* A collection of documents, keyed by path */
+export type DocumentCollection = Map;
diff --git a/packages/bundler/src/es6-module-bundler.ts b/packages/bundler/src/es6-module-bundler.ts
new file mode 100644
index 000000000..3d6d52215
--- /dev/null
+++ b/packages/bundler/src/es6-module-bundler.ts
@@ -0,0 +1,98 @@
+/**
+ * @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 generate from 'babel-generator';
+import * as babel from 'babel-types';
+import {ResolvedUrl} from 'polymer-analyzer';
+
+import {getAnalysisDocument} from './analyzer-utils';
+import {AssignedBundle, BundleManifest} from './bundle-manifest';
+import {Bundler} from './bundler';
+import {BundledDocument} from './document-collection';
+import {getModuleExportNames, getOrSetBundleModuleExportName} from './es6-module-utils';
+import {Es6Rewriter} from './es6-rewriter';
+import {ensureLeadingDot, stripUrlFileSearchAndHash} from './url-utils';
+
+/**
+ * Produces an ES6 Module BundledDocument.
+ */
+export async function bundle(
+ bundler: Bundler, manifest: BundleManifest, url: ResolvedUrl):
+ Promise {
+ const bundle = manifest.bundles.get(url);
+ if (!bundle) {
+ throw new Error(`No bundle found in manifest for url ${url}.`);
+ }
+ const assignedBundle = {url, bundle};
+ const generatedCode =
+ await prepareBundleModule(bundler, manifest, assignedBundle);
+ const es6Rewriter = new Es6Rewriter(bundler, manifest, assignedBundle);
+ const {code: rolledUpCode} = await es6Rewriter.rollup(url, generatedCode);
+ const document =
+ await bundler.analyzeContents(assignedBundle.url, rolledUpCode);
+ return {
+ ast: document.parsedDocument.ast,
+ content: document.parsedDocument.contents,
+ files: [...assignedBundle.bundle.files]
+ };
+}
+
+/**
+ * Generate code containing import statements to all bundled modules and
+ * export statements to re-export their namespaces and exports.
+ *
+ * Example: a bundle containing files `module-a.js` and `module-b.js` would
+ * result in a prepareBundleModule result like:
+ *
+ * import * as $moduleA from './module-a.js';
+ * import * as $moduleB from './module-b.js';
+ * import $moduleBDefault from './module-b.js';
+ * export {thing1, thing2} from './module-a.js';
+ * export {thing3} from './module-b.js';
+ * export {$moduleA, $moduleB, $moduleBDefault};
+ */
+async function prepareBundleModule(
+ bundler: Bundler, manifest: BundleManifest, assignedBundle: AssignedBundle):
+ Promise {
+ let bundleSource = babel.program([]);
+ const sourceAnalysis =
+ await bundler.analyzer.analyze([...assignedBundle.bundle.files]);
+ for (const sourceUrl of [...assignedBundle.bundle.files].sort()) {
+ const rebasedSourceUrl =
+ ensureLeadingDot(bundler.analyzer.urlResolver.relative(
+ stripUrlFileSearchAndHash(assignedBundle.url), sourceUrl));
+ const moduleDocument = getAnalysisDocument(sourceAnalysis, sourceUrl);
+ const moduleExports = getModuleExportNames(moduleDocument);
+ const starExportName =
+ getOrSetBundleModuleExportName(assignedBundle, sourceUrl, '*');
+ bundleSource.body.push(babel.importDeclaration(
+ [babel.importNamespaceSpecifier(babel.identifier(starExportName))],
+ babel.stringLiteral(rebasedSourceUrl)));
+ if (moduleExports.size > 0) {
+ bundleSource.body.push(babel.exportNamedDeclaration(
+ undefined, [babel.exportSpecifier(
+ babel.identifier(starExportName),
+ babel.identifier(starExportName))]));
+ bundleSource.body.push(babel.exportNamedDeclaration(
+ undefined,
+ [...moduleExports].map(
+ (e) => babel.exportSpecifier(
+ babel.identifier(e),
+ babel.identifier(getOrSetBundleModuleExportName(
+ assignedBundle, sourceUrl, e)))),
+ babel.stringLiteral(rebasedSourceUrl)));
+ }
+ }
+ const {code} = generate(bundleSource);
+ return code;
+ }
diff --git a/packages/bundler/src/es6-module-utils.ts b/packages/bundler/src/es6-module-utils.ts
new file mode 100644
index 000000000..c72e0196d
--- /dev/null
+++ b/packages/bundler/src/es6-module-utils.ts
@@ -0,0 +1,99 @@
+/**
+ * @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 {Analyzer, Document, ResolvedUrl} from 'polymer-analyzer';
+
+import {getAnalysisDocument} from './analyzer-utils';
+import {AssignedBundle, BundleManifest} from './bundle-manifest';
+import {getFileName} from './url-utils';
+import {camelCase} from './utils';
+
+/**
+ * Looks up and/or defines the unique name for an item exported with the given
+ * name in a module within a bundle.
+ */
+export function getOrSetBundleModuleExportName(
+ bundle: AssignedBundle, moduleUrl: ResolvedUrl, name: string): string {
+ let moduleExports = bundle.bundle.bundledExports.get(moduleUrl);
+ const bundledExports = bundle.bundle.bundledExports;
+ if (!moduleExports) {
+ moduleExports = new Map();
+ bundledExports.set(moduleUrl, moduleExports);
+ }
+ let exportName = moduleExports.get(name);
+ if (!exportName) {
+ let trialName = name;
+ let moduleFileNameIdentifier =
+ '$' + camelCase(getFileName(moduleUrl).replace(/\.[a-z0-9_]+$/, ''));
+ trialName =
+ trialName.replace(/^default$/, `${moduleFileNameIdentifier}Default`)
+ .replace(/^\*$/, moduleFileNameIdentifier)
+ .replace(/[^a-z0-9_]/gi, '$');
+ while (!exportName) {
+ if ([...bundledExports.values()].every(
+ (map) => [...map.values()].indexOf(trialName) === -1)) {
+ exportName = trialName;
+ } else {
+ if (trialName.match(/\$[0-9]+$/)) {
+ trialName = trialName.replace(/[0-9]+$/, (v) => `${parseInt(v) + 1}`);
+ } else {
+ trialName = `${trialName}$1`;
+ }
+ }
+ }
+ moduleExports.set(name, exportName);
+ }
+ return exportName;
+}
+
+/**
+ * Returns a set of every name exported by a module.
+ *
+ * TODO(usergenic): This does not include names brought in by the statement
+ * `export * from './module-a.js';`.
+ * https://github.com/Polymer/polymer-bundler/issues/641
+ */
+export function getModuleExportNames(document: Document): Set {
+ const exports_ = document.getFeatures({kind: 'export'});
+ const identifiers = new Set();
+ for (const export_ of exports_) {
+ for (const identifier of export_.identifiers) {
+ identifiers.add(identifier);
+ }
+ }
+ return identifiers;
+}
+
+/**
+ * Ensures that exported names from modules which have the same URL as their
+ * bundle will have precedence over other module exports, which will be
+ * counter-suffixed in the event of name collisions. This has no technical
+ * benefit, but it results in module export naming choices that are easier
+ * to reason about for developers and may aid in debugging.
+ */
+export async function reserveBundleModuleExportNames(
+ analyzer: Analyzer, manifest: BundleManifest) {
+ const es6ModuleBundles =
+ [...manifest.bundles]
+ .map(([url, bundle]) => ({url, bundle}))
+ .filter(({bundle}) => bundle.type === 'es6-module');
+ const analysis = await analyzer.analyze(es6ModuleBundles.map(({url}) => url));
+ for (const {url, bundle} of es6ModuleBundles) {
+ if (bundle.files.has(url)) {
+ const document = getAnalysisDocument(analysis, url);
+ for (const exportName of getModuleExportNames(document)) {
+ getOrSetBundleModuleExportName({url, bundle}, url, exportName);
+ }
+ }
+ }
+}
diff --git a/packages/bundler/src/es6-rewriter.ts b/packages/bundler/src/es6-rewriter.ts
new file mode 100644
index 000000000..c7d487a05
--- /dev/null
+++ b/packages/bundler/src/es6-rewriter.ts
@@ -0,0 +1,393 @@
+/**
+ * @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 traverse, {NodePath} from 'babel-traverse';
+import * as babel from 'babel-types';
+import * as clone from 'clone';
+import {FileRelativeUrl, Import, PackageRelativeUrl, ResolvedUrl} from 'polymer-analyzer';
+import {rollup} from 'rollup';
+
+import {getAnalysisDocument} from './analyzer-utils';
+import {serialize} from './babel-utils';
+import {AssignedBundle, BundleManifest} from './bundle-manifest';
+import {Bundler} from './bundler';
+import {getOrSetBundleModuleExportName} from './es6-module-utils';
+import {appendUrlPath, ensureLeadingDot, getFileExtension} from './url-utils';
+import {rewriteObject} from './utils';
+
+/**
+ * Utility class to rollup/merge ES6 modules code using rollup and rewrite
+ * import statements to point to appropriate bundles.
+ */
+export class Es6Rewriter {
+ constructor(
+ public bundler: Bundler,
+ public manifest: BundleManifest,
+ public bundle: AssignedBundle) {
+ }
+
+ async rollup(url: ResolvedUrl, code: string) {
+ // 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 =
+ await this.bundler.analyzer.analyze([...this.bundle.bundle.files]);
+ const external: string[] = [];
+ for (const [url, bundle] of this.manifest.bundles) {
+ if (url !== this.bundle.url) {
+ external.push(...[...bundle.files, url]);
+ }
+ }
+ // 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>();
+ const rollupBundle = await rollup({
+ input,
+ external,
+ onwarn: (warning: string) => {},
+ treeshake: false,
+ plugins: [
+ {
+ name: 'analyzerPlugin',
+ resolveId: (importee: string, importer?: string) => {
+ if (importee === input) {
+ return input;
+ }
+ if (importer) {
+ if (jsImportResolvedUrls.has(importer as ResolvedUrl)) {
+ const resolutions =
+ jsImportResolvedUrls.get(importer as ResolvedUrl)!;
+ if (resolutions.has(importee)) {
+ return resolutions.get(importee);
+ }
+ }
+ return this.bundler.analyzer.urlResolver.resolve(
+ importer === input ? url : importer as ResolvedUrl,
+ importee as FileRelativeUrl)! as string;
+ }
+ return this.bundler.analyzer.urlResolver.resolve(
+ importee as PackageRelativeUrl)! as string;
+ },
+ load: (id: ResolvedUrl) => {
+ if (id === input) {
+ return code;
+ }
+ if (this.bundle.bundle.files.has(id)) {
+ const document = getAnalysisDocument(analysis, id);
+ if (!jsImportResolvedUrls.has(id)) {
+ const jsImports = document.getFeatures({
+ kind: 'js-import',
+ imported: false,
+ externalPackages: true,
+ excludeBackreferences: true,
+ }) as Set;
+ const resolutions = new Map();
+ jsImportResolvedUrls.set(id, resolutions);
+ for (const jsImport of jsImports) {
+ const source = jsImport.astNode && jsImport.astNode.source &&
+ jsImport.astNode.source.value;
+ if (source && jsImport.document !== undefined) {
+ resolutions.set(source, jsImport.document.url);
+ }
+ }
+ }
+ return document.parsedDocument.contents;
+ }
+ },
+ },
+ ],
+ 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.
+ const rolledUpUrl = getFileExtension(url) === '.js' ?
+ url :
+ appendUrlPath(url, '_inline_es6_module.js');
+ const rolledUpDocument = await this.bundler.analyzeContents(
+ rolledUpUrl as ResolvedUrl, rolledUpCode);
+ const babelFile = rolledUpDocument.parsedDocument.ast;
+ this._rewriteImportStatements(url, babelFile);
+ this._deduplicateImportStatements(babelFile);
+ const {code: rewrittenCode} = serialize(babelFile);
+ return {code: rewrittenCode, map: undefined};
+ }
+
+ /**
+ * 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.
+ *
+ * Before:
+ * import {a} from './module-1.js';
+ * import {b} from './module-1.js';
+ * import {c} from './module-2.js';
+ * After:
+ * import {a,b} from './module-1.js';
+ * import {c} from './module-2.js';
+ */
+ private _deduplicateImportStatements(node: babel.Node) {
+ const importDeclarations = new Map();
+ traverse(node, {
+ noScope: true,
+ ImportDeclaration: {
+ enter(path: NodePath) {
+ const importDeclaration = path.node;
+ if (!babel.isImportDeclaration(importDeclaration)) {
+ return;
+ }
+ 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 import declarations source URLs reference the bundle URL for
+ * bundled files and import names to correspond to names as exported by
+ * bundles.
+ */
+ private _rewriteImportStatements(baseUrl: ResolvedUrl, node: babel.Node) {
+ const this_ = this;
+ traverse(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: NodePath) {
+ if (path.node.type === 'Import') {
+ this_._rewriteDynamicImport(baseUrl, node, path);
+ }
+ },
+ });
+
+ traverse(node, {
+ noScope: true,
+ ImportDeclaration: {
+ enter(path: NodePath) {
+ const importDeclaration = path.node as babel.ImportDeclaration;
+ 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 as ResolvedUrl;
+ 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 =
+ 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 =
+ 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(({$moduleA}) => $moduleA)
+ * .then((moduleA) => moduleA.doSomething());
+ */
+ private _rewriteDynamicImport(
+ baseUrl: ResolvedUrl,
+ root: babel.Node,
+ importNodePath: NodePath) {
+ 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 as FileRelativeUrl);
+ 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 =
+ 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 =
+ 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 = 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.objectPattern(
+ [babel.objectProperty(
+ babel.identifier(exportName),
+ babel.identifier(exportName),
+ undefined,
+ true) as any]),
+ ],
+ babel.identifier(exportName))]);
+ 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';
+ */
+ private _rewriteImportSpecifierName(
+ specifier: babel.ImportSpecifier,
+ source: ResolvedUrl,
+ sourceBundle: AssignedBundle) {
+ const originalExportName = specifier.imported.name;
+ const exportName = 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';
+ */
+ private _rewriteImportDefaultSpecifier(
+ specifier: babel.ImportDefaultSpecifier,
+ source: ResolvedUrl,
+ sourceBundle: AssignedBundle) {
+ const exportName =
+ 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;
+ }
+ const importSpecifier = specifier as any as babel.ImportSpecifier;
+ 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';
+ */
+ private _rewriteImportNamespaceSpecifier(
+ specifier: babel.ImportNamespaceSpecifier,
+ source: ResolvedUrl,
+ sourceBundle: AssignedBundle) {
+ const exportName =
+ getOrSetBundleModuleExportName(sourceBundle, source, '*');
+ // No rewrite necessary if * is the name, since this indicates there was no
+ // bundling of the namespace.
+ if (exportName === '*') {
+ return;
+ }
+ const importSpecifier = specifier as any as babel.ImportSpecifier;
+ Object.assign(
+ importSpecifier,
+ {type: 'ImportSpecifier', imported: babel.identifier(exportName)});
+ }
+}
diff --git a/packages/bundler/src/html-bundler.ts b/packages/bundler/src/html-bundler.ts
new file mode 100644
index 000000000..860d964bf
--- /dev/null
+++ b/packages/bundler/src/html-bundler.ts
@@ -0,0 +1,919 @@
+/**
+ * @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 clone from 'clone';
+import * as dom5 from 'dom5';
+import {ASTNode, parseFragment, serialize, treeAdapters} from 'parse5';
+import {Document, FileRelativeUrl, ParsedHtmlDocument, ResolvedUrl} from 'polymer-analyzer';
+
+import {getAnalysisDocument} from './analyzer-utils';
+import {AssignedBundle, BundleManifest} from './bundle-manifest';
+import {Bundler} from './bundler';
+import constants from './constants';
+import {BundledDocument} from './document-collection';
+import {Es6Rewriter} from './es6-rewriter';
+import * as matchers from './matchers';
+import {findAncestor, insertAfter, insertAllBefore, inSourceOrder, isSameNode, prepend, removeElementAndNewline, siblingsAfter, stripComments} from './parse5-utils';
+import {addOrUpdateSourcemapComment} from './source-map';
+import {updateSourcemapLocations} from './source-map';
+import encodeString from './third_party/UglifyJS2/encode-string';
+import {ensureTrailingSlash, getFileExtension, isTemplatedUrl, rewriteHrefBaseUrl, stripUrlFileSearchAndHash} from './url-utils';
+import {find, rewriteObject} from './utils';
+
+/**
+ * Produces an HTML BundledDocument.
+ */
+export async function bundle(
+ bundler: Bundler, manifest: BundleManifest, url: ResolvedUrl):
+ Promise {
+ const bundle = manifest.bundles.get(url);
+ if (!bundle) {
+ throw new Error(`No bundle found in manifest for url ${url}.`);
+ }
+ const assignedBundle = {url, bundle};
+ const htmlBundler = new HtmlBundler(bundler, assignedBundle, manifest);
+ return htmlBundler.bundle();
+}
+
+/**
+ * A single-use instance of this class produces a single HTML BundledDocument.
+ * Use the bundle directly is deprecated; it is exported only to support unit
+ * tests of its methods in html-bundler_test.ts for now. Please use the
+ * exported bundle function above.
+ */
+export class HtmlBundler {
+ protected document: Document;
+
+ constructor(
+ public bundler: Bundler,
+ public assignedBundle: AssignedBundle,
+ public manifest: BundleManifest) {
+ }
+
+ async bundle(): Promise {
+ this.document = await this._prepareBundleDocument();
+ let ast = clone(this.document.parsedDocument.ast);
+ dom5.removeFakeRootElements(ast);
+ this._injectHtmlImportsForBundle(ast);
+ this._rewriteAstToEmulateBaseTag(ast, this.assignedBundle.url);
+
+ // Re-analyzing the document using the updated ast to refresh the scanned
+ // imports, since we may now have appended some that were not initially
+ // present.
+ this.document = await this._reanalyze(serialize(ast));
+
+ await this._inlineHtmlImports(ast);
+
+ await this._updateExternalModuleScripts(ast);
+ if (this.bundler.enableScriptInlining) {
+ await this._inlineNonModuleScripts(ast);
+ await this._inlineModuleScripts(ast);
+ }
+ if (this.bundler.enableCssInlining) {
+ await this._inlineStylesheetLinks(ast);
+ await this._inlineStylesheetImports(ast);
+ }
+ if (this.bundler.stripComments) {
+ stripComments(ast);
+ }
+ this._removeEmptyHiddenDivs(ast);
+ if (this.bundler.sourcemaps) {
+ ast = updateSourcemapLocations(this.document, ast);
+ }
+ const content = serialize(ast);
+ const files = [...this.assignedBundle.bundle.files];
+ return {ast, content, files};
+ }
+
+ /**
+ * Walk through inline scripts of an import document.
+ * For each script create identity source maps unless one already exists.
+ *
+ * The generated script mapping detail is the relative location within
+ * the script tag. Later this will be updated to account for the
+ * line offset within the final bundle.
+ */
+ private async _addOrUpdateSourcemapsForInlineScripts(
+ originalDoc: Document,
+ reparsedDoc: ParsedHtmlDocument,
+ oldBaseUrl: ResolvedUrl) {
+ const inlineScripts =
+ dom5.queryAll(reparsedDoc.ast, matchers.inlineNonModuleScript);
+ const promises = inlineScripts.map(scriptAst => {
+ let content = dom5.getTextContent(scriptAst);
+ const sourceRange = reparsedDoc.sourceRangeForStartTag(scriptAst)!;
+ return addOrUpdateSourcemapComment(
+ this.bundler.analyzer,
+ oldBaseUrl,
+ content,
+ sourceRange.end.line,
+ sourceRange.end.column,
+ -sourceRange.end.line + 1,
+ -sourceRange.end.column)
+ .then(updatedContent => {
+ dom5.setTextContent(scriptAst, encodeString(updatedContent));
+ });
+ });
+
+ return Promise.all(promises);
+ }
+
+ /**
+ * Set the hidden div at the appropriate location within the document. The
+ * goal is to place the hidden div at the same place as the first html
+ * import. However, the div can't be placed in the `` of the document
+ * so if first import is found in the head, we prepend the div to the body.
+ * If there is no body, we'll just attach the hidden div to the document at
+ * the end.
+ */
+ private _attachHiddenDiv(ast: ASTNode, hiddenDiv: ASTNode) {
+ const firstHtmlImport = dom5.query(ast, matchers.eagerHtmlImport);
+ const body = dom5.query(ast, matchers.body);
+ if (body) {
+ if (firstHtmlImport &&
+ dom5.predicates.parentMatches(matchers.body)(firstHtmlImport)) {
+ insertAfter(firstHtmlImport, hiddenDiv);
+ } else {
+ prepend(body, hiddenDiv);
+ }
+ } else {
+ dom5.append(ast, hiddenDiv);
+ }
+ }
+
+ /**
+ * Creates a hidden container
to which inlined content will be
+ * appended.
+ */
+ private _createHiddenDiv(): ASTNode {
+ const hidden = dom5.constructors.element('div');
+ dom5.setAttribute(hidden, 'hidden', '');
+ dom5.setAttribute(hidden, 'by-polymer-bundler', '');
+ return hidden;
+ }
+
+ /**
+ * Append a `` node to `node` with a value of `url`
+ * for the "href" attribute.
+ */
+ private _createHtmlImport(url: FileRelativeUrl|ResolvedUrl): ASTNode {
+ const link = dom5.constructors.element('link');
+ dom5.setAttribute(link, 'rel', 'import');
+ dom5.setAttribute(link, 'href', url);
+ return link;
+ }
+
+ /**
+ * Given a document, search for the hidden div, if it isn't found, then
+ * create it. After creating it, attach it to the desired location. Then
+ * return it.
+ */
+ private _findOrCreateHiddenDiv(ast: ASTNode): ASTNode {
+ const hiddenDiv =
+ dom5.query(ast, matchers.hiddenDiv) || this._createHiddenDiv();
+ if (!hiddenDiv.parentNode) {
+ this._attachHiddenDiv(ast, hiddenDiv);
+ }
+ return hiddenDiv;
+ }
+
+ /**
+ * Add HTML Import elements for each file in the bundle. Efforts are made
+ * to ensure that imports are injected prior to any eager imports of other
+ * bundles which are known to depend on them, to preserve expectations of
+ * evaluation order.
+ */
+ private _injectHtmlImportsForBundle(ast: ASTNode) {
+ // Gather all the document's direct html imports. We want the direct (not
+ // transitive) imports only here, because we'll be using their AST nodes
+ // as targets to prepended injected imports to.
+ const existingImports = [
+ ...this.document.getFeatures(
+ {kind: 'html-import', noLazyImports: true, imported: false})
+ ].filter((i) => !i.lazy && i.document !== undefined);
+ const existingImportDependencies = new Map();
+ for (const {url, document} of existingImports) {
+ existingImportDependencies.set(
+ url,
+ [...document!.getFeatures({
+ kind: 'html-import',
+ imported: true,
+ noLazyImports: true,
+ })].filter((i) => i.lazy === false && i.document !== undefined)
+ .map((i) => i.document!.url));
+ }
+ // Every HTML file in the bundle is a candidate for injection into the
+ // document.
+ for (const importUrl of this.assignedBundle.bundle.files) {
+ // We only want to inject an HTML import to an HTML file.
+ if (getFileExtension(importUrl) !== '.html') {
+ continue;
+ }
+
+ // We don't want to inject the bundle into itself.
+ if (this.assignedBundle.url === importUrl) {
+ continue;
+ }
+
+ // If there is an existing import in the document that matches the
+ // import URL already, we don't need to inject one.
+ if (existingImports.find(
+ (e) =>
+ e.document !== undefined && e.document.url === importUrl)) {
+ continue;
+ }
+
+ // We are looking for the earliest eager import of an html document
+ // which has a dependency on the html import we want to inject.
+ let prependTarget = undefined;
+
+ // We are only concerned with imports that are not of files in this
+ // bundle.
+ for (const existingImport of existingImports.filter(
+ (e) => e.document !== undefined &&
+ !this.assignedBundle.bundle.files.has(e.document.url))) {
+ // If the existing import has a dependency on the import we are
+ // about to inject, it may be our new target.
+ if (existingImportDependencies.get(existingImport.document!.url)!
+ .indexOf(importUrl) !== -1) {
+ const newPrependTarget = dom5.query(
+ ast, (node) => isSameNode(node, existingImport.astNode));
+
+ // IF we don't have a target already or if the old target comes
+ // after the new one in the source code, the new one will replace
+ // the old one.
+ if (newPrependTarget &&
+ (!prependTarget ||
+ inSourceOrder(newPrependTarget, prependTarget))) {
+ prependTarget = newPrependTarget;
+ }
+ }
+ }
+
+ // Inject the new html import into the document.
+ const relativeImportUrl = this.bundler.analyzer.urlResolver.relative(
+ this.assignedBundle.url, importUrl);
+ const newHtmlImport = this._createHtmlImport(relativeImportUrl);
+ if (prependTarget) {
+ dom5.insertBefore(
+ prependTarget.parentNode!, prependTarget, newHtmlImport);
+ } else {
+ const hiddenDiv = this._findOrCreateHiddenDiv(ast);
+ dom5.append(hiddenDiv.parentNode!, newHtmlImport);
+ }
+ }
+ }
+
+ /**
+ * Inline the contents of the html document returned by the link tag's href
+ * at the location of the link tag and then remove the link tag. If the
+ * link is a `lazy-import` link, content will not be inlined.
+ */
+ private async _inlineHtmlImport(linkTag: ASTNode) {
+ const isLazy = dom5.getAttribute(linkTag, 'rel')!.match(/lazy-import/i);
+ const importHref = dom5.getAttribute(linkTag, 'href')! as FileRelativeUrl;
+ const resolvedImportUrl = this.bundler.analyzer.urlResolver.resolve(
+ this.assignedBundle.url, importHref);
+ if (resolvedImportUrl === undefined) {
+ return;
+ }
+ const importBundle = this.manifest.getBundleForFile(resolvedImportUrl);
+
+ // We don't want to process the same eager import again, but we want to
+ // process every lazy import we see.
+ if (!isLazy) {
+ // Just remove the import from the DOM if it is in the stripImports Set.
+ if (this.assignedBundle.bundle.stripImports.has(resolvedImportUrl)) {
+ removeElementAndNewline(linkTag);
+ return;
+ }
+
+ // We've never seen this import before, so we'll add it to the
+ // stripImports Set to guard against inlining it again in the future.
+ this.assignedBundle.bundle.stripImports.add(resolvedImportUrl);
+ }
+
+ // If we can't find a bundle for the referenced import, we will just leave
+ // the import link alone. Unless the file was specifically excluded, we
+ // need to record it as a "missing import".
+ if (!importBundle) {
+ if (!this.bundler.excludes.some(
+ (u) => u === resolvedImportUrl ||
+ resolvedImportUrl.startsWith(ensureTrailingSlash(u)))) {
+ this.assignedBundle.bundle.missingImports.add(resolvedImportUrl);
+ }
+ return;
+ }
+
+ // Don't inline an import into itself.
+ if (this.assignedBundle.url === resolvedImportUrl) {
+ removeElementAndNewline(linkTag);
+ return;
+ }
+
+ const importIsInAnotherBundle =
+ importBundle.url !== this.assignedBundle.url;
+
+ // If the import is in another bundle and that bundle is in the
+ // stripImports Set, we should not link to that bundle.
+ const stripLinkToImportBundle = importIsInAnotherBundle &&
+ this.assignedBundle.bundle.stripImports.has(importBundle.url) &&
+ // We just added resolvedImportUrl to stripImports, so we'll exclude
+ // the case where resolved import URL is not the import bundle. This
+ // scenario happens when importing a file from a bundle with the same
+ // name as the original import, like an entrypoint or lazy edge.
+ resolvedImportUrl !== importBundle.url;
+
+ // If the html import refers to a file which is bundled and has a
+ // different URL, then lets just rewrite the href to point to the bundle
+ // URL.
+ if (importIsInAnotherBundle) {
+ // We guard against inlining any other file from a bundle that has
+ // already been imported. A special exclusion is for lazy imports,
+ // which are not deduplicated here, since we can not infer developer's
+ // intent from here.
+ if (stripLinkToImportBundle && !isLazy) {
+ removeElementAndNewline(linkTag);
+ return;
+ }
+
+ const relative = this.bundler.analyzer.urlResolver.relative(
+ this.assignedBundle.url, importBundle.url) ||
+ importBundle.url;
+ dom5.setAttribute(linkTag, 'href', relative);
+ this.assignedBundle.bundle.stripImports.add(importBundle.url);
+ return;
+ }
+
+ // We don't actually inline a `lazy-import` because its loading is
+ // intended to be deferred until the client requests it.
+ if (isLazy) {
+ return;
+ }
+
+ // If the analyzer could not load the import document, we can't inline it,
+ // so lets skip it.
+ const htmlImport = find(
+ this.document.getFeatures(
+ {kind: 'html-import', imported: true, externalPackages: true}),
+ (i) =>
+ i.document !== undefined && i.document.url === resolvedImportUrl);
+ if (htmlImport === undefined || htmlImport.document === undefined) {
+ return;
+ }
+
+ // When inlining html documents, we'll parse it as a fragment so that we
+ // do not get html, head or body wrappers.
+ const importAst = parseFragment(
+ htmlImport.document.parsedDocument.contents, {locationInfo: true});
+ this._rewriteAstToEmulateBaseTag(importAst, resolvedImportUrl);
+ this._rewriteAstBaseUrl(importAst, resolvedImportUrl, this.document.url);
+
+ if (this.bundler.sourcemaps) {
+ const reparsedDoc = new ParsedHtmlDocument({
+ url: this.assignedBundle.url,
+ baseUrl: this.document.parsedDocument.baseUrl,
+ contents: htmlImport.document.parsedDocument.contents,
+ ast: importAst,
+ isInline: false,
+ locationOffset: undefined,
+ astNode: undefined,
+ });
+ await this._addOrUpdateSourcemapsForInlineScripts(
+ this.document, reparsedDoc, resolvedImportUrl);
+ }
+ const nestedImports = dom5.queryAll(importAst, matchers.htmlImport);
+
+ // Move all of the import doc content after the html import.
+ insertAllBefore(linkTag.parentNode!, linkTag, importAst.childNodes!);
+ removeElementAndNewline(linkTag);
+
+ // Record that the inlining took place.
+ this.assignedBundle.bundle.inlinedHtmlImports.add(resolvedImportUrl);
+
+ // Recursively process the nested imports.
+ for (const nestedImport of nestedImports) {
+ await this._inlineHtmlImport(nestedImport);
+ }
+ }
+
+ /**
+ * Replace html import links in the document with the contents of the
+ * imported file, but only once per URL.
+ */
+ private async _inlineHtmlImports(ast: ASTNode) {
+ const htmlImports = dom5.queryAll(ast, matchers.htmlImport);
+ for (const htmlImport of htmlImports) {
+ await this._inlineHtmlImport(htmlImport);
+ }
+ }
+
+ /**
+ * Update the `src` attribute of external `type=module` script tags to point
+ * at new bundle locations.
+ */
+ public async _updateExternalModuleScripts(ast: ASTNode) {
+ const scripts = dom5.queryAll(ast, matchers.externalModuleScript);
+ for (const script of scripts) {
+ const oldSrc = dom5.getAttribute(script, 'src');
+ const oldFileUrl = this.bundler.analyzer.urlResolver.resolve(
+ this.assignedBundle.url, oldSrc as FileRelativeUrl);
+ if (oldFileUrl === undefined) {
+ continue;
+ }
+ const bundle = this.manifest.getBundleForFile(oldFileUrl);
+ if (bundle === undefined) {
+ continue;
+ }
+ const newFileUrl = bundle.url;
+ const newSrc = this.bundler.analyzer.urlResolver.relative(
+ this.assignedBundle.url, newFileUrl);
+ dom5.setAttribute(script, 'src', newSrc);
+ }
+ }
+
+ /**
+ * Inlines the contents of external module scripts and rolls-up imported
+ * modules into inline scripts.
+ */
+ private async _inlineModuleScripts(ast: ASTNode) {
+ this.document = await this._reanalyze(serialize(ast));
+ rewriteObject(ast, this.document.parsedDocument.ast);
+ dom5.removeFakeRootElements(ast);
+ const es6Rewriter =
+ new Es6Rewriter(this.bundler, this.manifest, this.assignedBundle);
+ const inlineModuleScripts =
+ [...this.document.getFeatures({
+ kind: 'js-document',
+ imported: false,
+ externalPackages: true,
+ excludeBackreferences: true,
+ })].filter(({
+ isInline,
+ parsedDocument: {parsedAsSourceType}
+ }) => isInline && parsedAsSourceType === 'module');
+ for (const inlineModuleScript of inlineModuleScripts) {
+ const {code} = await es6Rewriter.rollup(
+ this.document.parsedDocument.baseUrl,
+ inlineModuleScript.parsedDocument.contents);
+ // Second argument 'true' tells encodeString to escape the ');
+ assert(idx === -1, '/script found, should be escaped');
+ });
+ });
+
+ suite('Inline CSS', () => {
+ const options = {inlineCss: true};
+
+ test('URLs for inlined styles are recorded in Bundle', async () => {
+ await bundle(inputPath);
+ assert.deepEqual([...documentBundle.inlinedStyles].sort(), [
+ 'imports/import-linked-style.css',
+ 'imports/regular-style.css',
+ 'imports/simple-style.css',
+ ].map(resolve));
+ });
+
+ test('External links are replaced with inlined styles', async () => {
+ const {ast: doc} = await bundle(inputPath, options);
+ const links = dom5.queryAll(doc, matchers.stylesheetImport);
+ const styles = dom5.queryAll(
+ doc, matchers.styleMatcher, [], dom5.childNodesIncludeTemplate);
+ assert.equal(links.length, 0);
+ assert.equal(styles.length, 5);
+ assert.match(dom5.getTextContent(styles[0]), /regular-style/);
+ assert.match(dom5.getTextContent(styles[1]), /simple-style/);
+ assert.match(dom5.getTextContent(styles[2]), /import-linked-style/);
+ assert.match(dom5.getTextContent(styles[3]), /simple-style/);
+ assert.match(dom5.getTextContent(styles[4]), /import-linked-style/);
+
+ // Verify the inlined url() values in the stylesheet are not rewritten
+ // to use the "imports/" prefix, since the stylesheet is inside a
+ // `` with an assetpath that defines the base url as
+ // "imports/".
+ assert.match(
+ dom5.getTextContent(styles[1]), /url\("assets\/platform\.png"\)/);
+ });
+
+ test('External style links in templates are inlined', async () => {
+ const {ast: doc} =
+ await bundle('external-in-template.html', {inlineCss: true});
+ const externalStyles = dom5.queryAll(
+ doc,
+ matchers.externalStyle,
+ undefined,
+ dom5.childNodesIncludeTemplate);
+ assert.equal(externalStyles.length, 0);
+
+ // There were two links to external styles in the file, now there
+ // should be two occurrences of inlined styles. Note they are the
+ // same external styles, so inlining should be happening for each
+ // occurrence of the external style.
+ const inlineStyles = dom5.queryAll(
+ doc,
+ matchers.styleMatcher,
+ undefined,
+ dom5.childNodesIncludeTemplate);
+ assert.equal(inlineStyles.length, 2);
+ assert.match(dom5.getTextContent(inlineStyles[0]), /content: 'external'/);
+ assert.match(dom5.getTextContent(inlineStyles[1]), /content: 'external'/);
+ });
+
+ test('Inlined styles observe containing dom-module assetpath', async () => {
+ const {ast: doc} =
+ await bundle('style-rewriting.html', {inlineCss: true});
+ const style = dom5.query(
+ doc, matchers.styleMatcher, dom5.childNodesIncludeTemplate)!;
+ assert.isNotNull(style);
+ assert.match(dom5.getTextContent(style), /url\("styles\/unicorn.png"\)/);
+ });
+
+ test('Inlining ignores assetpath if rewriteUrlsInTemplates', async () => {
+ const {ast: doc} = await bundle(
+ 'style-rewriting.html',
+ {inlineCss: true, rewriteUrlsInTemplates: true});
+ const style = dom5.query(
+ doc, matchers.styleMatcher, dom5.childNodesIncludeTemplate)!;
+ assert.isNotNull(style);
+ assert.match(
+ dom5.getTextContent(style),
+ /url\("style-rewriting\/styles\/unicorn.png"\)/);
+ });
+
+ test('Inlined styles have proper paths', async () => {
+ const {ast: doc} = await bundle('inline-styles.html', options);
+ const styles = dom5.queryAll(
+ doc, matchers.styleMatcher, [], dom5.childNodesIncludeTemplate);
+ assert.equal(styles.length, 2);
+ const content = dom5.getTextContent(styles[1]);
+ assert(content.search('imports/foo.jpg') > -1, 'path adjusted');
+ assert(content.search('@apply') > -1, '@apply kept');
+ });
+
+ test('Remote styles and media queries are preserved', async () => {
+ const input = 'imports/remote-stylesheet.html';
+ const {ast: doc} = await bundle(input, options);
+ const links = dom5.queryAll(doc, matchers.externalStyle);
+ assert.equal(links.length, 1);
+ assert.match(
+ dom5.getAttribute(links[0]!, 'href')!, /fonts.googleapis.com/);
+ const styles = dom5.queryAll(doc, matchers.styleMatcher);
+ assert.equal(styles.length, 1);
+ assert.equal(dom5.getAttribute(styles[0], 'media'), '(min-width: 800px)');
+ });
+
+ test('Inlined Polymer styles are moved into the ', async () => {
+ const {ast: doc} = await bundle('default.html', options);
+ const domModule = dom5.query(doc, preds.hasTagName('dom-module'))!;
+ assert(domModule);
+ const template = dom5.query(domModule, matchers.template)!;
+ assert(template);
+
+ const styles = dom5.queryAll(
+ template, matchers.styleMatcher, [], dom5.childNodesIncludeTemplate);
+ assert.equal(styles.length, 2);
+ assert.match(
+ dom5.getTextContent(styles[0]), /simple-style/, 'simple-style.css');
+ assert.match(
+ dom5.getTextContent(styles[1]),
+ /import-linked-style/,
+ 'import-linked-style.css');
+ });
+
+ test(
+ 'Inlined Polymer styles force dom-module to have template',
+ async () => {
+ const {ast: doc} = await bundle('inline-styles.html', options);
+ const domModule = dom5.query(doc, preds.hasTagName('dom-module'))!;
+ assert(domModule);
+ const template = dom5.query(domModule, matchers.template)!;
+ assert(template);
+ const style = dom5.query(
+ template, matchers.styleMatcher, dom5.childNodesIncludeTemplate);
+ assert(style);
+ });
+ });
+
+ suite('Regression Testing', () => {
+ // Ensure this https://github.com/Polymer/polymer-bundler/issues/596 doesn't
+ // happen again.
+ test('Bundled file should not import itself', async () => {
+ const {ast: doc} = await bundle('default.html', {
+ inlineCss: true,
+ analyzer: new Analyzer({urlLoader: new FsUrlLoader('.')}),
+ });
+
+ const link = dom5.query(
+ doc,
+ preds.AND(
+ matchers.htmlImport, preds.hasAttrValue('href', 'default.html')));
+ assert.isNull(link, `Found unexpected link to default.html (self)`);
+ });
+
+ test('Base tag emulation should not leak to other imports', async () => {
+ const {ast: doc} = await bundle('base.html');
+ const clickMe = dom5.query(doc, preds.hasTextValue('CLICK ME'));
+ assert.ok(clickMe);
+
+ // The base target from `test/html/imports/base.html` should apply to
+ // the anchor tag in it.
+ assert.equal(dom5.getAttribute(clickMe!, 'target'), 'foo-frame');
+
+ const doNotClickMe =
+ dom5.query(doc, preds.hasTextValue('DO NOT CLICK ME'));
+ assert.ok(doNotClickMe);
+
+ // The base target from `test/html/imports/base.html` should NOT apply
+ // to the anchor tag in `test/html/imports/base-foo/sub-base.html`
+ assert.isFalse(dom5.hasAttribute(doNotClickMe!, 'target'));
+ });
+
+ test('Complicated Ordering', async () => {
+ // refer to
+ // https://github.com/Polymer/polymer-bundler/tree/master/test/html/complicated/ordering.svg
+ // for visual reference on the document structure for this example
+ const {ast: doc} =
+ await bundle('complicated/A.html', {inlineScripts: true});
+ assert(doc);
+ const expected = ['A1', 'C', 'E', 'B', 'D', 'A2'];
+ const scripts = dom5.queryAll(doc, matchers.nonModuleScript);
+ const contents = scripts.map(function(s) {
+ return dom5.getTextContent(s).trim();
+ });
+ assert.deepEqual(contents, expected);
+ });
+
+ test('Assetpath rewriting', async () => {
+ const {ast: doc} = await bundle(
+ 'path-rewriting/src/app-main/app-main.html',
+ {analyzer: new Analyzer({urlLoader: new FsUrlLoader()})});
+ assert(doc);
+ const domModules = dom5.queryAll(doc, preds.hasTagName('dom-module'));
+ const assetpaths = domModules.map(
+ (domModule) =>
+ [dom5.getAttribute(domModule, 'id'),
+ dom5.getAttribute(domModule, 'assetpath')]);
+ assert.deepEqual(assetpaths, [
+ ['test-c', '../../bower_components/test-component/'],
+ ['test-b', '../../bower_components/test-component/src/elements/'],
+ ['test-a', '../../bower_components/test-component/'],
+
+ // We don't need an assetpath on app-main because its not been
+ // moved/inlined from another location.
+ ['app-main', null]
+ ]);
+ });
+
+ test('Bundler should not emit empty hidden divs', async () => {
+ const {ast: doc} = await bundle('import-empty.html');
+ assert(doc);
+ assert.isNull(dom5.query(doc, matchers.hiddenDiv));
+ });
+
+ test('Entrypoint body content should not be wrapped', async () => {
+ const {ast: doc} = await bundle('default.html');
+ assert(doc);
+ const myElement = dom5.query(doc, preds.hasTagName('my-element'));
+ assert(myElement);
+ assert(preds.NOT(preds.parentMatches(
+ preds.hasAttr('by-polymer-bundler')))(myElement));
+ });
+
+ test('eagerly importing a fragment', async () => {
+ const bundler = getBundler({
+ strategy: generateShellMergeStrategy(
+ resolve('imports/importing-fragments/shell.html')!),
+ });
+ const manifest = await bundler.generateManifest([
+ 'imports/eagerly-importing-a-fragment.html',
+ 'imports/importing-fragments/fragment-a.html',
+ 'imports/importing-fragments/fragment-b.html',
+ 'imports/importing-fragments/shell.html',
+ ].map(resolve));
+ const result = await bundler.bundle(manifest);
+ assert.equal(result.manifest.bundles.size, 4);
+ const shell = parse5.serialize(
+ result.documents
+ .get(resolve('imports/importing-fragments/shell.html'))!.ast);
+ const fragmentAAt = shell.indexOf('rel="import" href="fragment-a.html"');
+ const shellAt = shell.indexOf(`console.log('shell.html')`);
+ const sharedUtilAt = shell.indexOf(`console.log('shared-util.html')`);
+ assert.isTrue(
+ sharedUtilAt < fragmentAAt,
+ 'Inlined shared-util.html should come before fragment-a.html import');
+ assert.isTrue(
+ fragmentAAt < shellAt,
+ 'fragment-a.html import should come before script in shell.html');
+ });
+
+ test('Imports in templates should not inline', async () => {
+ const {ast: doc} = await bundle('inside-template.html');
+ const importMatcher = preds.AND(
+ preds.hasTagName('link'),
+ preds.hasAttrValue('rel', 'import'),
+ preds.hasAttr('href'));
+ const externalScriptMatcher = preds.AND(
+ preds.hasTagName('script'),
+ preds.hasAttrValue('src', 'external/external.js'));
+ assert(doc);
+ const imports = dom5.queryAll(
+ doc, importMatcher, undefined, dom5.childNodesIncludeTemplate);
+ assert.equal(imports.length, 1, 'import in template was inlined');
+ const unexpectedScript = dom5.query(doc, externalScriptMatcher);
+ assert.equal(
+ unexpectedScript,
+ null,
+ 'script in external.html should not be present');
+ });
+
+ test('Deprecated CSS imports should inline correctly', async () => {
+ const {ast: doc} = await bundle('css-imports.html', {inlineCss: true});
+
+ const styleA =
+ fs.readFileSync('test/html/css-import/import-a.css', 'utf-8');
+ const styleB =
+ fs.readFileSync('test/html/css-import/import-b.css', 'utf-8');
+
+ const elementA =
+ dom5.query(doc, dom5.predicates.hasAttrValue('id', 'element-a'))!;
+ assert(elementA, 'element-a not found.');
+ assert.include(parse5.serialize(elementA), styleA);
+ assert.notInclude(parse5.serialize(elementA), styleB);
+
+ const elementB =
+ dom5.query(doc, dom5.predicates.hasAttrValue('id', 'element-b'))!;
+ assert(elementB, 'element-b not found.');
+ assert.notInclude(parse5.serialize(elementB), styleA);
+ assert.include(parse5.serialize(elementB), styleB);
+ });
+ });
+});
diff --git a/packages/bundler/src/test/constants_test.ts b/packages/bundler/src/test/constants_test.ts
new file mode 100644
index 000000000..e545aedf0
--- /dev/null
+++ b/packages/bundler/src/test/constants_test.ts
@@ -0,0 +1,70 @@
+/**
+ * @license
+ * Copyright (c) 2014 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 chai from 'chai';
+import constants from '../constants';
+
+const assert = chai.assert;
+
+suite('Constants', () => {
+
+ suite('URLs', () => {
+
+ test('absolute urls', () => {
+ const abs = constants.ABS_URL;
+
+ assert(abs.test('data:charset=utf8,'), 'data urls');
+ assert(abs.test('http://foo.com'), 'http');
+ assert(abs.test('https://foo.com'), 'https');
+ assert(abs.test('mailto:foo@bar.com'), 'mailto');
+ assert(abs.test('tel:+49123123456'), 'phonecall');
+ // jshint -W107
+ assert(abs.test('javascript:;'), 'javascript');
+ // jshint +W107
+ assert(abs.test('sms:1-123-123456'), 'sms');
+ assert(abs.test('chrome-search:'), 'chrome search');
+ assert(abs.test('about:blank'), 'about');
+ assert(abs.test('wss://'), 'web socket');
+ assert(abs.test('b2:'), 'custom protocol');
+ assert(abs.test('//foo.com'), 'protocol-free');
+ assert(abs.test('/components/'), '/');
+ assert(abs.test('#foo'), 'hash url');
+ assert(!abs.test('../foo/bar.html'), '../');
+ assert(!abs.test('bar.html'), 'sibling dependency');
+ });
+
+ test('CSS URLs', () => {
+ const url = constants.URL;
+
+ assert('url(foo.html)'.match(url), 'naked');
+ assert('url(\'foo.html\')'.match(url), 'single quote');
+ assert('url("foo.html")'.match(url), 'double quote');
+ });
+
+ test('Template URLs', () => {
+ const tmpl = constants.URL_TEMPLATE;
+
+ assert('foo{{bar}}'.match(tmpl), 'curly postfix');
+ assert('{{foo}}bar'.match(tmpl), 'curly prefix');
+ assert('foo{{bar}}baz'.match(tmpl), 'curly infix');
+ assert('{{}}'.match(tmpl), 'empty curly');
+ assert('foo[[bar]]'.match(tmpl), 'square postfix');
+ assert('[[foo]]bar'.match(tmpl), 'square prefix');
+ assert('foo[[bar]]baz'.match(tmpl), 'square infix');
+ assert('[[]]'.match(tmpl), 'empty square');
+ });
+ });
+});
diff --git a/packages/bundler/src/test/deps-index_test.ts b/packages/bundler/src/test/deps-index_test.ts
new file mode 100644
index 000000000..b7d437e6c
--- /dev/null
+++ b/packages/bundler/src/test/deps-index_test.ts
@@ -0,0 +1,225 @@
+
+/**
+ * @license
+ * Copyright (c) 2014 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 chai from 'chai';
+import {Analyzer, FsUrlLoader, FsUrlResolver, PackageRelativeUrl, ResolvedUrl} from 'polymer-analyzer';
+
+import {buildDepsIndex} from '../deps-index';
+import {inMemoryAnalyzer} from './test-utils';
+
+chai.config.showDiff = true;
+
+suite('Bundler', () => {
+
+ function serializeMap(map: Map>): string {
+ let s = '';
+ for (const key of Array.from(map.keys()).sort()) {
+ const set = map.get(key)!;
+ s = s + `${key}:\n`;
+ for (const value of Array.from(set).sort()) {
+ s = s + ` - ${value}\n`;
+ }
+ }
+ return s;
+ }
+
+ suite('Deps index tests', () => {
+
+ let analyzer: Analyzer;
+ function resolve(url: string) {
+ return analyzer.resolveUrl(url as PackageRelativeUrl)! ||
+ url as ResolvedUrl;
+ }
+
+ test('with 3 endpoints', async () => {
+ analyzer = new Analyzer({
+ urlResolver:
+ new FsUrlResolver('test/html/shards/polymer_style_project'),
+ urlLoader: new FsUrlLoader('test/html/shards/polymer_style_project'),
+ });
+ const common = resolve('common.html');
+ const dep1 = resolve('dep1.html');
+ const dep2 = resolve('dep2.html');
+ const endpoint1 = resolve('endpoint1.html');
+ const endpoint2 = resolve('endpoint2.html');
+ const expectedEntrypointsToDeps = new Map([
+ [common, new Set([common])],
+ [endpoint1, new Set([common, dep1, endpoint1])],
+ [endpoint2, new Set([common, dep2, endpoint1, endpoint2, dep1])],
+ ]);
+ const index =
+ await buildDepsIndex([common, endpoint1, endpoint2], analyzer);
+ chai.assert.deepEqual(
+ serializeMap(index), serializeMap(expectedEntrypointsToDeps));
+ });
+
+ // Deps index currently treats lazy imports as eager imports.
+ test('with lazy imports', async () => {
+ analyzer = new Analyzer({
+ urlResolver: new FsUrlResolver('test/html/imports'),
+ urlLoader: new FsUrlLoader('test/html/imports')
+ });
+ const entrypoint = resolve('lazy-imports.html');
+ const lazyImport1 = resolve('lazy-imports/lazy-import-1.html');
+ const lazyImport2 = resolve('lazy-imports/lazy-import-2.html');
+ const lazyImport3 = resolve('lazy-imports/subfolder/lazy-import-3.html');
+ const shared1 = resolve('lazy-imports/shared-eager-import-1.html');
+ const shared2 = resolve('lazy-imports/shared-eager-import-2.html');
+ const shared3 =
+ resolve('lazy-imports/shared-eager-and-lazy-import-1.html');
+ const eager1 = resolve('lazy-imports/subfolder/eager-import-1.html');
+ const eager2 = resolve('lazy-imports/subfolder/eager-import-2.html');
+ const deeply1 = resolve('lazy-imports/deeply-lazy-import-1.html');
+ const deeply2 =
+ resolve('lazy-imports/deeply-lazy-imports-eager-import-1.html');
+ const expectedEntrypointsToDeps = new Map([
+ [entrypoint, new Set([entrypoint, shared3])],
+ [lazyImport1, new Set([lazyImport1, shared1, shared2])],
+ [
+ lazyImport2,
+ new Set([lazyImport2, shared1, shared2, shared3, eager1, eager2])
+ ],
+ [lazyImport3, new Set([lazyImport3])],
+ [shared3, new Set([shared3])],
+ [deeply1, new Set([deeply1, deeply2])],
+ ]);
+ const index = await buildDepsIndex([entrypoint], analyzer);
+ chai.assert.deepEqual(
+ serializeMap(index), serializeMap(expectedEntrypointsToDeps));
+ });
+
+ test('when an entrypoint imports an entrypoint', async () => {
+ analyzer = new Analyzer({
+ urlResolver: new FsUrlResolver('test/html/imports'),
+ urlLoader: new FsUrlLoader('test/html/imports')
+ });
+ const entrypoint = resolve('eagerly-importing-a-fragment.html');
+ const fragmentA = resolve('importing-fragments/fragment-a.html');
+ const fragmentB = resolve('importing-fragments/fragment-b.html');
+ const util = resolve('importing-fragments/shared-util.html');
+ const shell = resolve('importing-fragments/shell.html');
+ const expectedEntrypointsToDeps = new Map([
+ [entrypoint, new Set([entrypoint, shell])],
+ [fragmentA, new Set([fragmentA, util])],
+ [fragmentB, new Set([fragmentB, util])],
+ [shell, new Set([shell])],
+ ]);
+ const index = await buildDepsIndex(
+ [entrypoint, fragmentA, fragmentB, shell], analyzer);
+ chai.assert.deepEqual(
+ serializeMap(index), serializeMap(expectedEntrypointsToDeps));
+ });
+
+ suite('module imports', () => {
+
+ setup(() => {
+ analyzer = inMemoryAnalyzer({
+ 'multiple-external-modules.html': `
+
+
+
+ `,
+ 'multiple-inline-modules.html': `
+
+
+
+
+
+ `,
+ 'abc.js': `
+ import { upcase } from './upcase.js';
+ export const A = upcase('a');
+ export const B = upcase('b');
+ export const C = upcase('c');
+ `,
+ 'def.js': `
+ import { X, Y, Z } from './xyz.js';
+ const D = X + X;
+ const E = Y + Y;
+ const F = Z + Z;
+ export { D, E, F }
+ `,
+ 'omgz.js': `
+ import { upcase } from './upcase.js';
+ export const Z = upcase('omgz');
+ `,
+ 'upcase.js': `
+ export function upcase(str) {
+ return str.toUpperCase();
+ }
+ `,
+ 'xyz.js': `
+ import { upcase } from './upcase.js';
+ export const X = upcase('x');
+ export const Y = upcase('y');
+ export const Z = upcase('z');
+ `,
+ });
+ });
+
+ test('when external html script type module imports', async () => {
+ const entrypoint = resolve('multiple-external-modules.html');
+ const module1 = entrypoint + '>external#1>abc.js>es6-module';
+ const module2 = entrypoint + '>external#2>abc.js>es6-module';
+ const module3 = entrypoint + '>external#3>def.js>es6-module';
+ const abc = resolve('abc.js');
+ const def = resolve('def.js');
+ const xyz = resolve('xyz.js');
+ const upcase = resolve('upcase.js');
+ const index = await buildDepsIndex([entrypoint], analyzer);
+ const expectedEntrypointsToDeps = new Map([
+ [entrypoint, new Set([entrypoint])],
+ [module1, new Set([abc, upcase])],
+ [module2, new Set([abc, upcase])],
+ [module3, new Set([def, xyz, upcase])],
+ ]);
+ chai.assert.deepEqual(
+ serializeMap(index), serializeMap(expectedEntrypointsToDeps));
+ });
+
+ test('when inline html script type module imports', async () => {
+ const entrypoint = resolve('multiple-inline-modules.html');
+ const module1 = entrypoint + '>inline#1>es6-module';
+ const module2 = entrypoint + '>inline#2>es6-module';
+ const module3 = entrypoint + '>inline#3>es6-module';
+ const abc = resolve('abc.js');
+ const def = resolve('def.js');
+ const xyz = resolve('xyz.js');
+ const upcase = resolve('upcase.js');
+ const index = await buildDepsIndex([entrypoint], analyzer);
+ const expectedEntrypointsToDeps = new Map([
+ [entrypoint, new Set([entrypoint])],
+ [module1, new Set([abc, upcase])],
+ [module2, new Set([abc, xyz, upcase])],
+ [module3, new Set([def, xyz, upcase])],
+ ]);
+ chai.assert.deepEqual(
+ serializeMap(index), serializeMap(expectedEntrypointsToDeps));
+ });
+ });
+ });
+});
diff --git a/packages/bundler/src/test/es6-module-bundler_test.ts b/packages/bundler/src/test/es6-module-bundler_test.ts
new file mode 100644
index 000000000..b398b129c
--- /dev/null
+++ b/packages/bundler/src/test/es6-module-bundler_test.ts
@@ -0,0 +1,151 @@
+/**
+ * @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 {assert} from 'chai';
+
+import {Bundle, mergeBundles} from '../bundle-manifest';
+import {Bundler} from '../bundler';
+import {bundle} from '../es6-module-bundler';
+import {heredoc, inMemoryAnalyzer} from './test-utils';
+
+suite('Es6ModuleBundler', () => {
+
+ const analyzer = inMemoryAnalyzer({
+ 'multiple-inline-modules.html': `
+
+
+
+
+
+ `,
+ 'abc.js': `
+ import { upcase } from './upcase.js';
+ export const A = upcase('a');
+ export const B = upcase('b');
+ export const C = upcase('c');
+ `,
+ 'def.js': `
+ import { X, Y, Z } from './xyz.js';
+ const D = X + X;
+ const E = Y + Y;
+ const F = Z + Z;
+ export { D, E, F }
+ `,
+ 'omgz.js': `
+ import { upcase } from './upcase.js';
+ export const Z = upcase('omgz');
+ `,
+ 'upcase.js': `
+ export function upcase(str) {
+ return str.toUpperCase();
+ }
+ `,
+ 'xyz.js': `
+ import { upcase } from './upcase.js';
+ export const X = upcase('x');
+ export const Y = upcase('y');
+ export const Z = upcase('z');
+ `,
+ });
+
+ const abcUrl = analyzer.resolveUrl('abc.js')!;
+ const defUrl = analyzer.resolveUrl('def.js')!;
+ const omgzUrl = analyzer.resolveUrl('omgz.js')!;
+ const multipleInlineModulesUrl =
+ analyzer.resolveUrl('multiple-inline-modules.html')!;
+ const sharedBundleUrl = analyzer.resolveUrl('shared_bundle_1.js')!;
+ const xyzUrl = analyzer.resolveUrl('xyz.js')!;
+
+ test('inline modules', async () => {
+ const bundler = new Bundler({analyzer});
+ const manifest = await bundler.generateManifest([multipleInlineModulesUrl]);
+ const multipleInlineModulesBundle =
+ manifest.getBundleForFile(multipleInlineModulesUrl)!;
+ const sharedBundle = {
+ url: sharedBundleUrl,
+ bundle: manifest.bundles.get(sharedBundleUrl)!
+ };
+ assert.deepEqual(manifest.getBundleForFile(abcUrl)!, sharedBundle);
+ assert.deepEqual(
+ manifest.getBundleForFile(defUrl)!, multipleInlineModulesBundle);
+ const sharedBundleDocument =
+ await bundle(bundler, manifest, sharedBundleUrl);
+ assert.deepEqual(sharedBundleDocument.content, heredoc`
+ function upcase(str) {
+ return str.toUpperCase();
+ }
+
+ var upcase$1 = {
+ upcase: upcase
+ };
+ const A = upcase('a');
+ const B = upcase('b');
+ const C = upcase('c');
+ var abc = {
+ A: A,
+ B: B,
+ C: C
+ };
+ const X = upcase('x');
+ const Y = upcase('y');
+ const Z = upcase('z');
+ var xyz = {
+ X: X,
+ Y: Y,
+ Z: Z
+ };
+ export { abc as $abc, upcase$1 as $upcase, xyz as $xyz, A, B, C, upcase, X, Y, Z };`);
+ });
+
+ test('resolving name conflict in a shared bundle', async () => {
+ const bundler = new Bundler(
+ {analyzer, strategy: (bundles: Bundle[]) => [mergeBundles(bundles)]});
+ const manifest = await bundler.generateManifest([xyzUrl, omgzUrl]);
+ const sharedBundleDocument =
+ await bundle(bundler, manifest, sharedBundleUrl);
+ assert.deepEqual(sharedBundleDocument.content, heredoc`
+ function upcase(str) {
+ return str.toUpperCase();
+ }
+
+ var upcase$1 = {
+ upcase: upcase
+ };
+ const Z = upcase('omgz');
+ var omgz = {
+ Z: Z
+ };
+ const X = upcase('x');
+ const Y = upcase('y');
+ const Z$1 = upcase('z');
+ var xyz = {
+ X: X,
+ Y: Y,
+ Z: Z$1
+ };
+ export { omgz as $omgz, upcase$1 as $upcase, xyz as $xyz, Z, upcase, X, Y, Z$1 };`);
+ });
+});
diff --git a/packages/bundler/src/test/es6-module-bundling-scenarios_test.ts b/packages/bundler/src/test/es6-module-bundling-scenarios_test.ts
new file mode 100644
index 000000000..e9621fc31
--- /dev/null
+++ b/packages/bundler/src/test/es6-module-bundling-scenarios_test.ts
@@ -0,0 +1,271 @@
+/**
+ * @license
+ * Copyright (c) 2014 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 {assert} from 'chai';
+import {PackageRelativeUrl} from 'polymer-analyzer';
+
+import {generateShellMergeStrategy} from '../bundle-manifest';
+import {Bundler} from '../bundler';
+
+import {heredoc, inMemoryAnalyzer} from './test-utils';
+
+suite('Es6 Module Bundling', () => {
+ test('export from', async () => {
+ const analyzer = inMemoryAnalyzer({
+ 'a.js': `
+ export * from './b.js';
+ export const A = 'a';
+ `,
+ 'b.js': `
+ export * from './c.js';
+ export const B = 'b';
+ `,
+ 'c.js': `
+ export const C = 'c';
+ export {C as default};
+ `,
+ });
+ const aUrl = analyzer.resolveUrl('a.js')!;
+ const bundler = new Bundler({analyzer});
+ const {documents} =
+ await bundler.bundle(await bundler.generateManifest([aUrl]));
+ assert.deepEqual(documents.get(aUrl)!.content, heredoc`
+ const C = 'c';
+ var c = {
+ C: C,
+ default: C
+ };
+ const B = 'b';
+ var b = {
+ B: B,
+ C: C
+ };
+ const A = 'a';
+ var a = {
+ A: A,
+ B: B,
+ C: C
+ };
+ export { a as $a, b as $b, c as $c, C, B, A, C as C$1, B as B$1, C as C$2, C as $cDefault };`);
+ });
+
+ suite('rewriting import specifiers', () => {
+ const analyzer = inMemoryAnalyzer({
+ 'a.js': `
+ import bee from './b.js';
+ import * as b from './b.js';
+ import {honey} from './b.js';
+ import sea from './c.js';
+ import * as c from './c.js';
+ import {boat} from './c.js';
+ console.log(bee, b, honey);
+ console.log(sea, c, boat);
+ `,
+ 'b.js': `
+ import sea from './c.js';
+ export default bee = '🐝';
+ export const honey = '🍯';
+ export const beeSea = bee + sea;
+ `,
+ 'c.js': `
+ export default sea = '🌊';
+ export const boat = '⛵️';
+ `,
+ 'd.js': `
+ import {boat} from './c.js';
+ export default deer = '🦌';
+ export const deerBoat = deer + boat;
+ `,
+ });
+
+ const aUrl = analyzer.resolveUrl('a.js')!;
+ const bUrl = analyzer.resolveUrl('b.js')!;
+ const cUrl = analyzer.resolveUrl('c.js')!;
+ const dUrl = analyzer.resolveUrl('d.js')!;
+
+ test('non-shared bundles', async () => {
+ const bundler = new Bundler({analyzer});
+ const {documents} = await bundler.bundle(
+ await bundler.generateManifest([aUrl, bUrl, cUrl]));
+ assert.deepEqual(documents.get(aUrl)!.content, heredoc`
+ import { $b as bee, $bDefault as bee__default, honey } from './b.js';
+ import { $c as sea, $cDefault as sea__default, boat } from './c.js';
+ console.log(bee__default, bee, honey);
+ console.log(sea__default, sea, boat);`);
+ assert.deepEqual(documents.get(bUrl)!.content, heredoc`
+ import { $cDefault as sea } from './c.js';
+ var b = bee = '🐝';
+ const honey = '🍯';
+ const beeSea = bee + sea;
+ var b$1 = {
+ default: b,
+ honey: honey,
+ beeSea: beeSea
+ };
+ export { b$1 as $b, b as $bDefault, honey, beeSea };`);
+ assert.deepEqual(documents.get(cUrl)!.content, heredoc`
+ var c = sea = '🌊';
+ const boat = '⛵️';
+ var c$1 = {
+ default: c,
+ boat: boat
+ };
+ export { c$1 as $c, c as $cDefault, boat };`);
+ });
+
+ test('shared bundle', async () => {
+ const bundler = new Bundler({analyzer});
+ const {documents} =
+ await bundler.bundle(await bundler.generateManifest([bUrl, dUrl]));
+ assert.deepEqual(documents.get(bUrl)!.content, heredoc`
+ import { $cDefault as sea } from './shared_bundle_1.js';
+ var b = bee = '🐝';
+ const honey = '🍯';
+ const beeSea = bee + sea;
+ var b$1 = {
+ default: b,
+ honey: honey,
+ beeSea: beeSea
+ };
+ export { b$1 as $b, b as $bDefault, honey, beeSea };`);
+ assert.deepEqual(documents.get(dUrl)!.content, heredoc`
+ import { boat } from './shared_bundle_1.js';
+ var d = deer = '🦌';
+ const deerBoat = deer + boat;
+ var d$1 = {
+ default: d,
+ deerBoat: deerBoat
+ };
+ export { d$1 as $d, d as $dDefault, deerBoat };`);
+ });
+
+ test('shell bundle', async () => {
+ const bundler =
+ new Bundler({analyzer, strategy: generateShellMergeStrategy(bUrl)});
+ const {documents} =
+ await bundler.bundle(await bundler.generateManifest([aUrl, bUrl]));
+ assert.deepEqual(documents.get(aUrl)!.content, heredoc`
+ import { $b as bee, $bDefault as bee__default, honey, $c as sea, $cDefault as sea__default, boat } from './b.js';
+ console.log(bee__default, bee, honey);
+ console.log(sea__default, sea, boat);`);
+ assert.deepEqual(documents.get(bUrl)!.content, heredoc`
+ var sea$1 = sea = '🌊';
+ const boat = '⛵️';
+ var c = {
+ default: sea$1,
+ boat: boat
+ };
+ var b = bee = '🐝';
+ const honey = '🍯';
+ const beeSea = bee + sea$1;
+ var b$1 = {
+ default: b,
+ honey: honey,
+ beeSea: beeSea
+ };
+ export { b$1 as $b, c as $c, b as $bDefault, honey, beeSea, sea$1 as $cDefault, boat };`);
+ });
+ });
+
+ suite('dynamic imports', () => {
+ test('await expression', async () => {
+ const analyzer = inMemoryAnalyzer({
+ 'a.js': `
+ export async function go() {
+ const b = await import('./b.js');
+ console.log(b.bee);
+ }
+ `,
+ 'b.js': `
+ export const bee = '🐝';
+ `,
+ });
+ const aUrl = analyzer.urlResolver.resolve('a.js' as PackageRelativeUrl)!;
+ const bundler = new Bundler({analyzer});
+ const {documents} =
+ await bundler.bundle(await bundler.generateManifest([aUrl]));
+ assert.deepEqual(documents.get(aUrl)!.content, heredoc`
+ async function go() {
+ const b = await import('./b.js').then(({
+ $b
+ }) => $b);
+ console.log(b.bee);
+ }
+
+ var a = {
+ go: go
+ };
+ export { a as $a, go };`);
+ });
+
+ test('expression statement', async () => {
+ const analyzer = inMemoryAnalyzer({
+ 'a.js': `
+ import('./b.js').then((b) => console.log(b.bee));
+ `,
+ 'b.js': `
+ export const bee = '🐝';
+ `,
+ });
+ const aUrl = analyzer.resolveUrl('a.js')!;
+ const bundler = new Bundler({analyzer});
+ const {documents} =
+ await bundler.bundle(await bundler.generateManifest([aUrl]));
+ assert.deepEqual(documents.get(aUrl)!.content, heredoc`
+ import('./b.js').then(({
+ $b
+ }) => $b).then(b => console.log(b.bee));`);
+ });
+
+ test('updates external module script src', async () => {
+ const analyzer = inMemoryAnalyzer({
+ 'index.html': `
+
+ `,
+ 'a.js': `
+ import {b} from './b.js';
+ export const a = 'a' + b;
+ `,
+ 'b.js': `
+ export const b = 'b';
+ `,
+ });
+ const indexUrl = analyzer.resolveUrl('index.html')!;
+ const sharedBundleUrl = analyzer.resolveUrl('shared_bundle_1.js')!;
+ const bundler = new Bundler({analyzer, inlineScripts: false});
+ const manifest = await bundler.generateManifest([indexUrl]);
+ assert.deepEqual(
+ [...manifest.bundles.keys()], [indexUrl, sharedBundleUrl]);
+ const {documents} =
+ await bundler.bundle(await bundler.generateManifest([indexUrl]));
+
+ assert.deepEqual(documents.get(indexUrl)!.content, heredoc`
+
+ `);
+
+ assert.deepEqual(documents.get(sharedBundleUrl)!.content, heredoc`
+ const b = 'b';
+ var b$1 = {
+ b: b
+ };
+ const a = 'a' + b;
+ var a$1 = {
+ a: a
+ };
+ export { a$1 as $a, b$1 as $b, a, b };`);
+ });
+ });
+});
diff --git a/packages/bundler/src/test/html-bundler_test.ts b/packages/bundler/src/test/html-bundler_test.ts
new file mode 100644
index 000000000..077bf52ed
--- /dev/null
+++ b/packages/bundler/src/test/html-bundler_test.ts
@@ -0,0 +1,362 @@
+/**
+ * @license
+ * Copyright (c) 2014 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 chai from 'chai';
+import * as parse5 from 'parse5';
+
+import {AssignedBundle, BundleManifest} from '../bundle-manifest';
+import {Bundler} from '../bundler';
+import {bundle, HtmlBundler} from '../html-bundler';
+
+import {parse} from '../parse5-utils';
+import {getFileUrl} from '../url-utils';
+import {heredoc, inMemoryAnalyzer} from './test-utils';
+
+chai.config.showDiff = true;
+
+const assert = chai.assert;
+const stripSpace = (html: string): string =>
+ html.replace(/>\s+/g, '>').replace(/>/g, '>\n').trim();
+
+suite('HtmlBundler', () => {
+
+ test('inline es6 modules', async () => {
+ const analyzer = inMemoryAnalyzer({
+ 'multiple-inline-modules.html': `
+
+
+
+
+
+ `,
+ 'abc.js': `
+ import{upcase} from './upcase.js';
+ export const A = upcase('a');
+ export const B = upcase('b');
+ export const C = upcase('c');
+ `,
+ 'def.js': `
+ import{X, Y, Z} from './xyz.js';
+ const D = X + X;
+ const E = Y + Y;
+ const F = Z + Z;
+ export { D, E, F };
+ `,
+ 'omgz.js': `
+ import {upcase} from './upcase.js';
+ export const Z = upcase('omgz');
+ `,
+ 'upcase.js': `
+ export function upcase(str) {
+ return str.toUpperCase();
+ }
+ `,
+ 'xyz.js': `
+ import{upcase} from './upcase.js';
+ export const X = upcase('x');
+ export const Y = upcase('y');
+ export const Z = upcase('z');
+ `,
+ });
+ const bundler = new Bundler({analyzer});
+ const multipleInlineBundlesUrl =
+ analyzer.resolveUrl('multiple-inline-modules.html')!;
+ const manifest = await bundler.generateManifest([multipleInlineBundlesUrl]);
+ const multipleInlineBundlesBundleDocument =
+ await bundle(bundler, manifest, multipleInlineBundlesUrl);
+ assert.deepEqual(multipleInlineBundlesBundleDocument.content, heredoc`
+
+
+
+
+
+ `);
+ });
+
+ suite('unit tests of private rewriting methods', () => {
+ const importDocUrl = getFileUrl('foo/bar/my-element/index.html');
+ const mainDocUrl = getFileUrl('foo/bar/index.html');
+
+ let bundler: Bundler;
+ let htmlBundler: HtmlBundler;
+ let manifest: BundleManifest;
+ let bundle: AssignedBundle;
+
+ beforeEach(async () => {
+ bundler = new Bundler();
+ await bundler.analyzeContents(mainDocUrl, '', true);
+ manifest = await bundler.generateManifest([mainDocUrl]);
+ bundle = manifest.getBundleForFile(mainDocUrl)!;
+ htmlBundler = new HtmlBundler(bundler, bundle, manifest);
+ });
+
+ suite('Path rewriting', async () => {
+
+ test('Rewrite URLs', async () => {
+
+ const css = `
+ x-element {
+ background-image: url(foo.jpg);
+ }
+ x-bar {
+ background-image: url(data:xxxxx);
+ }
+ x-quuz {
+ background-image: url(\'https://foo.bar/baz.jpg\');
+ }
+ `;
+
+ const expected = `
+ x-element {
+ background-image: url("my-element/foo.jpg");
+ }
+ x-bar {
+ background-image: url("data:xxxxx");
+ }
+ x-quuz {
+ background-image: url("https://foo.bar/baz.jpg");
+ }
+ `;
+
+ const actual = htmlBundler['_rewriteCssTextBaseUrl'](
+ css, importDocUrl, mainDocUrl);
+ assert.deepEqual(actual, expected);
+ });
+
+ suite('Resolve Paths', () => {
+
+ test('excluding template elements', () => {
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const expected = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const ast = parse(html);
+ bundler.rewriteUrlsInTemplates = false;
+ htmlBundler['_rewriteAstBaseUrl'](ast, importDocUrl, mainDocUrl);
+
+ const actual = parse5.serialize(ast);
+ assert.deepEqual(
+ stripSpace(actual), stripSpace(expected), 'relative');
+ });
+
+ test('inside template elements (rewriteUrlsInTemplates=true)', () => {
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const expected = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const ast = parse(html);
+ bundler.rewriteUrlsInTemplates = true;
+ htmlBundler['_rewriteAstBaseUrl'](ast, importDocUrl, mainDocUrl);
+
+ const actual = parse5.serialize(ast);
+ assert.deepEqual(
+ stripSpace(actual), stripSpace(expected), 'relative');
+ });
+ });
+
+ test('Leave Templated URLs', () => {
+ const base = `
+
+
+ `;
+
+ const ast = parse(base);
+ htmlBundler['_rewriteAstBaseUrl'](ast, importDocUrl, mainDocUrl);
+
+ const actual = parse5.serialize(ast);
+ assert.deepEqual(
+ stripSpace(actual), stripSpace(base), 'templated urls');
+ });
+ });
+
+ suite('Document tag emulation', () => {
+
+ test('Resolve Paths with having a trailing /', () => {
+ const htmlBase = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ const expectedBase = `
+
+
+
+
+
+
+
+
+ `;
+
+ const ast = parse(htmlBase);
+ htmlBundler['_rewriteAstToEmulateBaseTag'](
+ ast, getFileUrl('the/doc/url'));
+
+ const actual = parse5.serialize(ast);
+ assert.deepEqual(stripSpace(actual), stripSpace(expectedBase), 'base');
+ });
+
+ // Old vulcanize did the wrong thing with base href that had no trailing
+ // slash, so this proves the behavior of bundler is correct in this case.
+ test('Resolve Paths with with no trailing slash', () => {
+ const htmlBase = `
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const expectedBase = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ const ast = parse(htmlBase);
+ htmlBundler['_rewriteAstToEmulateBaseTag'](
+ ast, getFileUrl('the/doc/url'));
+
+ const actual = parse5.serialize(ast);
+ assert.deepEqual(stripSpace(actual), stripSpace(expectedBase), 'base');
+ });
+
+ test('Apply to all links and forms without target', () => {
+ const htmlBase = `
+
+ LINK
+ OTHERLINK
+
+
+
+ `;
+
+ const ast = parse(htmlBase);
+ htmlBundler['_rewriteAstToEmulateBaseTag'](
+ ast, getFileUrl('the/doc/url'));
+
+ const actual = parse5.serialize(ast);
+ assert.deepEqual(
+ stripSpace(actual), stripSpace(expectedBase), 'base target');
+ });
+ });
+ });
+});
diff --git a/packages/bundler/src/test/parse5-utils_test.ts b/packages/bundler/src/test/parse5-utils_test.ts
new file mode 100644
index 000000000..029d9b6c0
--- /dev/null
+++ b/packages/bundler/src/test/parse5-utils_test.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright (c) 2014 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 chai from 'chai';
+import * as clone from 'clone';
+import * as dom5 from 'dom5';
+import * as parse5 from 'parse5';
+
+import * as ast from '../parse5-utils';
+
+
+const assert = chai.assert;
+
+suite('AST Utils', function() {
+
+ test('inSourceOrder', () => {
+ const html = parse5.parseFragment(`
+ ohhi
+ good
+ bye
+ `, {locationInfo: true});
+ const spans = dom5.queryAll(html, dom5.predicates.hasTagName('span'));
+ assert.isTrue(ast.inSourceOrder(spans[0], spans[1]), 'oh -> hi');
+ assert.isTrue(ast.inSourceOrder(spans[0], spans[3]), 'oh -> bye');
+ assert.isTrue(ast.inSourceOrder(spans[2], spans[3]), 'good -> bye');
+ assert.isFalse(ast.inSourceOrder(spans[3], spans[1]), 'bye <- hi');
+ assert.isFalse(ast.inSourceOrder(spans[1], spans[0]), 'hi <- oh');
+ });
+
+ test('isSameNode', () => {
+ const html = parse5.parseFragment(
+ `
+
+
diff --git a/packages/bundler/test/html/external-in-template.html b/packages/bundler/test/html/external-in-template.html
new file mode 100644
index 000000000..10e8ade96
--- /dev/null
+++ b/packages/bundler/test/html/external-in-template.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+ External Resources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/bundler/test/html/external.html b/packages/bundler/test/html/external.html
new file mode 100644
index 000000000..49711ce52
--- /dev/null
+++ b/packages/bundler/test/html/external.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ External Resources
+
+
+
+
+
+
+
diff --git a/packages/bundler/test/html/external/external.css b/packages/bundler/test/html/external/external.css
new file mode 100644
index 000000000..b638ee69c
--- /dev/null
+++ b/packages/bundler/test/html/external/external.css
@@ -0,0 +1,12 @@
+/*
+ @license
+ Copyright (c) 2016 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
+*/
+body {
+ content: 'external';
+}
diff --git a/packages/bundler/test/html/external/external.js b/packages/bundler/test/html/external/external.js
new file mode 100644
index 000000000..63d9fa8bf
--- /dev/null
+++ b/packages/bundler/test/html/external/external.js
@@ -0,0 +1,10 @@
+/**
+ @license
+ Copyright (c) 2016 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
+*/
+var external = true;
diff --git a/packages/bundler/test/html/firebase.html b/packages/bundler/test/html/firebase.html
new file mode 100644
index 000000000..c841a6b36
--- /dev/null
+++ b/packages/bundler/test/html/firebase.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ Firebase
+
+
+
+
+
diff --git a/packages/bundler/test/html/import-empty.html b/packages/bundler/test/html/import-empty.html
new file mode 100644
index 000000000..251af9ae8
--- /dev/null
+++ b/packages/bundler/test/html/import-empty.html
@@ -0,0 +1,3 @@
+
+
+
Nothing special
diff --git a/packages/bundler/test/html/import-in-body.html b/packages/bundler/test/html/import-in-body.html
new file mode 100644
index 000000000..fc1ca239f
--- /dev/null
+++ b/packages/bundler/test/html/import-in-body.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/bundler/test/html/imports/another-simple-import.html b/packages/bundler/test/html/imports/another-simple-import.html
new file mode 100644
index 000000000..b6d77a3e7
--- /dev/null
+++ b/packages/bundler/test/html/imports/another-simple-import.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ o hai
+
+
+
diff --git a/packages/bundler/test/html/imports/base-foo/sub-base.html b/packages/bundler/test/html/imports/base-foo/sub-base.html
new file mode 100644
index 000000000..aee4976df
--- /dev/null
+++ b/packages/bundler/test/html/imports/base-foo/sub-base.html
@@ -0,0 +1,4 @@
+
+
+From Sub-Base!
+DO NOT CLICK ME
diff --git a/packages/bundler/test/html/imports/base.html b/packages/bundler/test/html/imports/base.html
new file mode 100644
index 000000000..9a152e7c8
--- /dev/null
+++ b/packages/bundler/test/html/imports/base.html
@@ -0,0 +1,3 @@
+
+
+CLICK ME
diff --git a/packages/bundler/test/html/imports/body-import.html b/packages/bundler/test/html/imports/body-import.html
new file mode 100644
index 000000000..a3e21ccae
--- /dev/null
+++ b/packages/bundler/test/html/imports/body-import.html
@@ -0,0 +1,16 @@
+
+
+
+