Skip to content

Commit

Permalink
Merge pull request #18 from stephencookdev/1.0.0-beta
Browse files Browse the repository at this point in the history
1.0.0 release
  • Loading branch information
stephencookdev authored Feb 22, 2018
2 parents 7f80942 + 5a42bd0 commit 76c7adc
Show file tree
Hide file tree
Showing 12 changed files with 3,242 additions and 134 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Stephen Cook <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
100 changes: 61 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@ This plugin measures your webpack build speed, giving an output like this:
## Install

```bash
npm install --save speed-measure-webpack-plugin
npm install --save-dev speed-measure-webpack-plugin
```

or

```bash
yarn add speed-measure-webpack-plugin
yarn add -D speed-measure-webpack-plugin
```

## Migrating

SMP follows [semver](https://semver.org/). If upgrading a major version, you can consult [the migration guide](./migration.md).

## Requirements

SMP requires at least Node v6.

## Usage

Change your webpack config from
Expand All @@ -43,53 +51,32 @@ to
```javascript
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const webpackConfig = {
plugins: SpeedMeasurePlugin.wrapPlugins({
MyPlugin: new MyPlugin(),
MyOtherPlugin: new MyOtherPlugin()
})
}
```

If you're using `webpack-merge`, then you can do:

```javascript
const smp = new SpeedMeasurePlugin();

const baseConfig = {
plugins: smp.wrapPlugins({
MyPlugin: new MyPlugin()
}).concat(smp)
// ^ note the `.concat(smp)`
};

const envSpecificConfig = {
plugins: smp.wrapPlugins({
MyOtherPlugin: new MyOtherPlugin()
})
// ^ note no `.concat(smp)`
}

const finalWebpackConfig = webpackMerge([
baseConfig,
envSpecificConfig
]);

const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
});
```

and you're done! SMP will now be printing timing output to the console by default

## Options

Options are passed in to the constructor
Options are (optionally) passed in to the constructor

```javascript
const smp = new SpeedMeasurePlugin(options);
```

or as the second argument to the static `wrapPlugins`
### `options.disable`

```javascript
SpeedMeasurePlugin.wrapPlugins(pluginMap, options);
```
Type: `Boolean`<br>
Default: `false`

If truthy, this plugin does nothing at all. It is recommended to set this with something similar to `{ disable: !process.env.MEASURE }` to allow opt-in measurements with a `MEASURE=true npm run build`

### `options.outputFormat`

Expand All @@ -111,9 +98,44 @@ Default: `console.log`
* If a string, it specifies the path to a file to output to.
* If a function, it will call the function with the output as the first parameter

### `options.disable`
### `options.pluginNames`

Type: `Object`<br>
Default: `{}`

By default, SMP derives plugin names through `plugin.constructor.name`. For some
plugins this doesn't work (or you may want to override this default). This option
takes an object of `pluginName: PluginConstructor`, e.g.

```javascript
const uglify = new UglifyJSPlugin();
const smp = new SpeedMeasurePlugin({
pluginNames: {
customUglifyName: uglify
}
});

const webpackConfig = smp.wrap({
plugins: [
uglify
]
});
```

### `options.granularLoaderData` _(experimental)_

Type: `Boolean`<br>
Default: `false`

If truthy, this plugin does nothing at all. It is recommended to set this with something similar to `{ disable: !process.env.MEASURE }` to allow opt-in measurements with a `MEASURE=true npm run build`
If truthy, this plugin will attempt to break down the loader timing data to give per-loader timing information.

Points of note that the following loaders will have inaccurate results in this mode:

* loaders using separate processes (e.g. `thread-loader`) - these make it difficult to get timing information on the subsequent loaders, as they're not attached to the main thread
* loaders emitting file output (e.g. `file-loader`) - the time taken in outputting the actual file is not included in the running time of the loader

These are restrictions from technical limitations - ideally we would find solutions to these problems before removing the _(experimental)_ flag on this options

## License

[MIT](/LICENSE)
24 changes: 13 additions & 11 deletions WrappedPlugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@ let idInc = 0;

const genPluginMethod = (orig, pluginName, smp, type) =>
function(method, func) {
const timeEventName = pluginName + "/" + type + "/" + method;
const wrappedFunc = (...args) => {
const id = idInc++;
const timeEventName = pluginName + "/" + type + "/" + method;
// we don't know if there's going to be a callback applied to a particular
// call, so we just set it multiple times, letting each one override the last
let endCallCount = 0;
const addEndEvent = () => {
endCallCount++;
smp.addTimeEvent("plugins", timeEventName, "end", { id });
};
const addEndEvent = () =>
smp.addTimeEvent("plugins", timeEventName, "end", {
id,
// we need to allow failure, since webpack can finish compilation and
// cause our callbacks to fall on deaf ears
allowFailure: true,
});

smp.addTimeEvent("plugins", timeEventName, "start", {
id,
name: pluginName,
});
// invoke an end event immediately in case the callback here causes webpack
// to complete compilation. If this gets invoked and not the subsequent
// call, then our data will be inaccurate, sadly
addEndEvent();
const ret = func.apply(
this,
args.map(a => wrap(a, pluginName, smp, addEndEvent))
);

// If the end event was invoked as a callback immediately, we can
// don't want to add another end event here (and it can actually cause
// errors, if webpack has finished compilation entirely)
if (!endCallCount) addEndEvent();
addEndEvent();

return ret;
};
Expand Down
21 changes: 7 additions & 14 deletions colours.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
const greenFg = "\x1b[32m";
const yellowFg = "\x1b[33m";
const redFg = "\x1b[31m";
const blackBg = "\x1b[40m";
const bold = "\x1b[1m";
const end = "\x1b[0m";
const chalk = require("chalk");

module.exports.fg = (text, time) => {
let colour;
if (time >= 0) colour = greenFg;
if (time > 2000) colour = yellowFg;
if (time > 10000) colour = redFg;
let textModifier = chalk.bold;
if (time > 10000) textModifier = textModifier.red;
else if (time > 2000) textModifier = textModifier.yellow;
else textModifier = textModifier.green;

return (colour || "") + bold + text + end;
return textModifier(text);
};

module.exports.bg = text => blackBg + greenFg + bold + text + end;

module.exports.stripColours = text => text.replace(/\x1b\[[0-9]+m/g, "");
module.exports.bg = text => chalk.bgBlack.green.bold(text);
82 changes: 55 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
const path = require("path");
const fs = require("fs");
const chalk = require("chalk");
const { WrappedPlugin } = require("./WrappedPlugin");
const { getModuleName, getLoaderNames } = require("./utils");
const { getModuleName, getLoaderNames, prependLoader } = require("./utils");
const {
getHumanOutput,
getMiscOutput,
getPluginsOutput,
getLoadersOutput,
smpTag,
} = require("./output");
const { stripColours } = require("./colours");

const NS = path.dirname(fs.realpathSync(__filename));

module.exports = class SpeedMeasurePlugin {
constructor(options) {
this.options = options || {};

this.timeEventData = {};
this.smpPluginAdded = false;

this.wrapPlugins = this.wrapPlugins.bind(this);
this.wrap = this.wrap.bind(this);
this.getOutput = this.getOutput.bind(this);
this.addTimeEvent = this.addTimeEvent.bind(this);
this.apply = this.apply.bind(this);
this.provideLoaderTiming = this.provideLoaderTiming.bind(this);
}

static wrapPlugins(plugins, options) {
if (options.disable) return Object.keys(plugins).map(k => plugins[k]);

const smp = new SpeedMeasurePlugin(options);

const wrappedPlugins = smp.wrapPlugins(plugins);
wrap(config) {
if (this.options.disable) return config;

config.plugins = (config.plugins || []).map(plugin => {
const pluginName =
Object.keys(this.options.pluginNames || {}).find(
pluginName => plugin === this.options.pluginNames[pluginName]
) ||
(plugin.constructor && plugin.constructor.name) ||
"(unable to deduce plugin name)";
return new WrappedPlugin(plugin, pluginName, this);
});

return wrappedPlugins.concat(smp);
}
if (config.module && this.options.granularLoaderData) {
config.module = prependLoader(config.module);
}

wrapPlugins(plugins) {
if (Array.isArray(plugins)) {
let i = 1;
plugins = plugins.reduce((acc, p) => {
acc["plugin " + i++] = p;
return acc;
});
if (!this.smpPluginAdded) {
config.plugins = config.plugins.concat(this);
this.smpPluginAdded = true;
}
plugins = plugins || {};

return Object.keys(plugins).map(
pluginName => new WrappedPlugin(plugins[pluginName], pluginName, this)
);
return config;
}

getOutput() {
Expand All @@ -65,6 +71,9 @@ module.exports = class SpeedMeasurePlugin {
}

addTimeEvent(category, event, eventType, data = {}) {
const allowFailure = data.allowFailure;
delete data.allowFailure;

const tED = this.timeEventData;
if (!tED[category]) tED[category] = {};
if (!tED[category][event]) tED[category][event] = [];
Expand All @@ -85,12 +94,13 @@ module.exports = class SpeedMeasurePlugin {
const eventToModify =
matchingEvent || (data.fillLast && eventList.find(e => !e.end));
if (!eventToModify) {
console.log(
console.error(
"Could not find a matching event to end",
category,
event,
data
);
if (allowFailure) return;
throw new Error("No matching event!");
}

Expand All @@ -107,15 +117,19 @@ module.exports = class SpeedMeasurePlugin {
compiler.plugin("done", () => {
this.addTimeEvent("misc", "compile", "end", { fillLast: true });

const outputToFile = typeof this.options.outputTarget === "string";
chalk.enabled = !outputToFile;
const output = this.getOutput();
if (typeof this.options.outputTarget === "string") {
const strippedOutput = stripColours(output);
chalk.enabled = true;
if (outputToFile) {
const writeMethod = fs.existsSync(this.options.outputTarget)
? fs.appendFile
: fs.writeFile;
writeMethod(this.options.outputTarget, strippedOutput + "\n", err => {
writeMethod(this.options.outputTarget, output + "\n", err => {
if (err) throw err;
console.log("Outputted timing info to " + this.options.outputTarget);
console.log(
smpTag() + "Outputted timing info to " + this.options.outputTarget
);
});
} else {
const outputFunc = this.options.outputTarget || console.log;
Expand All @@ -126,6 +140,10 @@ module.exports = class SpeedMeasurePlugin {
});

compiler.plugin("compilation", compilation => {
compilation.plugin("normal-module-loader", loaderContext => {
loaderContext[NS] = this.provideLoaderTiming;
});

compilation.plugin("build-module", module => {
const name = getModuleName(module);
if (name) {
Expand All @@ -148,4 +166,14 @@ module.exports = class SpeedMeasurePlugin {
});
});
}

provideLoaderTiming(info) {
const infoData = { id: info.id };
if (info.type !== "end") {
infoData.loader = info.loaderName;
infoData.name = info.module;
}

this.addTimeEvent("loaders", "build-specific", info.type, infoData);
}
};
Loading

0 comments on commit 76c7adc

Please sign in to comment.