Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: added transformAll option #596

Merged
merged 3 commits into from Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Expand Up @@ -87,6 +87,7 @@ module.exports = {
| [`force`](#force) | `{Boolean}` | `false` | Overwrites files already in `compilation.assets` (usually added by other plugins/loaders). |
| [`priority`](#priority) | `{Number}` | `0` | Allows you to specify the copy priority. |
| [`transform`](#transform) | `{Object}` | `undefined` | Allows to modify the file contents. Enable `transform` caching. You can use `{ transform: {cache: { key: 'my-cache-key' }} }` to invalidate the cache. |
| [`transformAll`](#transformAll) | `{Function}` | `undefined` | Allows you to modify the contents of multiple files and save the result to one file. |
| [`noErrorOnMissing`](#noerroronmissing) | `{Boolean}` | `false` | Doesn't generate an error on missing file(s). |
| [`info`](#info) | `{Object\|Function}` | `undefined` | Allows to add assets info. |

Expand Down Expand Up @@ -730,6 +731,45 @@ module.exports = {
};
```

#### `transformAll`

Type: `Function`
Default: `undefined`

Allows you to modify the contents of multiple files and save the result to one file.

> ℹ️ The `to` option must be specified and point to a file. It is allowed to use only `[contenthash]` and `[fullhash]` template strings.

**webpack.config.js**

```js
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: "src/**/*.txt",
to: "dest/file.txt",
// The `assets` argument is an assets array for the pattern.from ("src/**/*.txt")
transformAll(assets) {
const result = assets.reduce((accumulator, asset) => {
// The asset content can be obtained from `asset.source` using `source` method.
// The asset content is a [`Buffer`](https://nodejs.org/api/buffer.html) object, it could be converted to a `String` to be processed using `content.toString()`
const content = asset.data;

accumulator = `${accumulator}${content}\n`;
return accumulator;
}, "");

return result;
},
},
],
}),
],
};
```

### `noErrorOnMissing`

Type: `Boolean`
Expand Down
148 changes: 130 additions & 18 deletions src/index.js
Expand Up @@ -69,6 +69,27 @@ class CopyPlugin {
});
}

static getContentHash(compiler, compilation, source) {
const { outputOptions } = compilation;
const {
hashDigest,
hashDigestLength,
hashFunction,
hashSalt,
} = outputOptions;
const hash = compiler.webpack.util.createHash(hashFunction);

if (hashSalt) {
hash.update(hashSalt);
}

hash.update(source);

const fullContentHash = hash.digest(hashDigest);

return fullContentHash.slice(0, hashDigestLength);
}

static async runPattern(
compiler,
compilation,
Expand Down Expand Up @@ -456,7 +477,7 @@ class CopyPlugin {
if (transform.transformer) {
logger.log(`transforming content for '${absoluteFilename}'...`);

const buffer = result.source.source();
const buffer = result.source.buffer();

if (transform.cache) {
const defaultCacheKeys = {
Expand Down Expand Up @@ -526,23 +547,11 @@ class CopyPlugin {
`interpolating template '${filename}' for '${sourceFilename}'...`
);

const { outputOptions } = compilation;
const {
hashDigest,
hashDigestLength,
hashFunction,
hashSalt,
} = outputOptions;
const hash = compiler.webpack.util.createHash(hashFunction);

if (hashSalt) {
hash.update(hashSalt);
}

hash.update(result.source.source());

const fullContentHash = hash.digest(hashDigest);
const contentHash = fullContentHash.slice(0, hashDigestLength);
const contentHash = CopyPlugin.getContentHash(
compiler,
compilation,
result.source.buffer()
);
const ext = path.extname(result.sourceFilename);
const base = path.basename(result.sourceFilename);
const name = base.slice(0, base.length - ext.length);
Expand Down Expand Up @@ -634,6 +643,109 @@ class CopyPlugin {
}

if (assets && assets.length > 0) {
if (item.transformAll) {
if (typeof item.to === "undefined") {
compilation.errors.push(
new Error(
`Invalid "pattern.to" for the "pattern.from": "${item.from}" and "pattern.transformAll" function. The "to" option must be specified.`
)
);

return;
}

assets.sort((a, b) =>
a.absoluteFilename > b.absoluteFilename
? 1
: a.absoluteFilename < b.absoluteFilename
? -1
: 0
);

const mergedEtag =
assets.length === 1
? cache.getLazyHashedEtag(assets[0].source.buffer())
: assets.reduce((accumulator, asset, i) => {
// eslint-disable-next-line no-param-reassign
accumulator = cache.mergeEtags(
i === 1
? cache.getLazyHashedEtag(
accumulator.source.buffer()
)
: accumulator,
cache.getLazyHashedEtag(asset.source.buffer())
);

return accumulator;
});

const cacheKeys = `transformAll|${serialize({
version,
from: item.from,
to: item.to,
transformAll: item.transformAll,
})}`;
const eTag = cache.getLazyHashedEtag(mergedEtag);
const cacheItem = cache.getItemCache(cacheKeys, eTag);
let transformedAsset = await cacheItem.getPromise();

if (!transformedAsset) {
transformedAsset = { filename: item.to };

try {
transformedAsset.data = await item.transformAll(
assets.map((asset) => {
return {
data: asset.source.buffer(),
sourceFilename: asset.sourceFilename,
absoluteFilename: asset.absoluteFilename,
};
})
);
} catch (error) {
compilation.errors.push(error);

return;
}

if (template.test(item.to)) {
const contentHash = CopyPlugin.getContentHash(
compiler,
compilation,
transformedAsset.data
);

const {
path: interpolatedFilename,
info: assetInfo,
} = compilation.getPathWithInfo(
normalizePath(item.to),
{
contentHash,
chunk: {
hash: contentHash,
contentHash,
},
}
);

transformedAsset.filename = interpolatedFilename;
transformedAsset.info = assetInfo;
}

const { RawSource } = compiler.webpack.sources;

transformedAsset.source = new RawSource(
transformedAsset.data
);
transformedAsset.force = item.force;

await cacheItem.storePromise(transformedAsset);
}

assets = [transformedAsset];
}

const priority = item.priority || 0;

if (!assetMap.has(priority)) {
Expand Down
3 changes: 3 additions & 0 deletions src/options.json
Expand Up @@ -27,6 +27,9 @@
"filter": {
"instanceof": "Function"
},
"transformAll": {
"instanceof": "Function"
},
"toType": {
"enum": ["dir", "file", "template"]
},
Expand Down
21 changes: 21 additions & 0 deletions test/__snapshots__/transformAll-option.test.js.snap
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`cache should work with the "memory" cache: assets 1`] = `
Object {
"file.txt": "new::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::",
}
`;

exports[`cache should work with the "memory" cache: assets 2`] = `
Object {
"file.txt": "new::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::",
}
`;

exports[`cache should work with the "memory" cache: errors 1`] = `Array []`;

exports[`cache should work with the "memory" cache: errors 2`] = `Array []`;

exports[`cache should work with the "memory" cache: warnings 1`] = `Array []`;

exports[`cache should work with the "memory" cache: warnings 2`] = `Array []`;