UNPKG

polymer-bundler

Version:
845 lines 41.9 kB
"use strict"; /** * @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 */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const clone = require("clone"); const dom5 = require("dom5"); const parse5_1 = require("parse5"); const polymer_analyzer_1 = require("polymer-analyzer"); const url_1 = require("url"); const analyzer_utils_1 = require("./analyzer-utils"); const babel_utils_1 = require("./babel-utils"); const constants_1 = require("./constants"); const es6_rewriter_1 = require("./es6-rewriter"); const matchers = require("./matchers"); const parse5_utils_1 = require("./parse5-utils"); const source_map_1 = require("./source-map"); const source_map_2 = require("./source-map"); const encode_string_1 = require("./third_party/UglifyJS2/encode-string"); const url_utils_1 = require("./url-utils"); const utils_1 = require("./utils"); /** * Produces an HTML BundledDocument. */ function bundle(bundler, manifest, url) { return __awaiter(this, void 0, void 0, function* () { 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(); }); } exports.bundle = 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. */ class HtmlBundler { constructor(bundler, assignedBundle, manifest) { this.bundler = bundler; this.assignedBundle = assignedBundle; this.manifest = manifest; } bundle() { return __awaiter(this, void 0, void 0, function* () { this.document = yield 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 = yield this._reanalyze(parse5_1.serialize(ast)); yield this._inlineHtmlImports(ast); yield this._updateExternalModuleScriptTags(ast); if (this.bundler.enableScriptInlining) { yield this._inlineNonModuleScripts(ast); yield this._rewriteExternalModuleScriptTagsAsImports(ast); yield this._rollupInlineModuleScripts(ast); } if (this.bundler.enableCssInlining) { yield this._inlineStylesheetLinks(ast); yield this._inlineStylesheetImports(ast); } if (this.bundler.stripComments) { parse5_utils_1.stripComments(ast); } this._removeEmptyHiddenDivs(ast); if (this.bundler.sourcemaps) { ast = source_map_2.updateSourcemapLocations(this.document, ast); } const content = parse5_1.serialize(ast); const files = [...this.assignedBundle.bundle.files]; return { language: 'html', 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. */ _addOrUpdateSourcemapsForInlineScripts(originalDoc, reparsedDoc, oldBaseUrl) { return __awaiter(this, void 0, void 0, function* () { const inlineScripts = dom5.queryAll(reparsedDoc.ast, matchers.inlineNonModuleScript); const promises = inlineScripts.map((scriptAst) => { const content = dom5.getTextContent(scriptAst); const sourceRange = reparsedDoc.sourceRangeForStartTag(scriptAst); return source_map_1.addOrUpdateSourcemapComment(this.bundler.analyzer, oldBaseUrl, content, sourceRange.end.line, sourceRange.end.column, -sourceRange.end.line + 1, -sourceRange.end.column) .then((updatedContent) => { dom5.setTextContent(scriptAst, encode_string_1.default(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 `<head>` 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. */ _attachHiddenDiv(ast, hiddenDiv) { const firstHtmlImport = dom5.query(ast, matchers.eagerHtmlImport); const body = dom5.query(ast, matchers.body); if (body) { if (firstHtmlImport && dom5.predicates.parentMatches(matchers.body)(firstHtmlImport)) { parse5_utils_1.insertAfter(firstHtmlImport, hiddenDiv); } else { parse5_utils_1.prepend(body, hiddenDiv); } } else { dom5.append(ast, hiddenDiv); } } /** * Creates a hidden container <div> to which inlined content will be * appended. */ _createHiddenDiv() { const hidden = dom5.constructors.element('div'); dom5.setAttribute(hidden, 'hidden', ''); dom5.setAttribute(hidden, 'by-polymer-bundler', ''); return hidden; } /** * Append a `<link rel="import" ...>` node to `node` with a value of `url` * for the "href" attribute. */ _createHtmlImport(url) { 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. */ _findOrCreateHiddenDiv(ast) { 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. */ _injectHtmlImportsForBundle(ast) { // 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 (url_utils_1.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) => existingImport.astNode !== undefined && existingImport.astNode.language === 'html' && parse5_utils_1.isSameNode(node, existingImport.astNode.node)); // 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 || parse5_utils_1.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. */ _inlineHtmlImport(linkTag) { return __awaiter(this, void 0, void 0, function* () { const isLazy = dom5.getAttribute(linkTag, 'rel').match(/lazy-import/i); const importHref = dom5.getAttribute(linkTag, 'href'); 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)) { parse5_utils_1.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(url_utils_1.ensureTrailingSlash(u)))) { this.assignedBundle.bundle.missingImports.add(resolvedImportUrl); } return; } // Don't inline an import into itself. if (this.assignedBundle.url === resolvedImportUrl) { parse5_utils_1.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) { parse5_utils_1.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 = utils_1.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 = parse5_1.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 polymer_analyzer_1.ParsedHtmlDocument({ url: this.assignedBundle.url, baseUrl: this.document.parsedDocument.baseUrl, contents: htmlImport.document.parsedDocument.contents, ast: importAst, isInline: false, locationOffset: undefined, astNode: undefined, }); yield this._addOrUpdateSourcemapsForInlineScripts(this.document, reparsedDoc, resolvedImportUrl); } const nestedImports = dom5.queryAll(importAst, matchers.htmlImport); // Move all of the import doc content after the html import. parse5_utils_1.insertAllBefore(linkTag.parentNode, linkTag, importAst.childNodes); parse5_utils_1.removeElementAndNewline(linkTag); // Record that the inlining took place. this.assignedBundle.bundle.inlinedHtmlImports.add(resolvedImportUrl); // Recursively process the nested imports. for (const nestedImport of nestedImports) { yield this._inlineHtmlImport(nestedImport); } }); } /** * Replace html import links in the document with the contents of the * imported file, but only once per URL. */ _inlineHtmlImports(ast) { return __awaiter(this, void 0, void 0, function* () { const htmlImports = dom5.queryAll(ast, matchers.htmlImport); for (const htmlImport of htmlImports) { yield this._inlineHtmlImport(htmlImport); } }); } /** * Update the `src` attribute of external `type=module` script tags to point * at new bundle locations. */ _updateExternalModuleScriptTags(ast) { return __awaiter(this, void 0, void 0, function* () { 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.document.parsedDocument.baseUrl, oldSrc); if (oldFileUrl === undefined) { continue; } const bundle = this.manifest.getBundleForFile(oldFileUrl); if (bundle === undefined) { continue; } // Do not rewrite the src if the current bundle is going to be the new // home of the code. if (bundle.url === this.assignedBundle.url) { 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. */ _rollupInlineModuleScripts(ast) { return __awaiter(this, void 0, void 0, function* () { this.document = yield this._reanalyze(parse5_1.serialize(ast)); utils_1.rewriteObject(ast, this.document.parsedDocument.ast); dom5.removeFakeRootElements(ast); const es6Rewriter = new es6_rewriter_1.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 ast = clone(inlineModuleScript.parsedDocument.ast); const importResolutions = es6Rewriter.getEs6ImportResolutions(inlineModuleScript); es6Rewriter.rewriteEs6SourceUrlsToResolved(ast, importResolutions); const serializedCode = babel_utils_1.serialize(ast).code; const { code } = yield es6Rewriter.rollup(this.document.parsedDocument.baseUrl, serializedCode, inlineModuleScript); if (inlineModuleScript.astNode && inlineModuleScript.astNode.language === 'html') { // Second argument 'true' tells encodeString to escape the <script> // content. dom5.setTextContent(inlineModuleScript.astNode.node, encode_string_1.default(`\n${code}\n`, true)); } } }); } /** * Replace all external module script tags: * `<script type="module" src="..."></script>` * with inline script tags containing import: * `<script type="module">import '...';</script>` * And these will be subsequently rolled up by call to * `this._rollupInlineModuleScripts()`. */ _rewriteExternalModuleScriptTagsAsImports(ast) { return __awaiter(this, void 0, void 0, function* () { for (const scriptTag of dom5.queryAll(ast, matchers.externalModuleScript)) { const scriptHref = dom5.getAttribute(scriptTag, 'src'); const resolvedImportUrl = this.bundler.analyzer.urlResolver.resolve(this.document.parsedDocument.baseUrl, scriptHref); if (resolvedImportUrl === undefined) { return; } // We won't inline a module script if its not supposed to be in this // bundle. if (!this.assignedBundle.bundle.files.has(resolvedImportUrl)) { return; } const scriptContent = `import ${JSON.stringify(resolvedImportUrl)};`; dom5.removeAttribute(scriptTag, 'src'); dom5.setTextContent(scriptTag, encode_string_1.default(scriptContent, true)); } }); } /** * Inlines the contents of the document returned by the script tag's src URL * into the script tag content and removes the src attribute. */ _inlineNonModuleScript(scriptTag) { return __awaiter(this, void 0, void 0, function* () { const scriptHref = dom5.getAttribute(scriptTag, 'src'); const resolvedImportUrl = this.bundler.analyzer.urlResolver.resolve(this.document.parsedDocument.baseUrl, scriptHref); if (resolvedImportUrl === undefined) { return; } if (this.bundler.excludes.some((e) => resolvedImportUrl === e || resolvedImportUrl.startsWith(url_utils_1.ensureTrailingSlash(e)))) { return; } const scriptImport = utils_1.find(this.document.getFeatures({ kind: 'html-script', imported: true, externalPackages: true }), (i) => i.document !== undefined && i.document.url === resolvedImportUrl); if (scriptImport === undefined || scriptImport.document === undefined) { this.assignedBundle.bundle.missingImports.add(resolvedImportUrl); return; } let scriptContent = scriptImport.document.parsedDocument.contents; if (this.bundler.sourcemaps) { // it's easier to calculate offsets if the external script contents // don't start on the same line as the script tag. Offset the map // appropriately. scriptContent = yield source_map_1.addOrUpdateSourcemapComment(this.bundler.analyzer, resolvedImportUrl, '\n' + scriptContent, -1, 0, 1, 0); } dom5.removeAttribute(scriptTag, 'src'); // Second argument 'true' tells encodeString to escape the <script> content. dom5.setTextContent(scriptTag, encode_string_1.default(scriptContent, true)); // Record that the inlining took place. this.assignedBundle.bundle.inlinedScripts.add(resolvedImportUrl); return scriptContent; }); } /** * Replace all external javascript tags (`<script src="...">`) * with `<script>` tags containing the file contents inlined. */ _inlineNonModuleScripts(ast) { return __awaiter(this, void 0, void 0, function* () { const scriptImports = dom5.queryAll(ast, matchers.externalNonModuleScript); for (const externalScript of scriptImports) { yield this._inlineNonModuleScript(externalScript); } }); } /** * Inlines the contents of the stylesheet returned by the link tag's href * URL into a style tag and removes the link tag. */ _inlineStylesheet(cssLink) { return __awaiter(this, void 0, void 0, function* () { const stylesheetHref = dom5.getAttribute(cssLink, 'href'); const resolvedImportUrl = this.bundler.analyzer.urlResolver.resolve(this.assignedBundle.url, stylesheetHref); if (resolvedImportUrl === undefined) { return; } if (this.bundler.excludes.some((e) => resolvedImportUrl === e || resolvedImportUrl.startsWith(url_utils_1.ensureTrailingSlash(e)))) { return; } const stylesheetImport = // HACK(usergenic): clang-format workaround utils_1.find(this.document.getFeatures({ kind: 'html-style', imported: true, externalPackages: true }), (i) => i.document !== undefined && i.document.url === resolvedImportUrl) || utils_1.find(this.document.getFeatures({ kind: 'css-import', imported: true, externalPackages: true }), (i) => i.document !== undefined && i.document.url === resolvedImportUrl); if (stylesheetImport === undefined || stylesheetImport.document === undefined) { this.assignedBundle.bundle.missingImports.add(resolvedImportUrl); return; } const stylesheetContent = stylesheetImport.document.parsedDocument.contents; const media = dom5.getAttribute(cssLink, 'media'); let newBaseUrl = this.assignedBundle.url; // If the css link we are about to inline is inside of a dom-module, the // new base URL must be calculated using the assetpath of the dom-module // if present, since Polymer will honor assetpath when resolving URLs in // `<style>` tags, even inside of `<template>` tags. const parentDomModule = parse5_utils_1.findAncestor(cssLink, dom5.predicates.hasTagName('dom-module')); if (!this.bundler.rewriteUrlsInTemplates && parentDomModule && dom5.hasAttribute(parentDomModule, 'assetpath')) { const assetPath = (dom5.getAttribute(parentDomModule, 'assetpath') || ''); if (assetPath) { newBaseUrl = this.bundler.analyzer.urlResolver.resolve(newBaseUrl, assetPath); } } const resolvedStylesheetContent = this._rewriteCssTextBaseUrl(stylesheetContent, resolvedImportUrl, newBaseUrl); const styleNode = dom5.constructors.element('style'); if (media) { dom5.setAttribute(styleNode, 'media', media); } dom5.replace(cssLink, styleNode); dom5.setTextContent(styleNode, resolvedStylesheetContent); // Record that the inlining took place. this.assignedBundle.bundle.inlinedStyles.add(resolvedImportUrl); return styleNode; }); } /** * Replace all polymer stylesheet imports (`<link rel="import" type="css">`) * with `<style>` tags containing the file contents, with internal URLs * relatively transposed as necessary. */ _inlineStylesheetImports(ast) { return __awaiter(this, void 0, void 0, function* () { const cssImports = dom5.queryAll(ast, matchers.stylesheetImport); let lastInlined; for (const cssLink of cssImports) { const style = yield this._inlineStylesheet(cssLink); if (style) { this._moveDomModuleStyleIntoTemplate(style, lastInlined); lastInlined = style; } } }); } /** * Replace all external stylesheet references, in `<link rel="stylesheet">` * tags with `<style>` tags containing file contents, with internal URLs * relatively transposed as necessary. */ _inlineStylesheetLinks(ast) { return __awaiter(this, void 0, void 0, function* () { const cssLinks = dom5.queryAll(ast, matchers.externalStyle, undefined, dom5.childNodesIncludeTemplate); for (const cssLink of cssLinks) { yield this._inlineStylesheet(cssLink); } }); } /** * Old Polymer supported `<style>` tag in `<dom-module>` but outside of * `<template>`. This is also where the deprecated Polymer CSS import tag * `<link rel="import" type="css">` would generate inline `<style>`. * Migrates these `<style>` tags into available `<template>` of the * `<dom-module>`. Will create a `<template>` container if not present. * * TODO(usergenic): Why is this in bundler... shouldn't this be some kind of * polyup or pre-bundle operation? */ _moveDomModuleStyleIntoTemplate(style, refStyle) { const domModule = dom5.nodeWalkAncestors(style, dom5.predicates.hasTagName('dom-module')); if (!domModule) { return; } let template = dom5.query(domModule, matchers.template); if (!template) { template = dom5.constructors.element('template'); parse5_1.treeAdapters.default.setTemplateContent(template, dom5.constructors.fragment()); parse5_utils_1.prepend(domModule, template); } parse5_utils_1.removeElementAndNewline(style); // Ignore the refStyle object if it is contained within a different // dom-module. if (refStyle && !dom5.query(domModule, (n) => n === refStyle, dom5.childNodesIncludeTemplate)) { refStyle = undefined; } // keep ordering if previding with a reference style if (!refStyle) { parse5_utils_1.prepend(parse5_1.treeAdapters.default.getTemplateContent(template), style); } else { parse5_utils_1.insertAfter(refStyle, style); } } /** * When an HTML Import is encountered in the head of the document, it needs * to be moved into the hidden div and any subsequent order-dependent * imperatives (imports, styles, scripts) must also be move into the * hidden div. */ _moveOrderedImperativesFromHeadIntoHiddenDiv(ast) { const head = dom5.query(ast, matchers.head); if (!head) { return; } const firstHtmlImport = dom5.query(head, matchers.eagerHtmlImport); if (!firstHtmlImport) { return; } for (const node of [firstHtmlImport].concat(parse5_utils_1.siblingsAfter(firstHtmlImport))) { if (matchers.orderedImperative(node)) { parse5_utils_1.removeElementAndNewline(node); dom5.append(this._findOrCreateHiddenDiv(ast), node); } } } /** * Move any remaining htmlImports that are not inside the hidden div * already, into the hidden div. */ _moveUnhiddenHtmlImportsIntoHiddenDiv(ast) { const unhiddenHtmlImports = dom5.queryAll(ast, dom5.predicates.AND(matchers.eagerHtmlImport, dom5.predicates.NOT(matchers.inHiddenDiv))); for (const htmlImport of unhiddenHtmlImports) { parse5_utils_1.removeElementAndNewline(htmlImport); dom5.append(this._findOrCreateHiddenDiv(ast), htmlImport); } } /** * Generate a fresh document to bundle contents into. If we're building * a bundle which is based on an existing file, we should load that file and * prepare it as the bundle document, otherwise we'll create a clean/empty * HTML document. */ _prepareBundleDocument() { return __awaiter(this, void 0, void 0, function* () { if (!this.assignedBundle.bundle.files.has(this.assignedBundle.url)) { return this._reanalyze(''); } const analysis = yield this.bundler.analyzer.analyze([this.assignedBundle.url]); const document = analyzer_utils_1.assertIsHtmlDocument(analyzer_utils_1.getAnalysisDocument(analysis, this.assignedBundle.url)); const ast = clone(document.parsedDocument.ast); this._moveOrderedImperativesFromHeadIntoHiddenDiv(ast); this._moveUnhiddenHtmlImportsIntoHiddenDiv(ast); dom5.removeFakeRootElements(ast); return this._reanalyze(parse5_1.serialize(ast)); }); } /** * Fetch a new copy of an analyzed document serializing an AST and analyzing * it. */ _reanalyze(code) { return __awaiter(this, void 0, void 0, function* () { return analyzer_utils_1.assertIsHtmlDocument(yield this.bundler.analyzeContents(this.assignedBundle.url, code)); }); } /** * Removes all empty hidden container divs from the AST. */ _removeEmptyHiddenDivs(ast) { for (const div of dom5.queryAll(ast, matchers.hiddenDiv)) { if (parse5_1.serialize(div).trim() === '') { dom5.remove(div); } } } /** * Walk through an import document, and rewrite all URLs so they are * correctly relative to the main document URL as they've been * imported from the import URL. */ _rewriteAstBaseUrl(ast, oldBaseUrl, newBaseUrl) { this._rewriteElementAttrsBaseUrl(ast, oldBaseUrl, newBaseUrl); this._rewriteStyleTagsBaseUrl(ast, oldBaseUrl, newBaseUrl); this._setDomModuleAssetpaths(ast, oldBaseUrl, newBaseUrl); } /** * Given an import document with a base tag, transform all of its URLs and * set link and form target attributes and remove the base tag. */ _rewriteAstToEmulateBaseTag(ast, docUrl) { const baseTag = dom5.query(ast, matchers.base); const p = dom5.predicates; // If there's no base tag, there's nothing to do. if (!baseTag) { return; } for (const baseTag of dom5.queryAll(ast, matchers.base)) { parse5_utils_1.removeElementAndNewline(baseTag); } if (dom5.predicates.hasAttr('href')(baseTag)) { const baseUrl = this.bundler.analyzer.urlResolver.resolve(docUrl, dom5.getAttribute(baseTag, 'href')); if (baseUrl) { this._rewriteAstBaseUrl(ast, baseUrl, docUrl); } } if (p.hasAttr('target')(baseTag)) { const baseTarget = dom5.getAttribute(baseTag, 'target'); const tagsToTarget = dom5.queryAll(ast, p.AND(p.OR(p.hasTagName('a'), p.hasTagName('form')), p.NOT(p.hasAttr('target')))); for (const tag of tagsToTarget) { dom5.setAttribute(tag, 'target', baseTarget); } } } /** * Given a string of CSS, return a version where all occurrences of URLs, * have been rewritten based on the relationship of the old base URL to the * new base URL. */ _rewriteCssTextBaseUrl(cssText, oldBaseUrl, newBaseUrl) { return cssText.replace(constants_1.default.URL, (match) => { let path = match.replace(/["']/g, '').slice(4, -1); path = this._rewriteHrefBaseUrl(path, oldBaseUrl, newBaseUrl); return 'url("' + path + '")'; }); } /** * Find all element attributes which express URLs and rewrite them so they * are based on the relationship of the old base URL to the new base URL. */ _rewriteElementAttrsBaseUrl(ast, oldBaseUrl, newBaseUrl) { const nodes = dom5.queryAll(ast, matchers.elementsWithUrlAttrsToRewrite, undefined, this.bundler.rewriteUrlsInTemplates ? dom5.childNodesIncludeTemplate : dom5.defaultChildNodes); for (const node of nodes) { for (const attr of constants_1.default.URL_ATTR) { const attrValue = dom5.getAttribute(node, attr); if (attrValue && !url_utils_1.isTemplatedUrl(attrValue)) { let relUrl; if (attr === 'style') { relUrl = this._rewriteCssTextBaseUrl(attrValue, oldBaseUrl, newBaseUrl); } else { relUrl = this._rewriteHrefBaseUrl(attrValue, oldBaseUrl, newBaseUrl); } dom5.setAttribute(node, attr, relUrl); } } } } _rewriteHrefBaseUrl(href, oldBaseUrl, newBaseUrl) { const resolvedHref = this.bundler.analyzer.urlResolver.resolve(oldBaseUrl, href); // If we can't resolve the href, we need to return it as-is, since we can't // relativize or rewrite it. if (typeof resolvedHref === 'undefined') { return href; } const parsedHref = url_1.parse(href); // If the href was initially expressed with a protocol in the URL, we should // not attempt to relativize or rewrite it. if (typeof parsedHref.protocol === 'string') { return href; } // If the href was originally expressed as an absolute path, we should not // attempt to relativize it. if (parsedHref.pathname && url_utils_1.isAbsolutePath(parsedHref.pathname)) { return href; } // Return a new relative form of the given URL. return this.bundler.analyzer.urlResolver.relative(newBaseUrl, resolvedHref); } /** * Find all URLs in imported style nodes and rewrite them so they are based * on the relationship of the old base URL to the new base URL. */ _rewriteStyleTagsBaseUrl(ast, oldBaseUrl, newBaseUrl) { const childNodesOption = this.bundler.rewriteUrlsInTemplates ? dom5.childNodesIncludeTemplate : dom5.defaultChildNodes; // If `rewriteUrlsInTemplates` is `true`, include `<style>` tags that are // inside `<template>`. const styleNodes = dom5.queryAll(ast, matchers.styleMatcher, undefined, childNodesOption); // Unless rewriteUrlsInTemplates is on, if a `<style>` tag is anywhere // inside a `<dom-module>` tag, then it should not have its URLs // rewritten. if (!this.bundler.rewriteUrlsInTemplates) { for (const domModule of dom5.queryAll(ast, dom5.predicates.hasTagName('dom-module'))) { for (const styleNode of dom5.queryAll(domModule, matchers.styleMatcher, undefined, childNodesOption)) { const styleNodeIndex = styleNodes.indexOf(styleNode); if (styleNodeIndex > -1) { styleNodes.splice(styleNodeIndex, 1); } } } } for (const node of styleNodes) { let styleText = dom5.getTextContent(node); styleText = this._rewriteCssTextBaseUrl(styleText, oldBaseUrl, newBaseUrl); dom5.setTextContent(node, styleText); } } /** * Set the assetpath attribute of all imported dom-modules which don't yet * have them if the base URLs are different. */ _setDomModuleAssetpaths(ast, oldBaseUrl, newBaseUrl) { const domModules = dom5.queryAll(ast, matchers.domModuleWithoutAssetpath); for (let i = 0, node; i < domModules.length; i++) { node = domModules[i]; const assetPathUrl = this.bundler.analyzer.urlResolver.relative(newBaseUrl, url_utils_1.stripUrlFileSearchAndHash(oldBaseUrl)); // There's no reason to set an assetpath on a dom-module if its // different from the document's base. if (assetPathUrl !== '') { dom5.setAttribute(node, 'assetpath', assetPathUrl); } } } } exports.HtmlBundler = HtmlBundler; //# sourceMappingURL=html-bundler.js.map