Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: added the transformAll option (#596)
  • Loading branch information
cap-Bernardito committed Mar 22, 2021
1 parent 4ca7f80 commit dde71f0
Show file tree
Hide file tree
Showing 7 changed files with 453 additions and 35 deletions.
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.

This comment has been minimized.

Copy link
@emkayy

emkayy Oct 11, 2021

The documentation is wrong or at least confusing. The property asset.source does not exist.
This should include the structure of the object:

{
  data: [buffer], // file content can be obtained via asset.data.toString()
  sourceFilename: [string],
  absoluteFilename: [string],
}

Below the example should be
const content = asset.data.toString();
as in the test cases.

@cap-Bernardito

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Oct 11, 2021

Member

PR welcome

// 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 []`;

0 comments on commit dde71f0

Please sign in to comment.