diff --git a/src/build/bundle.ts b/src/build/bundle.ts index b79de06e..69b24aaf 100644 --- a/src/build/bundle.ts +++ b/src/build/bundle.ts @@ -8,6 +8,7 @@ * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ +import * as dom5 from 'dom5'; import * as gulpif from 'gulp-if'; import * as path from 'path'; import {Transform} from 'stream'; @@ -129,17 +130,30 @@ export class Bundler { } } - let sharedDeps = bundles.get(this.sharedBundleUrl); - let excludes = sharedDeps ? sharedDeps.concat(this.sharedBundleUrl) : []; + let sharedDepsBundle = this.shell || this.sharedBundleUrl; + let sharedDeps = bundles.get(sharedDepsBundle); let promises = []; + if (this.shell) { + let shellFile = this.streamResolver._files.get(this.shell); + console.assert(shellFile != null); + let newShellContent = this._addSharedImportsToShell(bundles); + shellFile.contents = new Buffer(newShellContent); + } + for (let entrypoint of this.allEntrypoints) { - let relativeSharedImportUrl = path.relative(entrypoint, this.sharedBundlePath); + let addedImports = (entrypoint == this.shell || !this.shell) + ? [] + : [path.relative(path.dirname(entrypoint), sharedDepsBundle)] + let excludes = (entrypoint == this.shell) + ? [] + : sharedDeps.concat(sharedDepsBundle); + promises.push(new Promise((resolve, reject) => { var vulcanize = new Vulcanize({ abspath: null, fsResolver: this.streamResolver, - addedImports: [relativeSharedImportUrl], + addedImports: addedImports, stripExcludes: excludes, inlineScripts: true, inlineCss: true, @@ -161,7 +175,7 @@ export class Bundler { })); } // vulcanize the shared bundle - if (sharedDeps) { + if (!this.shell && sharedDeps) { promises.push(this._generateSharedBundle(sharedDeps)); } @@ -176,6 +190,42 @@ export class Bundler { }); } + _addSharedImportsToShell(bundles: Map): string { + console.assert(this.shell != null); + let shellDeps = bundles.get(this.shell) + .map((d) => path.relative(path.dirname(this.shell), d)); + + let file = this.streamResolver._files.get(this.shell); + console.assert(file != null); + let contents = file.contents.toString(); + let doc = dom5.parse(contents); + let imports = dom5.queryAll(doc, dom5.predicates.AND( + dom5.predicates.hasTagName('link'), + dom5.predicates.hasAttrValue('rel', 'import') + )); + + // Remove all imports that are in the shared deps list so that we prefer + // the ordering or shared deps. Any imports left should be independent of + // ordering of shared deps. + let shellDepsSet = new Set(shellDeps); + for (let _import of imports) { + if (shellDepsSet.has(dom5.getAttribute(_import, 'href'))) { + dom5.remove(_import); + } + } + + // Append all shared imports to the end of + let head = dom5.query(doc, dom5.predicates.hasTagName('head')); + for (let dep of shellDeps) { + let newImport = dom5.constructors.element('link'); + dom5.setAttribute(newImport, 'rel', 'import'); + dom5.setAttribute(newImport, 'href', dep); + dom5.append(head, newImport); + } + let newContents = dom5.serialize(doc); + return newContents; + } + _generateSharedBundle(sharedDeps: string[]): Promise { return new Promise((resolve, reject) => { let contents = sharedDeps @@ -253,8 +303,13 @@ export class Bundler { for (let dep of dependencies) { let entrypointCount = depsToEntrypoints.get(dep).length; if (entrypointCount > 1) { - addImport(this.sharedBundleUrl, dep); - addImport(entrypoint, this.sharedBundleUrl); + if (this.shell) { + addImport(this.shell, dep); + // addImport(entrypoint, this.shell); + } else { + addImport(this.sharedBundleUrl, dep); + addImport(entrypoint, this.sharedBundleUrl); + } } else { addImport(entrypoint, dep); } diff --git a/test/build/bundle_test.js b/test/build/bundle_test.js index 515ce5ac..9c9b076f 100644 --- a/test/build/bundle_test.js +++ b/test/build/bundle_test.js @@ -90,27 +90,73 @@ suite('Bundler', () => { assert.isFalse(hasImport(doc, '/root/framework.html')); })); - test('shell and 1 entrypoint', () => setupTest({ - shell: '/root/shell.html', - entrypoints: ['/root/entrypointA.html'], + test('two entrypoints', () => setupTest({ + entrypoints: ['/root/shell.html', '/root/entrypointA.html'], files: [framework(), shell(), entrypointA()], }).then((files) => { - // shell doesn't have framework + // shell doesn't import framework let shellDoc = dom5.parse(getFile('shell.html')); assert.isFalse(hasMarker(shellDoc, 'framework')); assert.isFalse(hasImport(shellDoc, '/root/framework.html')); - // entrypoint doesn't have framework + // entrypoint doesn't import framework let entrypointDoc = dom5.parse(getFile('entrypointA.html')); assert.isFalse(hasMarker(entrypointDoc, 'framework')); assert.isFalse(hasImport(entrypointDoc, '/root/framework.html')); - // shared-bundle has framework + // No shared-bundle bundles framework let sharedDoc = dom5.parse(getFile('shared-bundle.html')); assert.isTrue(hasMarker(sharedDoc, 'framework')); assert.isFalse(hasImport(sharedDoc, '/root/framework.html')); })); + test('shell and entrypoint', () => setupTest({ + shell: '/root/shell.html', + entrypoints: ['/root/entrypointA.html'], + files: [framework(), shell(), entrypointA()], + }).then((files) => { + // shell bundles framework + let shellDoc = dom5.parse(getFile('shell.html')); + assert.isTrue(hasMarker(shellDoc, 'framework')); + assert.isFalse(hasImport(shellDoc, '/root/framework.html')); + + // entrypoint doesn't import framework + let entrypointDoc = dom5.parse(getFile('entrypointA.html')); + assert.isFalse(hasMarker(entrypointDoc, 'framework')); + assert.isFalse(hasImport(entrypointDoc, '/root/framework.html')); + + // No shared-bundle with a shell + assert.isNotOk(getFile('shared-bundle.html')); + })); + + test('shell and entrypoints with shared dependency', () => setupTest({ + shell: '/root/shell.html', + entrypoints: ['/root/entrypointB.html', '/root/entrypointC.html'], + files: [framework(), shell(), entrypointB(), entrypointC(), commonDep()], + }).then((files) => { + // shell bundles framework + let shellDoc = dom5.parse(getFile('shell.html')); + assert.isTrue(hasMarker(shellDoc, 'framework')); + assert.isFalse(hasImport(shellDoc, '/root/framework.html')); + + // shell bundles commonDep + assert.isTrue(hasMarker(shellDoc, 'commonDep')); + assert.isFalse(hasImport(shellDoc, '/root/commonDep.html')); + + // entrypoint B doesn't import commonDep + let entrypointBDoc = dom5.parse(getFile('entrypointB.html')); + assert.isFalse(hasMarker(entrypointBDoc, 'commonDep')); + assert.isFalse(hasImport(entrypointBDoc, '/root/commonDep.html')); + + // entrypoint C doesn't import commonDep + let entrypointCDoc = dom5.parse(getFile('entrypointC.html')); + assert.isFalse(hasMarker(entrypointCDoc, 'commonDep')); + assert.isFalse(hasImport(entrypointCDoc, '/root/commonDep.html')); + + // No shared-bundle with a shell + assert.isNotOk(getFile('shared-bundle.html')); + })); + }); const F = (filename, contents) => new File({ @@ -133,3 +179,17 @@ const entrypointA = () => F('entrypointA.html', `
`); + +const entrypointB = () => F('entrypointB.html', ` + +
+`); + +const entrypointC = () => F('entrypointC.html', ` + +
+`); + +const commonDep = () => F('commonDep.html', ` +
+`);