1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | import * as dom5 from 'dom5/lib/index-next';
|
16 | import * as parse5 from 'parse5';
|
17 | import * as path from 'path';
|
18 | import {Analyzer, PackageRelativeUrl, ResolvedUrl, UrlResolver} from 'polymer-analyzer';
|
19 | import {ProjectConfig} from 'polymer-project-config';
|
20 |
|
21 | import File = require('vinyl');
|
22 |
|
23 | import {pathFromUrl, urlFromPath, LocalFsPath} from './path-transformers';
|
24 | import {FileMapUrlLoader} from './file-map-url-loader';
|
25 | import {AsyncTransformStream} from './streams';
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export 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 |
|
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 |
|
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 |
|
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 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | export 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 |
|
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 |
|
153 | function absUrl(url: string): PackageRelativeUrl {
|
154 | return (url.startsWith('/') ? url : '/' + url) as PackageRelativeUrl;
|
155 | }
|