Skip to content

Commit

Permalink
feat: add keepTimes option
Browse files Browse the repository at this point in the history
  • Loading branch information
akx committed Mar 9, 2020
1 parent 034fd3f commit 05b70ff
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ module.exports = {
| [`ignore`](#ignore) | `{Array}` | `[]` | Array of globs to ignore (applied to `from`) |
| [`context`](#context) | `{String}` | `compiler.options.context` | A path that determines how to interpret the `from` path, shared for all patterns |
| [`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 |
| [`keepTimes`](#keeptimes) | `{Boolean}` | `false` | Copy the original access and modification over to the destination files, when possible |

#### `logLevel`

Expand Down Expand Up @@ -568,6 +569,18 @@ module.exports = {
};
```

#### `keepTimes`

Attempt to copy source files' access and modification times over to the destination files.

**webpack.config.js**

```js
module.exports = {
plugins: [new CopyPlugin([...patterns], { keepTimes: true })],
};
```

## Contributing

Please take a moment to read our contributing guidelines if you haven't yet done so.
Expand Down
6 changes: 6 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import schema from './options.json';
import preProcessPattern from './preProcessPattern';
import processPattern from './processPattern';
import postProcessPattern from './postProcessPattern';
import updateTimes from './updateTimes';

class CopyPlugin {
constructor(patterns = [], options = {}) {
Expand Down Expand Up @@ -52,6 +53,7 @@ class CopyPlugin {
output: compiler.options.output.path,
ignore: this.options.ignore || [],
copyUnmodified: this.options.copyUnmodified,
keepTimes: this.options.keepTimes,
concurrency: this.options.concurrency,
};

Expand Down Expand Up @@ -117,6 +119,10 @@ class CopyPlugin {
}
}

if (this.options.keepTimes) {
updateTimes(compiler, compilation, logger);
}

logger.debug('finishing after-emit');

callback();
Expand Down
3 changes: 3 additions & 0 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
},
"transformPath": {
"instanceof": "Function"
},
"keepTimes": {
"type": "boolean"
}
},
"required": ["from"]
Expand Down
4 changes: 4 additions & 0 deletions src/postProcessPattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export default function postProcessPattern(globalRef, pattern, file) {
source() {
return content;
},
copyPluginTimes: {
atime: stats.atime,
mtime: stats.mtime,
},
};
});
});
Expand Down
57 changes: 57 additions & 0 deletions src/updateTimes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Attempt to get an Utimes function for the compiler's output filesystem.
*/
function getUtimesFunction(compiler) {
if (compiler.outputFileSystem.utimes) {
// Webpack 5+ on Node will use graceful-fs for outputFileSystem so utimes is always there.
// Other custom outputFileSystems could also have utimes.
return compiler.outputFileSystem.utimes.bind(compiler.outputFileSystem);
} else if (
compiler.outputFileSystem.constructor &&
compiler.outputFileSystem.constructor.name === 'NodeOutputFileSystem'
) {
// Default NodeOutputFileSystem can just use fs.utimes, but we need to late-import it in case
// we're running in a web context and statically importing `fs` might be a bad idea.
// eslint-disable-next-line global-require
return require('fs').utimes;
}
return null;
}

/**
* Update the times of disk files for which we have recorded a source time
* @param compiler
* @param compilation
* @param logger
*/
function updateTimes(compiler, compilation, logger) {
const utimes = getUtimesFunction(compiler);
let nUpdated = 0;
for (const [name, asset] of Object.entries(compilation.assets)) {
// eslint-disable-next-line no-underscore-dangle
const times = asset.copyPluginTimes;
if (times) {
const targetPath =
asset.existsAt ||
compiler.outputFileSystem.join(compiler.outputPath, name);
if (!utimes) {
logger.warn(
`unable to update time for ${targetPath} using current file system`
);
} else {
// TODO: process these errors in a better way and/or wait for completion?
utimes(targetPath, times.atime, times.mtime, (err) => {
if (err) {
logger.warn(`${targetPath}: utimes: ${err}`);
}
});
nUpdated += 1;
}
}
}
if (nUpdated > 0) {
logger.info(`times updated for ${nUpdated} copied files`);
}
}

export default updateTimes;
33 changes: 33 additions & 0 deletions test/CopyPlugin.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'path';
import fs from 'fs';

import { MockCompiler } from './helpers/mocks';
import { run, runEmit, runChange } from './helpers/run';
Expand Down Expand Up @@ -250,6 +251,38 @@ describe('apply function', () => {
.then(done)
.catch(done);
});

it('should copy file modification times when told to', (done) => {
const origStat = fs.statSync(path.join(FIXTURES_DIR, 'file.txt'));
const utimeCalls = {};
const compiler = new MockCompiler();
// Patch in some things that are missing by default...
compiler.outputFileSystem.join = (a, b) => path.join(a || '', b);
compiler.outputFileSystem.utimes = (pth, atime, mtime, callback) => {
utimeCalls[pth] = { atime, mtime };
callback(null);
};

runEmit({
compiler,
expectedAssetKeys: ['file.txt'],
options: {
keepTimes: true,
},
patterns: [
{
from: 'file.txt',
},
],
})
.then(() => {
const { atime, mtime } = utimeCalls['file.txt'];
expect(atime).toEqual(origStat.atime);
expect(mtime).toEqual(origStat.mtime);
})
.then(done)
.catch(done);
});
});

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

0 comments on commit 05b70ff

Please sign in to comment.