HttpUriPlugin.js 44 KB

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