UNPKG

10.1 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt
7 * The complete set of authors may be found at
8 * http://polymer.github.io/AUTHORS.txt
9 * The complete set of contributors may be found at
10 * http://polymer.github.io/CONTRIBUTORS.txt
11 * Code distributed by Google as part of the polymer project is also
12 * subject to an additional IP rights grant found at
13 * http://polymer.github.io/PATENTS.txt
14 */
15Object.defineProperty(exports, "__esModule", { value: true });
16const dom5 = require("dom5/lib/index-next");
17const htmlMinifier = require("html-minifier");
18const parse5 = require("parse5");
19const externalJs = require("./external-js");
20const html_splitter_1 = require("./html-splitter");
21const js_transform_1 = require("./js-transform");
22const p = dom5.predicates;
23const isJsScript = p.AND(p.hasTagName('script'), p.OR(p.NOT(p.hasAttr('type')), p.hasAttrValue('type', 'text/javascript'), p.hasAttrValue('type', 'application/javascript'), p.hasAttrValue('type', 'module')));
24const isJsScriptOrHtmlImport = p.OR(isJsScript, p.AND(p.hasTagName('link'), p.hasSpaceSeparatedAttrValue('rel', 'import')));
25/**
26 * Transform some HTML according to the given options.
27 */
28function htmlTransform(html, options) {
29 if (options.js && options.js.moduleResolution === 'node' &&
30 !options.js.filePath) {
31 throw new Error('Cannot perform node module resolution without filePath.');
32 }
33 const document = parse5.parse(html, {
34 locationInfo: true,
35 });
36 removeFakeNodes(document);
37 const allScripts = [...dom5.queryAll(document, isJsScript)];
38 const shouldTransformEsModuleToAmd = options.js &&
39 options.js.transformModulesToAmd &&
40 // Assume that if this document has a nomodule script, the author is
41 // already handling browsers that don't support modules, and we don't
42 // need to transform them (even if the configuration was set).
43 // TODO(aomarks) Check this for HtmlSplitter case too.
44 !allScripts.some((node) => dom5.hasAttribute(node, 'nomodule'));
45 let wctScript, firstModuleScript;
46 for (const script of allScripts) {
47 const isModule = dom5.getAttribute(script, 'type') === 'module';
48 if (isModule) {
49 if (firstModuleScript === undefined) {
50 firstModuleScript = script;
51 }
52 if (shouldTransformEsModuleToAmd) {
53 transformEsModuleToAmd(script, options.js);
54 continue; // Bypass the standard jsTransform below.
55 }
56 }
57 const isInline = !dom5.hasAttribute(script, 'src');
58 if (isInline) {
59 // Note that scripts split by HtmlSplitter are always external, so we
60 // don't have to check for that case here.
61 const newJs = js_transform_1.jsTransform(dom5.getTextContent(script), Object.assign({}, options.js, { transformModulesToAmd: false }));
62 dom5.setTextContent(script, newJs);
63 }
64 else if (wctScript === undefined) {
65 const src = dom5.getAttribute(script, 'src') || '';
66 if (src.includes('web-component-tester/browser.js') ||
67 src.includes('wct-browser-legacy/browser.js') ||
68 src.includes('wct-mocha/wct-mocha.js')) {
69 wctScript = script;
70 }
71 }
72 }
73 if (options.injectAmdLoader && shouldTransformEsModuleToAmd &&
74 firstModuleScript !== undefined) {
75 const fragment = parse5.parseFragment('<script></script>\n');
76 dom5.setTextContent(fragment.childNodes[0], externalJs.getAmdLoader());
77 const amdLoaderScript = fragment.childNodes[0];
78 // Inject as late as possible (just before the first module is declared, if
79 // there is one) because there may be some UMD dependencies that we want to
80 // continue to load in global mode instead of AMD mode (which is detected by
81 // the presence of the `require` global).
82 // TODO(aomarks) If we don't define require, we can inject earlier.
83 dom5.insertBefore(firstModuleScript.parentNode, firstModuleScript, fragment);
84 if (wctScript !== undefined) {
85 addWctTimingHack(wctScript, amdLoaderScript);
86 }
87 }
88 const injectScript = (js) => {
89 const fragment = parse5.parseFragment('<script></script>\n');
90 dom5.setTextContent(fragment.childNodes[0], js);
91 const firstJsScriptOrHtmlImport = dom5.query(document, isJsScriptOrHtmlImport);
92 if (firstJsScriptOrHtmlImport) {
93 dom5.insertBefore(firstJsScriptOrHtmlImport.parentNode, firstJsScriptOrHtmlImport, fragment);
94 }
95 else {
96 const headOrDocument = dom5.query(document, dom5.predicates.hasTagName('head')) || document;
97 dom5.append(headOrDocument, fragment);
98 }
99 };
100 let babelHelpers;
101 switch (options.injectBabelHelpers) {
102 case undefined:
103 case 'none':
104 break;
105 case 'full':
106 babelHelpers = externalJs.getBabelHelpersFull();
107 break;
108 case 'amd':
109 babelHelpers = externalJs.getBabelHelpersAmd();
110 break;
111 default:
112 const never = options.injectBabelHelpers;
113 throw new Error(`Unknown injectBabelHelpers value: ${never}`);
114 }
115 if (babelHelpers !== undefined) {
116 injectScript(babelHelpers);
117 }
118 if (options.injectRegeneratorRuntime === true) {
119 injectScript(externalJs.getRegeneratorRuntime());
120 }
121 html = parse5.serialize(document);
122 if (options.minifyHtml) {
123 html = htmlMinifier.minify(html, {
124 collapseWhitespace: true,
125 removeComments: true,
126 });
127 }
128 return html;
129}
130exports.htmlTransform = htmlTransform;
131function transformEsModuleToAmd(script, jsOptions) {
132 // We're not a module anymore.
133 dom5.removeAttribute(script, 'type');
134 if (html_splitter_1.scriptWasSplitByHtmlSplitter(script)) {
135 // Nothing else to do here. If we're using HtmlSplitter, the JsTransformer
136 // is responsible for doing this transformation.
137 return;
138 }
139 const isExternal = dom5.hasAttribute(script, 'src');
140 if (isExternal) {
141 const src = dom5.getAttribute(script, 'src');
142 dom5.removeAttribute(script, 'src');
143 dom5.setTextContent(script, `define(['${src}']);`);
144 }
145 else {
146 // Transform inline scripts with the AMD Babel plugin transformer.
147 const newJs = js_transform_1.jsTransform(dom5.getTextContent(script), Object.assign({}, jsOptions, { transformModulesToAmd: true }));
148 dom5.setTextContent(script, newJs);
149 }
150}
151function addWctTimingHack(wctScript, amdLoaderScript) {
152 // This looks like a Web Component Tester script, and we have converted ES
153 // modules to AMD. Converting a module to AMD means that `DOMContentLoaded`
154 // will fire before the AMD loader resolves and executes the modules. Since
155 // WCT listens for `DOMContentLoaded`, this means test suites in modules will
156 // not have been registered by the time WCT starts running tests.
157 //
158 // To address this, we inject a block of JS that uses WCT's `waitFor` hook
159 // to defer running tests until our AMD modules have loaded. If WCT finds a
160 // `waitFor`, it passes it a callback that will run the tests, instead of
161 // running tests immediately.
162 //
163 // Note we must do this as late as possible, before the WCT script, because
164 // users may be setting their own `waitFor` that musn't clobber ours.
165 // Likewise we must call theirs if we find it.
166 dom5.insertBefore(wctScript.parentNode, wctScript, parse5.parseFragment(`
167<script>
168 // Injected by polymer-build to defer WCT until all AMD modules are loaded.
169 (function() {
170 window.WCT = window.WCT || {};
171 var originalWaitFor = window.WCT.waitFor;
172 window.WCT.waitFor = function(cb) {
173 window._wctCallback = function() {
174 if (originalWaitFor) {
175 originalWaitFor(cb);
176 } else {
177 cb();
178 }
179 };
180 };
181 }());
182</script>
183`));
184 // Monkey patch `define` to keep track of loaded AMD modules. Note this
185 // assumes that all modules are registered before `DOMContentLoaded`, but
186 // that's an assumption WCT normally makes anyway. Do this right after the AMD
187 // loader is loaded, and hence before the first module is registered.
188 dom5.insertAfter(amdLoaderScript.parentNode, amdLoaderScript, parse5.parseFragment(`
189<script>
190 // Injected by polymer-build to defer WCT until all AMD modules are loaded.
191 (function() {
192 var originalDefine = window.define;
193 var moduleCount = 0;
194 window.define = function(deps, factory) {
195 moduleCount++;
196 originalDefine(deps, function() {
197 if (factory) {
198 factory.apply(undefined, arguments);
199 }
200 moduleCount--;
201 if (moduleCount === 0) {
202 window._wctCallback();
203 }
204 });
205 };
206 })();
207</script>
208`));
209}
210/**
211 * parse5 will inject <html>, <head>, and <body> tags if they aren't already
212 * there. Undo this so that we make fewer unnecessary transformations.
213 *
214 * Note that the given document must have been parsed with `locationInfo: true`,
215 * or else this function will always remove these tags.
216 *
217 * TODO(aomarks) Move to dom5.
218 */
219function removeFakeNodes(document) {
220 const suspects = [];
221 const html = (document.childNodes || []).find((child) => child.tagName === 'html');
222 if (html !== undefined) {
223 suspects.push(html);
224 for (const child of html.childNodes || []) {
225 if (child.tagName === 'head' || child.tagName === 'body') {
226 suspects.push(child);
227 }
228 }
229 }
230 for (const suspect of suspects) {
231 // No location means it wasn't in the original source.
232 if (!suspect.__location) {
233 dom5.removeNodeSaveChildren(suspect);
234 }
235 }
236}
237//# sourceMappingURL=html-transform.js.map
\No newline at end of file