| [9af201e] | 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Tobias Koppers @sokra
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const EventEmitter = require("events");
|
|---|
| 9 | const { basename, extname } = require("path");
|
|---|
| 10 | const {
|
|---|
| 11 | // eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|---|
| 12 | createBrotliDecompress,
|
|---|
| 13 | createGunzip,
|
|---|
| 14 | createInflate
|
|---|
| 15 | } = require("zlib");
|
|---|
| 16 | const NormalModule = require("../NormalModule");
|
|---|
| 17 | const createHash = require("../util/createHash");
|
|---|
| 18 | const { dirname, join, mkdirp } = require("../util/fs");
|
|---|
| 19 | const memoize = require("../util/memoize");
|
|---|
| 20 |
|
|---|
| 21 | /** @typedef {import("http").IncomingMessage} IncomingMessage */
|
|---|
| 22 | /** @typedef {import("http").OutgoingHttpHeaders} OutgoingHttpHeaders */
|
|---|
| 23 | /** @typedef {import("http").RequestOptions} RequestOptions */
|
|---|
| 24 | /** @typedef {import("net").Socket} Socket */
|
|---|
| 25 | /** @typedef {import("stream").Readable} Readable */
|
|---|
| 26 | /** @typedef {import("../../declarations/plugins/schemes/HttpUriPlugin").HttpUriPluginOptions} HttpUriPluginOptions */
|
|---|
| 27 | /** @typedef {import("../Compiler")} Compiler */
|
|---|
| 28 | /** @typedef {import("../FileSystemInfo").Snapshot} Snapshot */
|
|---|
| 29 | /** @typedef {import("../Module").BuildInfo} BuildInfo */
|
|---|
| 30 | /** @typedef {import("../NormalModuleFactory").ResourceDataWithData} ResourceDataWithData */
|
|---|
| 31 | /** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
|
|---|
| 32 |
|
|---|
| 33 | const getHttp = memoize(() => require("http"));
|
|---|
| 34 | const getHttps = memoize(() => require("https"));
|
|---|
| 35 |
|
|---|
| 36 | const MAX_REDIRECTS = 5;
|
|---|
| 37 |
|
|---|
| 38 | /** @typedef {(url: URL, requestOptions: RequestOptions, callback: (incomingMessage: IncomingMessage) => void) => EventEmitter} Fetch */
|
|---|
| 39 |
|
|---|
| 40 | /**
|
|---|
| 41 | * Defines the events map type used by this module.
|
|---|
| 42 | * @typedef {object} EventsMap
|
|---|
| 43 | * @property {[Error]} error
|
|---|
| 44 | */
|
|---|
| 45 |
|
|---|
| 46 | /**
|
|---|
| 47 | * Returns fn.
|
|---|
| 48 | * @param {typeof import("http") | typeof import("https")} request request
|
|---|
| 49 | * @param {string | URL | undefined} proxy proxy
|
|---|
| 50 | * @returns {Fetch} fn
|
|---|
| 51 | */
|
|---|
| 52 | const proxyFetch = (request, proxy) => (url, options, callback) => {
|
|---|
| 53 | /** @type {EventEmitter<EventsMap>} */
|
|---|
| 54 | const eventEmitter = new EventEmitter();
|
|---|
| 55 |
|
|---|
| 56 | /**
|
|---|
| 57 | * Processes the provided socket.
|
|---|
| 58 | * @param {Socket=} socket socket
|
|---|
| 59 | * @returns {void}
|
|---|
| 60 | */
|
|---|
| 61 | const doRequest = (socket) => {
|
|---|
| 62 | request
|
|---|
| 63 | .get(url, { ...options, ...(socket && { socket }) }, callback)
|
|---|
| 64 | .on("error", eventEmitter.emit.bind(eventEmitter, "error"));
|
|---|
| 65 | };
|
|---|
| 66 |
|
|---|
| 67 | if (proxy) {
|
|---|
| 68 | const { hostname: host, port } = new URL(proxy);
|
|---|
| 69 |
|
|---|
| 70 | getHttp()
|
|---|
| 71 | .request({
|
|---|
| 72 | host, // IP address of proxy server
|
|---|
| 73 | port, // port of proxy server
|
|---|
| 74 | method: "CONNECT",
|
|---|
| 75 | path: url.host
|
|---|
| 76 | })
|
|---|
| 77 | .on("connect", (res, socket) => {
|
|---|
| 78 | if (res.statusCode === 200) {
|
|---|
| 79 | // connected to proxy server
|
|---|
| 80 | doRequest(socket);
|
|---|
| 81 | } else {
|
|---|
| 82 | eventEmitter.emit(
|
|---|
| 83 | "error",
|
|---|
| 84 | new Error(
|
|---|
| 85 | `Failed to connect to proxy server "${proxy}": ${res.statusCode} ${res.statusMessage}`
|
|---|
| 86 | )
|
|---|
| 87 | );
|
|---|
| 88 | }
|
|---|
| 89 | })
|
|---|
| 90 | .on("error", (err) => {
|
|---|
| 91 | eventEmitter.emit(
|
|---|
| 92 | "error",
|
|---|
| 93 | new Error(
|
|---|
| 94 | `Failed to connect to proxy server "${proxy}": ${err.message}`
|
|---|
| 95 | )
|
|---|
| 96 | );
|
|---|
| 97 | })
|
|---|
| 98 | .end();
|
|---|
| 99 | } else {
|
|---|
| 100 | doRequest();
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | return eventEmitter;
|
|---|
| 104 | };
|
|---|
| 105 |
|
|---|
| 106 | /** @typedef {() => void} InProgressWriteItem */
|
|---|
| 107 | /** @type {InProgressWriteItem[] | undefined} */
|
|---|
| 108 | let inProgressWrite;
|
|---|
| 109 |
|
|---|
| 110 | /**
|
|---|
| 111 | * Returns safe path.
|
|---|
| 112 | * @param {string} str path
|
|---|
| 113 | * @returns {string} safe path
|
|---|
| 114 | */
|
|---|
| 115 | const toSafePath = (str) =>
|
|---|
| 116 | str.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "").replace(/[^a-z0-9._-]+/gi, "_");
|
|---|
| 117 |
|
|---|
| 118 | /**
|
|---|
| 119 | * Returns integrity.
|
|---|
| 120 | * @param {Buffer} content content
|
|---|
| 121 | * @returns {string} integrity
|
|---|
| 122 | */
|
|---|
| 123 | const computeIntegrity = (content) => {
|
|---|
| 124 | const hash = createHash("sha512");
|
|---|
| 125 | hash.update(content);
|
|---|
| 126 | const integrity = `sha512-${hash.digest("base64")}`;
|
|---|
| 127 | return integrity;
|
|---|
| 128 | };
|
|---|
| 129 |
|
|---|
| 130 | /**
|
|---|
| 131 | * Returns true, if integrity matches.
|
|---|
| 132 | * @param {Buffer} content content
|
|---|
| 133 | * @param {string} integrity integrity
|
|---|
| 134 | * @returns {boolean} true, if integrity matches
|
|---|
| 135 | */
|
|---|
| 136 | const verifyIntegrity = (content, integrity) => {
|
|---|
| 137 | if (integrity === "ignore") return true;
|
|---|
| 138 | return computeIntegrity(content) === integrity;
|
|---|
| 139 | };
|
|---|
| 140 |
|
|---|
| 141 | /**
|
|---|
| 142 | * Parses key value pairs.
|
|---|
| 143 | * @param {string} str input
|
|---|
| 144 | * @returns {Record<string, string>} parsed
|
|---|
| 145 | */
|
|---|
| 146 | const parseKeyValuePairs = (str) => {
|
|---|
| 147 | /** @type {Record<string, string>} */
|
|---|
| 148 | const result = {};
|
|---|
| 149 | for (const item of str.split(",")) {
|
|---|
| 150 | const i = item.indexOf("=");
|
|---|
| 151 | if (i >= 0) {
|
|---|
| 152 | const key = item.slice(0, i).trim();
|
|---|
| 153 | const value = item.slice(i + 1).trim();
|
|---|
| 154 | result[key] = value;
|
|---|
| 155 | } else {
|
|---|
| 156 | const key = item.trim();
|
|---|
| 157 | if (!key) continue;
|
|---|
| 158 | result[key] = key;
|
|---|
| 159 | }
|
|---|
| 160 | }
|
|---|
| 161 | return result;
|
|---|
| 162 | };
|
|---|
| 163 |
|
|---|
| 164 | /**
|
|---|
| 165 | * Parses cache control.
|
|---|
| 166 | * @param {string | undefined} cacheControl Cache-Control header
|
|---|
| 167 | * @param {number} requestTime timestamp of request
|
|---|
| 168 | * @returns {{ storeCache: boolean, storeLock: boolean, validUntil: number }} Logic for storing in cache and lockfile cache
|
|---|
| 169 | */
|
|---|
| 170 | const parseCacheControl = (cacheControl, requestTime) => {
|
|---|
| 171 | // When false resource is not stored in cache
|
|---|
| 172 | let storeCache = true;
|
|---|
| 173 | // When false resource is not stored in lockfile cache
|
|---|
| 174 | let storeLock = true;
|
|---|
| 175 | // Resource is only revalidated, after that timestamp and when upgrade is chosen
|
|---|
| 176 | let validUntil = 0;
|
|---|
| 177 | if (cacheControl) {
|
|---|
| 178 | const parsed = parseKeyValuePairs(cacheControl);
|
|---|
| 179 | if (parsed["no-cache"]) storeCache = storeLock = false;
|
|---|
| 180 | if (parsed["max-age"] && !Number.isNaN(Number(parsed["max-age"]))) {
|
|---|
| 181 | validUntil = requestTime + Number(parsed["max-age"]) * 1000;
|
|---|
| 182 | }
|
|---|
| 183 | if (parsed["must-revalidate"]) validUntil = 0;
|
|---|
| 184 | }
|
|---|
| 185 | return {
|
|---|
| 186 | storeLock,
|
|---|
| 187 | storeCache,
|
|---|
| 188 | validUntil
|
|---|
| 189 | };
|
|---|
| 190 | };
|
|---|
| 191 |
|
|---|
| 192 | /**
|
|---|
| 193 | * Defines the lockfile entry type used by this module.
|
|---|
| 194 | * @typedef {object} LockfileEntry
|
|---|
| 195 | * @property {string} resolved
|
|---|
| 196 | * @property {string} integrity
|
|---|
| 197 | * @property {string} contentType
|
|---|
| 198 | */
|
|---|
| 199 |
|
|---|
| 200 | /**
|
|---|
| 201 | * Are lockfile entries equal.
|
|---|
| 202 | * @param {LockfileEntry} a first lockfile entry
|
|---|
| 203 | * @param {LockfileEntry} b second lockfile entry
|
|---|
| 204 | * @returns {boolean} true when equal, otherwise false
|
|---|
| 205 | */
|
|---|
| 206 | const areLockfileEntriesEqual = (a, b) =>
|
|---|
| 207 | a.resolved === b.resolved &&
|
|---|
| 208 | a.integrity === b.integrity &&
|
|---|
| 209 | a.contentType === b.contentType;
|
|---|
| 210 |
|
|---|
| 211 | /**
|
|---|
| 212 | * Returns , integrity: ${string}, contentType: ${string}`} stringified entry.
|
|---|
| 213 | * @param {LockfileEntry} entry lockfile entry
|
|---|
| 214 | * @returns {`resolved: ${string}, integrity: ${string}, contentType: ${string}`} stringified entry
|
|---|
| 215 | */
|
|---|
| 216 | const entryToString = (entry) =>
|
|---|
| 217 | `resolved: ${entry.resolved}, integrity: ${entry.integrity}, contentType: ${entry.contentType}`;
|
|---|
| 218 |
|
|---|
| 219 | /**
|
|---|
| 220 | * Sanitize URL for inclusion in error messages
|
|---|
| 221 | * @param {string} href URL string to sanitize
|
|---|
| 222 | * @returns {string} sanitized URL text for logs/errors
|
|---|
| 223 | */
|
|---|
| 224 | const sanitizeUrlForError = (href) => {
|
|---|
| 225 | try {
|
|---|
| 226 | const u = new URL(href);
|
|---|
| 227 | return `${u.protocol}//${u.host}`;
|
|---|
| 228 | } catch (_err) {
|
|---|
| 229 | return String(href)
|
|---|
| 230 | .slice(0, 200)
|
|---|
| 231 | .replace(/[\r\n]/g, "");
|
|---|
| 232 | }
|
|---|
| 233 | };
|
|---|
| 234 |
|
|---|
| 235 | class Lockfile {
|
|---|
| 236 | constructor() {
|
|---|
| 237 | /** @type {number} */
|
|---|
| 238 | this.version = 1;
|
|---|
| 239 | /** @type {Map<string, LockfileEntry | "ignore" | "no-cache">} */
|
|---|
| 240 | this.entries = new Map();
|
|---|
| 241 | }
|
|---|
| 242 |
|
|---|
| 243 | /**
|
|---|
| 244 | * Parses the provided source and updates the parser state.
|
|---|
| 245 | * @param {string} content content of the lockfile
|
|---|
| 246 | * @returns {Lockfile} lockfile
|
|---|
| 247 | */
|
|---|
| 248 | static parse(content) {
|
|---|
| 249 | // TODO handle merge conflicts
|
|---|
| 250 | const data = JSON.parse(content);
|
|---|
| 251 | if (data.version !== 1) {
|
|---|
| 252 | throw new Error(`Unsupported lockfile version ${data.version}`);
|
|---|
| 253 | }
|
|---|
| 254 | const lockfile = new Lockfile();
|
|---|
| 255 | for (const key of Object.keys(data)) {
|
|---|
| 256 | if (key === "version") continue;
|
|---|
| 257 | const entry = data[key];
|
|---|
| 258 | lockfile.entries.set(
|
|---|
| 259 | key,
|
|---|
| 260 | typeof entry === "string"
|
|---|
| 261 | ? entry
|
|---|
| 262 | : {
|
|---|
| 263 | resolved: key,
|
|---|
| 264 | ...entry
|
|---|
| 265 | }
|
|---|
| 266 | );
|
|---|
| 267 | }
|
|---|
| 268 | return lockfile;
|
|---|
| 269 | }
|
|---|
| 270 |
|
|---|
| 271 | /**
|
|---|
| 272 | * Returns a string representation.
|
|---|
| 273 | * @returns {string} stringified lockfile
|
|---|
| 274 | */
|
|---|
| 275 | toString() {
|
|---|
| 276 | let str = "{\n";
|
|---|
| 277 | const entries = [...this.entries].sort(([a], [b]) => (a < b ? -1 : 1));
|
|---|
| 278 | for (const [key, entry] of entries) {
|
|---|
| 279 | if (typeof entry === "string") {
|
|---|
| 280 | str += ` ${JSON.stringify(key)}: ${JSON.stringify(entry)},\n`;
|
|---|
| 281 | } else {
|
|---|
| 282 | str += ` ${JSON.stringify(key)}: { `;
|
|---|
| 283 | if (entry.resolved !== key) {
|
|---|
| 284 | str += `"resolved": ${JSON.stringify(entry.resolved)}, `;
|
|---|
| 285 | }
|
|---|
| 286 | str += `"integrity": ${JSON.stringify(
|
|---|
| 287 | entry.integrity
|
|---|
| 288 | )}, "contentType": ${JSON.stringify(entry.contentType)} },\n`;
|
|---|
| 289 | }
|
|---|
| 290 | }
|
|---|
| 291 | str += ` "version": ${this.version}\n}\n`;
|
|---|
| 292 | return str;
|
|---|
| 293 | }
|
|---|
| 294 | }
|
|---|
| 295 |
|
|---|
| 296 | /**
|
|---|
| 297 | * Defines the fn without key callback type used by this module.
|
|---|
| 298 | * @template R
|
|---|
| 299 | * @typedef {(err: Error | null, result?: R) => void} FnWithoutKeyCallback
|
|---|
| 300 | */
|
|---|
| 301 |
|
|---|
| 302 | /**
|
|---|
| 303 | * Defines the fn without key type used by this module.
|
|---|
| 304 | * @template R
|
|---|
| 305 | * @typedef {(callback: FnWithoutKeyCallback<R>) => void} FnWithoutKey
|
|---|
| 306 | */
|
|---|
| 307 |
|
|---|
| 308 | /**
|
|---|
| 309 | * Caches d without key.
|
|---|
| 310 | * @template R
|
|---|
| 311 | * @param {FnWithoutKey<R>} fn function
|
|---|
| 312 | * @returns {FnWithoutKey<R>} cached function
|
|---|
| 313 | */
|
|---|
| 314 | const cachedWithoutKey = (fn) => {
|
|---|
| 315 | let inFlight = false;
|
|---|
| 316 | /** @type {Error | undefined} */
|
|---|
| 317 | let cachedError;
|
|---|
| 318 | /** @type {R | undefined} */
|
|---|
| 319 | let cachedResult;
|
|---|
| 320 | /** @type {FnWithoutKeyCallback<R>[] | undefined} */
|
|---|
| 321 | let cachedCallbacks;
|
|---|
| 322 | return (callback) => {
|
|---|
| 323 | if (inFlight) {
|
|---|
| 324 | if (cachedResult !== undefined) return callback(null, cachedResult);
|
|---|
| 325 | if (cachedError !== undefined) return callback(cachedError);
|
|---|
| 326 | if (cachedCallbacks === undefined) cachedCallbacks = [callback];
|
|---|
| 327 | else cachedCallbacks.push(callback);
|
|---|
| 328 | return;
|
|---|
| 329 | }
|
|---|
| 330 | inFlight = true;
|
|---|
| 331 | fn((err, result) => {
|
|---|
| 332 | if (err) cachedError = err;
|
|---|
| 333 | else cachedResult = result;
|
|---|
| 334 | const callbacks = cachedCallbacks;
|
|---|
| 335 | cachedCallbacks = undefined;
|
|---|
| 336 | callback(err, result);
|
|---|
| 337 | if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
|
|---|
| 338 | });
|
|---|
| 339 | };
|
|---|
| 340 | };
|
|---|
| 341 |
|
|---|
| 342 | /**
|
|---|
| 343 | * Defines the fn with key callback type used by this module.
|
|---|
| 344 | * @template R
|
|---|
| 345 | * @typedef {(err: Error | null, result?: R) => void} FnWithKeyCallback
|
|---|
| 346 | */
|
|---|
| 347 |
|
|---|
| 348 | /**
|
|---|
| 349 | * Defines the fn with key type used by this module.
|
|---|
| 350 | * @template T
|
|---|
| 351 | * @template R
|
|---|
| 352 | * @typedef {(item: T, callback: FnWithKeyCallback<R>) => void} FnWithKey
|
|---|
| 353 | */
|
|---|
| 354 |
|
|---|
| 355 | /**
|
|---|
| 356 | * Returns } cached function.
|
|---|
| 357 | * @template T
|
|---|
| 358 | * @template R
|
|---|
| 359 | * @param {FnWithKey<T, R>} fn function
|
|---|
| 360 | * @param {FnWithKey<T, R>=} forceFn function for the second try
|
|---|
| 361 | * @returns {FnWithKey<T, R> & { force: FnWithKey<T, R> }} cached function
|
|---|
| 362 | */
|
|---|
| 363 | const cachedWithKey = (fn, forceFn = fn) => {
|
|---|
| 364 | /**
|
|---|
| 365 | * Defines the cache entry type used by this module.
|
|---|
| 366 | * @template R
|
|---|
| 367 | * @typedef {{ result?: R, error?: Error, callbacks?: FnWithKeyCallback<R>[], force?: true }} CacheEntry
|
|---|
| 368 | */
|
|---|
| 369 | /** @type {Map<T, CacheEntry<R>>} */
|
|---|
| 370 | const cache = new Map();
|
|---|
| 371 | /**
|
|---|
| 372 | * Processes the provided arg.
|
|---|
| 373 | * @param {T} arg arg
|
|---|
| 374 | * @param {FnWithKeyCallback<R>} callback callback
|
|---|
| 375 | * @returns {void}
|
|---|
| 376 | */
|
|---|
| 377 | const resultFn = (arg, callback) => {
|
|---|
| 378 | const cacheEntry = cache.get(arg);
|
|---|
| 379 | if (cacheEntry !== undefined) {
|
|---|
| 380 | if (cacheEntry.result !== undefined) {
|
|---|
| 381 | return callback(null, cacheEntry.result);
|
|---|
| 382 | }
|
|---|
| 383 | if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
|
|---|
| 384 | if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
|
|---|
| 385 | else cacheEntry.callbacks.push(callback);
|
|---|
| 386 | return;
|
|---|
| 387 | }
|
|---|
| 388 | /** @type {CacheEntry<R>} */
|
|---|
| 389 | const newCacheEntry = {
|
|---|
| 390 | result: undefined,
|
|---|
| 391 | error: undefined,
|
|---|
| 392 | callbacks: undefined
|
|---|
| 393 | };
|
|---|
| 394 | cache.set(arg, newCacheEntry);
|
|---|
| 395 | fn(arg, (err, result) => {
|
|---|
| 396 | if (err) newCacheEntry.error = err;
|
|---|
| 397 | else newCacheEntry.result = result;
|
|---|
| 398 | const callbacks = newCacheEntry.callbacks;
|
|---|
| 399 | newCacheEntry.callbacks = undefined;
|
|---|
| 400 | callback(err, result);
|
|---|
| 401 | if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
|
|---|
| 402 | });
|
|---|
| 403 | };
|
|---|
| 404 | /**
|
|---|
| 405 | * Processes the provided arg.
|
|---|
| 406 | * @param {T} arg arg
|
|---|
| 407 | * @param {FnWithKeyCallback<R>} callback callback
|
|---|
| 408 | * @returns {void}
|
|---|
| 409 | */
|
|---|
| 410 | resultFn.force = (arg, callback) => {
|
|---|
| 411 | const cacheEntry = cache.get(arg);
|
|---|
| 412 | if (cacheEntry !== undefined && cacheEntry.force) {
|
|---|
| 413 | if (cacheEntry.result !== undefined) {
|
|---|
| 414 | return callback(null, cacheEntry.result);
|
|---|
| 415 | }
|
|---|
| 416 | if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
|
|---|
| 417 | if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
|
|---|
| 418 | else cacheEntry.callbacks.push(callback);
|
|---|
| 419 | return;
|
|---|
| 420 | }
|
|---|
| 421 | /** @type {CacheEntry<R>} */
|
|---|
| 422 | const newCacheEntry = {
|
|---|
| 423 | result: undefined,
|
|---|
| 424 | error: undefined,
|
|---|
| 425 | callbacks: undefined,
|
|---|
| 426 | force: true
|
|---|
| 427 | };
|
|---|
| 428 | cache.set(arg, newCacheEntry);
|
|---|
| 429 | forceFn(arg, (err, result) => {
|
|---|
| 430 | if (err) newCacheEntry.error = err;
|
|---|
| 431 | else newCacheEntry.result = result;
|
|---|
| 432 | const callbacks = newCacheEntry.callbacks;
|
|---|
| 433 | newCacheEntry.callbacks = undefined;
|
|---|
| 434 | callback(err, result);
|
|---|
| 435 | if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
|
|---|
| 436 | });
|
|---|
| 437 | };
|
|---|
| 438 | return resultFn;
|
|---|
| 439 | };
|
|---|
| 440 |
|
|---|
| 441 | /**
|
|---|
| 442 | * Defines the lockfile cache type used by this module.
|
|---|
| 443 | * @typedef {object} LockfileCache
|
|---|
| 444 | * @property {Lockfile} lockfile lockfile
|
|---|
| 445 | * @property {Snapshot} snapshot snapshot
|
|---|
| 446 | */
|
|---|
| 447 |
|
|---|
| 448 | /**
|
|---|
| 449 | * Defines the resolve content result type used by this module.
|
|---|
| 450 | * @typedef {object} ResolveContentResult
|
|---|
| 451 | * @property {LockfileEntry} entry lockfile entry
|
|---|
| 452 | * @property {Buffer} content content
|
|---|
| 453 | * @property {boolean} storeLock need store lockfile
|
|---|
| 454 | */
|
|---|
| 455 |
|
|---|
| 456 | /** @typedef {{ storeCache: boolean, storeLock: boolean, validUntil: number, etag: string | undefined, fresh: boolean }} FetchResultMeta */
|
|---|
| 457 | /** @typedef {FetchResultMeta & { location: string }} RedirectFetchResult */
|
|---|
| 458 | /** @typedef {FetchResultMeta & { entry: LockfileEntry, content: Buffer }} ContentFetchResult */
|
|---|
| 459 | /** @typedef {RedirectFetchResult | ContentFetchResult} FetchResult */
|
|---|
| 460 |
|
|---|
| 461 | /** @typedef {(uri: string) => boolean} AllowedUriFn */
|
|---|
| 462 |
|
|---|
| 463 | const PLUGIN_NAME = "HttpUriPlugin";
|
|---|
| 464 |
|
|---|
| 465 | class HttpUriPlugin {
|
|---|
| 466 | /**
|
|---|
| 467 | * Creates an instance of HttpUriPlugin.
|
|---|
| 468 | * @param {HttpUriPluginOptions} options options
|
|---|
| 469 | */
|
|---|
| 470 | constructor(options) {
|
|---|
| 471 | /** @type {HttpUriPluginOptions} */
|
|---|
| 472 | this.options = options;
|
|---|
| 473 | }
|
|---|
| 474 |
|
|---|
| 475 | /**
|
|---|
| 476 | * Applies the plugin by registering its hooks on the compiler.
|
|---|
| 477 | * @param {Compiler} compiler the compiler instance
|
|---|
| 478 | * @returns {void}
|
|---|
| 479 | */
|
|---|
| 480 | apply(compiler) {
|
|---|
| 481 | compiler.hooks.validate.tap(PLUGIN_NAME, () => {
|
|---|
| 482 | compiler.validate(
|
|---|
| 483 | () => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
|
|---|
| 484 | this.options,
|
|---|
| 485 | {
|
|---|
| 486 | name: "Http Uri Plugin",
|
|---|
| 487 | baseDataPath: "options"
|
|---|
| 488 | },
|
|---|
| 489 | (options) =>
|
|---|
| 490 | require("../../schemas/plugins/schemes/HttpUriPlugin.check")(options)
|
|---|
| 491 | );
|
|---|
| 492 | });
|
|---|
| 493 |
|
|---|
| 494 | const proxy =
|
|---|
| 495 | this.options.proxy || process.env.http_proxy || process.env.HTTP_PROXY;
|
|---|
| 496 | /**
|
|---|
| 497 | * @type {{ scheme: "http" | "https", fetch: Fetch }[]}
|
|---|
| 498 | */
|
|---|
| 499 | const schemes = [
|
|---|
| 500 | {
|
|---|
| 501 | scheme: "http",
|
|---|
| 502 | fetch: proxyFetch(getHttp(), proxy)
|
|---|
| 503 | },
|
|---|
| 504 | {
|
|---|
| 505 | scheme: "https",
|
|---|
| 506 | fetch: proxyFetch(getHttps(), proxy)
|
|---|
| 507 | }
|
|---|
| 508 | ];
|
|---|
| 509 | /** @type {LockfileCache} */
|
|---|
| 510 | let lockfileCache;
|
|---|
| 511 | compiler.hooks.compilation.tap(
|
|---|
| 512 | PLUGIN_NAME,
|
|---|
| 513 | (compilation, { normalModuleFactory }) => {
|
|---|
| 514 | const intermediateFs =
|
|---|
| 515 | /** @type {IntermediateFileSystem} */
|
|---|
| 516 | (compiler.intermediateFileSystem);
|
|---|
| 517 | const fs = compilation.inputFileSystem;
|
|---|
| 518 | const cache = compilation.getCache(`webpack.${PLUGIN_NAME}`);
|
|---|
| 519 | const logger = compilation.getLogger(`webpack.${PLUGIN_NAME}`);
|
|---|
| 520 | /** @type {string} */
|
|---|
| 521 | const lockfileLocation =
|
|---|
| 522 | this.options.lockfileLocation ||
|
|---|
| 523 | join(
|
|---|
| 524 | intermediateFs,
|
|---|
| 525 | compiler.context,
|
|---|
| 526 | compiler.name
|
|---|
| 527 | ? `${toSafePath(compiler.name)}.webpack.lock`
|
|---|
| 528 | : "webpack.lock"
|
|---|
| 529 | );
|
|---|
| 530 | /** @type {string | false} */
|
|---|
| 531 | const cacheLocation =
|
|---|
| 532 | this.options.cacheLocation !== undefined
|
|---|
| 533 | ? this.options.cacheLocation
|
|---|
| 534 | : `${lockfileLocation}.data`;
|
|---|
| 535 | const upgrade = this.options.upgrade || false;
|
|---|
| 536 | const frozen = this.options.frozen || false;
|
|---|
| 537 | const hashFunction = "sha512";
|
|---|
| 538 | const hashDigest = "hex";
|
|---|
| 539 | const hashDigestLength = 20;
|
|---|
| 540 | const allowedUris = this.options.allowedUris;
|
|---|
| 541 |
|
|---|
| 542 | let warnedAboutEol = false;
|
|---|
| 543 |
|
|---|
| 544 | /** @type {Map<string, string>} */
|
|---|
| 545 | const cacheKeyCache = new Map();
|
|---|
| 546 | /**
|
|---|
| 547 | * Returns the key.
|
|---|
| 548 | * @param {string} url the url
|
|---|
| 549 | * @returns {string} the key
|
|---|
| 550 | */
|
|---|
| 551 | const getCacheKey = (url) => {
|
|---|
| 552 | const cachedResult = cacheKeyCache.get(url);
|
|---|
| 553 | if (cachedResult !== undefined) return cachedResult;
|
|---|
| 554 | const result = _getCacheKey(url);
|
|---|
| 555 | cacheKeyCache.set(url, result);
|
|---|
| 556 | return result;
|
|---|
| 557 | };
|
|---|
| 558 |
|
|---|
| 559 | /**
|
|---|
| 560 | * Returns the key.
|
|---|
| 561 | * @param {string} url the url
|
|---|
| 562 | * @returns {string} the key
|
|---|
| 563 | */
|
|---|
| 564 | const _getCacheKey = (url) => {
|
|---|
| 565 | const parsedUrl = new URL(url);
|
|---|
| 566 | const folder = toSafePath(parsedUrl.origin);
|
|---|
| 567 | const name = toSafePath(parsedUrl.pathname);
|
|---|
| 568 | const query = toSafePath(parsedUrl.search);
|
|---|
| 569 | let ext = extname(name);
|
|---|
| 570 | if (ext.length > 20) ext = "";
|
|---|
| 571 | const basename = ext ? name.slice(0, -ext.length) : name;
|
|---|
| 572 | const hash = createHash(hashFunction);
|
|---|
| 573 | hash.update(url);
|
|---|
| 574 | const digest = hash.digest(hashDigest).slice(0, hashDigestLength);
|
|---|
| 575 | return `${folder.slice(-50)}/${`${basename}${
|
|---|
| 576 | query ? `_${query}` : ""
|
|---|
| 577 | }`.slice(0, 150)}_${digest}${ext}`;
|
|---|
| 578 | };
|
|---|
| 579 |
|
|---|
| 580 | const getLockfile = cachedWithoutKey(
|
|---|
| 581 | /**
|
|---|
| 582 | * Handles the callback logic for this hook.
|
|---|
| 583 | * @param {(err: Error | null, lockfile?: Lockfile) => void} callback callback
|
|---|
| 584 | * @returns {void}
|
|---|
| 585 | */
|
|---|
| 586 | (callback) => {
|
|---|
| 587 | const readLockfile = () => {
|
|---|
| 588 | intermediateFs.readFile(lockfileLocation, (err, buffer) => {
|
|---|
| 589 | if (err && err.code !== "ENOENT") {
|
|---|
| 590 | compilation.missingDependencies.add(lockfileLocation);
|
|---|
| 591 | return callback(err);
|
|---|
| 592 | }
|
|---|
| 593 | compilation.fileDependencies.add(lockfileLocation);
|
|---|
| 594 | compilation.fileSystemInfo.createSnapshot(
|
|---|
| 595 | compiler.fsStartTime,
|
|---|
| 596 | buffer ? [lockfileLocation] : [],
|
|---|
| 597 | [],
|
|---|
| 598 | buffer ? [] : [lockfileLocation],
|
|---|
| 599 | { timestamp: true },
|
|---|
| 600 | (err, s) => {
|
|---|
| 601 | if (err) return callback(err);
|
|---|
| 602 | const lockfile = buffer
|
|---|
| 603 | ? Lockfile.parse(buffer.toString("utf8"))
|
|---|
| 604 | : new Lockfile();
|
|---|
| 605 | lockfileCache = {
|
|---|
| 606 | lockfile,
|
|---|
| 607 | snapshot: /** @type {Snapshot} */ (s)
|
|---|
| 608 | };
|
|---|
| 609 | callback(null, lockfile);
|
|---|
| 610 | }
|
|---|
| 611 | );
|
|---|
| 612 | });
|
|---|
| 613 | };
|
|---|
| 614 | if (lockfileCache) {
|
|---|
| 615 | compilation.fileSystemInfo.checkSnapshotValid(
|
|---|
| 616 | lockfileCache.snapshot,
|
|---|
| 617 | (err, valid) => {
|
|---|
| 618 | if (err) return callback(err);
|
|---|
| 619 | if (!valid) return readLockfile();
|
|---|
| 620 | callback(null, lockfileCache.lockfile);
|
|---|
| 621 | }
|
|---|
| 622 | );
|
|---|
| 623 | } else {
|
|---|
| 624 | readLockfile();
|
|---|
| 625 | }
|
|---|
| 626 | }
|
|---|
| 627 | );
|
|---|
| 628 |
|
|---|
| 629 | /** @typedef {Map<string, LockfileEntry | "ignore" | "no-cache">} LockfileUpdates */
|
|---|
| 630 |
|
|---|
| 631 | /** @type {LockfileUpdates | undefined} */
|
|---|
| 632 | let lockfileUpdates;
|
|---|
| 633 |
|
|---|
| 634 | /**
|
|---|
| 635 | * Stores the provided lockfile.
|
|---|
| 636 | * @param {Lockfile} lockfile lockfile instance
|
|---|
| 637 | * @param {string} url url to store
|
|---|
| 638 | * @param {LockfileEntry | "ignore" | "no-cache"} entry lockfile entry
|
|---|
| 639 | */
|
|---|
| 640 | const storeLockEntry = (lockfile, url, entry) => {
|
|---|
| 641 | const oldEntry = lockfile.entries.get(url);
|
|---|
| 642 | if (lockfileUpdates === undefined) lockfileUpdates = new Map();
|
|---|
| 643 | lockfileUpdates.set(url, entry);
|
|---|
| 644 | lockfile.entries.set(url, entry);
|
|---|
| 645 | if (!oldEntry) {
|
|---|
| 646 | logger.log(`${url} added to lockfile`);
|
|---|
| 647 | } else if (typeof oldEntry === "string") {
|
|---|
| 648 | if (typeof entry === "string") {
|
|---|
| 649 | logger.log(`${url} updated in lockfile: ${oldEntry} -> ${entry}`);
|
|---|
| 650 | } else {
|
|---|
| 651 | logger.log(
|
|---|
| 652 | `${url} updated in lockfile: ${oldEntry} -> ${entry.resolved}`
|
|---|
| 653 | );
|
|---|
| 654 | }
|
|---|
| 655 | } else if (typeof entry === "string") {
|
|---|
| 656 | logger.log(
|
|---|
| 657 | `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry}`
|
|---|
| 658 | );
|
|---|
| 659 | } else if (oldEntry.resolved !== entry.resolved) {
|
|---|
| 660 | logger.log(
|
|---|
| 661 | `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry.resolved}`
|
|---|
| 662 | );
|
|---|
| 663 | } else if (oldEntry.integrity !== entry.integrity) {
|
|---|
| 664 | logger.log(`${url} updated in lockfile: content changed`);
|
|---|
| 665 | } else if (oldEntry.contentType !== entry.contentType) {
|
|---|
| 666 | logger.log(
|
|---|
| 667 | `${url} updated in lockfile: ${oldEntry.contentType} -> ${entry.contentType}`
|
|---|
| 668 | );
|
|---|
| 669 | } else {
|
|---|
| 670 | logger.log(`${url} updated in lockfile`);
|
|---|
| 671 | }
|
|---|
| 672 | };
|
|---|
| 673 |
|
|---|
| 674 | /**
|
|---|
| 675 | * Stores the provided lockfile.
|
|---|
| 676 | * @param {Lockfile} lockfile lockfile
|
|---|
| 677 | * @param {string} url url
|
|---|
| 678 | * @param {ResolveContentResult} result result
|
|---|
| 679 | * @param {(err: Error | null, result?: ResolveContentResult) => void} callback callback
|
|---|
| 680 | * @returns {void}
|
|---|
| 681 | */
|
|---|
| 682 | const storeResult = (lockfile, url, result, callback) => {
|
|---|
| 683 | if (result.storeLock) {
|
|---|
| 684 | storeLockEntry(lockfile, url, result.entry);
|
|---|
| 685 | if (!cacheLocation || !result.content) {
|
|---|
| 686 | return callback(null, result);
|
|---|
| 687 | }
|
|---|
| 688 | const key = getCacheKey(result.entry.resolved);
|
|---|
| 689 | const filePath = join(intermediateFs, cacheLocation, key);
|
|---|
| 690 | mkdirp(intermediateFs, dirname(intermediateFs, filePath), (err) => {
|
|---|
| 691 | if (err) return callback(err);
|
|---|
| 692 | intermediateFs.writeFile(filePath, result.content, (err) => {
|
|---|
| 693 | if (err) return callback(err);
|
|---|
| 694 | callback(null, result);
|
|---|
| 695 | });
|
|---|
| 696 | });
|
|---|
| 697 | } else {
|
|---|
| 698 | storeLockEntry(lockfile, url, "no-cache");
|
|---|
| 699 | callback(null, result);
|
|---|
| 700 | }
|
|---|
| 701 | };
|
|---|
| 702 |
|
|---|
| 703 | for (const { scheme, fetch } of schemes) {
|
|---|
| 704 | /**
|
|---|
| 705 | * Validate redirect location.
|
|---|
| 706 | * @param {string} location Location header value (relative or absolute)
|
|---|
| 707 | * @param {string} base current absolute URL
|
|---|
| 708 | * @returns {string} absolute, validated redirect target
|
|---|
| 709 | */
|
|---|
| 710 | const validateRedirectLocation = (location, base) => {
|
|---|
| 711 | /** @type {URL} */
|
|---|
| 712 | let nextUrl;
|
|---|
| 713 | try {
|
|---|
| 714 | nextUrl = new URL(location, base);
|
|---|
| 715 | } catch (err) {
|
|---|
| 716 | throw new Error(
|
|---|
| 717 | `Invalid redirect URL: ${sanitizeUrlForError(location)}`,
|
|---|
| 718 | { cause: err }
|
|---|
| 719 | );
|
|---|
| 720 | }
|
|---|
| 721 | if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") {
|
|---|
| 722 | throw new Error(
|
|---|
| 723 | `Redirected URL uses disallowed protocol: ${sanitizeUrlForError(nextUrl.href)}`
|
|---|
| 724 | );
|
|---|
| 725 | }
|
|---|
| 726 | if (!isAllowed(nextUrl.href)) {
|
|---|
| 727 | throw new Error(
|
|---|
| 728 | `${nextUrl.href} doesn't match the allowedUris policy after redirect. These URIs are allowed:\n${allowedUris
|
|---|
| 729 | .map((uri) => ` - ${uri}`)
|
|---|
| 730 | .join("\n")}`
|
|---|
| 731 | );
|
|---|
| 732 | }
|
|---|
| 733 | return nextUrl.href;
|
|---|
| 734 | };
|
|---|
| 735 | /**
|
|---|
| 736 | * Processes the provided url.
|
|---|
| 737 | * @param {string} url URL
|
|---|
| 738 | * @param {string | null} integrity integrity
|
|---|
| 739 | * @param {(err: Error | null, resolveContentResult?: ResolveContentResult) => void} callback callback
|
|---|
| 740 | * @param {number=} redirectCount number of followed redirects
|
|---|
| 741 | */
|
|---|
| 742 | const resolveContent = (
|
|---|
| 743 | url,
|
|---|
| 744 | integrity,
|
|---|
| 745 | callback,
|
|---|
| 746 | redirectCount = 0
|
|---|
| 747 | ) => {
|
|---|
| 748 | /**
|
|---|
| 749 | * Processes the provided err.
|
|---|
| 750 | * @param {Error | null} err error
|
|---|
| 751 | * @param {FetchResult=} _result fetch result
|
|---|
| 752 | * @returns {void}
|
|---|
| 753 | */
|
|---|
| 754 | const handleResult = (err, _result) => {
|
|---|
| 755 | if (err) return callback(err);
|
|---|
| 756 |
|
|---|
| 757 | const result = /** @type {FetchResult} */ (_result);
|
|---|
| 758 |
|
|---|
| 759 | if ("location" in result) {
|
|---|
| 760 | // Validate redirect target before following
|
|---|
| 761 | /** @type {string} */
|
|---|
| 762 | let absolute;
|
|---|
| 763 | try {
|
|---|
| 764 | absolute = validateRedirectLocation(result.location, url);
|
|---|
| 765 | } catch (err_) {
|
|---|
| 766 | return callback(/** @type {Error} */ (err_));
|
|---|
| 767 | }
|
|---|
| 768 | if (redirectCount >= MAX_REDIRECTS) {
|
|---|
| 769 | return callback(new Error("Too many redirects"));
|
|---|
| 770 | }
|
|---|
| 771 | return resolveContent(
|
|---|
| 772 | absolute,
|
|---|
| 773 | integrity,
|
|---|
| 774 | (err, innerResult) => {
|
|---|
| 775 | if (err) return callback(err);
|
|---|
| 776 | const { entry, content, storeLock } =
|
|---|
| 777 | /** @type {ResolveContentResult} */ (innerResult);
|
|---|
| 778 | callback(null, {
|
|---|
| 779 | entry,
|
|---|
| 780 | content,
|
|---|
| 781 | storeLock: storeLock && result.storeLock
|
|---|
| 782 | });
|
|---|
| 783 | },
|
|---|
| 784 | redirectCount + 1
|
|---|
| 785 | );
|
|---|
| 786 | }
|
|---|
| 787 |
|
|---|
| 788 | if (
|
|---|
| 789 | !result.fresh &&
|
|---|
| 790 | integrity &&
|
|---|
| 791 | result.entry.integrity !== integrity &&
|
|---|
| 792 | !verifyIntegrity(result.content, integrity)
|
|---|
| 793 | ) {
|
|---|
| 794 | return fetchContent.force(url, handleResult);
|
|---|
| 795 | }
|
|---|
| 796 |
|
|---|
| 797 | return callback(null, {
|
|---|
| 798 | entry: result.entry,
|
|---|
| 799 | content: result.content,
|
|---|
| 800 | storeLock: result.storeLock
|
|---|
| 801 | });
|
|---|
| 802 | };
|
|---|
| 803 |
|
|---|
| 804 | fetchContent(url, handleResult);
|
|---|
| 805 | };
|
|---|
| 806 |
|
|---|
| 807 | /**
|
|---|
| 808 | * Processes the provided url.
|
|---|
| 809 | * @param {string} url URL
|
|---|
| 810 | * @param {FetchResult | RedirectFetchResult | undefined} cachedResult result from cache
|
|---|
| 811 | * @param {(err: Error | null, fetchResult?: FetchResult) => void} callback callback
|
|---|
| 812 | * @returns {void}
|
|---|
| 813 | */
|
|---|
| 814 | const fetchContentRaw = (url, cachedResult, callback) => {
|
|---|
| 815 | const requestTime = Date.now();
|
|---|
| 816 | /** @type {OutgoingHttpHeaders} */
|
|---|
| 817 | const headers = {
|
|---|
| 818 | "accept-encoding": "gzip, deflate, br",
|
|---|
| 819 | "user-agent": "webpack"
|
|---|
| 820 | };
|
|---|
| 821 |
|
|---|
| 822 | if (cachedResult && cachedResult.etag) {
|
|---|
| 823 | headers["if-none-match"] = cachedResult.etag;
|
|---|
| 824 | }
|
|---|
| 825 |
|
|---|
| 826 | fetch(new URL(url), { headers }, (res) => {
|
|---|
| 827 | const etag = res.headers.etag;
|
|---|
| 828 | const location = res.headers.location;
|
|---|
| 829 | const cacheControl = res.headers["cache-control"];
|
|---|
| 830 | const { storeLock, storeCache, validUntil } = parseCacheControl(
|
|---|
| 831 | cacheControl,
|
|---|
| 832 | requestTime
|
|---|
| 833 | );
|
|---|
| 834 | /**
|
|---|
| 835 | * Processes the provided partial result.
|
|---|
| 836 | * @param {Partial<Pick<FetchResultMeta, "fresh">> & (Pick<RedirectFetchResult, "location"> | Pick<ContentFetchResult, "content" | "entry">)} partialResult result
|
|---|
| 837 | * @returns {void}
|
|---|
| 838 | */
|
|---|
| 839 | const finishWith = (partialResult) => {
|
|---|
| 840 | if ("location" in partialResult) {
|
|---|
| 841 | logger.debug(
|
|---|
| 842 | `GET ${url} [${res.statusCode}] -> ${partialResult.location}`
|
|---|
| 843 | );
|
|---|
| 844 | } else {
|
|---|
| 845 | logger.debug(
|
|---|
| 846 | `GET ${url} [${res.statusCode}] ${Math.ceil(
|
|---|
| 847 | partialResult.content.length / 1024
|
|---|
| 848 | )} kB${!storeLock ? " no-cache" : ""}`
|
|---|
| 849 | );
|
|---|
| 850 | }
|
|---|
| 851 | const result = {
|
|---|
| 852 | ...partialResult,
|
|---|
| 853 | fresh: true,
|
|---|
| 854 | storeLock,
|
|---|
| 855 | storeCache,
|
|---|
| 856 | validUntil,
|
|---|
| 857 | etag
|
|---|
| 858 | };
|
|---|
| 859 | if (!storeCache) {
|
|---|
| 860 | logger.log(
|
|---|
| 861 | `${url} can't be stored in cache, due to Cache-Control header: ${cacheControl}`
|
|---|
| 862 | );
|
|---|
| 863 | return callback(null, result);
|
|---|
| 864 | }
|
|---|
| 865 | cache.store(
|
|---|
| 866 | url,
|
|---|
| 867 | null,
|
|---|
| 868 | {
|
|---|
| 869 | ...result,
|
|---|
| 870 | fresh: false
|
|---|
| 871 | },
|
|---|
| 872 | (err) => {
|
|---|
| 873 | if (err) {
|
|---|
| 874 | logger.warn(
|
|---|
| 875 | `${url} can't be stored in cache: ${err.message}`
|
|---|
| 876 | );
|
|---|
| 877 | logger.debug(err.stack);
|
|---|
| 878 | }
|
|---|
| 879 | callback(null, result);
|
|---|
| 880 | }
|
|---|
| 881 | );
|
|---|
| 882 | };
|
|---|
| 883 | if (res.statusCode === 304) {
|
|---|
| 884 | const result = /** @type {FetchResult} */ (cachedResult);
|
|---|
| 885 | if (
|
|---|
| 886 | result.validUntil < validUntil ||
|
|---|
| 887 | result.storeLock !== storeLock ||
|
|---|
| 888 | result.storeCache !== storeCache ||
|
|---|
| 889 | result.etag !== etag
|
|---|
| 890 | ) {
|
|---|
| 891 | return finishWith(result);
|
|---|
| 892 | }
|
|---|
| 893 | logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
|
|---|
| 894 | return callback(null, { ...result, fresh: true });
|
|---|
| 895 | }
|
|---|
| 896 | if (
|
|---|
| 897 | location &&
|
|---|
| 898 | res.statusCode &&
|
|---|
| 899 | res.statusCode >= 301 &&
|
|---|
| 900 | res.statusCode <= 308
|
|---|
| 901 | ) {
|
|---|
| 902 | /** @type {string} */
|
|---|
| 903 | let absolute;
|
|---|
| 904 | try {
|
|---|
| 905 | absolute = validateRedirectLocation(location, url);
|
|---|
| 906 | } catch (err) {
|
|---|
| 907 | logger.log(
|
|---|
| 908 | `GET ${url} [${res.statusCode}] -> ${String(location)} (rejected: ${/** @type {Error} */ (err).message})`
|
|---|
| 909 | );
|
|---|
| 910 | return callback(/** @type {Error} */ (err));
|
|---|
| 911 | }
|
|---|
| 912 | const result = { location: absolute };
|
|---|
| 913 | if (
|
|---|
| 914 | !cachedResult ||
|
|---|
| 915 | !("location" in cachedResult) ||
|
|---|
| 916 | cachedResult.location !== result.location ||
|
|---|
| 917 | cachedResult.validUntil < validUntil ||
|
|---|
| 918 | cachedResult.storeLock !== storeLock ||
|
|---|
| 919 | cachedResult.storeCache !== storeCache ||
|
|---|
| 920 | cachedResult.etag !== etag
|
|---|
| 921 | ) {
|
|---|
| 922 | return finishWith(result);
|
|---|
| 923 | }
|
|---|
| 924 | logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
|
|---|
| 925 | return callback(null, {
|
|---|
| 926 | ...result,
|
|---|
| 927 | fresh: true,
|
|---|
| 928 | storeLock,
|
|---|
| 929 | storeCache,
|
|---|
| 930 | validUntil,
|
|---|
| 931 | etag
|
|---|
| 932 | });
|
|---|
| 933 | }
|
|---|
| 934 | const contentType = res.headers["content-type"] || "";
|
|---|
| 935 | /** @type {Buffer[]} */
|
|---|
| 936 | const bufferArr = [];
|
|---|
| 937 |
|
|---|
| 938 | const contentEncoding = res.headers["content-encoding"];
|
|---|
| 939 | /** @type {Readable} */
|
|---|
| 940 | let stream = res;
|
|---|
| 941 | if (contentEncoding === "gzip") {
|
|---|
| 942 | stream = stream.pipe(createGunzip());
|
|---|
| 943 | } else if (contentEncoding === "br") {
|
|---|
| 944 | stream = stream.pipe(createBrotliDecompress());
|
|---|
| 945 | } else if (contentEncoding === "deflate") {
|
|---|
| 946 | stream = stream.pipe(createInflate());
|
|---|
| 947 | }
|
|---|
| 948 |
|
|---|
| 949 | stream.on(
|
|---|
| 950 | "data",
|
|---|
| 951 | /**
|
|---|
| 952 | * Handles the callback logic for this hook.
|
|---|
| 953 | * @param {Buffer} chunk chunk
|
|---|
| 954 | */
|
|---|
| 955 | (chunk) => {
|
|---|
| 956 | bufferArr.push(chunk);
|
|---|
| 957 | }
|
|---|
| 958 | );
|
|---|
| 959 |
|
|---|
| 960 | stream.on("end", () => {
|
|---|
| 961 | if (!res.complete) {
|
|---|
| 962 | logger.log(`GET ${url} [${res.statusCode}] (terminated)`);
|
|---|
| 963 | return callback(new Error(`${url} request was terminated`));
|
|---|
| 964 | }
|
|---|
| 965 |
|
|---|
| 966 | const content = Buffer.concat(bufferArr);
|
|---|
| 967 |
|
|---|
| 968 | if (res.statusCode !== 200) {
|
|---|
| 969 | logger.log(`GET ${url} [${res.statusCode}]`);
|
|---|
| 970 | return callback(
|
|---|
| 971 | new Error(
|
|---|
| 972 | `${url} request status code = ${
|
|---|
| 973 | res.statusCode
|
|---|
| 974 | }\n${content.toString("utf8")}`
|
|---|
| 975 | )
|
|---|
| 976 | );
|
|---|
| 977 | }
|
|---|
| 978 |
|
|---|
| 979 | const integrity = computeIntegrity(content);
|
|---|
| 980 | const entry = { resolved: url, integrity, contentType };
|
|---|
| 981 |
|
|---|
| 982 | finishWith({
|
|---|
| 983 | entry,
|
|---|
| 984 | content
|
|---|
| 985 | });
|
|---|
| 986 | });
|
|---|
| 987 | }).on("error", (err) => {
|
|---|
| 988 | logger.log(`GET ${url} (error)`);
|
|---|
| 989 | err.message += `\nwhile fetching ${url}`;
|
|---|
| 990 | callback(err);
|
|---|
| 991 | });
|
|---|
| 992 | };
|
|---|
| 993 |
|
|---|
| 994 | const fetchContent = cachedWithKey(
|
|---|
| 995 | /**
|
|---|
| 996 | * Handles the callback logic for this hook.
|
|---|
| 997 | * @param {string} url URL
|
|---|
| 998 | * @param {(err: Error | null, result?: FetchResult) => void} callback callback
|
|---|
| 999 | * @returns {void}
|
|---|
| 1000 | */
|
|---|
| 1001 | (url, callback) => {
|
|---|
| 1002 | cache.get(url, null, (err, cachedResult) => {
|
|---|
| 1003 | if (err) return callback(err);
|
|---|
| 1004 | if (cachedResult) {
|
|---|
| 1005 | const isValid = cachedResult.validUntil >= Date.now();
|
|---|
| 1006 | if (isValid) return callback(null, cachedResult);
|
|---|
| 1007 | }
|
|---|
| 1008 | fetchContentRaw(url, cachedResult, callback);
|
|---|
| 1009 | });
|
|---|
| 1010 | },
|
|---|
| 1011 | (url, callback) => fetchContentRaw(url, undefined, callback)
|
|---|
| 1012 | );
|
|---|
| 1013 |
|
|---|
| 1014 | /**
|
|---|
| 1015 | * Checks whether this http uri plugin is allowed.
|
|---|
| 1016 | * @param {string} uri uri
|
|---|
| 1017 | * @returns {boolean} true when allowed, otherwise false
|
|---|
| 1018 | */
|
|---|
| 1019 | const isAllowed = (uri) => {
|
|---|
| 1020 | /** @type {URL} */
|
|---|
| 1021 | let parsedUri;
|
|---|
| 1022 | try {
|
|---|
| 1023 | // Parse the URI to prevent userinfo bypass attacks
|
|---|
| 1024 | // (e.g., http://allowed@malicious/path where @malicious is the actual host)
|
|---|
| 1025 | parsedUri = new URL(uri);
|
|---|
| 1026 | } catch (_err) {
|
|---|
| 1027 | return false;
|
|---|
| 1028 | }
|
|---|
| 1029 | for (const allowed of allowedUris) {
|
|---|
| 1030 | if (typeof allowed === "string") {
|
|---|
| 1031 | /** @type {URL} */
|
|---|
| 1032 | let parsedAllowed;
|
|---|
| 1033 | try {
|
|---|
| 1034 | parsedAllowed = new URL(allowed);
|
|---|
| 1035 | } catch (_err) {
|
|---|
| 1036 | continue;
|
|---|
| 1037 | }
|
|---|
| 1038 | if (parsedUri.href.startsWith(parsedAllowed.href)) {
|
|---|
| 1039 | return true;
|
|---|
| 1040 | }
|
|---|
| 1041 | } else if (typeof allowed === "function") {
|
|---|
| 1042 | if (allowed(parsedUri.href)) return true;
|
|---|
| 1043 | } else if (allowed.test(parsedUri.href)) {
|
|---|
| 1044 | return true;
|
|---|
| 1045 | }
|
|---|
| 1046 | }
|
|---|
| 1047 | return false;
|
|---|
| 1048 | };
|
|---|
| 1049 |
|
|---|
| 1050 | /** @typedef {{ entry: LockfileEntry, content: Buffer }} Info */
|
|---|
| 1051 |
|
|---|
| 1052 | const getInfo = cachedWithKey(
|
|---|
| 1053 | /**
|
|---|
| 1054 | * Processes the provided url.
|
|---|
| 1055 | * @param {string} url the url
|
|---|
| 1056 | * @param {(err: Error | null, info?: Info) => void} callback callback
|
|---|
| 1057 | * @returns {void}
|
|---|
| 1058 | */
|
|---|
| 1059 | // eslint-disable-next-line no-loop-func
|
|---|
| 1060 | (url, callback) => {
|
|---|
| 1061 | if (!isAllowed(url)) {
|
|---|
| 1062 | return callback(
|
|---|
| 1063 | new Error(
|
|---|
| 1064 | `${url} doesn't match the allowedUris policy. These URIs are allowed:\n${allowedUris
|
|---|
| 1065 | .map((uri) => ` - ${uri}`)
|
|---|
| 1066 | .join("\n")}`
|
|---|
| 1067 | )
|
|---|
| 1068 | );
|
|---|
| 1069 | }
|
|---|
| 1070 | getLockfile((err, _lockfile) => {
|
|---|
| 1071 | if (err) return callback(err);
|
|---|
| 1072 | const lockfile = /** @type {Lockfile} */ (_lockfile);
|
|---|
| 1073 | const entryOrString = lockfile.entries.get(url);
|
|---|
| 1074 | if (!entryOrString) {
|
|---|
| 1075 | if (frozen) {
|
|---|
| 1076 | return callback(
|
|---|
| 1077 | new Error(
|
|---|
| 1078 | `${url} has no lockfile entry and lockfile is frozen`
|
|---|
| 1079 | )
|
|---|
| 1080 | );
|
|---|
| 1081 | }
|
|---|
| 1082 | resolveContent(url, null, (err, result) => {
|
|---|
| 1083 | if (err) return callback(err);
|
|---|
| 1084 | storeResult(
|
|---|
| 1085 | /** @type {Lockfile} */
|
|---|
| 1086 | (lockfile),
|
|---|
| 1087 | url,
|
|---|
| 1088 | /** @type {ResolveContentResult} */
|
|---|
| 1089 | (result),
|
|---|
| 1090 | callback
|
|---|
| 1091 | );
|
|---|
| 1092 | });
|
|---|
| 1093 | return;
|
|---|
| 1094 | }
|
|---|
| 1095 | if (typeof entryOrString === "string") {
|
|---|
| 1096 | const entryTag = entryOrString;
|
|---|
| 1097 | resolveContent(url, null, (err, _result) => {
|
|---|
| 1098 | if (err) return callback(err);
|
|---|
| 1099 | const result =
|
|---|
| 1100 | /** @type {ResolveContentResult} */
|
|---|
| 1101 | (_result);
|
|---|
| 1102 | if (!result.storeLock || entryTag === "ignore") {
|
|---|
| 1103 | return callback(null, result);
|
|---|
| 1104 | }
|
|---|
| 1105 | if (frozen) {
|
|---|
| 1106 | return callback(
|
|---|
| 1107 | new Error(
|
|---|
| 1108 | `${url} used to have ${entryTag} lockfile entry and has content now, but lockfile is frozen`
|
|---|
| 1109 | )
|
|---|
| 1110 | );
|
|---|
| 1111 | }
|
|---|
| 1112 | if (!upgrade) {
|
|---|
| 1113 | return callback(
|
|---|
| 1114 | new Error(
|
|---|
| 1115 | `${url} used to have ${entryTag} lockfile entry and has content now.
|
|---|
| 1116 | This should be reflected in the lockfile, so this lockfile entry must be upgraded, but upgrading is not enabled.
|
|---|
| 1117 | Remove this line from the lockfile to force upgrading.`
|
|---|
| 1118 | )
|
|---|
| 1119 | );
|
|---|
| 1120 | }
|
|---|
| 1121 | storeResult(lockfile, url, result, callback);
|
|---|
| 1122 | });
|
|---|
| 1123 | return;
|
|---|
| 1124 | }
|
|---|
| 1125 | let entry = entryOrString;
|
|---|
| 1126 | /**
|
|---|
| 1127 | * Processes the provided locked content.
|
|---|
| 1128 | * @param {Buffer=} lockedContent locked content
|
|---|
| 1129 | */
|
|---|
| 1130 | const doFetch = (lockedContent) => {
|
|---|
| 1131 | resolveContent(url, entry.integrity, (err, _result) => {
|
|---|
| 1132 | if (err) {
|
|---|
| 1133 | if (lockedContent) {
|
|---|
| 1134 | logger.warn(
|
|---|
| 1135 | `Upgrade request to ${url} failed: ${err.message}`
|
|---|
| 1136 | );
|
|---|
| 1137 | logger.debug(err.stack);
|
|---|
| 1138 | return callback(null, {
|
|---|
| 1139 | entry,
|
|---|
| 1140 | content: lockedContent
|
|---|
| 1141 | });
|
|---|
| 1142 | }
|
|---|
| 1143 | return callback(err);
|
|---|
| 1144 | }
|
|---|
| 1145 | const result =
|
|---|
| 1146 | /** @type {ResolveContentResult} */
|
|---|
| 1147 | (_result);
|
|---|
| 1148 | if (!result.storeLock) {
|
|---|
| 1149 | // When the lockfile entry should be no-cache
|
|---|
| 1150 | // we need to update the lockfile
|
|---|
| 1151 | if (frozen) {
|
|---|
| 1152 | return callback(
|
|---|
| 1153 | new Error(
|
|---|
| 1154 | `${url} has a lockfile entry and is no-cache now, but lockfile is frozen\nLockfile: ${entryToString(
|
|---|
| 1155 | entry
|
|---|
| 1156 | )}`
|
|---|
| 1157 | )
|
|---|
| 1158 | );
|
|---|
| 1159 | }
|
|---|
| 1160 | storeResult(lockfile, url, result, callback);
|
|---|
| 1161 | return;
|
|---|
| 1162 | }
|
|---|
| 1163 | if (!areLockfileEntriesEqual(result.entry, entry)) {
|
|---|
| 1164 | // When the lockfile entry is outdated
|
|---|
| 1165 | // we need to update the lockfile
|
|---|
| 1166 | if (frozen) {
|
|---|
| 1167 | return callback(
|
|---|
| 1168 | new Error(
|
|---|
| 1169 | `${url} has an outdated lockfile entry, but lockfile is frozen\nLockfile: ${entryToString(
|
|---|
| 1170 | entry
|
|---|
| 1171 | )}\nExpected: ${entryToString(result.entry)}`
|
|---|
| 1172 | )
|
|---|
| 1173 | );
|
|---|
| 1174 | }
|
|---|
| 1175 | storeResult(lockfile, url, result, callback);
|
|---|
| 1176 | return;
|
|---|
| 1177 | }
|
|---|
| 1178 | if (!lockedContent && cacheLocation) {
|
|---|
| 1179 | // When the lockfile cache content is missing
|
|---|
| 1180 | // we need to update the lockfile
|
|---|
| 1181 | if (frozen) {
|
|---|
| 1182 | return callback(
|
|---|
| 1183 | new Error(
|
|---|
| 1184 | `${url} is missing content in the lockfile cache, but lockfile is frozen\nLockfile: ${entryToString(
|
|---|
| 1185 | entry
|
|---|
| 1186 | )}`
|
|---|
| 1187 | )
|
|---|
| 1188 | );
|
|---|
| 1189 | }
|
|---|
| 1190 | storeResult(lockfile, url, result, callback);
|
|---|
| 1191 | return;
|
|---|
| 1192 | }
|
|---|
| 1193 | return callback(null, result);
|
|---|
| 1194 | });
|
|---|
| 1195 | };
|
|---|
| 1196 | if (cacheLocation) {
|
|---|
| 1197 | // When there is a lockfile cache
|
|---|
| 1198 | // we read the content from there
|
|---|
| 1199 | const key = getCacheKey(entry.resolved);
|
|---|
| 1200 | const filePath = join(intermediateFs, cacheLocation, key);
|
|---|
| 1201 | fs.readFile(filePath, (err, result) => {
|
|---|
| 1202 | if (err) {
|
|---|
| 1203 | if (err.code === "ENOENT") return doFetch();
|
|---|
| 1204 | return callback(err);
|
|---|
| 1205 | }
|
|---|
| 1206 | const content = /** @type {Buffer} */ (result);
|
|---|
| 1207 | /**
|
|---|
| 1208 | * Continue with cached content.
|
|---|
| 1209 | * @param {Buffer | undefined} _result result
|
|---|
| 1210 | * @returns {void}
|
|---|
| 1211 | */
|
|---|
| 1212 | const continueWithCachedContent = (_result) => {
|
|---|
| 1213 | if (!upgrade) {
|
|---|
| 1214 | // When not in upgrade mode, we accept the result from the lockfile cache
|
|---|
| 1215 | return callback(null, { entry, content });
|
|---|
| 1216 | }
|
|---|
| 1217 | return doFetch(content);
|
|---|
| 1218 | };
|
|---|
| 1219 | if (!verifyIntegrity(content, entry.integrity)) {
|
|---|
| 1220 | /** @type {Buffer | undefined} */
|
|---|
| 1221 | let contentWithChangedEol;
|
|---|
| 1222 | let isEolChanged = false;
|
|---|
| 1223 | try {
|
|---|
| 1224 | contentWithChangedEol = Buffer.from(
|
|---|
| 1225 | content.toString("utf8").replace(/\r\n/g, "\n")
|
|---|
| 1226 | );
|
|---|
| 1227 | isEolChanged = verifyIntegrity(
|
|---|
| 1228 | contentWithChangedEol,
|
|---|
| 1229 | entry.integrity
|
|---|
| 1230 | );
|
|---|
| 1231 | } catch (_err) {
|
|---|
| 1232 | // ignore
|
|---|
| 1233 | }
|
|---|
| 1234 | if (isEolChanged) {
|
|---|
| 1235 | if (!warnedAboutEol) {
|
|---|
| 1236 | const explainer = `Incorrect end of line sequence was detected in the lockfile cache.
|
|---|
| 1237 | The lockfile cache is protected by integrity checks, so any external modification will lead to a corrupted lockfile cache.
|
|---|
| 1238 | When using git make sure to configure .gitattributes correctly for the lockfile cache:
|
|---|
| 1239 | **/*webpack.lock.data/** -text
|
|---|
| 1240 | This will avoid that the end of line sequence is changed by git on Windows.`;
|
|---|
| 1241 | if (frozen) {
|
|---|
| 1242 | logger.error(explainer);
|
|---|
| 1243 | } else {
|
|---|
| 1244 | logger.warn(explainer);
|
|---|
| 1245 | logger.info(
|
|---|
| 1246 | "Lockfile cache will be automatically fixed now, but when lockfile is frozen this would result in an error."
|
|---|
| 1247 | );
|
|---|
| 1248 | }
|
|---|
| 1249 | warnedAboutEol = true;
|
|---|
| 1250 | }
|
|---|
| 1251 | if (!frozen) {
|
|---|
| 1252 | // "fix" the end of line sequence of the lockfile content
|
|---|
| 1253 | logger.log(
|
|---|
| 1254 | `${filePath} fixed end of line sequence (\\r\\n instead of \\n).`
|
|---|
| 1255 | );
|
|---|
| 1256 | intermediateFs.writeFile(
|
|---|
| 1257 | filePath,
|
|---|
| 1258 | /** @type {Buffer} */
|
|---|
| 1259 | (contentWithChangedEol),
|
|---|
| 1260 | (err) => {
|
|---|
| 1261 | if (err) return callback(err);
|
|---|
| 1262 | continueWithCachedContent(
|
|---|
| 1263 | /** @type {Buffer} */
|
|---|
| 1264 | (contentWithChangedEol)
|
|---|
| 1265 | );
|
|---|
| 1266 | }
|
|---|
| 1267 | );
|
|---|
| 1268 | return;
|
|---|
| 1269 | }
|
|---|
| 1270 | }
|
|---|
| 1271 | if (frozen) {
|
|---|
| 1272 | return callback(
|
|---|
| 1273 | new Error(
|
|---|
| 1274 | `${
|
|---|
| 1275 | entry.resolved
|
|---|
| 1276 | } integrity mismatch, expected content with integrity ${
|
|---|
| 1277 | entry.integrity
|
|---|
| 1278 | } but got ${computeIntegrity(content)}.
|
|---|
| 1279 | Lockfile corrupted (${
|
|---|
| 1280 | isEolChanged
|
|---|
| 1281 | ? "end of line sequence was unexpectedly changed"
|
|---|
| 1282 | : "incorrectly merged? changed by other tools?"
|
|---|
| 1283 | }).
|
|---|
| 1284 | Run build with un-frozen lockfile to automatically fix lockfile.`
|
|---|
| 1285 | )
|
|---|
| 1286 | );
|
|---|
| 1287 | }
|
|---|
| 1288 | // "fix" the lockfile entry to the correct integrity
|
|---|
| 1289 | // the content has priority over the integrity value
|
|---|
| 1290 | entry = {
|
|---|
| 1291 | ...entry,
|
|---|
| 1292 | integrity: computeIntegrity(content)
|
|---|
| 1293 | };
|
|---|
| 1294 | storeLockEntry(lockfile, url, entry);
|
|---|
| 1295 | }
|
|---|
| 1296 | continueWithCachedContent(result);
|
|---|
| 1297 | });
|
|---|
| 1298 | } else {
|
|---|
| 1299 | doFetch();
|
|---|
| 1300 | }
|
|---|
| 1301 | });
|
|---|
| 1302 | }
|
|---|
| 1303 | );
|
|---|
| 1304 |
|
|---|
| 1305 | /**
|
|---|
| 1306 | * Respond with url module.
|
|---|
| 1307 | * @param {URL} url url
|
|---|
| 1308 | * @param {ResourceDataWithData} resourceData resource data
|
|---|
| 1309 | * @param {(err: Error | null, result: true | void) => void} callback callback
|
|---|
| 1310 | */
|
|---|
| 1311 | const respondWithUrlModule = (url, resourceData, callback) => {
|
|---|
| 1312 | getInfo(url.href, (err, _result) => {
|
|---|
| 1313 | if (err) return callback(err);
|
|---|
| 1314 | const result = /** @type {Info} */ (_result);
|
|---|
| 1315 | resourceData.resource = url.href;
|
|---|
| 1316 | resourceData.path = url.origin + url.pathname;
|
|---|
| 1317 | resourceData.query = url.search;
|
|---|
| 1318 | resourceData.fragment = url.hash;
|
|---|
| 1319 | resourceData.context = new URL(
|
|---|
| 1320 | ".",
|
|---|
| 1321 | result.entry.resolved
|
|---|
| 1322 | ).href.slice(0, -1);
|
|---|
| 1323 | resourceData.data.mimetype = result.entry.contentType;
|
|---|
| 1324 | callback(null, true);
|
|---|
| 1325 | });
|
|---|
| 1326 | };
|
|---|
| 1327 | normalModuleFactory.hooks.resolveForScheme
|
|---|
| 1328 | .for(scheme)
|
|---|
| 1329 | .tapAsync(PLUGIN_NAME, (resourceData, resolveData, callback) => {
|
|---|
| 1330 | respondWithUrlModule(
|
|---|
| 1331 | new URL(resourceData.resource),
|
|---|
| 1332 | resourceData,
|
|---|
| 1333 | callback
|
|---|
| 1334 | );
|
|---|
| 1335 | });
|
|---|
| 1336 | normalModuleFactory.hooks.resolveInScheme
|
|---|
| 1337 | .for(scheme)
|
|---|
| 1338 | .tapAsync(PLUGIN_NAME, (resourceData, data, callback) => {
|
|---|
| 1339 | // Only handle relative urls (./xxx, ../xxx, /xxx, //xxx)
|
|---|
| 1340 | if (
|
|---|
| 1341 | data.dependencyType !== "url" &&
|
|---|
| 1342 | !/^\.{0,2}\//.test(resourceData.resource)
|
|---|
| 1343 | ) {
|
|---|
| 1344 | return callback();
|
|---|
| 1345 | }
|
|---|
| 1346 | respondWithUrlModule(
|
|---|
| 1347 | new URL(resourceData.resource, `${data.context}/`),
|
|---|
| 1348 | resourceData,
|
|---|
| 1349 | callback
|
|---|
| 1350 | );
|
|---|
| 1351 | });
|
|---|
| 1352 | const hooks = NormalModule.getCompilationHooks(compilation);
|
|---|
| 1353 | hooks.readResourceForScheme
|
|---|
| 1354 | .for(scheme)
|
|---|
| 1355 | .tapAsync(PLUGIN_NAME, (resource, module, callback) =>
|
|---|
| 1356 | getInfo(resource, (err, _result) => {
|
|---|
| 1357 | if (err) return callback(err);
|
|---|
| 1358 | const result = /** @type {Info} */ (_result);
|
|---|
| 1359 | if (module) {
|
|---|
| 1360 | /** @type {BuildInfo} */
|
|---|
| 1361 | (module.buildInfo).resourceIntegrity = result.entry.integrity;
|
|---|
| 1362 | }
|
|---|
| 1363 | callback(null, result.content);
|
|---|
| 1364 | })
|
|---|
| 1365 | );
|
|---|
| 1366 | hooks.needBuild.tapAsync(PLUGIN_NAME, (module, context, callback) => {
|
|---|
| 1367 | if (module.resource && module.resource.startsWith(`${scheme}://`)) {
|
|---|
| 1368 | getInfo(module.resource, (err, _result) => {
|
|---|
| 1369 | if (err) return callback(err);
|
|---|
| 1370 | const result = /** @type {Info} */ (_result);
|
|---|
| 1371 | if (
|
|---|
| 1372 | result.entry.integrity !==
|
|---|
| 1373 | /** @type {BuildInfo} */
|
|---|
| 1374 | (module.buildInfo).resourceIntegrity
|
|---|
| 1375 | ) {
|
|---|
| 1376 | return callback(null, true);
|
|---|
| 1377 | }
|
|---|
| 1378 | callback();
|
|---|
| 1379 | });
|
|---|
| 1380 | } else {
|
|---|
| 1381 | return callback();
|
|---|
| 1382 | }
|
|---|
| 1383 | });
|
|---|
| 1384 | }
|
|---|
| 1385 | compilation.hooks.finishModules.tapAsync(
|
|---|
| 1386 | PLUGIN_NAME,
|
|---|
| 1387 | (modules, callback) => {
|
|---|
| 1388 | if (!lockfileUpdates) return callback();
|
|---|
| 1389 | const ext = extname(lockfileLocation);
|
|---|
| 1390 | const tempFile = join(
|
|---|
| 1391 | intermediateFs,
|
|---|
| 1392 | dirname(intermediateFs, lockfileLocation),
|
|---|
| 1393 | `.${basename(lockfileLocation, ext)}.${
|
|---|
| 1394 | (Math.random() * 10000) | 0
|
|---|
| 1395 | }${ext}`
|
|---|
| 1396 | );
|
|---|
| 1397 |
|
|---|
| 1398 | const writeDone = () => {
|
|---|
| 1399 | const nextOperation =
|
|---|
| 1400 | /** @type {InProgressWriteItem[]} */
|
|---|
| 1401 | (inProgressWrite).shift();
|
|---|
| 1402 | if (nextOperation) {
|
|---|
| 1403 | nextOperation();
|
|---|
| 1404 | } else {
|
|---|
| 1405 | inProgressWrite = undefined;
|
|---|
| 1406 | }
|
|---|
| 1407 | };
|
|---|
| 1408 | const runWrite = () => {
|
|---|
| 1409 | intermediateFs.readFile(lockfileLocation, (err, buffer) => {
|
|---|
| 1410 | if (err && err.code !== "ENOENT") {
|
|---|
| 1411 | writeDone();
|
|---|
| 1412 | return callback(err);
|
|---|
| 1413 | }
|
|---|
| 1414 | const lockfile = buffer
|
|---|
| 1415 | ? Lockfile.parse(buffer.toString("utf8"))
|
|---|
| 1416 | : new Lockfile();
|
|---|
| 1417 | for (const [key, value] of /** @type {LockfileUpdates} */ (
|
|---|
| 1418 | lockfileUpdates
|
|---|
| 1419 | )) {
|
|---|
| 1420 | lockfile.entries.set(key, value);
|
|---|
| 1421 | }
|
|---|
| 1422 | intermediateFs.writeFile(
|
|---|
| 1423 | tempFile,
|
|---|
| 1424 | lockfile.toString(),
|
|---|
| 1425 | (err) => {
|
|---|
| 1426 | if (err) {
|
|---|
| 1427 | writeDone();
|
|---|
| 1428 | return (
|
|---|
| 1429 | /** @type {NonNullable<IntermediateFileSystem["unlink"]>} */
|
|---|
| 1430 | (intermediateFs.unlink)(tempFile, () => callback(err))
|
|---|
| 1431 | );
|
|---|
| 1432 | }
|
|---|
| 1433 | intermediateFs.rename(tempFile, lockfileLocation, (err) => {
|
|---|
| 1434 | if (err) {
|
|---|
| 1435 | writeDone();
|
|---|
| 1436 | return (
|
|---|
| 1437 | /** @type {NonNullable<IntermediateFileSystem["unlink"]>} */
|
|---|
| 1438 | (intermediateFs.unlink)(tempFile, () => callback(err))
|
|---|
| 1439 | );
|
|---|
| 1440 | }
|
|---|
| 1441 | writeDone();
|
|---|
| 1442 | callback();
|
|---|
| 1443 | });
|
|---|
| 1444 | }
|
|---|
| 1445 | );
|
|---|
| 1446 | });
|
|---|
| 1447 | };
|
|---|
| 1448 | if (inProgressWrite) {
|
|---|
| 1449 | inProgressWrite.push(runWrite);
|
|---|
| 1450 | } else {
|
|---|
| 1451 | inProgressWrite = [];
|
|---|
| 1452 | runWrite();
|
|---|
| 1453 | }
|
|---|
| 1454 | }
|
|---|
| 1455 | );
|
|---|
| 1456 | }
|
|---|
| 1457 | );
|
|---|
| 1458 | }
|
|---|
| 1459 | }
|
|---|
| 1460 |
|
|---|
| 1461 | module.exports = HttpUriPlugin;
|
|---|