UNPKG

2.98 kBPlain TextView Raw
1import * as dom5 from 'dom5/lib/index-next';
2import {predicates as p} from 'dom5/lib/index-next';
3import * as parse5 from 'parse5';
4import * as url from 'url';
5
6import File = require('vinyl');
7import {AsyncTransformStream, getFileContents} from './streams';
8
9const attrValueMatches = (attrName: string, regex: RegExp) => {
10 return (node: parse5.ASTNode) => {
11 const attrValue = dom5.getAttribute(node, attrName);
12 return attrValue != null && regex.test(attrValue);
13 };
14};
15
16const webcomponentsLoaderRegex = /\bwebcomponents\-(loader|lite|bundle)\.js\b/;
17const webcomponentsLoaderMatcher = p.AND(
18 p.hasTagName('script'), attrValueMatches('src', webcomponentsLoaderRegex));
19
20/**
21 * Wraps `addCustomElementsEs5Adapter()` in a `stream.Transform`.
22 */
23export class CustomElementsEs5AdapterInjector extends
24 AsyncTransformStream<File, File> {
25 constructor() {
26 super({objectMode: true});
27 }
28
29 protected async *
30 _transformIter(files: AsyncIterable<File>): AsyncIterable<File> {
31 for await (const file of files) {
32 if (file.contents === null || file.extname !== '.html') {
33 yield file;
34 continue;
35 }
36 const contents = await getFileContents(file);
37 const updatedContents = addCustomElementsEs5Adapter(contents);
38 if (contents === updatedContents) {
39 yield file;
40 } else {
41 const updatedFile = file.clone();
42 updatedFile.contents = Buffer.from(updatedContents, 'utf-8');
43 yield updatedFile;
44 }
45 }
46 }
47}
48
49/**
50 * Please avoid using this function because the API is likely to change. Prefer
51 * the interface provided by `PolymerProject.addCustomElementsEs5Adapter`.
52 *
53 * When compiling ES6 classes down to ES5 we need to include a special shim so
54 * that compiled custom elements will still work on browsers that support native
55 * custom elements.
56 *
57 * TODO(fks) 03-28-2017: Add tests.
58 */
59export function addCustomElementsEs5Adapter(html: string): string {
60 // Only modify this file if we find a web components polyfill. This is a
61 // heuristic to identify the entry point HTML file. Ultimately we should
62 // explicitly transform only the entry point by having the project config.
63 if (!webcomponentsLoaderRegex.test(html)) {
64 return html;
65 }
66 const parsed = parse5.parse(html, {locationInfo: true});
67 const script = dom5.query(parsed, webcomponentsLoaderMatcher);
68 if (!script) {
69 return html;
70 }
71
72 // Collect important dom references & create fragments for injection.
73 const loaderScriptUrl = dom5.getAttribute(script, 'src')!;
74 const adapterScriptUrl =
75 url.resolve(loaderScriptUrl, 'custom-elements-es5-adapter.js');
76 const es5AdapterFragment = parse5.parseFragment(`
77 <script>if (!window.customElements) { document.write('<!--'); }</script>
78 <script type="text/javascript" src="${adapterScriptUrl}"></script>
79 <!--! do not remove -->
80`);
81
82 dom5.insertBefore(script.parentNode!, script, es5AdapterFragment);
83 return parse5.serialize(parsed);
84}