Skip to content

Commit 333152c

Browse files
authored
Feature/promise support (#29)
* Allow `opts.transform` to be a `Promise` as well as a `Function`. Fixes #27 * *Bug*: Correctly fail plugin if `opts.transform` throws in webpack4. * *Test*: Test errors in all versions of webpack.
1 parent a0174ae commit 333152c

16 files changed

+287
-35
lines changed

HISTORY.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
History
22
=======
33

4+
## Unreleased
5+
6+
* *Feature*: Allow `opts.transform` to be a `Promise` as well as a `Function`.
7+
[#27](https://github.com/FormidableLabs/webpack-stats-plugin/issues/27)
8+
* *Bug*: Correctly fail plugin if `opts.transform` throws in webpack4.
9+
* *Test*: Test errors in all versions of webpack.
10+
411
## 0.2.0
512

613
* **Breaking**: Update to node4+.

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ module.exports = {
7676
}
7777
```
7878

79+
### Promise transform
80+
81+
You can use an asynchronous promise to transform as well:
82+
83+
```js
84+
const StatsWriterPlugin = require("webpack-stats-plugin").StatsWriterPlugin;
85+
86+
module.exports = {
87+
plugins: [
88+
new StatsWriterPlugin({
89+
filename: "stats-transform-promise.json",
90+
transform(data) {
91+
return Promise.resolve().then(() => JSON.stringify({
92+
main: data.assetsByChunkName.main
93+
}, null, INDENT));
94+
}
95+
})
96+
]
97+
}
98+
```
99+
79100
## Plugins
80101

81102
* [`StatsWriterPlugin(opts)`](#statswriterplugin-opts-)
@@ -84,7 +105,7 @@ module.exports = {
84105
* **opts** (`Object`) options
85106
* **opts.filename** (`String`) output file name (Default: "stat.json")
86107
* **opts.fields** (`Array`) fields of stats obj to keep (Default: \["assetsByChunkName"\])
87-
* **opts.transform** (`Function`) transform stats obj (Default: `JSON.stringify()`)
108+
* **opts.transform** (`Function|Promise`) transform stats obj (Default: `JSON.stringify()`)
88109

89110
Stats writer module.
90111

lib/stats-writer-plugin.js

+30-14
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const DEFAULT_TRANSFORM = (data) => JSON.stringify(data, null, INDENT);
4444
* @param {Object} opts options
4545
* @param {String} opts.filename output file name (Default: "stat.json")
4646
* @param {Array} opts.fields fields of stats obj to keep (Default: \["assetsByChunkName"\])
47-
* @param {Function} opts.transform transform stats obj (Default: `JSON.stringify()`)
47+
* @param {Function|Promise} opts.transform transform stats obj (Default: `JSON.stringify()`)
4848
*
4949
* @api public
5050
*/
@@ -59,7 +59,7 @@ class StatsWriterPlugin {
5959

6060
apply(compiler) {
6161
if (compiler.hooks) {
62-
compiler.hooks.emit.tap("stats-writer-plugin", this.emitStats.bind(this));
62+
compiler.hooks.emit.tapPromise("stats-writer-plugin", this.emitStats.bind(this));
6363
} else {
6464
compiler.plugin("emit", this.emitStats.bind(this));
6565
}
@@ -80,20 +80,36 @@ class StatsWriterPlugin {
8080
}
8181

8282
// Transform to string.
83-
const statsStr = this.opts.transform(stats, {
84-
compiler: curCompiler
85-
});
83+
let err;
84+
return Promise.resolve()
8685

87-
curCompiler.assets[this.opts.filename] = {
88-
source() {
89-
return statsStr;
90-
},
91-
size() {
92-
return statsStr.length;
93-
}
94-
};
86+
// Transform.
87+
.then(() => this.opts.transform(stats, {
88+
compiler: curCompiler
89+
}))
90+
.catch((e) => { err = e; })
9591

96-
return callback && callback();
92+
// Finish up.
93+
.then((statsStr) => {
94+
// Handle errors.
95+
if (err) {
96+
curCompiler.errors.push(err);
97+
if (callback) { return void callback(err); }
98+
throw err;
99+
}
100+
101+
// Add to assets.
102+
curCompiler.assets[this.opts.filename] = {
103+
source() {
104+
return statsStr;
105+
},
106+
size() {
107+
return statsStr.length;
108+
}
109+
};
110+
111+
if (callback) { return void callback(); }
112+
});
97113
}
98114
}
99115

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"test": "yarn run test:clean && yarn run test:build && yarn run test:run",
2828
"test:run": "mocha 'test/**/*.spec.js'",
2929
"test:clean": "rm -rf test/webpack*/build*",
30-
"test:build:single": "cd node_modules/webpack${VERS} && node index.js --config ../../test/webpack${VERS}/webpack.config.js",
30+
"test:build:single": "cd node_modules/webpack${VERS} && node index.js --config ../../test/webpack${VERS}/webpack.config${WP_EXTRA}.js",
3131
"test:build": "builder envs test:build:single '[{\"VERS\":1},{\"VERS\":2},{\"VERS\":3},{\"VERS\":4}]' --buffer",
3232
"check": "yarn run lint && yarn run test"
3333
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"main": "HASH.main.js"
3+
}

test/func.spec.js

+107-19
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,30 @@
55
/**
66
* Functional tests.
77
*
8+
* ## Builds
9+
*
810
* Build webpack1-4 to actual files, read those in, and then assert against
911
* "expected" fixtures in `test/expected`.
12+
*
13+
* ## Failures
14+
*
15+
* Use `builder` to do multi-process executions and assert against the logged
16+
* error output.
1017
*/
1118
const path = require("path");
1219
const pify = require("pify");
1320
const fs = pify(require("fs"));
1421
const expect = require("chai").expect;
22+
const cp = require("child_process");
23+
const builderCli = require.resolve("builder/bin/builder");
1524

1625
const BUILD_DIRS = ["build", "build2"];
1726
const WEBPACKS = [1, 2, 3, 4].map((n) => `webpack${n}`); // eslint-disable-line no-magic-numbers
1827

28+
// Detect if node4 + webpack4 so we can skip.
29+
const isSkipped = (webpack) => webpack === "webpack4" &&
30+
process.version.match(/(v|)([0-9]+)/)[2] === "4"; // eslint-disable-line no-magic-numbers
31+
1932
// Specific hash regex to abstract.
2033
const HASH_RE = /[0-9a-f]{20}/gm;
2134

@@ -55,31 +68,106 @@ const readBuild = (buildDir) => {
5568
);
5669
};
5770

58-
let expecteds;
59-
const actuals = {};
71+
// Promise-friendly spawn.
72+
const spawn = function () {
73+
const stdout = [];
74+
const stderr = [];
75+
76+
return new Promise((resolve) => {
77+
const proc = cp.spawn.apply(cp, arguments); // eslint-disable-line prefer-spread
78+
proc.stdout.on("data", (data) => {
79+
stdout.push(data.toString());
80+
});
81+
proc.stderr.on("data", (data) => {
82+
stderr.push(data.toString());
83+
});
84+
proc.on("close", (code, signal) => resolve({
85+
code,
86+
signal,
87+
stdout: stdout.length ? stdout.join("") : null,
88+
stderr: stderr.length ? stderr.join("") : null
89+
}));
90+
});
91+
};
92+
93+
describe("builds", () => {
94+
let expecteds;
95+
const actuals = {};
6096

61-
// Read in expected fixtures.
62-
before(() => readBuild("expected").then((data) => { expecteds = data; }));
97+
// Read in expected fixtures.
98+
before(() => readBuild("expected").then((data) => { expecteds = data; }));
6399

64-
// Dynamically create suites and tests.
65-
WEBPACKS.forEach((webpack) => {
66-
before(() => readBuild(webpack).then((data) => { actuals[webpack] = data; }));
100+
// Dynamically create suites and tests.
101+
WEBPACKS.forEach((webpack) => {
102+
before(() => readBuild(webpack).then((data) => { actuals[webpack] = data; }));
67103

68-
describe(webpack, () => {
69-
it("matches expected files", function () {
70-
const actual = actuals[webpack];
104+
describe(webpack, () => {
105+
it("matches expected files", function () {
106+
const actual = actuals[webpack];
71107

72-
// Allow webpack4 to have no files if all other webpacks have the right
73-
// number of files to account for node4 not being supported.
74-
if (webpack === "webpack4" && !actual &&
75-
actuals.webpack1 && actuals.webpack2 && actuals.webpack3) {
76-
// Dynamically skip.
77-
return void this.skip(); // eslint-disable-line no-invalid-this
78-
}
108+
// Allow webpack4 to have no files if all other webpacks have the right
109+
// number of files to account for node4 not being supported.
110+
if (isSkipped(webpack)) {
111+
// Dynamically skip.
112+
return void this.skip(); // eslint-disable-line no-invalid-this
113+
}
79114

80-
Object.keys(expecteds).forEach((fileKey) => {
81-
expect(actual[fileKey], fileKey).to.equal(expecteds[fileKey]);
115+
Object.keys(expecteds).forEach((fileKey) => {
116+
expect(actual[fileKey], fileKey).to.equal(expecteds[fileKey]);
117+
});
82118
});
83119
});
84120
});
85121
});
122+
123+
describe("failures", () => {
124+
// Dynamically figure out if doing webpack4.
125+
const VERSIONS = [].concat(
126+
[{ VERS: 1 }, { VERS: 2 }, { VERS: 3 }],
127+
isSkipped("webpack4") ? [] : [{ VERS: 4 }]
128+
);
129+
const NUM_ERRS = VERSIONS.length;
130+
131+
it("fails with synchronous error", () => {
132+
// Use builder to concurrently run:
133+
// `webpack<VERS> --config test/webpack<VERS>/webpack.config.fail-sync.js`
134+
return spawn(builderCli,
135+
[
136+
"envs", "test:build:single",
137+
JSON.stringify(VERSIONS),
138+
"--env", JSON.stringify({ WP_EXTRA: ".fail-sync" }),
139+
"--buffer", "--bail=false"
140+
]
141+
)
142+
.then((obj) => {
143+
expect(obj.code).to.equal(1);
144+
expect(obj.stderr).to.contain(`Hit ${NUM_ERRS} errors`);
145+
146+
const exps = Array(NUM_ERRS).fill("Error: SYNC");
147+
const errs = obj.stderr.match(/(Error\: SYNC)/gm);
148+
expect(errs).to.eql(exps);
149+
});
150+
});
151+
152+
it("fails with promise rejection", () => {
153+
// Use builder to concurrently run:
154+
// `webpack<VERS> --config test/webpack<VERS>/webpack.config.fail-promise.js`
155+
return spawn(builderCli,
156+
[
157+
"envs", "test:build:single",
158+
JSON.stringify(VERSIONS),
159+
"--env", JSON.stringify({ WP_EXTRA: ".fail-promise" }),
160+
"--buffer", "--bail=false"
161+
]
162+
)
163+
.then((obj) => {
164+
expect(obj.code).to.equal(1);
165+
expect(obj.stderr).to.contain(`Hit ${NUM_ERRS} errors`);
166+
167+
const exps = Array(NUM_ERRS).fill("Error: PROMISE");
168+
const errs = obj.stderr.match(/(Error\: PROMISE)/gm);
169+
expect(errs).to.eql(exps);
170+
});
171+
});
172+
173+
});

test/mocha.opts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
--recursive
2+
--timeout 10000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/**
4+
* Fail promise.
5+
*/
6+
const base = require("./webpack.config");
7+
const fail = require("../webpack4/webpack.config.fail-promise");
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: fail.plugins
11+
});
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/**
4+
* Fail synchronously.
5+
*/
6+
const base = require("./webpack.config");
7+
const fail = require("../webpack4/webpack.config.fail-sync");
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: fail.plugins
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/**
4+
* Fail promise.
5+
*/
6+
const base = require("./webpack.config");
7+
const fail = require("../webpack4/webpack.config.fail-promise");
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: fail.plugins
11+
});
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/**
4+
* Fail synchronously.
5+
*/
6+
const base = require("./webpack.config");
7+
const fail = require("../webpack4/webpack.config.fail-sync");
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: fail.plugins
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/**
4+
* Fail promise.
5+
*/
6+
const base = require("./webpack.config");
7+
const fail = require("../webpack4/webpack.config.fail-promise");
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: fail.plugins
11+
});
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/**
4+
* Fail synchronously.
5+
*/
6+
const base = require("./webpack.config");
7+
const fail = require("../webpack4/webpack.config.fail-sync");
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: fail.plugins
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use strict";
2+
3+
/**
4+
* Fail promise.
5+
*/
6+
const base = require("./webpack.config");
7+
const StatsWriterPlugin = require("../../index").StatsWriterPlugin;
8+
9+
module.exports = Object.assign({}, base, {
10+
plugins: [
11+
new StatsWriterPlugin({
12+
filename: "stats-transform-fail-promise.json",
13+
transform() {
14+
return Promise.reject(new Error("PROMISE"));
15+
}
16+
})
17+
]
18+
});

0 commit comments

Comments
 (0)