Skip to content

Commit c892451

Browse files
feat: persistent cache between compilations (webpack@5 only) (#541)
1 parent 93936a0 commit c892451

File tree

4 files changed

+279
-84
lines changed

4 files changed

+279
-84
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ npm-debug.log*
99
/reports
1010
/node_modules
1111
/test/fixtures/\[special\$directory\]
12+
/test/outputs
1213

1314
.DS_Store
1415
Thumbs.db

src/index.js

Lines changed: 155 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,58 @@ class CopyPlugin {
3737
this.options = options.options || {};
3838
}
3939

40+
static async createSnapshot(compilation, startTime, dependency) {
41+
if (!compilation.fileSystemInfo) {
42+
return;
43+
}
44+
45+
// eslint-disable-next-line consistent-return
46+
return new Promise((resolve, reject) => {
47+
compilation.fileSystemInfo.createSnapshot(
48+
startTime,
49+
[dependency],
50+
// eslint-disable-next-line no-undefined
51+
undefined,
52+
// eslint-disable-next-line no-undefined
53+
undefined,
54+
null,
55+
(error, snapshot) => {
56+
if (error) {
57+
reject(error);
58+
59+
return;
60+
}
61+
62+
resolve(snapshot);
63+
}
64+
);
65+
});
66+
}
67+
68+
static async checkSnapshotValid(compilation, snapshot) {
69+
if (!compilation.fileSystemInfo) {
70+
return;
71+
}
72+
73+
// eslint-disable-next-line consistent-return
74+
return new Promise((resolve, reject) => {
75+
compilation.fileSystemInfo.checkSnapshotValid(
76+
snapshot,
77+
(error, isValid) => {
78+
if (error) {
79+
reject(error);
80+
81+
return;
82+
}
83+
84+
resolve(isValid);
85+
}
86+
);
87+
});
88+
}
89+
4090
// eslint-disable-next-line class-methods-use-this
41-
async runPattern(compiler, compilation, logger, inputPattern) {
91+
static async runPattern(compiler, compilation, logger, cache, inputPattern) {
4292
const pattern =
4393
typeof inputPattern === 'string'
4494
? { from: inputPattern }
@@ -49,7 +99,6 @@ class CopyPlugin {
4999
pattern.to = path.normalize(
50100
typeof pattern.to !== 'undefined' ? pattern.to : ''
51101
);
52-
53102
pattern.context = path.normalize(
54103
typeof pattern.context !== 'undefined'
55104
? !path.isAbsolute(pattern.context)
@@ -266,64 +315,109 @@ class CopyPlugin {
266315
compilation.fileDependencies.add(file.absoluteFrom);
267316
}
268317

269-
logger.debug(`reading "${file.absoluteFrom}" to write to assets`);
318+
let source;
270319

271-
let data;
320+
if (cache) {
321+
const cacheEntry = await cache.getPromise(file.relativeFrom, null);
272322

273-
try {
274-
data = await readFile(inputFileSystem, file.absoluteFrom);
275-
} catch (error) {
276-
compilation.errors.push(error);
323+
if (cacheEntry) {
324+
const isValidSnapshot = await CopyPlugin.checkSnapshotValid(
325+
compilation,
326+
cacheEntry.snapshot
327+
);
277328

278-
return;
329+
if (isValidSnapshot) {
330+
({ source } = cacheEntry);
331+
}
332+
}
279333
}
280334

281-
if (pattern.transform) {
282-
logger.log(`transforming content for "${file.absoluteFrom}"`);
283-
284-
if (pattern.cacheTransform) {
285-
const cacheDirectory = pattern.cacheTransform.directory
286-
? pattern.cacheTransform.directory
287-
: typeof pattern.cacheTransform === 'string'
288-
? pattern.cacheTransform
289-
: findCacheDir({ name: 'copy-webpack-plugin' }) || os.tmpdir();
290-
let defaultCacheKeys = {
291-
version,
292-
transform: pattern.transform,
293-
contentHash: crypto.createHash('md4').update(data).digest('hex'),
294-
};
295-
296-
if (typeof pattern.cacheTransform.keys === 'function') {
297-
defaultCacheKeys = await pattern.cacheTransform.keys(
298-
defaultCacheKeys,
299-
file.absoluteFrom
300-
);
301-
} else {
302-
defaultCacheKeys = {
303-
...defaultCacheKeys,
304-
...pattern.cacheTransform.keys,
335+
if (!source) {
336+
let startTime;
337+
338+
if (cache) {
339+
startTime = Date.now();
340+
}
341+
342+
logger.debug(`reading "${file.absoluteFrom}" to write to assets`);
343+
344+
let data;
345+
346+
try {
347+
data = await readFile(inputFileSystem, file.absoluteFrom);
348+
} catch (error) {
349+
compilation.errors.push(error);
350+
351+
return;
352+
}
353+
354+
if (pattern.transform) {
355+
logger.log(`transforming content for "${file.absoluteFrom}"`);
356+
357+
if (pattern.cacheTransform) {
358+
const cacheDirectory = pattern.cacheTransform.directory
359+
? pattern.cacheTransform.directory
360+
: typeof pattern.cacheTransform === 'string'
361+
? pattern.cacheTransform
362+
: findCacheDir({ name: 'copy-webpack-plugin' }) || os.tmpdir();
363+
let defaultCacheKeys = {
364+
version,
365+
transform: pattern.transform,
366+
contentHash: crypto
367+
.createHash('md4')
368+
.update(data)
369+
.digest('hex'),
305370
};
306-
}
307371

308-
const cacheKeys = serialize(defaultCacheKeys);
372+
if (typeof pattern.cacheTransform.keys === 'function') {
373+
defaultCacheKeys = await pattern.cacheTransform.keys(
374+
defaultCacheKeys,
375+
file.absoluteFrom
376+
);
377+
} else {
378+
defaultCacheKeys = {
379+
...defaultCacheKeys,
380+
...pattern.cacheTransform.keys,
381+
};
382+
}
309383

310-
try {
311-
const result = await cacache.get(cacheDirectory, cacheKeys);
384+
const cacheKeys = serialize(defaultCacheKeys);
312385

313-
logger.debug(
314-
`getting cached transformation for "${file.absoluteFrom}"`
315-
);
386+
try {
387+
const result = await cacache.get(cacheDirectory, cacheKeys);
316388

317-
({ data } = result);
318-
} catch (_ignoreError) {
319-
data = await pattern.transform(data, file.absoluteFrom);
389+
logger.debug(
390+
`getting cached transformation for "${file.absoluteFrom}"`
391+
);
320392

321-
logger.debug(`caching transformation for "${file.absoluteFrom}"`);
393+
({ data } = result);
394+
} catch (_ignoreError) {
395+
data = await pattern.transform(data, file.absoluteFrom);
396+
397+
logger.debug(
398+
`caching transformation for "${file.absoluteFrom}"`
399+
);
322400

323-
await cacache.put(cacheDirectory, cacheKeys, data);
401+
await cacache.put(cacheDirectory, cacheKeys, data);
402+
}
403+
} else {
404+
data = await pattern.transform(data, file.absoluteFrom);
324405
}
325-
} else {
326-
data = await pattern.transform(data, file.absoluteFrom);
406+
}
407+
408+
source = new RawSource(data);
409+
410+
if (cache) {
411+
const snapshot = await CopyPlugin.createSnapshot(
412+
compilation,
413+
startTime,
414+
file.relativeFrom
415+
);
416+
417+
await cache.storePromise(file.relativeFrom, null, {
418+
source,
419+
snapshot,
420+
});
327421
}
328422
}
329423

@@ -349,7 +443,7 @@ class CopyPlugin {
349443
{ resourcePath: file.absoluteFrom },
350444
file.webpackTo,
351445
{
352-
content: data,
446+
content: source.source(),
353447
context: pattern.context,
354448
}
355449
);
@@ -374,7 +468,7 @@ class CopyPlugin {
374468
}
375469

376470
// eslint-disable-next-line no-param-reassign
377-
file.data = data;
471+
file.source = source;
378472
// eslint-disable-next-line no-param-reassign
379473
file.targetPath = normalizePath(file.webpackTo);
380474
// eslint-disable-next-line no-param-reassign
@@ -392,6 +486,10 @@ class CopyPlugin {
392486

393487
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
394488
const logger = compilation.getLogger('copy-webpack-plugin');
489+
const cache = compilation.getCache
490+
? compilation.getCache('CopyWebpackPlugin')
491+
: // eslint-disable-next-line no-undefined
492+
undefined;
395493

396494
compilation.hooks.additionalAssets.tapAsync(
397495
'copy-webpack-plugin',
@@ -404,7 +502,13 @@ class CopyPlugin {
404502
assets = await Promise.all(
405503
this.patterns.map((item) =>
406504
limit(async () =>
407-
this.runPattern(compiler, compilation, logger, item)
505+
CopyPlugin.runPattern(
506+
compiler,
507+
compilation,
508+
logger,
509+
cache,
510+
item
511+
)
408512
)
409513
)
410514
);
@@ -426,12 +530,10 @@ class CopyPlugin {
426530
absoluteFrom,
427531
targetPath,
428532
webpackTo,
429-
data,
533+
source,
430534
force,
431535
} = asset;
432536

433-
const source = new RawSource(data);
434-
435537
// For old version webpack 4
436538
/* istanbul ignore if */
437539
if (typeof compilation.emitAsset !== 'function') {

test/CopyPlugin.test.js

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'path';
22

33
import webpack from 'webpack';
4+
import del from 'del';
45

56
import CopyPlugin from '../src';
67

@@ -633,9 +634,72 @@ describe('CopyPlugin', () => {
633634
.then(done)
634635
.catch(done);
635636
});
637+
});
638+
639+
describe('cache', () => {
640+
it('should work with the "memory" cache', async () => {
641+
const compiler = getCompiler({
642+
cache: {
643+
type: 'memory',
644+
},
645+
});
646+
647+
new CopyPlugin({
648+
patterns: [
649+
{
650+
from: path.resolve(__dirname, './fixtures/directory'),
651+
},
652+
],
653+
}).apply(compiler);
654+
655+
const { stats } = await compile(compiler);
656+
657+
if (webpack.version[0] === '4') {
658+
expect(
659+
Object.keys(stats.compilation.assets).filter(
660+
(assetName) => stats.compilation.assets[assetName].emitted
661+
).length
662+
).toBe(5);
663+
} else {
664+
expect(stats.compilation.emittedAssets.size).toBe(5);
665+
}
666+
667+
expect(readAssets(compiler, stats)).toMatchSnapshot('assets');
668+
expect(stats.compilation.errors).toMatchSnapshot('errors');
669+
expect(stats.compilation.warnings).toMatchSnapshot('warnings');
670+
671+
await new Promise(async (resolve) => {
672+
const { stats: newStats } = await compile(compiler);
673+
674+
if (webpack.version[0] === '4') {
675+
expect(
676+
Object.keys(newStats.compilation.assets).filter(
677+
(assetName) => newStats.compilation.assets[assetName].emitted
678+
).length
679+
).toBe(4);
680+
} else {
681+
expect(newStats.compilation.emittedAssets.size).toBe(0);
682+
}
636683

637-
it('should work and do not emit unchanged assets', async () => {
638-
const compiler = getCompiler();
684+
expect(readAssets(compiler, newStats)).toMatchSnapshot('assets');
685+
expect(newStats.compilation.errors).toMatchSnapshot('errors');
686+
expect(newStats.compilation.warnings).toMatchSnapshot('warnings');
687+
688+
resolve();
689+
});
690+
});
691+
692+
it('should work with the "filesystem" cache', async () => {
693+
const cacheDirectory = path.resolve(__dirname, './outputs/.cache');
694+
695+
await del(cacheDirectory);
696+
697+
const compiler = getCompiler({
698+
cache: {
699+
type: 'filesystem',
700+
cacheDirectory,
701+
},
702+
});
639703

640704
new CopyPlugin({
641705
patterns: [
@@ -671,7 +735,7 @@ describe('CopyPlugin', () => {
671735
).length
672736
).toBe(4);
673737
} else {
674-
expect(newStats.compilation.emittedAssets.size).toBe(4);
738+
expect(newStats.compilation.emittedAssets.size).toBe(0);
675739
}
676740

677741
expect(readAssets(compiler, newStats)).toMatchSnapshot('assets');

0 commit comments

Comments
 (0)