1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | import * as path from 'path';
|
16 | import {Analyzer, FsUrlResolver, Import, PackageRelativeUrl, ResolvedUrl} from 'polymer-analyzer';
|
17 | import {buildDepsIndex} from 'polymer-bundler/lib/deps-index';
|
18 | import {ProjectConfig} from 'polymer-project-config';
|
19 |
|
20 | import File = require('vinyl');
|
21 |
|
22 | import {urlFromPath, LocalFsPath} from './path-transformers';
|
23 | import {FileMapUrlLoader} from './file-map-url-loader';
|
24 | import {AsyncTransformStream} from './streams';
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | export type ResourceType = 'document'|'script'|'style'|'image'|'font';
|
54 | export interface PushManifestEntry {
|
55 | type?: ResourceType;
|
56 | weight?: 1;
|
57 | }
|
58 | export interface PushManifestEntryCollection {
|
59 | [dependencyAbsoluteUrl: string]: PushManifestEntry;
|
60 | }
|
61 | export interface PushManifest {
|
62 | [requestAbsoluteUrl: string]: PushManifestEntryCollection;
|
63 | }
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | const extensionToTypeMapping = new Map<string, ResourceType>([
|
69 | ['.css', 'style'],
|
70 | ['.gif', 'image'],
|
71 | ['.html', 'document'],
|
72 | ['.png', 'image'],
|
73 | ['.jpg', 'image'],
|
74 | ['.js', 'script'],
|
75 | ['.json', 'script'],
|
76 | ['.svg', 'image'],
|
77 | ['.webp', 'image'],
|
78 | ['.woff', 'font'],
|
79 | ['.woff2', 'font'],
|
80 | ]);
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | function getResourceTypeFromUrl(url: string): ResourceType|undefined {
|
86 | return extensionToTypeMapping.get(path.extname(url));
|
87 | }
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | function getResourceTypeFromImport(importFeature: Import): ResourceType|
|
95 | undefined {
|
96 | const importKinds = importFeature.kinds;
|
97 | if (importKinds.has('css-import') || importKinds.has('html-style')) {
|
98 | return 'style';
|
99 | }
|
100 | if (importKinds.has('html-import')) {
|
101 | return 'document';
|
102 | }
|
103 | if (importKinds.has('html-script')) {
|
104 | return 'script';
|
105 | }
|
106 |
|
107 |
|
108 |
|
109 | return getResourceTypeFromUrl(importFeature.url);
|
110 | }
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | function createPushEntryFromImport(importFeature: Import): PushManifestEntry {
|
116 | return {
|
117 | type: getResourceTypeFromImport(importFeature),
|
118 | weight: 1,
|
119 | };
|
120 | }
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 | async function generatePushManifestEntryForUrl(
|
127 | analyzer: Analyzer,
|
128 | url: ResolvedUrl): Promise<PushManifestEntryCollection> {
|
129 | const analysis = await analyzer.analyze([url]);
|
130 | const result = analysis.getDocument(url);
|
131 |
|
132 | if (result.successful === false) {
|
133 | const message = result.error && result.error.message || 'unknown';
|
134 | throw new Error(`Unable to get document ${url}: ${message}`);
|
135 | }
|
136 |
|
137 | const analyzedDocument = result.value;
|
138 | const rawImports = [...analyzedDocument.getFeatures({
|
139 | kind: 'import',
|
140 | externalPackages: true,
|
141 | imported: true,
|
142 | noLazyImports: true,
|
143 | })];
|
144 | const importsToPush = rawImports.filter(
|
145 | (i) => !(i.type === 'html-import' && i.lazy) &&
|
146 | !(i.kinds.has('html-script-back-reference')));
|
147 | const pushManifestEntries: PushManifestEntryCollection = {};
|
148 |
|
149 | for (const analyzedImport of importsToPush) {
|
150 |
|
151 |
|
152 |
|
153 | const analyzedImportUrl = analyzedImport.url;
|
154 | const relativeImportUrl = analyzer.urlResolver.relative(analyzedImportUrl);
|
155 | const analyzedImportEntry = pushManifestEntries[relativeImportUrl];
|
156 | if (!analyzedImportEntry) {
|
157 | pushManifestEntries[relativeImportUrl] =
|
158 | createPushEntryFromImport(analyzedImport);
|
159 | }
|
160 | }
|
161 |
|
162 | return pushManifestEntries;
|
163 | }
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | export class AddPushManifest extends AsyncTransformStream<File, File> {
|
171 | files: Map<ResolvedUrl, File>;
|
172 | outPath: LocalFsPath;
|
173 | private config: ProjectConfig;
|
174 | private analyzer: Analyzer;
|
175 | private basePath: PackageRelativeUrl;
|
176 |
|
177 | constructor(
|
178 | config: ProjectConfig, outPath?: LocalFsPath,
|
179 | basePath?: PackageRelativeUrl) {
|
180 | super({objectMode: true});
|
181 | this.files = new Map();
|
182 | this.config = config;
|
183 | this.analyzer = new Analyzer({
|
184 | urlLoader: new FileMapUrlLoader(this.files),
|
185 | urlResolver: new FsUrlResolver(config.root),
|
186 | });
|
187 | this.outPath =
|
188 | path.join(this.config.root, outPath || 'push-manifest.json') as
|
189 | LocalFsPath;
|
190 | this.basePath = (basePath || '') as PackageRelativeUrl;
|
191 | }
|
192 |
|
193 | protected async *
|
194 | _transformIter(files: AsyncIterable<File>): AsyncIterable<File> {
|
195 | for await (const file of files) {
|
196 | this.files.set(
|
197 | this.analyzer.resolveUrl(urlFromPath(
|
198 | this.config.root as LocalFsPath, file.path as LocalFsPath))!,
|
199 | file);
|
200 | yield file;
|
201 | }
|
202 |
|
203 |
|
204 | const pushManifest = await this.generatePushManifest();
|
205 | const pushManifestContents = JSON.stringify(pushManifest, undefined, ' ');
|
206 |
|
207 | yield new File({
|
208 | path: this.outPath,
|
209 | contents: Buffer.from(pushManifestContents),
|
210 | });
|
211 | }
|
212 |
|
213 | async generatePushManifest(): Promise<PushManifest> {
|
214 |
|
215 |
|
216 |
|
217 | const depsIndex = await buildDepsIndex(
|
218 | this.config.allFragments.map(
|
219 | (path) => this.analyzer.resolveUrl(urlFromPath(
|
220 | this.config.root as LocalFsPath, path as LocalFsPath))!),
|
221 | this.analyzer);
|
222 |
|
223 |
|
224 | const allFragments =
|
225 | new Set([...depsIndex.keys()].filter((url) => !url.includes('>')));
|
226 |
|
227 |
|
228 |
|
229 | const mainPushEntrypointUrl = this.analyzer.resolveUrl(urlFromPath(
|
230 | this.config.root as LocalFsPath,
|
231 | this.config.shell as LocalFsPath ||
|
232 | this.config.entrypoint as LocalFsPath))!;
|
233 | allFragments.add(mainPushEntrypointUrl);
|
234 |
|
235 |
|
236 | const pushManifest: PushManifest = {};
|
237 | for (const fragment of allFragments) {
|
238 | const absoluteFragmentUrl =
|
239 | '/' + this.analyzer.urlResolver.relative(fragment);
|
240 | pushManifest[absoluteFragmentUrl] =
|
241 | await generatePushManifestEntryForUrl(this.analyzer, fragment);
|
242 | }
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | const normalize = (p: string) =>
|
254 | path.posix.join(this.basePath, p).replace(/^\/+/, '');
|
255 |
|
256 | const normalized: PushManifest = {};
|
257 | for (const source of Object.keys(pushManifest)) {
|
258 | const targets: PushManifestEntryCollection = {};
|
259 | for (const target of Object.keys(pushManifest[source])) {
|
260 | targets[normalize(target)] = pushManifest[source][target];
|
261 | }
|
262 | normalized[normalize(source)] = targets;
|
263 | }
|
264 | return normalized;
|
265 | }
|
266 | }
|