Skip to content

Commit 05b70ff

Browse files
committed
feat: add keepTimes option
Refs webpack-contrib#396
1 parent 034fd3f commit 05b70ff

6 files changed

+116
-0
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ module.exports = {
505505
| [`ignore`](#ignore) | `{Array}` | `[]` | Array of globs to ignore (applied to `from`) |
506506
| [`context`](#context) | `{String}` | `compiler.options.context` | A path that determines how to interpret the `from` path, shared for all patterns |
507507
| [`copyUnmodified`](#copyunmodified) | `{Boolean}` | `false` | Copies files, regardless of modification when using watch or `webpack-dev-server`. All files are copied on first build, regardless of this option |
508+
| [`keepTimes`](#keeptimes) | `{Boolean}` | `false` | Copy the original access and modification over to the destination files, when possible |
508509

509510
#### `logLevel`
510511

@@ -568,6 +569,18 @@ module.exports = {
568569
};
569570
```
570571

572+
#### `keepTimes`
573+
574+
Attempt to copy source files' access and modification times over to the destination files.
575+
576+
**webpack.config.js**
577+
578+
```js
579+
module.exports = {
580+
plugins: [new CopyPlugin([...patterns], { keepTimes: true })],
581+
};
582+
```
583+
571584
## Contributing
572585

573586
Please take a moment to read our contributing guidelines if you haven't yet done so.

src/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import schema from './options.json';
77
import preProcessPattern from './preProcessPattern';
88
import processPattern from './processPattern';
99
import postProcessPattern from './postProcessPattern';
10+
import updateTimes from './updateTimes';
1011

1112
class CopyPlugin {
1213
constructor(patterns = [], options = {}) {
@@ -52,6 +53,7 @@ class CopyPlugin {
5253
output: compiler.options.output.path,
5354
ignore: this.options.ignore || [],
5455
copyUnmodified: this.options.copyUnmodified,
56+
keepTimes: this.options.keepTimes,
5557
concurrency: this.options.concurrency,
5658
};
5759

@@ -117,6 +119,10 @@ class CopyPlugin {
117119
}
118120
}
119121

122+
if (this.options.keepTimes) {
123+
updateTimes(compiler, compilation, logger);
124+
}
125+
120126
logger.debug('finishing after-emit');
121127

122128
callback();

src/options.json

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
},
6868
"transformPath": {
6969
"instanceof": "Function"
70+
},
71+
"keepTimes": {
72+
"type": "boolean"
7073
}
7174
},
7275
"required": ["from"]

src/postProcessPattern.js

+4
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ export default function postProcessPattern(globalRef, pattern, file) {
197197
source() {
198198
return content;
199199
},
200+
copyPluginTimes: {
201+
atime: stats.atime,
202+
mtime: stats.mtime,
203+
},
200204
};
201205
});
202206
});

src/updateTimes.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Attempt to get an Utimes function for the compiler's output filesystem.
3+
*/
4+
function getUtimesFunction(compiler) {
5+
if (compiler.outputFileSystem.utimes) {
6+
// Webpack 5+ on Node will use graceful-fs for outputFileSystem so utimes is always there.
7+
// Other custom outputFileSystems could also have utimes.
8+
return compiler.outputFileSystem.utimes.bind(compiler.outputFileSystem);
9+
} else if (
10+
compiler.outputFileSystem.constructor &&
11+
compiler.outputFileSystem.constructor.name === 'NodeOutputFileSystem'
12+
) {
13+
// Default NodeOutputFileSystem can just use fs.utimes, but we need to late-import it in case
14+
// we're running in a web context and statically importing `fs` might be a bad idea.
15+
// eslint-disable-next-line global-require
16+
return require('fs').utimes;
17+
}
18+
return null;
19+
}
20+
21+
/**
22+
* Update the times of disk files for which we have recorded a source time
23+
* @param compiler
24+
* @param compilation
25+
* @param logger
26+
*/
27+
function updateTimes(compiler, compilation, logger) {
28+
const utimes = getUtimesFunction(compiler);
29+
let nUpdated = 0;
30+
for (const [name, asset] of Object.entries(compilation.assets)) {
31+
// eslint-disable-next-line no-underscore-dangle
32+
const times = asset.copyPluginTimes;
33+
if (times) {
34+
const targetPath =
35+
asset.existsAt ||
36+
compiler.outputFileSystem.join(compiler.outputPath, name);
37+
if (!utimes) {
38+
logger.warn(
39+
`unable to update time for ${targetPath} using current file system`
40+
);
41+
} else {
42+
// TODO: process these errors in a better way and/or wait for completion?
43+
utimes(targetPath, times.atime, times.mtime, (err) => {
44+
if (err) {
45+
logger.warn(`${targetPath}: utimes: ${err}`);
46+
}
47+
});
48+
nUpdated += 1;
49+
}
50+
}
51+
}
52+
if (nUpdated > 0) {
53+
logger.info(`times updated for ${nUpdated} copied files`);
54+
}
55+
}
56+
57+
export default updateTimes;

test/CopyPlugin.test.js

+33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'path';
2+
import fs from 'fs';
23

34
import { MockCompiler } from './helpers/mocks';
45
import { run, runEmit, runChange } from './helpers/run';
@@ -250,6 +251,38 @@ describe('apply function', () => {
250251
.then(done)
251252
.catch(done);
252253
});
254+
255+
it('should copy file modification times when told to', (done) => {
256+
const origStat = fs.statSync(path.join(FIXTURES_DIR, 'file.txt'));
257+
const utimeCalls = {};
258+
const compiler = new MockCompiler();
259+
// Patch in some things that are missing by default...
260+
compiler.outputFileSystem.join = (a, b) => path.join(a || '', b);
261+
compiler.outputFileSystem.utimes = (pth, atime, mtime, callback) => {
262+
utimeCalls[pth] = { atime, mtime };
263+
callback(null);
264+
};
265+
266+
runEmit({
267+
compiler,
268+
expectedAssetKeys: ['file.txt'],
269+
options: {
270+
keepTimes: true,
271+
},
272+
patterns: [
273+
{
274+
from: 'file.txt',
275+
},
276+
],
277+
})
278+
.then(() => {
279+
const { atime, mtime } = utimeCalls['file.txt'];
280+
expect(atime).toEqual(origStat.atime);
281+
expect(mtime).toEqual(origStat.mtime);
282+
})
283+
.then(done)
284+
.catch(done);
285+
});
253286
});
254287

255288
describe('difference path segment separation', () => {

0 commit comments

Comments
 (0)