UNPKG

22.8 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2016 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 */
15var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16 return new (P || (P = Promise))(function (resolve, reject) {
17 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
18 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
19 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
20 step((generator = generator.apply(thisArg, _arguments || [])).next());
21 });
22};
23var __asyncValues = (this && this.__asyncValues) || function (o) {
24 if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
25 var m = o[Symbol.asyncIterator], i;
26 return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
27 function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
28 function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
29};
30var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
31var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
32 if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
33 var g = generator.apply(thisArg, _arguments || []), i, q = [];
34 return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
35 function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
36 function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
37 function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
38 function fulfill(value) { resume("next", value); }
39 function reject(value) { resume("throw", value); }
40 function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
41};
42Object.defineProperty(exports, "__esModule", { value: true });
43const path = require("path");
44const logging = require("plylog");
45const polymer_analyzer_1 = require("polymer-analyzer");
46const stream_1 = require("stream");
47const vinyl_fs_1 = require("vinyl-fs");
48const path_transformers_1 = require("./path-transformers");
49const streams_1 = require("./streams");
50const logger = logging.getLogger('cli.build.analyzer');
51/**
52 * A stream that tells the BuildAnalyzer to resolve each file it sees. It's
53 * important that files are resolved here in a seperate stream, so that analysis
54 * and file loading/resolution can't block each other while waiting.
55 */
56class ResolveTransform extends streams_1.AsyncTransformStream {
57 constructor(buildAnalyzer) {
58 super({ objectMode: true });
59 this._buildAnalyzer = buildAnalyzer;
60 }
61 _transformIter(files) {
62 return __asyncGenerator(this, arguments, function* _transformIter_1() {
63 var e_1, _a;
64 try {
65 for (var files_1 = __asyncValues(files), files_1_1; files_1_1 = yield __await(files_1.next()), !files_1_1.done;) {
66 const file = files_1_1.value;
67 this._buildAnalyzer.resolveFile(file);
68 yield yield __await(file);
69 }
70 }
71 catch (e_1_1) { e_1 = { error: e_1_1 }; }
72 finally {
73 try {
74 if (files_1_1 && !files_1_1.done && (_a = files_1.return)) yield __await(_a.call(files_1));
75 }
76 finally { if (e_1) throw e_1.error; }
77 }
78 });
79 }
80}
81/**
82 * A stream to analyze every file that passes through it. This is used to
83 * analyze important application fragments as they pass through the source
84 * stream.
85 *
86 * We create a new stream to handle this because the alternative (attaching
87 * event listeners directly to the existing sources stream) would
88 * start the flow of data before the user was ready to consume it. By
89 * analyzing inside of the stream instead of via "data" event listeners, the
90 * source stream will remain paused until the user is ready to start the stream
91 * themselves.
92 */
93class AnalyzeTransform extends streams_1.AsyncTransformStream {
94 constructor(buildAnalyzer) {
95 // A high `highWaterMark` value is needed to keep this from pausing the
96 // entire source stream.
97 // TODO(fks) 02-02-2017: Move analysis out of the source stream itself so
98 // that it no longer blocks during analysis.
99 super({ objectMode: true, highWaterMark: 10000 });
100 this._buildAnalyzer = buildAnalyzer;
101 }
102 _transformIter(files) {
103 return __asyncGenerator(this, arguments, function* _transformIter_2() {
104 var e_2, _a;
105 try {
106 for (var files_2 = __asyncValues(files), files_2_1; files_2_1 = yield __await(files_2.next()), !files_2_1.done;) {
107 const file = files_2_1.value;
108 yield __await(this._buildAnalyzer.analyzeFile(file));
109 yield yield __await(file);
110 }
111 }
112 catch (e_2_1) { e_2 = { error: e_2_1 }; }
113 finally {
114 try {
115 if (files_2_1 && !files_2_1.done && (_a = files_2.return)) yield __await(_a.call(files_2));
116 }
117 finally { if (e_2) throw e_2.error; }
118 }
119 });
120 }
121}
122class BuildAnalyzer {
123 constructor(config,
124 /** If null is given, we do not log warnings. */
125 streamToWarnTo = process.stdout) {
126 this.streamToWarnTo = streamToWarnTo;
127 this.started = false;
128 this.sourceFilesLoaded = false;
129 this.files = new Map();
130 this.warnings = new Set();
131 this._dependencyAnalysis = {
132 depsToFragments: new Map(),
133 fragmentToDeps: new Map(),
134 fragmentToFullDeps: new Map()
135 };
136 this.config = config;
137 this.loader = new StreamLoader(this);
138 this.analyzer = new polymer_analyzer_1.Analyzer({
139 urlLoader: this.loader,
140 // TODO(usergenic): Add option to polymer-build to propagate a protocol
141 // and host option to the FsUrlResolver.
142 urlResolver: new polymer_analyzer_1.FsUrlResolver(config.root),
143 moduleResolution: config.moduleResolution === 'none' ?
144 undefined :
145 config.moduleResolution,
146 });
147 this.allFragmentsToAnalyze =
148 new Set(this.config.allFragments.map((f) => f));
149 this.analyzeDependencies = new Promise((resolve, _reject) => {
150 this._resolveDependencyAnalysis = resolve;
151 });
152 const lintOptions = (this.config.lint || {});
153 const warningCodesToIgnore = new Set(lintOptions.ignoreWarnings || []);
154 // These are expected, as we never want to load remote URLs like
155 // `https://example.com/` when we're building
156 warningCodesToIgnore.add('not-loadable');
157 this._warningsFilter = new polymer_analyzer_1.WarningFilter({ warningCodesToIgnore, minimumSeverity: polymer_analyzer_1.Severity.WARNING });
158 }
159 /**
160 * Start analysis by setting up the sources and dependencies analysis
161 * pipelines and starting the source stream. Files will not be loaded from
162 * disk until this is called. Can be called multiple times but will only run
163 * set up once.
164 */
165 startAnalysis() {
166 if (this.started) {
167 return;
168 }
169 this.started = true;
170 // Create the base streams for sources & dependencies to be read from.
171 this._dependenciesStream = new stream_1.PassThrough({ objectMode: true });
172 this._sourcesStream = vinyl_fs_1.src(this.config.sources, {
173 cwdbase: true,
174 nodir: true,
175 });
176 // _sourcesProcessingStream: Pipe the sources stream through...
177 // 1. The resolver stream, to resolve each file loaded via the analyzer
178 // 2. The analyzer stream, to analyze app fragments for dependencies
179 this._sourcesProcessingStream =
180 this._sourcesStream
181 .on('error', (err) => this._sourcesProcessingStream.emit('error', err))
182 .pipe(new ResolveTransform(this))
183 .on('error', (err) => this._sourcesProcessingStream.emit('error', err))
184 .on('end', this.onSourcesStreamComplete.bind(this))
185 .pipe(new AnalyzeTransform(this));
186 // _dependenciesProcessingStream: Pipe the dependencies stream through...
187 // 1. The vinyl loading stream, to load file objects from file paths
188 // 2. The resolver stream, to resolve each loaded file for the analyzer
189 this._dependenciesProcessingStream =
190 this._dependenciesStream
191 .on('error', (err) => this._dependenciesProcessingStream.emit('error', err))
192 .pipe(new streams_1.VinylReaderTransform())
193 .on('error', (err) => this._dependenciesProcessingStream.emit('error', err))
194 .pipe(new ResolveTransform(this));
195 }
196 /**
197 * Return _dependenciesOutputStream, which will contain fully loaded file
198 * objects for each dependency after analysis.
199 */
200 dependencies() {
201 this.startAnalysis();
202 return this._dependenciesProcessingStream;
203 }
204 /**
205 * Return _sourcesOutputStream, which will contain fully loaded file
206 * objects for each source after analysis.
207 */
208 sources() {
209 this.startAnalysis();
210 return this._sourcesProcessingStream;
211 }
212 /**
213 * Resolve a file in our loader so that the analyzer can read it.
214 */
215 resolveFile(file) {
216 const filePath = file.path;
217 this.addFile(file);
218 // If our resolver is waiting for this file, resolve its deferred loader
219 if (this.loader.hasDeferredFile(filePath)) {
220 this.loader.resolveDeferredFile(filePath, file);
221 }
222 }
223 /**
224 * Analyze a file to find additional dependencies to load. Currently we only
225 * get dependencies for application fragments. When all fragments are
226 * analyzed, we call _done() to signal that analysis is complete.
227 */
228 analyzeFile(file) {
229 return __awaiter(this, void 0, void 0, function* () {
230 const filePath = file.path;
231 // If the file is a fragment, begin analysis on its dependencies
232 if (this.config.isFragment(filePath)) {
233 const deps = yield this._getDependencies(this.analyzer.resolveUrl(path_transformers_1.urlFromPath(this.config.root, filePath)));
234 this._addDependencies(filePath, deps);
235 this.allFragmentsToAnalyze.delete(filePath);
236 // If there are no more fragments to analyze, we are done
237 if (this.allFragmentsToAnalyze.size === 0) {
238 this._done();
239 }
240 }
241 });
242 }
243 /**
244 * Perform some checks once we know that `_sourcesStream` is done loading.
245 */
246 onSourcesStreamComplete() {
247 // Emit an error if there are missing source files still deferred. Otherwise
248 // this would cause the analyzer to hang.
249 for (const filePath of this.loader.deferredFiles.keys()) {
250 if (this.config.isSource(filePath)) {
251 const err = new Error(`Not found: ${filePath}`);
252 this.loader.rejectDeferredFile(filePath, err);
253 }
254 }
255 // Set sourceFilesLoaded so that future files aren't accidentally deferred
256 this.sourceFilesLoaded = true;
257 }
258 /**
259 * Helper function for emitting a general analysis error onto both file
260 * streams.
261 */
262 emitAnalysisError(err) {
263 this._sourcesProcessingStream.emit('error', err);
264 this._dependenciesProcessingStream.emit('error', err);
265 }
266 /**
267 * Called when analysis is complete and there are no more files to analyze.
268 * Checks for serious errors before resolving its dependency analysis and
269 * ending the dependency stream (which it controls).
270 */
271 _done() {
272 this.printWarnings();
273 const allWarningCount = this.countWarningsByType();
274 const errorWarningCount = allWarningCount.get(polymer_analyzer_1.Severity.ERROR);
275 // If any ERROR warnings occurred, propagate an error in each build stream.
276 if (errorWarningCount > 0) {
277 this.emitAnalysisError(new Error(`${errorWarningCount} error(s) occurred during build.`));
278 return;
279 }
280 // If analysis somehow finished with files that still needed to be loaded,
281 // propagate an error in each build stream.
282 for (const filePath of this.loader.deferredFiles.keys()) {
283 const err = new Error(`Not found: ${filePath}`);
284 this.loader.rejectDeferredFile(filePath, err);
285 return;
286 }
287 // Resolve our dependency analysis promise now that we have seen all files
288 this._dependenciesStream.end();
289 this._resolveDependencyAnalysis(this._dependencyAnalysis);
290 }
291 getFile(filepath) {
292 const url = path_transformers_1.urlFromPath(this.config.root, filepath);
293 return this.getFileByUrl(url);
294 }
295 getFileByUrl(url) {
296 // TODO(usergenic): url carefulness bug, take an extra careful look at this.
297 if (url.startsWith('/')) {
298 url = url.substring(1);
299 }
300 return this.files.get(url);
301 }
302 /**
303 * A side-channel to add files to the loader that did not come through the
304 * stream transformation. This is for generated files, like
305 * shared-bundle.html. This should probably be refactored so that the files
306 * can be injected into the stream.
307 */
308 addFile(file) {
309 logger.debug(`addFile: ${file.path}`);
310 // Badly-behaved upstream transformers (looking at you gulp-html-minifier)
311 // may use posix path separators on Windows.
312 const filepath = path.normalize(file.path);
313 // Store only root-relative paths, in URL/posix format
314 this.files.set(path_transformers_1.urlFromPath(this.config.root, filepath), file);
315 }
316 printWarnings() {
317 if (this.streamToWarnTo === null) {
318 return;
319 }
320 const warningPrinter = new polymer_analyzer_1.WarningPrinter(this.streamToWarnTo);
321 warningPrinter.printWarnings(this.warnings);
322 }
323 countWarningsByType() {
324 const errorCountMap = new Map();
325 errorCountMap.set(polymer_analyzer_1.Severity.INFO, 0);
326 errorCountMap.set(polymer_analyzer_1.Severity.WARNING, 0);
327 errorCountMap.set(polymer_analyzer_1.Severity.ERROR, 0);
328 for (const warning of this.warnings) {
329 errorCountMap.set(warning.severity, errorCountMap.get(warning.severity) + 1);
330 }
331 return errorCountMap;
332 }
333 /**
334 * Attempts to retreive document-order transitive dependencies for `url`.
335 */
336 _getDependencies(url) {
337 return __awaiter(this, void 0, void 0, function* () {
338 const analysis = yield this.analyzer.analyze([url]);
339 const result = analysis.getDocument(url);
340 if (result.successful === false) {
341 const message = result.error && result.error.message || 'unknown';
342 throw new Error(`Unable to get document ${url}: ${message}`);
343 }
344 const doc = result.value;
345 doc.getWarnings({ imported: true })
346 .filter((w) => !this._warningsFilter.shouldIgnore(w))
347 .forEach((w) => this.warnings.add(w));
348 const scripts = new Set();
349 const styles = new Set();
350 const imports = new Set();
351 const importFeatures = doc.getFeatures({ kind: 'import', externalPackages: true, imported: true });
352 for (const importFeature of importFeatures) {
353 const importUrl = importFeature.url;
354 if (!this.analyzer.canLoad(importUrl)) {
355 logger.debug(`ignoring external dependency: ${importUrl}`);
356 }
357 else if (importFeature.type === 'html-script') {
358 scripts.add(this.analyzer.urlResolver.relative(importUrl));
359 }
360 else if (importFeature.type === 'html-style') {
361 styles.add(this.analyzer.urlResolver.relative(importUrl));
362 }
363 else if (importFeature.type === 'html-import') {
364 imports.add(this.analyzer.urlResolver.relative(importUrl));
365 }
366 else {
367 logger.debug(`unexpected import type encountered: ${importFeature.type}`);
368 }
369 }
370 const deps = {
371 scripts: [...scripts],
372 styles: [...styles],
373 imports: [...imports],
374 };
375 logger.debug(`dependencies analyzed for: ${url}`, deps);
376 return deps;
377 });
378 }
379 _addDependencies(filePath, deps) {
380 // Make sure function is being called properly
381 if (!this.allFragmentsToAnalyze.has(filePath)) {
382 throw new Error(`Dependency analysis incorrectly called for ${filePath}`);
383 }
384 const relativeUrl = path_transformers_1.urlFromPath(this.config.root, filePath);
385 // Add dependencies to _dependencyAnalysis object, and push them through
386 // the dependency stream.
387 this._dependencyAnalysis.fragmentToFullDeps.set(relativeUrl, deps);
388 this._dependencyAnalysis.fragmentToDeps.set(relativeUrl, deps.imports);
389 deps.imports.forEach((url) => {
390 const entrypointList = this._dependencyAnalysis.depsToFragments.get(url);
391 if (entrypointList) {
392 entrypointList.push(relativeUrl);
393 }
394 else {
395 this._dependencyAnalysis.depsToFragments.set(url, [relativeUrl]);
396 }
397 });
398 }
399 /**
400 * Check that the source stream has not already completed loading by the
401 * time
402 * this file was analyzed.
403 */
404 sourcePathAnalyzed(filePath) {
405 // If we've analyzed a new path to a source file after the sources
406 // stream has completed, we can assume that that file does not
407 // exist. Reject with a "Not Found" error.
408 if (this.sourceFilesLoaded) {
409 throw new Error(`Not found: "${filePath}"`);
410 }
411 // Source files are loaded automatically through the vinyl source
412 // stream. If it hasn't been seen yet, defer resolving until it has
413 // been loaded by vinyl.
414 logger.debug('dependency is a source file, ignoring...', { dep: filePath });
415 }
416 /**
417 * Push the given filepath into the dependencies stream for loading.
418 * Each dependency is only pushed through once to avoid duplicates.
419 */
420 dependencyPathAnalyzed(filePath) {
421 if (this.getFile(filePath)) {
422 logger.debug('dependency has already been pushed, ignoring...', { dep: filePath });
423 return;
424 }
425 logger.debug('new dependency analyzed, pushing into dependency stream...', filePath);
426 this._dependenciesStream.push(filePath);
427 }
428}
429exports.BuildAnalyzer = BuildAnalyzer;
430class StreamLoader {
431 constructor(buildAnalyzer) {
432 // Store files that have not yet entered the Analyzer stream here.
433 // Later, when the file is seen, the DeferredFileCallback can be
434 // called with the file contents to resolve its loading.
435 this.deferredFiles = new Map();
436 this._buildAnalyzer = buildAnalyzer;
437 this.config = this._buildAnalyzer.config;
438 }
439 hasDeferredFile(filePath) {
440 return this.deferredFiles.has(filePath);
441 }
442 hasDeferredFiles() {
443 return this.deferredFiles.size > 0;
444 }
445 resolveDeferredFile(filePath, file) {
446 const deferredCallbacks = this.deferredFiles.get(filePath);
447 if (deferredCallbacks == null) {
448 throw new Error(`Internal error: could not get deferredCallbacks for ${filePath}`);
449 }
450 deferredCallbacks.resolve(file.contents.toString());
451 this.deferredFiles.delete(filePath);
452 }
453 rejectDeferredFile(filePath, err) {
454 const deferredCallbacks = this.deferredFiles.get(filePath);
455 if (deferredCallbacks == null) {
456 throw new Error(`Internal error: could not get deferredCallbacks for ${filePath}`);
457 }
458 deferredCallbacks.reject(err);
459 this.deferredFiles.delete(filePath);
460 }
461 // We can't load external dependencies.
462 canLoad(url) {
463 return url.startsWith('file:///');
464 }
465 load(url) {
466 return __awaiter(this, void 0, void 0, function* () {
467 logger.debug(`loading: ${url}`);
468 if (!this.canLoad(url)) {
469 throw new Error('Unable to load ${url}.');
470 }
471 const urlPath = this._buildAnalyzer.analyzer.urlResolver.relative(url);
472 const filePath = path_transformers_1.pathFromUrl(this.config.root, urlPath);
473 const file = this._buildAnalyzer.getFile(filePath);
474 if (file) {
475 return file.contents.toString();
476 }
477 return new Promise((resolve, reject) => {
478 this.deferredFiles.set(filePath, { resolve, reject });
479 try {
480 if (this.config.isSource(filePath)) {
481 this._buildAnalyzer.sourcePathAnalyzed(filePath);
482 }
483 else {
484 this._buildAnalyzer.dependencyPathAnalyzed(filePath);
485 }
486 }
487 catch (err) {
488 this.rejectDeferredFile(filePath, err);
489 }
490 });
491 });
492 }
493}
494exports.StreamLoader = StreamLoader;
495//# sourceMappingURL=analyzer.js.map
\No newline at end of file