|
7 | 7 | const parseJson = require("json-parse-better-errors");
|
8 | 8 | const asyncLib = require("neo-async");
|
9 | 9 | const path = require("path");
|
| 10 | +const { Source } = require("webpack-sources"); |
10 | 11 | const util = require("util");
|
11 | 12 | const {
|
12 | 13 | Tapable,
|
@@ -188,6 +189,11 @@ class Compiler extends Tapable {
|
188 | 189 |
|
189 | 190 | /** @type {boolean} */
|
190 | 191 | this.watchMode = false;
|
| 192 | + |
| 193 | + /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */ |
| 194 | + this._assetEmittingSourceCache = new WeakMap(); |
| 195 | + /** @private @type {Map<string, number>} */ |
| 196 | + this._assetEmittingWrittenFiles = new Map(); |
191 | 197 | }
|
192 | 198 |
|
193 | 199 | watch(watchOptions, handler) {
|
@@ -328,19 +334,86 @@ class Compiler extends Tapable {
|
328 | 334 | outputPath,
|
329 | 335 | targetFile
|
330 | 336 | );
|
331 |
| - if (source.existsAt === targetPath) { |
332 |
| - source.emitted = false; |
333 |
| - return callback(); |
334 |
| - } |
335 |
| - let content = source.source(); |
336 |
| - |
337 |
| - if (!Buffer.isBuffer(content)) { |
338 |
| - content = Buffer.from(content, "utf8"); |
| 337 | + // TODO webpack 5 remove futureEmitAssets option and make it on by default |
| 338 | + if (this.options.output.futureEmitAssets) { |
| 339 | + // check if the target file has already been written by this Compiler |
| 340 | + const targetFileGeneration = this._assetEmittingWrittenFiles.get( |
| 341 | + targetPath |
| 342 | + ); |
| 343 | + |
| 344 | + // create an cache entry for this Source if not already existing |
| 345 | + let cacheEntry = this._assetEmittingSourceCache.get(source); |
| 346 | + if (cacheEntry === undefined) { |
| 347 | + cacheEntry = { |
| 348 | + sizeOnlySource: undefined, |
| 349 | + writtenTo: new Map() |
| 350 | + }; |
| 351 | + this._assetEmittingSourceCache.set(source, cacheEntry); |
| 352 | + } |
| 353 | + |
| 354 | + // if the target file has already been written |
| 355 | + if (targetFileGeneration !== undefined) { |
| 356 | + // check if the Source has been written to this target file |
| 357 | + const writtenGeneration = cacheEntry.writtenTo.get(targetPath); |
| 358 | + if (writtenGeneration === targetFileGeneration) { |
| 359 | + // if yes, we skip writing the file |
| 360 | + // as it's already there |
| 361 | + // (we assume one doesn't remove files while the Compiler is running) |
| 362 | + return callback(); |
| 363 | + } |
| 364 | + } |
| 365 | + |
| 366 | + // get the binary (Buffer) content from the Source |
| 367 | + /** @type {Buffer} */ |
| 368 | + let content; |
| 369 | + if (source.buffer) { |
| 370 | + content = source.buffer(); |
| 371 | + } else { |
| 372 | + const bufferOrString = source.source(); |
| 373 | + if (Buffer.isBuffer(bufferOrString)) { |
| 374 | + content = bufferOrString; |
| 375 | + } else { |
| 376 | + content = Buffer.from(bufferOrString, "utf8"); |
| 377 | + } |
| 378 | + } |
| 379 | + |
| 380 | + // Create a replacement resource which only allows to ask for size |
| 381 | + // This allows to GC all memory allocated by the Source |
| 382 | + // (expect when the Source is stored in any other cache) |
| 383 | + cacheEntry.sizeOnlySource = new SizeOnlySource(content.length); |
| 384 | + compilation.assets[file] = cacheEntry.sizeOnlySource; |
| 385 | + |
| 386 | + // Write the file to output file system |
| 387 | + this.outputFileSystem.writeFile(targetPath, content, err => { |
| 388 | + if (err) return callback(err); |
| 389 | + |
| 390 | + // information marker that the asset has been emitted |
| 391 | + compilation.emittedAssets.add(file); |
| 392 | + |
| 393 | + // cache the information that the Source has been written to that location |
| 394 | + const newGeneration = |
| 395 | + targetFileGeneration === undefined |
| 396 | + ? 1 |
| 397 | + : targetFileGeneration + 1; |
| 398 | + cacheEntry.writtenTo.set(targetPath, newGeneration); |
| 399 | + this._assetEmittingWrittenFiles.set(targetPath, newGeneration); |
| 400 | + callback(); |
| 401 | + }); |
| 402 | + } else { |
| 403 | + if (source.existsAt === targetPath) { |
| 404 | + source.emitted = false; |
| 405 | + return callback(); |
| 406 | + } |
| 407 | + let content = source.source(); |
| 408 | + |
| 409 | + if (!Buffer.isBuffer(content)) { |
| 410 | + content = Buffer.from(content, "utf8"); |
| 411 | + } |
| 412 | + |
| 413 | + source.existsAt = targetPath; |
| 414 | + source.emitted = true; |
| 415 | + this.outputFileSystem.writeFile(targetPath, content, callback); |
339 | 416 | }
|
340 |
| - |
341 |
| - source.existsAt = targetPath; |
342 |
| - source.emitted = true; |
343 |
| - this.outputFileSystem.writeFile(targetPath, content, callback); |
344 | 417 | };
|
345 | 418 |
|
346 | 419 | if (targetFile.match(/\/|\\/)) {
|
@@ -563,3 +636,48 @@ class Compiler extends Tapable {
|
563 | 636 | }
|
564 | 637 |
|
565 | 638 | module.exports = Compiler;
|
| 639 | + |
| 640 | +class SizeOnlySource extends Source { |
| 641 | + constructor(size) { |
| 642 | + super(); |
| 643 | + this._size = size; |
| 644 | + } |
| 645 | + |
| 646 | + _error() { |
| 647 | + return new Error( |
| 648 | + "Content and Map of this Source is no longer available (only size() is supported)" |
| 649 | + ); |
| 650 | + } |
| 651 | + |
| 652 | + size() { |
| 653 | + return this._size; |
| 654 | + } |
| 655 | + |
| 656 | + /** |
| 657 | + * @param {any} options options |
| 658 | + * @returns {string} the source |
| 659 | + */ |
| 660 | + source(options) { |
| 661 | + throw this._error(); |
| 662 | + } |
| 663 | + |
| 664 | + node() { |
| 665 | + throw this._error(); |
| 666 | + } |
| 667 | + |
| 668 | + listMap() { |
| 669 | + throw this._error(); |
| 670 | + } |
| 671 | + |
| 672 | + map() { |
| 673 | + throw this._error(); |
| 674 | + } |
| 675 | + |
| 676 | + listNode() { |
| 677 | + throw this._error(); |
| 678 | + } |
| 679 | + |
| 680 | + updateHash() { |
| 681 | + throw this._error(); |
| 682 | + } |
| 683 | +} |
0 commit comments