UNPKG

34.9 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const EventEmitter = require("events");
9const { extname, basename } = require("path");
10const { URL } = require("url");
11const { createGunzip, createBrotliDecompress, createInflate } = require("zlib");
12const NormalModule = require("../NormalModule");
13const createSchemaValidation = require("../util/create-schema-validation");
14const createHash = require("../util/createHash");
15const { mkdirp, dirname, join } = require("../util/fs");
16const memoize = require("../util/memoize");
17
18/** @typedef {import("../../declarations/plugins/schemes/HttpUriPlugin").HttpUriPluginOptions} HttpUriPluginOptions */
19/** @typedef {import("../Compiler")} Compiler */
20
21const getHttp = memoize(() => require("http"));
22const getHttps = memoize(() => require("https"));
23const proxyFetch = (request, proxy) => (url, options, callback) => {
24 const eventEmitter = new EventEmitter();
25 const doRequest = socket =>
26 request
27 .get(url, { ...options, ...(socket && { socket }) }, callback)
28 .on("error", eventEmitter.emit.bind(eventEmitter, "error"));
29
30 if (proxy) {
31 const { hostname: host, port } = new URL(proxy);
32
33 getHttp()
34 .request({
35 host, // IP address of proxy server
36 port, // port of proxy server
37 method: "CONNECT",
38 path: url.host
39 })
40 .on("connect", (res, socket) => {
41 if (res.statusCode === 200) {
42 // connected to proxy server
43 doRequest(socket);
44 }
45 })
46 .on("error", err => {
47 eventEmitter.emit(
48 "error",
49 new Error(
50 `Failed to connect to proxy server "${proxy}": ${err.message}`
51 )
52 );
53 })
54 .end();
55 } else {
56 doRequest();
57 }
58
59 return eventEmitter;
60};
61
62/** @type {(() => void)[] | undefined} */
63let inProgressWrite = undefined;
64
65const validate = createSchemaValidation(
66 require("../../schemas/plugins/schemes/HttpUriPlugin.check.js"),
67 () => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
68 {
69 name: "Http Uri Plugin",
70 baseDataPath: "options"
71 }
72);
73
74const toSafePath = str =>
75 str
76 .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
77 .replace(/[^a-zA-Z0-9._-]+/g, "_");
78
79const computeIntegrity = content => {
80 const hash = createHash("sha512");
81 hash.update(content);
82 const integrity = "sha512-" + hash.digest("base64");
83 return integrity;
84};
85
86const verifyIntegrity = (content, integrity) => {
87 if (integrity === "ignore") return true;
88 return computeIntegrity(content) === integrity;
89};
90
91/**
92 * @param {string} str input
93 * @returns {Record<string, string>} parsed
94 */
95const parseKeyValuePairs = str => {
96 /** @type {Record<string, string>} */
97 const result = {};
98 for (const item of str.split(",")) {
99 const i = item.indexOf("=");
100 if (i >= 0) {
101 const key = item.slice(0, i).trim();
102 const value = item.slice(i + 1).trim();
103 result[key] = value;
104 } else {
105 const key = item.trim();
106 if (!key) continue;
107 result[key] = key;
108 }
109 }
110 return result;
111};
112
113const parseCacheControl = (cacheControl, requestTime) => {
114 // When false resource is not stored in cache
115 let storeCache = true;
116 // When false resource is not stored in lockfile cache
117 let storeLock = true;
118 // Resource is only revalidated, after that timestamp and when upgrade is chosen
119 let validUntil = 0;
120 if (cacheControl) {
121 const parsed = parseKeyValuePairs(cacheControl);
122 if (parsed["no-cache"]) storeCache = storeLock = false;
123 if (parsed["max-age"] && !isNaN(+parsed["max-age"])) {
124 validUntil = requestTime + +parsed["max-age"] * 1000;
125 }
126 if (parsed["must-revalidate"]) validUntil = 0;
127 }
128 return {
129 storeLock,
130 storeCache,
131 validUntil
132 };
133};
134
135/**
136 * @typedef {Object} LockfileEntry
137 * @property {string} resolved
138 * @property {string} integrity
139 * @property {string} contentType
140 */
141
142const areLockfileEntriesEqual = (a, b) => {
143 return (
144 a.resolved === b.resolved &&
145 a.integrity === b.integrity &&
146 a.contentType === b.contentType
147 );
148};
149
150const entryToString = entry => {
151 return `resolved: ${entry.resolved}, integrity: ${entry.integrity}, contentType: ${entry.contentType}`;
152};
153
154class Lockfile {
155 constructor() {
156 this.version = 1;
157 /** @type {Map<string, LockfileEntry | "ignore" | "no-cache">} */
158 this.entries = new Map();
159 }
160
161 static parse(content) {
162 // TODO handle merge conflicts
163 const data = JSON.parse(content);
164 if (data.version !== 1)
165 throw new Error(`Unsupported lockfile version ${data.version}`);
166 const lockfile = new Lockfile();
167 for (const key of Object.keys(data)) {
168 if (key === "version") continue;
169 const entry = data[key];
170 lockfile.entries.set(
171 key,
172 typeof entry === "string"
173 ? entry
174 : {
175 resolved: key,
176 ...entry
177 }
178 );
179 }
180 return lockfile;
181 }
182
183 toString() {
184 let str = "{\n";
185 const entries = Array.from(this.entries).sort(([a], [b]) =>
186 a < b ? -1 : 1
187 );
188 for (const [key, entry] of entries) {
189 if (typeof entry === "string") {
190 str += ` ${JSON.stringify(key)}: ${JSON.stringify(entry)},\n`;
191 } else {
192 str += ` ${JSON.stringify(key)}: { `;
193 if (entry.resolved !== key)
194 str += `"resolved": ${JSON.stringify(entry.resolved)}, `;
195 str += `"integrity": ${JSON.stringify(
196 entry.integrity
197 )}, "contentType": ${JSON.stringify(entry.contentType)} },\n`;
198 }
199 }
200 str += ` "version": ${this.version}\n}\n`;
201 return str;
202 }
203}
204
205/**
206 * @template R
207 * @param {function(function(Error=, R=): void): void} fn function
208 * @returns {function(function((Error | null)=, R=): void): void} cached function
209 */
210const cachedWithoutKey = fn => {
211 let inFlight = false;
212 /** @type {Error | undefined} */
213 let cachedError = undefined;
214 /** @type {R | undefined} */
215 let cachedResult = undefined;
216 /** @type {(function(Error=, R=): void)[] | undefined} */
217 let cachedCallbacks = undefined;
218 return callback => {
219 if (inFlight) {
220 if (cachedResult !== undefined) return callback(null, cachedResult);
221 if (cachedError !== undefined) return callback(cachedError);
222 if (cachedCallbacks === undefined) cachedCallbacks = [callback];
223 else cachedCallbacks.push(callback);
224 return;
225 }
226 inFlight = true;
227 fn((err, result) => {
228 if (err) cachedError = err;
229 else cachedResult = result;
230 const callbacks = cachedCallbacks;
231 cachedCallbacks = undefined;
232 callback(err, result);
233 if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
234 });
235 };
236};
237
238/**
239 * @template T
240 * @template R
241 * @param {function(T, function(Error=, R=): void): void} fn function
242 * @param {function(T, function(Error=, R=): void): void=} forceFn function for the second try
243 * @returns {(function(T, function((Error | null)=, R=): void): void) & { force: function(T, function((Error | null)=, R=): void): void }} cached function
244 */
245const cachedWithKey = (fn, forceFn = fn) => {
246 /** @typedef {{ result?: R, error?: Error, callbacks?: (function((Error | null)=, R=): void)[], force?: true }} CacheEntry */
247 /** @type {Map<T, CacheEntry>} */
248 const cache = new Map();
249 const resultFn = (arg, callback) => {
250 const cacheEntry = cache.get(arg);
251 if (cacheEntry !== undefined) {
252 if (cacheEntry.result !== undefined)
253 return callback(null, cacheEntry.result);
254 if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
255 if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
256 else cacheEntry.callbacks.push(callback);
257 return;
258 }
259 /** @type {CacheEntry} */
260 const newCacheEntry = {
261 result: undefined,
262 error: undefined,
263 callbacks: undefined
264 };
265 cache.set(arg, newCacheEntry);
266 fn(arg, (err, result) => {
267 if (err) newCacheEntry.error = err;
268 else newCacheEntry.result = result;
269 const callbacks = newCacheEntry.callbacks;
270 newCacheEntry.callbacks = undefined;
271 callback(err, result);
272 if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
273 });
274 };
275 resultFn.force = (arg, callback) => {
276 const cacheEntry = cache.get(arg);
277 if (cacheEntry !== undefined && cacheEntry.force) {
278 if (cacheEntry.result !== undefined)
279 return callback(null, cacheEntry.result);
280 if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
281 if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
282 else cacheEntry.callbacks.push(callback);
283 return;
284 }
285 /** @type {CacheEntry} */
286 const newCacheEntry = {
287 result: undefined,
288 error: undefined,
289 callbacks: undefined,
290 force: true
291 };
292 cache.set(arg, newCacheEntry);
293 forceFn(arg, (err, result) => {
294 if (err) newCacheEntry.error = err;
295 else newCacheEntry.result = result;
296 const callbacks = newCacheEntry.callbacks;
297 newCacheEntry.callbacks = undefined;
298 callback(err, result);
299 if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
300 });
301 };
302 return resultFn;
303};
304
305class HttpUriPlugin {
306 /**
307 * @param {HttpUriPluginOptions} options options
308 */
309 constructor(options) {
310 validate(options);
311 this._lockfileLocation = options.lockfileLocation;
312 this._cacheLocation = options.cacheLocation;
313 this._upgrade = options.upgrade;
314 this._frozen = options.frozen;
315 this._allowedUris = options.allowedUris;
316 this._proxy = options.proxy;
317 }
318
319 /**
320 * Apply the plugin
321 * @param {Compiler} compiler the compiler instance
322 * @returns {void}
323 */
324 apply(compiler) {
325 const proxy =
326 this._proxy || process.env["http_proxy"] || process.env["HTTP_PROXY"];
327 const schemes = [
328 {
329 scheme: "http",
330 fetch: proxyFetch(getHttp(), proxy)
331 },
332 {
333 scheme: "https",
334 fetch: proxyFetch(getHttps(), proxy)
335 }
336 ];
337 let lockfileCache;
338 compiler.hooks.compilation.tap(
339 "HttpUriPlugin",
340 (compilation, { normalModuleFactory }) => {
341 const intermediateFs = compiler.intermediateFileSystem;
342 const fs = compilation.inputFileSystem;
343 const cache = compilation.getCache("webpack.HttpUriPlugin");
344 const logger = compilation.getLogger("webpack.HttpUriPlugin");
345 const lockfileLocation =
346 this._lockfileLocation ||
347 join(
348 intermediateFs,
349 compiler.context,
350 compiler.name
351 ? `${toSafePath(compiler.name)}.webpack.lock`
352 : "webpack.lock"
353 );
354 const cacheLocation =
355 this._cacheLocation !== undefined
356 ? this._cacheLocation
357 : lockfileLocation + ".data";
358 const upgrade = this._upgrade || false;
359 const frozen = this._frozen || false;
360 const hashFunction = "sha512";
361 const hashDigest = "hex";
362 const hashDigestLength = 20;
363 const allowedUris = this._allowedUris;
364
365 let warnedAboutEol = false;
366
367 const cacheKeyCache = new Map();
368 /**
369 * @param {string} url the url
370 * @returns {string} the key
371 */
372 const getCacheKey = url => {
373 const cachedResult = cacheKeyCache.get(url);
374 if (cachedResult !== undefined) return cachedResult;
375 const result = _getCacheKey(url);
376 cacheKeyCache.set(url, result);
377 return result;
378 };
379
380 /**
381 * @param {string} url the url
382 * @returns {string} the key
383 */
384 const _getCacheKey = url => {
385 const parsedUrl = new URL(url);
386 const folder = toSafePath(parsedUrl.origin);
387 const name = toSafePath(parsedUrl.pathname);
388 const query = toSafePath(parsedUrl.search);
389 let ext = extname(name);
390 if (ext.length > 20) ext = "";
391 const basename = ext ? name.slice(0, -ext.length) : name;
392 const hash = createHash(hashFunction);
393 hash.update(url);
394 const digest = hash.digest(hashDigest).slice(0, hashDigestLength);
395 return `${folder.slice(-50)}/${`${basename}${
396 query ? `_${query}` : ""
397 }`.slice(0, 150)}_${digest}${ext}`;
398 };
399
400 const getLockfile = cachedWithoutKey(
401 /**
402 * @param {function((Error | null)=, Lockfile=): void} callback callback
403 * @returns {void}
404 */
405 callback => {
406 const readLockfile = () => {
407 intermediateFs.readFile(lockfileLocation, (err, buffer) => {
408 if (err && err.code !== "ENOENT") {
409 compilation.missingDependencies.add(lockfileLocation);
410 return callback(err);
411 }
412 compilation.fileDependencies.add(lockfileLocation);
413 compilation.fileSystemInfo.createSnapshot(
414 compiler.fsStartTime,
415 buffer ? [lockfileLocation] : [],
416 [],
417 buffer ? [] : [lockfileLocation],
418 { timestamp: true },
419 (err, snapshot) => {
420 if (err) return callback(err);
421 const lockfile = buffer
422 ? Lockfile.parse(buffer.toString("utf-8"))
423 : new Lockfile();
424 lockfileCache = {
425 lockfile,
426 snapshot
427 };
428 callback(null, lockfile);
429 }
430 );
431 });
432 };
433 if (lockfileCache) {
434 compilation.fileSystemInfo.checkSnapshotValid(
435 lockfileCache.snapshot,
436 (err, valid) => {
437 if (err) return callback(err);
438 if (!valid) return readLockfile();
439 callback(null, lockfileCache.lockfile);
440 }
441 );
442 } else {
443 readLockfile();
444 }
445 }
446 );
447
448 /** @type {Map<string, LockfileEntry | "ignore" | "no-cache"> | undefined} */
449 let lockfileUpdates = undefined;
450 const storeLockEntry = (lockfile, url, entry) => {
451 const oldEntry = lockfile.entries.get(url);
452 if (lockfileUpdates === undefined) lockfileUpdates = new Map();
453 lockfileUpdates.set(url, entry);
454 lockfile.entries.set(url, entry);
455 if (!oldEntry) {
456 logger.log(`${url} added to lockfile`);
457 } else if (typeof oldEntry === "string") {
458 if (typeof entry === "string") {
459 logger.log(`${url} updated in lockfile: ${oldEntry} -> ${entry}`);
460 } else {
461 logger.log(
462 `${url} updated in lockfile: ${oldEntry} -> ${entry.resolved}`
463 );
464 }
465 } else if (typeof entry === "string") {
466 logger.log(
467 `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry}`
468 );
469 } else if (oldEntry.resolved !== entry.resolved) {
470 logger.log(
471 `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry.resolved}`
472 );
473 } else if (oldEntry.integrity !== entry.integrity) {
474 logger.log(`${url} updated in lockfile: content changed`);
475 } else if (oldEntry.contentType !== entry.contentType) {
476 logger.log(
477 `${url} updated in lockfile: ${oldEntry.contentType} -> ${entry.contentType}`
478 );
479 } else {
480 logger.log(`${url} updated in lockfile`);
481 }
482 };
483
484 const storeResult = (lockfile, url, result, callback) => {
485 if (result.storeLock) {
486 storeLockEntry(lockfile, url, result.entry);
487 if (!cacheLocation || !result.content)
488 return callback(null, result);
489 const key = getCacheKey(result.entry.resolved);
490 const filePath = join(intermediateFs, cacheLocation, key);
491 mkdirp(intermediateFs, dirname(intermediateFs, filePath), err => {
492 if (err) return callback(err);
493 intermediateFs.writeFile(filePath, result.content, err => {
494 if (err) return callback(err);
495 callback(null, result);
496 });
497 });
498 } else {
499 storeLockEntry(lockfile, url, "no-cache");
500 callback(null, result);
501 }
502 };
503
504 for (const { scheme, fetch } of schemes) {
505 /**
506 *
507 * @param {string} url URL
508 * @param {string} integrity integrity
509 * @param {function((Error | null)=, { entry: LockfileEntry, content: Buffer, storeLock: boolean }=): void} callback callback
510 */
511 const resolveContent = (url, integrity, callback) => {
512 const handleResult = (err, result) => {
513 if (err) return callback(err);
514 if ("location" in result) {
515 return resolveContent(
516 result.location,
517 integrity,
518 (err, innerResult) => {
519 if (err) return callback(err);
520 callback(null, {
521 entry: innerResult.entry,
522 content: innerResult.content,
523 storeLock: innerResult.storeLock && result.storeLock
524 });
525 }
526 );
527 } else {
528 if (
529 !result.fresh &&
530 integrity &&
531 result.entry.integrity !== integrity &&
532 !verifyIntegrity(result.content, integrity)
533 ) {
534 return fetchContent.force(url, handleResult);
535 }
536 return callback(null, {
537 entry: result.entry,
538 content: result.content,
539 storeLock: result.storeLock
540 });
541 }
542 };
543 fetchContent(url, handleResult);
544 };
545
546 /** @typedef {{ storeCache: boolean, storeLock: boolean, validUntil: number, etag: string | undefined, fresh: boolean }} FetchResultMeta */
547 /** @typedef {FetchResultMeta & { location: string }} RedirectFetchResult */
548 /** @typedef {FetchResultMeta & { entry: LockfileEntry, content: Buffer }} ContentFetchResult */
549 /** @typedef {RedirectFetchResult | ContentFetchResult} FetchResult */
550
551 /**
552 * @param {string} url URL
553 * @param {FetchResult | RedirectFetchResult} cachedResult result from cache
554 * @param {function((Error | null)=, FetchResult=): void} callback callback
555 * @returns {void}
556 */
557 const fetchContentRaw = (url, cachedResult, callback) => {
558 const requestTime = Date.now();
559 fetch(
560 new URL(url),
561 {
562 headers: {
563 "accept-encoding": "gzip, deflate, br",
564 "user-agent": "webpack",
565 "if-none-match": cachedResult
566 ? cachedResult.etag || null
567 : null
568 }
569 },
570 res => {
571 const etag = res.headers["etag"];
572 const location = res.headers["location"];
573 const cacheControl = res.headers["cache-control"];
574 const { storeLock, storeCache, validUntil } = parseCacheControl(
575 cacheControl,
576 requestTime
577 );
578 /**
579 * @param {Partial<Pick<FetchResultMeta, "fresh">> & (Pick<RedirectFetchResult, "location"> | Pick<ContentFetchResult, "content" | "entry">)} partialResult result
580 * @returns {void}
581 */
582 const finishWith = partialResult => {
583 if ("location" in partialResult) {
584 logger.debug(
585 `GET ${url} [${res.statusCode}] -> ${partialResult.location}`
586 );
587 } else {
588 logger.debug(
589 `GET ${url} [${res.statusCode}] ${Math.ceil(
590 partialResult.content.length / 1024
591 )} kB${!storeLock ? " no-cache" : ""}`
592 );
593 }
594 const result = {
595 ...partialResult,
596 fresh: true,
597 storeLock,
598 storeCache,
599 validUntil,
600 etag
601 };
602 if (!storeCache) {
603 logger.log(
604 `${url} can't be stored in cache, due to Cache-Control header: ${cacheControl}`
605 );
606 return callback(null, result);
607 }
608 cache.store(
609 url,
610 null,
611 {
612 ...result,
613 fresh: false
614 },
615 err => {
616 if (err) {
617 logger.warn(
618 `${url} can't be stored in cache: ${err.message}`
619 );
620 logger.debug(err.stack);
621 }
622 callback(null, result);
623 }
624 );
625 };
626 if (res.statusCode === 304) {
627 if (
628 cachedResult.validUntil < validUntil ||
629 cachedResult.storeLock !== storeLock ||
630 cachedResult.storeCache !== storeCache ||
631 cachedResult.etag !== etag
632 ) {
633 return finishWith(cachedResult);
634 } else {
635 logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
636 return callback(null, {
637 ...cachedResult,
638 fresh: true
639 });
640 }
641 }
642 if (
643 location &&
644 res.statusCode >= 301 &&
645 res.statusCode <= 308
646 ) {
647 const result = {
648 location: new URL(location, url).href
649 };
650 if (
651 !cachedResult ||
652 !("location" in cachedResult) ||
653 cachedResult.location !== result.location ||
654 cachedResult.validUntil < validUntil ||
655 cachedResult.storeLock !== storeLock ||
656 cachedResult.storeCache !== storeCache ||
657 cachedResult.etag !== etag
658 ) {
659 return finishWith(result);
660 } else {
661 logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
662 return callback(null, {
663 ...result,
664 fresh: true,
665 storeLock,
666 storeCache,
667 validUntil,
668 etag
669 });
670 }
671 }
672 const contentType = res.headers["content-type"] || "";
673 const bufferArr = [];
674
675 const contentEncoding = res.headers["content-encoding"];
676 let stream = res;
677 if (contentEncoding === "gzip") {
678 stream = stream.pipe(createGunzip());
679 } else if (contentEncoding === "br") {
680 stream = stream.pipe(createBrotliDecompress());
681 } else if (contentEncoding === "deflate") {
682 stream = stream.pipe(createInflate());
683 }
684
685 stream.on("data", chunk => {
686 bufferArr.push(chunk);
687 });
688
689 stream.on("end", () => {
690 if (!res.complete) {
691 logger.log(`GET ${url} [${res.statusCode}] (terminated)`);
692 return callback(new Error(`${url} request was terminated`));
693 }
694
695 const content = Buffer.concat(bufferArr);
696
697 if (res.statusCode !== 200) {
698 logger.log(`GET ${url} [${res.statusCode}]`);
699 return callback(
700 new Error(
701 `${url} request status code = ${
702 res.statusCode
703 }\n${content.toString("utf-8")}`
704 )
705 );
706 }
707
708 const integrity = computeIntegrity(content);
709 const entry = { resolved: url, integrity, contentType };
710
711 finishWith({
712 entry,
713 content
714 });
715 });
716 }
717 ).on("error", err => {
718 logger.log(`GET ${url} (error)`);
719 err.message += `\nwhile fetching ${url}`;
720 callback(err);
721 });
722 };
723
724 const fetchContent = cachedWithKey(
725 /**
726 * @param {string} url URL
727 * @param {function((Error | null)=, { validUntil: number, etag?: string, entry: LockfileEntry, content: Buffer, fresh: boolean } | { validUntil: number, etag?: string, location: string, fresh: boolean }=): void} callback callback
728 * @returns {void}
729 */ (url, callback) => {
730 cache.get(url, null, (err, cachedResult) => {
731 if (err) return callback(err);
732 if (cachedResult) {
733 const isValid = cachedResult.validUntil >= Date.now();
734 if (isValid) return callback(null, cachedResult);
735 }
736 fetchContentRaw(url, cachedResult, callback);
737 });
738 },
739 (url, callback) => fetchContentRaw(url, undefined, callback)
740 );
741
742 const isAllowed = uri => {
743 for (const allowed of allowedUris) {
744 if (typeof allowed === "string") {
745 if (uri.startsWith(allowed)) return true;
746 } else if (typeof allowed === "function") {
747 if (allowed(uri)) return true;
748 } else {
749 if (allowed.test(uri)) return true;
750 }
751 }
752 return false;
753 };
754
755 const getInfo = cachedWithKey(
756 /**
757 * @param {string} url the url
758 * @param {function((Error | null)=, { entry: LockfileEntry, content: Buffer }=): void} callback callback
759 * @returns {void}
760 */
761 (url, callback) => {
762 if (!isAllowed(url)) {
763 return callback(
764 new Error(
765 `${url} doesn't match the allowedUris policy. These URIs are allowed:\n${allowedUris
766 .map(uri => ` - ${uri}`)
767 .join("\n")}`
768 )
769 );
770 }
771 getLockfile((err, lockfile) => {
772 if (err) return callback(err);
773 const entryOrString = lockfile.entries.get(url);
774 if (!entryOrString) {
775 if (frozen) {
776 return callback(
777 new Error(
778 `${url} has no lockfile entry and lockfile is frozen`
779 )
780 );
781 }
782 resolveContent(url, null, (err, result) => {
783 if (err) return callback(err);
784 storeResult(lockfile, url, result, callback);
785 });
786 return;
787 }
788 if (typeof entryOrString === "string") {
789 const entryTag = entryOrString;
790 resolveContent(url, null, (err, result) => {
791 if (err) return callback(err);
792 if (!result.storeLock || entryTag === "ignore")
793 return callback(null, result);
794 if (frozen) {
795 return callback(
796 new Error(
797 `${url} used to have ${entryTag} lockfile entry and has content now, but lockfile is frozen`
798 )
799 );
800 }
801 if (!upgrade) {
802 return callback(
803 new Error(
804 `${url} used to have ${entryTag} lockfile entry and has content now.
805This should be reflected in the lockfile, so this lockfile entry must be upgraded, but upgrading is not enabled.
806Remove this line from the lockfile to force upgrading.`
807 )
808 );
809 }
810 storeResult(lockfile, url, result, callback);
811 });
812 return;
813 }
814 let entry = entryOrString;
815 const doFetch = lockedContent => {
816 resolveContent(url, entry.integrity, (err, result) => {
817 if (err) {
818 if (lockedContent) {
819 logger.warn(
820 `Upgrade request to ${url} failed: ${err.message}`
821 );
822 logger.debug(err.stack);
823 return callback(null, {
824 entry,
825 content: lockedContent
826 });
827 }
828 return callback(err);
829 }
830 if (!result.storeLock) {
831 // When the lockfile entry should be no-cache
832 // we need to update the lockfile
833 if (frozen) {
834 return callback(
835 new Error(
836 `${url} has a lockfile entry and is no-cache now, but lockfile is frozen\nLockfile: ${entryToString(
837 entry
838 )}`
839 )
840 );
841 }
842 storeResult(lockfile, url, result, callback);
843 return;
844 }
845 if (!areLockfileEntriesEqual(result.entry, entry)) {
846 // When the lockfile entry is outdated
847 // we need to update the lockfile
848 if (frozen) {
849 return callback(
850 new Error(
851 `${url} has an outdated lockfile entry, but lockfile is frozen\nLockfile: ${entryToString(
852 entry
853 )}\nExpected: ${entryToString(result.entry)}`
854 )
855 );
856 }
857 storeResult(lockfile, url, result, callback);
858 return;
859 }
860 if (!lockedContent && cacheLocation) {
861 // When the lockfile cache content is missing
862 // we need to update the lockfile
863 if (frozen) {
864 return callback(
865 new Error(
866 `${url} is missing content in the lockfile cache, but lockfile is frozen\nLockfile: ${entryToString(
867 entry
868 )}`
869 )
870 );
871 }
872 storeResult(lockfile, url, result, callback);
873 return;
874 }
875 return callback(null, result);
876 });
877 };
878 if (cacheLocation) {
879 // When there is a lockfile cache
880 // we read the content from there
881 const key = getCacheKey(entry.resolved);
882 const filePath = join(intermediateFs, cacheLocation, key);
883 fs.readFile(filePath, (err, result) => {
884 const content = /** @type {Buffer} */ (result);
885 if (err) {
886 if (err.code === "ENOENT") return doFetch();
887 return callback(err);
888 }
889 const continueWithCachedContent = result => {
890 if (!upgrade) {
891 // When not in upgrade mode, we accept the result from the lockfile cache
892 return callback(null, { entry, content });
893 }
894 return doFetch(content);
895 };
896 if (!verifyIntegrity(content, entry.integrity)) {
897 let contentWithChangedEol;
898 let isEolChanged = false;
899 try {
900 contentWithChangedEol = Buffer.from(
901 content.toString("utf-8").replace(/\r\n/g, "\n")
902 );
903 isEolChanged = verifyIntegrity(
904 contentWithChangedEol,
905 entry.integrity
906 );
907 } catch (e) {
908 // ignore
909 }
910 if (isEolChanged) {
911 if (!warnedAboutEol) {
912 const explainer = `Incorrect end of line sequence was detected in the lockfile cache.
913The lockfile cache is protected by integrity checks, so any external modification will lead to a corrupted lockfile cache.
914When using git make sure to configure .gitattributes correctly for the lockfile cache:
915 **/*webpack.lock.data/** -text
916This will avoid that the end of line sequence is changed by git on Windows.`;
917 if (frozen) {
918 logger.error(explainer);
919 } else {
920 logger.warn(explainer);
921 logger.info(
922 "Lockfile cache will be automatically fixed now, but when lockfile is frozen this would result in an error."
923 );
924 }
925 warnedAboutEol = true;
926 }
927 if (!frozen) {
928 // "fix" the end of line sequence of the lockfile content
929 logger.log(
930 `${filePath} fixed end of line sequence (\\r\\n instead of \\n).`
931 );
932 intermediateFs.writeFile(
933 filePath,
934 contentWithChangedEol,
935 err => {
936 if (err) return callback(err);
937 continueWithCachedContent(contentWithChangedEol);
938 }
939 );
940 return;
941 }
942 }
943 if (frozen) {
944 return callback(
945 new Error(
946 `${
947 entry.resolved
948 } integrity mismatch, expected content with integrity ${
949 entry.integrity
950 } but got ${computeIntegrity(content)}.
951Lockfile corrupted (${
952 isEolChanged
953 ? "end of line sequence was unexpectedly changed"
954 : "incorrectly merged? changed by other tools?"
955 }).
956Run build with un-frozen lockfile to automatically fix lockfile.`
957 )
958 );
959 } else {
960 // "fix" the lockfile entry to the correct integrity
961 // the content has priority over the integrity value
962 entry = {
963 ...entry,
964 integrity: computeIntegrity(content)
965 };
966 storeLockEntry(lockfile, url, entry);
967 }
968 }
969 continueWithCachedContent(result);
970 });
971 } else {
972 doFetch();
973 }
974 });
975 }
976 );
977
978 const respondWithUrlModule = (url, resourceData, callback) => {
979 getInfo(url.href, (err, result) => {
980 if (err) return callback(err);
981 resourceData.resource = url.href;
982 resourceData.path = url.origin + url.pathname;
983 resourceData.query = url.search;
984 resourceData.fragment = url.hash;
985 resourceData.context = new URL(
986 ".",
987 result.entry.resolved
988 ).href.slice(0, -1);
989 resourceData.data.mimetype = result.entry.contentType;
990 callback(null, true);
991 });
992 };
993 normalModuleFactory.hooks.resolveForScheme
994 .for(scheme)
995 .tapAsync(
996 "HttpUriPlugin",
997 (resourceData, resolveData, callback) => {
998 respondWithUrlModule(
999 new URL(resourceData.resource),
1000 resourceData,
1001 callback
1002 );
1003 }
1004 );
1005 normalModuleFactory.hooks.resolveInScheme
1006 .for(scheme)
1007 .tapAsync("HttpUriPlugin", (resourceData, data, callback) => {
1008 // Only handle relative urls (./xxx, ../xxx, /xxx, //xxx)
1009 if (
1010 data.dependencyType !== "url" &&
1011 !/^\.{0,2}\//.test(resourceData.resource)
1012 ) {
1013 return callback();
1014 }
1015 respondWithUrlModule(
1016 new URL(resourceData.resource, data.context + "/"),
1017 resourceData,
1018 callback
1019 );
1020 });
1021 const hooks = NormalModule.getCompilationHooks(compilation);
1022 hooks.readResourceForScheme
1023 .for(scheme)
1024 .tapAsync("HttpUriPlugin", (resource, module, callback) => {
1025 return getInfo(resource, (err, result) => {
1026 if (err) return callback(err);
1027 module.buildInfo.resourceIntegrity = result.entry.integrity;
1028 callback(null, result.content);
1029 });
1030 });
1031 hooks.needBuild.tapAsync(
1032 "HttpUriPlugin",
1033 (module, context, callback) => {
1034 if (
1035 module.resource &&
1036 module.resource.startsWith(`${scheme}://`)
1037 ) {
1038 getInfo(module.resource, (err, result) => {
1039 if (err) return callback(err);
1040 if (
1041 result.entry.integrity !==
1042 module.buildInfo.resourceIntegrity
1043 ) {
1044 return callback(null, true);
1045 }
1046 callback();
1047 });
1048 } else {
1049 return callback();
1050 }
1051 }
1052 );
1053 }
1054 compilation.hooks.finishModules.tapAsync(
1055 "HttpUriPlugin",
1056 (modules, callback) => {
1057 if (!lockfileUpdates) return callback();
1058 const ext = extname(lockfileLocation);
1059 const tempFile = join(
1060 intermediateFs,
1061 dirname(intermediateFs, lockfileLocation),
1062 `.${basename(lockfileLocation, ext)}.${
1063 (Math.random() * 10000) | 0
1064 }${ext}`
1065 );
1066
1067 const writeDone = () => {
1068 const nextOperation = inProgressWrite.shift();
1069 if (nextOperation) {
1070 nextOperation();
1071 } else {
1072 inProgressWrite = undefined;
1073 }
1074 };
1075 const runWrite = () => {
1076 intermediateFs.readFile(lockfileLocation, (err, buffer) => {
1077 if (err && err.code !== "ENOENT") {
1078 writeDone();
1079 return callback(err);
1080 }
1081 const lockfile = buffer
1082 ? Lockfile.parse(buffer.toString("utf-8"))
1083 : new Lockfile();
1084 for (const [key, value] of lockfileUpdates) {
1085 lockfile.entries.set(key, value);
1086 }
1087 intermediateFs.writeFile(tempFile, lockfile.toString(), err => {
1088 if (err) {
1089 writeDone();
1090 return intermediateFs.unlink(tempFile, () => callback(err));
1091 }
1092 intermediateFs.rename(tempFile, lockfileLocation, err => {
1093 if (err) {
1094 writeDone();
1095 return intermediateFs.unlink(tempFile, () =>
1096 callback(err)
1097 );
1098 }
1099 writeDone();
1100 callback();
1101 });
1102 });
1103 });
1104 };
1105 if (inProgressWrite) {
1106 inProgressWrite.push(runWrite);
1107 } else {
1108 inProgressWrite = [];
1109 runWrite();
1110 }
1111 }
1112 );
1113 }
1114 );
1115 }
1116}
1117
1118module.exports = HttpUriPlugin;