UNPKG

5.48 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4 * This code may only be used under the BSD style license found at
5 * http://polymer.github.io/LICENSE.txt
6 * The complete set of authors may be found at
7 * http://polymer.github.io/AUTHORS.txt
8 * The complete set of contributors may be found at
9 * http://polymer.github.io/CONTRIBUTORS.txt
10 * Code distributed by Google as part of the polymer project is also
11 * subject to an additional IP rights grant found at
12 * http://polymer.github.io/PATENTS.txt
13 */
14
15import * as dom5 from 'dom5/lib/index-next';
16import * as parse5 from 'parse5';
17import * as path from 'path';
18import {Analyzer, PackageRelativeUrl, ResolvedUrl, UrlResolver} from 'polymer-analyzer';
19import {ProjectConfig} from 'polymer-project-config';
20
21import File = require('vinyl');
22
23import {pathFromUrl, urlFromPath, LocalFsPath} from './path-transformers';
24import {FileMapUrlLoader} from './file-map-url-loader';
25import {AsyncTransformStream} from './streams';
26
27/**
28 * A stream that modifies HTML files to include prefetch links for all of the
29 * file's transitive dependencies.
30 */
31export class AddPrefetchLinks extends AsyncTransformStream<File, File> {
32 files: Map<ResolvedUrl, File>;
33 private _analyzer: Analyzer;
34 private _config: ProjectConfig;
35
36 constructor(config: ProjectConfig) {
37 super({objectMode: true});
38 this.files = new Map();
39 this._config = config;
40 this._analyzer =
41 new Analyzer({urlLoader: new FileMapUrlLoader(this.files)});
42 }
43
44 protected async *
45 _transformIter(files: AsyncIterable<File>): AsyncIterable<File> {
46 const htmlFileUrls: ResolvedUrl[] = [];
47
48 // Map all files; pass-through all non-HTML files.
49 for await (const file of files) {
50 const fileUrl = this._analyzer.resolveUrl(urlFromPath(
51 this._config.root as LocalFsPath, file.path as LocalFsPath))!;
52 this.files.set(fileUrl, file);
53 if (path.extname(file.path) !== '.html') {
54 yield file;
55 } else {
56 htmlFileUrls.push(fileUrl);
57 }
58 }
59
60 // Analyze each HTML file and add prefetch links.
61 const analysis = await this._analyzer.analyze(htmlFileUrls);
62
63 for (const documentUrl of htmlFileUrls) {
64 const result = analysis.getDocument(documentUrl);
65
66 if (result.successful === false) {
67 const message = result.error && result.error.message;
68 console.warn(`Unable to get document ${documentUrl}: ${message}`);
69 continue;
70 }
71
72 const document = result.value;
73 const allDependencyUrls =
74 [...document.getFeatures({
75 kind: 'import',
76 externalPackages: true,
77 imported: true,
78 noLazyImports: true
79 })].filter((d) => d.document !== undefined && !d.lazy)
80 .map((d) => d.document!.url);
81
82 const directDependencyUrls =
83 [...document.getFeatures({
84 kind: 'import',
85 externalPackages: true,
86 imported: false,
87 noLazyImports: true
88 })].filter((d) => d.document !== undefined && !d.lazy)
89 .map((d) => d.document!.url);
90
91 const onlyTransitiveDependencyUrls = allDependencyUrls.filter(
92 (d) => directDependencyUrls.indexOf(d) === -1);
93
94 // No need to transform a file if it has no dependencies to prefetch.
95 if (onlyTransitiveDependencyUrls.length === 0) {
96 yield this.files.get(documentUrl)!;
97 continue;
98 }
99
100 const prefetchUrls = new Set(onlyTransitiveDependencyUrls);
101
102 const html = createLinks(
103 this._analyzer.urlResolver,
104 document.parsedDocument.contents,
105 document.parsedDocument.baseUrl,
106 prefetchUrls,
107 document.url ===
108 this._analyzer.resolveUrl(urlFromPath(
109 this._config.root as LocalFsPath,
110 this._config.entrypoint as LocalFsPath)));
111 const filePath = pathFromUrl(
112 this._config.root as LocalFsPath,
113 this._analyzer.urlResolver.relative(documentUrl));
114 yield new File({contents: Buffer.from(html, 'utf-8'), path: filePath});
115 }
116 }
117}
118
119/**
120 * Returns the given HTML updated with import or prefetch links for the given
121 * dependencies. The given url and deps are expected to be project-relative
122 * URLs (e.g. "index.html" or "src/view.html") unless absolute parameter is
123 * `true` and there is no base tag in the document.
124 */
125export function createLinks(
126 urlResolver: UrlResolver,
127 html: string,
128 baseUrl: ResolvedUrl,
129 deps: Set<ResolvedUrl>,
130 absolute: boolean = false): string {
131 const ast = parse5.parse(html, {locationInfo: true});
132 const baseTag = dom5.query(ast, dom5.predicates.hasTagName('base'));
133 const baseTagHref = baseTag ? dom5.getAttribute(baseTag, 'href') : '';
134
135 // parse5 always produces a <head> element.
136 const head = dom5.query(ast, dom5.predicates.hasTagName('head'))!;
137 for (const dep of deps) {
138 let href;
139 if (absolute && !baseTagHref) {
140 href = absUrl(urlResolver.relative(dep));
141 } else {
142 href = urlResolver.relative(baseUrl, dep);
143 }
144 const link = dom5.constructors.element('link');
145 dom5.setAttribute(link, 'rel', 'prefetch');
146 dom5.setAttribute(link, 'href', href);
147 dom5.append(head, link);
148 }
149 dom5.removeFakeRootElements(ast);
150 return parse5.serialize(ast);
151}
152
153function absUrl(url: string): PackageRelativeUrl {
154 return (url.startsWith('/') ? url : '/' + url) as PackageRelativeUrl;
155}