HttpUriPlugin.js 42 KB

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