HttpUriPlugin.js 45 KB

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