CachedSource.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Source = require("./Source");
  7. const streamAndGetSourceAndMap = require("./helpers/streamAndGetSourceAndMap");
  8. const streamChunksOfRawSource = require("./helpers/streamChunksOfRawSource");
  9. const streamChunksOfSourceMap = require("./helpers/streamChunksOfSourceMap");
  10. const {
  11. isDualStringBufferCachingEnabled,
  12. } = require("./helpers/stringBufferUtils");
  13. /** @typedef {import("./Source").ClearCacheOptions} ClearCacheOptions */
  14. /** @typedef {import("./Source").HashLike} HashLike */
  15. /** @typedef {import("./Source").MapOptions} MapOptions */
  16. /** @typedef {import("./Source").RawSourceMap} RawSourceMap */
  17. /** @typedef {import("./Source").SourceAndMap} SourceAndMap */
  18. /** @typedef {import("./Source").SourceValue} SourceValue */
  19. /** @typedef {import("./helpers/getGeneratedSourceInfo").GeneratedSourceInfo} GeneratedSourceInfo */
  20. /** @typedef {import("./helpers/streamChunks").OnChunk} OnChunk */
  21. /** @typedef {import("./helpers/streamChunks").OnName} OnName */
  22. /** @typedef {import("./helpers/streamChunks").OnSource} OnSource */
  23. /** @typedef {import("./helpers/streamChunks").Options} Options */
  24. /**
  25. * @typedef {object} BufferedMap
  26. * @property {number} version version
  27. * @property {string[]} sources sources
  28. * @property {string[]} names name
  29. * @property {string=} sourceRoot source root
  30. * @property {(Buffer | "")[]=} sourcesContent sources content
  31. * @property {Buffer=} mappings mappings
  32. * @property {string} file file
  33. */
  34. /**
  35. * @param {null | RawSourceMap} map map
  36. * @returns {null | BufferedMap} buffered map
  37. */
  38. const mapToBufferedMap = (map) => {
  39. if (typeof map !== "object" || !map) return map;
  40. const bufferedMap =
  41. /** @type {BufferedMap} */
  42. (/** @type {unknown} */ ({ ...map }));
  43. if (map.mappings) {
  44. bufferedMap.mappings = Buffer.from(map.mappings, "utf8");
  45. }
  46. if (map.sourcesContent) {
  47. bufferedMap.sourcesContent = map.sourcesContent.map(
  48. (str) => str && Buffer.from(str, "utf8"),
  49. );
  50. }
  51. return bufferedMap;
  52. };
  53. /**
  54. * @param {null | BufferedMap} bufferedMap buffered map
  55. * @returns {null | RawSourceMap} map
  56. */
  57. const bufferedMapToMap = (bufferedMap) => {
  58. if (typeof bufferedMap !== "object" || !bufferedMap) return bufferedMap;
  59. const map =
  60. /** @type {RawSourceMap} */
  61. (/** @type {unknown} */ ({ ...bufferedMap }));
  62. if (bufferedMap.mappings) {
  63. map.mappings = bufferedMap.mappings.toString("utf8");
  64. }
  65. if (bufferedMap.sourcesContent) {
  66. map.sourcesContent = bufferedMap.sourcesContent.map(
  67. (buffer) => buffer && buffer.toString("utf8"),
  68. );
  69. }
  70. return map;
  71. };
  72. /** @typedef {{ map?: null | RawSourceMap, bufferedMap?: null | BufferedMap }} BufferEntry */
  73. /** @typedef {Map<string, BufferEntry>} BufferedMaps */
  74. const CACHE_KEY_EMPTY = "{}";
  75. const CACHE_KEY_COLUMNS_FALSE = '{"columns":false}';
  76. const CACHE_KEY_COLUMNS_TRUE = '{"columns":true}';
  77. /**
  78. * Fast-path replacement for `JSON.stringify(options)` when used as a cache
  79. * key. MapOptions / streamChunks Options are both small boolean-only shapes
  80. * and the overwhelmingly common shapes (`undefined`, `{}`, `{columns}`) can
  81. * be keyed without calling `JSON.stringify`, which dominates short-circuit
  82. * cache lookups. Falls back to `JSON.stringify` for any other shape so keys
  83. * remain compatible with previously cached `BufferedMaps` entries.
  84. * @param {undefined | MapOptions | Options} options options
  85. * @returns {string} cache key
  86. */
  87. const getCacheKey = (options) => {
  88. if (!options) return CACHE_KEY_EMPTY;
  89. const { columns } = options;
  90. if (
  91. /** @type {Options} */ (options).source === undefined &&
  92. /** @type {Options} */ (options).finalSource === undefined &&
  93. /** @type {MapOptions} */ (options).module === undefined
  94. ) {
  95. if (columns === undefined) return CACHE_KEY_EMPTY;
  96. return columns ? CACHE_KEY_COLUMNS_TRUE : CACHE_KEY_COLUMNS_FALSE;
  97. }
  98. return JSON.stringify(options);
  99. };
  100. /**
  101. * @typedef {object} CachedData
  102. * @property {boolean=} source source
  103. * @property {Buffer} buffer buffer
  104. * @property {number=} size size
  105. * @property {BufferedMaps} maps maps
  106. * @property {(string | Buffer)[]=} hash hash
  107. */
  108. class CachedSource extends Source {
  109. /**
  110. * @param {Source | (() => Source)} source source
  111. * @param {CachedData=} cachedData cached data
  112. */
  113. constructor(source, cachedData) {
  114. super();
  115. /**
  116. * @private
  117. * @type {Source | (() => Source)}
  118. */
  119. this._source = source;
  120. /**
  121. * @private
  122. * @type {undefined | string}
  123. */
  124. this._cachedSource = undefined;
  125. // Split on `cachedData` once instead of re-evaluating the ternary for
  126. // every field. Under the interpreter (and CodSpeed's simulation) each
  127. // ternary is a separate branch; consolidating cuts the per-instance
  128. // branch count roughly in half.
  129. if (cachedData) {
  130. /**
  131. * @private
  132. * @type {boolean | undefined}
  133. */
  134. this._cachedSourceType = cachedData.source;
  135. /**
  136. * @private
  137. * @type {Buffer | undefined}
  138. */
  139. this._cachedBuffer = cachedData.buffer;
  140. /**
  141. * @private
  142. * @type {number | undefined}
  143. */
  144. this._cachedSize = cachedData.size;
  145. /**
  146. * @private
  147. * @type {BufferedMaps}
  148. */
  149. this._cachedMaps = cachedData.maps;
  150. /**
  151. * @private
  152. * @type {(string | Buffer)[] | undefined}
  153. */
  154. this._cachedHashUpdate = cachedData.hash;
  155. } else {
  156. this._cachedSourceType = undefined;
  157. this._cachedBuffer = undefined;
  158. this._cachedSize = undefined;
  159. this._cachedMaps = new Map();
  160. this._cachedHashUpdate = undefined;
  161. }
  162. }
  163. /**
  164. * @returns {CachedData} cached data
  165. */
  166. getCachedData() {
  167. /** @type {BufferedMaps} */
  168. const bufferedMaps = new Map();
  169. for (const pair of this._cachedMaps) {
  170. const [, cacheEntry] = pair;
  171. if (cacheEntry.bufferedMap === undefined) {
  172. cacheEntry.bufferedMap = mapToBufferedMap(
  173. this._getMapFromCacheEntry(cacheEntry),
  174. );
  175. }
  176. bufferedMaps.set(pair[0], {
  177. map: undefined,
  178. bufferedMap: cacheEntry.bufferedMap,
  179. });
  180. }
  181. return {
  182. // `CachedData.buffer` is required (it is the on-disk
  183. // serialization format consumed by the
  184. // `new CachedSource(source, cachedData)` constructor).
  185. // `_cachedBuffer` is populated by `buffer()` calls but a
  186. // caller may invoke `getCachedData()` after `clearCache()`
  187. // has dropped it; `this.buffer()` rehydrates via the
  188. // wrapped source so the contract holds in every state.
  189. buffer: this.buffer(),
  190. source:
  191. this._cachedSourceType !== undefined
  192. ? this._cachedSourceType
  193. : typeof this._cachedSource === "string"
  194. ? true
  195. : Buffer.isBuffer(this._cachedSource)
  196. ? false
  197. : undefined,
  198. size: this._cachedSize,
  199. maps: bufferedMaps,
  200. hash: this._cachedHashUpdate,
  201. };
  202. }
  203. originalLazy() {
  204. return this._source;
  205. }
  206. original() {
  207. if (typeof this._source === "function") this._source = this._source();
  208. return this._source;
  209. }
  210. /**
  211. * @returns {SourceValue} source
  212. */
  213. source() {
  214. // Fully inlined _getCachedSource: both warm- and cold-cache paths skip
  215. // the prototype method lookup / stack frame the interpreter would
  216. // otherwise pay on every call.
  217. if (this._cachedSource !== undefined) return this._cachedSource;
  218. const cachedBuffer = this._cachedBuffer;
  219. const cachedSourceType = this._cachedSourceType;
  220. if (cachedBuffer !== undefined && cachedSourceType !== undefined) {
  221. const value = cachedSourceType
  222. ? cachedBuffer.toString("utf8")
  223. : cachedBuffer;
  224. if (isDualStringBufferCachingEnabled()) {
  225. this._cachedSource = /** @type {string} */ (value);
  226. }
  227. return /** @type {string} */ (value);
  228. }
  229. return (this._cachedSource =
  230. /** @type {string} */
  231. (this.original().source()));
  232. }
  233. /**
  234. * @private
  235. * @param {BufferEntry} cacheEntry cache entry
  236. * @returns {null | RawSourceMap} raw source map
  237. */
  238. _getMapFromCacheEntry(cacheEntry) {
  239. if (cacheEntry.map !== undefined) {
  240. return cacheEntry.map;
  241. } else if (cacheEntry.bufferedMap !== undefined) {
  242. return (cacheEntry.map = bufferedMapToMap(cacheEntry.bufferedMap));
  243. }
  244. return null;
  245. }
  246. /**
  247. * @private
  248. * @returns {undefined | string} cached source
  249. */
  250. _getCachedSource() {
  251. if (this._cachedSource !== undefined) return this._cachedSource;
  252. if (this._cachedBuffer && this._cachedSourceType !== undefined) {
  253. const value = this._cachedSourceType
  254. ? this._cachedBuffer.toString("utf8")
  255. : this._cachedBuffer;
  256. if (isDualStringBufferCachingEnabled()) {
  257. this._cachedSource = /** @type {string} */ (value);
  258. }
  259. return /** @type {string} */ (value);
  260. }
  261. }
  262. /**
  263. * @returns {Buffer} buffer
  264. */
  265. buffer() {
  266. if (this._cachedBuffer !== undefined) return this._cachedBuffer;
  267. if (this._cachedBuffers !== undefined) {
  268. return (this._cachedBuffer = Buffer.concat(this._cachedBuffers));
  269. }
  270. if (this._cachedSource !== undefined) {
  271. const value = Buffer.isBuffer(this._cachedSource)
  272. ? this._cachedSource
  273. : Buffer.from(this._cachedSource, "utf8");
  274. if (isDualStringBufferCachingEnabled()) {
  275. this._cachedBuffer = value;
  276. }
  277. return value;
  278. }
  279. if (typeof this.original().buffer === "function") {
  280. return (this._cachedBuffer = this.original().buffer());
  281. }
  282. const bufferOrString = this.source();
  283. if (Buffer.isBuffer(bufferOrString)) {
  284. return (this._cachedBuffer = bufferOrString);
  285. }
  286. const value = Buffer.from(bufferOrString, "utf8");
  287. if (isDualStringBufferCachingEnabled()) {
  288. this._cachedBuffer = value;
  289. }
  290. return value;
  291. }
  292. /**
  293. * @returns {Buffer[]} buffers
  294. */
  295. buffers() {
  296. if (this._cachedBuffers !== undefined) return this._cachedBuffers;
  297. if (this._cachedBuffer !== undefined) {
  298. return (this._cachedBuffers = [this._cachedBuffer]);
  299. }
  300. const original = this.original();
  301. if (typeof original.buffers === "function") {
  302. return (this._cachedBuffers = original.buffers());
  303. }
  304. return (this._cachedBuffers = [this.buffer()]);
  305. }
  306. /**
  307. * @returns {number} size
  308. */
  309. size() {
  310. if (this._cachedSize !== undefined) return this._cachedSize;
  311. if (this._cachedBuffer !== undefined) {
  312. return (this._cachedSize = this._cachedBuffer.length);
  313. }
  314. const source = this._getCachedSource();
  315. if (source !== undefined) {
  316. return (this._cachedSize = Buffer.byteLength(source));
  317. }
  318. return (this._cachedSize = this.original().size());
  319. }
  320. /**
  321. * @param {MapOptions=} options map options
  322. * @returns {SourceAndMap} source and map
  323. */
  324. sourceAndMap(options) {
  325. const key = getCacheKey(options);
  326. const cacheEntry = this._cachedMaps.get(key);
  327. // Look for a cached map
  328. if (cacheEntry !== undefined) {
  329. // We have a cached map in some representation
  330. const map = this._getMapFromCacheEntry(cacheEntry);
  331. // Either get the cached source or compute it
  332. return { source: this.source(), map };
  333. }
  334. // Look for a cached source
  335. let source = this._getCachedSource();
  336. // Compute the map
  337. let map;
  338. if (source !== undefined) {
  339. map = this.original().map(options);
  340. } else {
  341. // Compute the source and map together.
  342. const sourceAndMap = this.original().sourceAndMap(options);
  343. source = /** @type {string} */ (sourceAndMap.source);
  344. map = sourceAndMap.map;
  345. this._cachedSource = source;
  346. }
  347. this._cachedMaps.set(key, {
  348. map,
  349. bufferedMap: undefined,
  350. });
  351. return { source, map };
  352. }
  353. /**
  354. * @param {Options} options options
  355. * @param {OnChunk} onChunk called for each chunk of code
  356. * @param {OnSource} onSource called for each source
  357. * @param {OnName} onName called for each name
  358. * @returns {GeneratedSourceInfo} generated source info
  359. */
  360. streamChunks(options, onChunk, onSource, onName) {
  361. const key = getCacheKey(options);
  362. if (
  363. this._cachedMaps.has(key) &&
  364. (this._cachedBuffer !== undefined || this._cachedSource !== undefined)
  365. ) {
  366. const { source, map } = this.sourceAndMap(options);
  367. if (map) {
  368. return streamChunksOfSourceMap(
  369. /** @type {string} */
  370. (source),
  371. map,
  372. onChunk,
  373. onSource,
  374. onName,
  375. Boolean(options && options.finalSource),
  376. true,
  377. );
  378. }
  379. return streamChunksOfRawSource(
  380. /** @type {string} */
  381. (source),
  382. onChunk,
  383. onSource,
  384. onName,
  385. Boolean(options && options.finalSource),
  386. );
  387. }
  388. const sourceAndMap = streamAndGetSourceAndMap(
  389. this.original(),
  390. options,
  391. onChunk,
  392. onSource,
  393. onName,
  394. );
  395. this._cachedSource = sourceAndMap.source;
  396. this._cachedMaps.set(key, {
  397. map: /** @type {RawSourceMap} */ (sourceAndMap.map),
  398. bufferedMap: undefined,
  399. });
  400. return sourceAndMap.result;
  401. }
  402. /**
  403. * @param {MapOptions=} options map options
  404. * @returns {RawSourceMap | null} map
  405. */
  406. map(options) {
  407. const key = getCacheKey(options);
  408. const cacheEntry = this._cachedMaps.get(key);
  409. if (cacheEntry !== undefined) {
  410. return this._getMapFromCacheEntry(cacheEntry);
  411. }
  412. const map = this.original().map(options);
  413. this._cachedMaps.set(key, {
  414. map,
  415. bufferedMap: undefined,
  416. });
  417. return map;
  418. }
  419. /**
  420. * Release cached data held by this source. clearCache is a memory
  421. * hint: it never affects correctness or output, only how expensive
  422. * the next read is. Subclasses override; the base is a no-op so
  423. * every Source supports the call. Composite sources always recurse
  424. * into wrapped sources. When the same child is reachable via several
  425. * parents (e.g. modules shared across webpack chunks), pass a shared
  426. * `visited` WeakSet so each subtree is walked at most once.
  427. * Not safe to call concurrently with source/map/sourceAndMap/
  428. * streamChunks/updateHash on the same instance.
  429. * @param {ClearCacheOptions=} options selectors
  430. * @param {WeakSet<Source>=} visited de-duplication set shared across calls
  431. * @returns {void}
  432. */
  433. clearCache(options, visited) {
  434. if (visited !== undefined && visited.has(this)) return;
  435. const clearSource = !options || options.source !== false;
  436. const clearMaps = !options || options.maps !== false;
  437. if (clearSource) {
  438. this._cachedSource = undefined;
  439. this._cachedSourceType = undefined;
  440. this._cachedBuffer = undefined;
  441. this._cachedBuffers = undefined;
  442. }
  443. if (clearMaps) {
  444. // Reusing the Map avoids per-call allocation churn when builds
  445. // call clearCache thousands of times.
  446. this._cachedMaps.clear();
  447. }
  448. if (typeof this._source !== "function") {
  449. let v = visited;
  450. if (v === undefined) v = new WeakSet();
  451. v.add(this);
  452. this._source.clearCache(options, v);
  453. } else if (visited !== undefined) {
  454. visited.add(this);
  455. }
  456. }
  457. /**
  458. * @param {HashLike} hash hash
  459. * @returns {void}
  460. */
  461. updateHash(hash) {
  462. if (this._cachedHashUpdate !== undefined) {
  463. for (const item of this._cachedHashUpdate) hash.update(item);
  464. return;
  465. }
  466. /** @type {(string | Buffer)[]} */
  467. const update = [];
  468. /** @type {string | undefined} */
  469. let currentString;
  470. const tracker = {
  471. /**
  472. * @param {string | Buffer} item item
  473. * @returns {void}
  474. */
  475. update: (item) => {
  476. if (typeof item === "string" && item.length < 10240) {
  477. if (currentString === undefined) {
  478. currentString = item;
  479. } else {
  480. currentString += item;
  481. if (currentString.length > 102400) {
  482. update.push(Buffer.from(currentString));
  483. currentString = undefined;
  484. }
  485. }
  486. } else {
  487. if (currentString !== undefined) {
  488. update.push(Buffer.from(currentString));
  489. currentString = undefined;
  490. }
  491. update.push(item);
  492. }
  493. },
  494. };
  495. this.original().updateHash(/** @type {HashLike} */ (tracker));
  496. if (currentString !== undefined) {
  497. update.push(Buffer.from(currentString));
  498. }
  499. for (const item of update) hash.update(item);
  500. this._cachedHashUpdate = update;
  501. }
  502. }
  503. module.exports = CachedSource;