Skip to content
This repository has been archived by the owner on Feb 18, 2024. It is now read-only.

Support Config.toString() with name hints #53

Merged
merged 6 commits into from
May 15, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ You cannot use both `.before()` and `.after()` on the same plugin.
config
.plugin(name)
.before(otherName)

// Example

config
Expand Down Expand Up @@ -1136,3 +1136,63 @@ config
config => config.devtool('source-map')
);
```

### Inspecting generated configuration

You can inspect the generated webpack config using `config.toString()`. This will generate a stringified version of the config with comment hints for named rules, uses and plugins:

``` js
config
.module
.rule('compile')
.test(/\.js$/)
.use('babel')
.loader('babel-loader');

config.toString();

/*
{
module: {
rules: [
/* config.module.rule('compile') */
{
test: /\.js$/,
use: [
/* config.module.rule('compile').use('babel') */
{
loader: 'babel-loader'
}
]
}
]
}
}
*/
```

By default the generated string cannot be used directly as real webpack config if it contains functions and plugins that need to be required. In order to generate usable config, you can customize how functions and plugins are stringified by setting a special `__expression` property on them:

``` js
class MyPlugin {}
MyPlugin.__expression = `require('my-plugin')`;

function myFunction () {}
myFunction.__expression = `require('my-function')`;

config
.plugin('example')
.use(MyPlugin, [{ fn: myFunction }]);

config.toString();

/*
{
plugins: [
new (require('my-plugin'))({
fn: require('my-function')
})
]
}
*/
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"changelog": "changelog mozilla-neutrino/webpack-chain all --markdown > CHANGELOG.md"
},
"dependencies": {
"deepmerge": "^1.5.2"
"deepmerge": "^1.5.2",
"javascript-stringify": "^1.6.0"
},
"devDependencies": {
"ava": "^0.25.0",
Expand Down
51 changes: 50 additions & 1 deletion src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ module.exports = class extends ChainedMap {

plugin(name) {
if (!this.plugins.has(name)) {
this.plugins.set(name, new Plugin(this));
this.plugins.set(name, new Plugin(this, name));
}

return this.plugins.get(name);
Expand All @@ -78,6 +78,55 @@ module.exports = class extends ChainedMap {
}));
}

toString({
verbose = false,
configPrefix = 'config'
} = {}) {
const stringify = require('javascript-stringify');

const config = this.toConfig();

return stringify(config, (value, indent, stringify) => {
// improve plugin output
if (value && value.__pluginName) {
const prefix = `/* ${configPrefix}.plugin('${value.__pluginName}') */\n`;
const constructorName = value.__pluginConstructorName;

if (constructorName) {
// get correct indentation for args by stringifying the args array and
// discarding the square brackets.
const args = stringify(value.__pluginArgs).slice(1, -1);
return prefix + `new ${constructorName}(${args})`;
} else {
return prefix + stringify({ args: value.__pluginArgs || [] });
Copy link
Member

@edmorley edmorley May 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there known cases where this path is taken, or is it for defensive purposes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly defensive when I found out about the case of copy-webpack-plugin, but this is probably unnecessary as a plugin has to be new-able and thus must have a name.

}
}

// improve rule/use output
if (value && value.__ruleNames) {
const prefix = `/* ${configPrefix}.module.rule('${
value.__ruleNames[0]
}')${
value.__ruleNames.slice(1).map(r => `.oneOf('${r}')`).join('')
}${
value.__useName ? `.use('${value.__useName}')` : ``
} */\n`;
return prefix + stringify(value);
}

// shorten long functions
if (typeof value === 'function') {
if (value.__expression) {
return value.__expression;
} else if (!verbose && value.toString().length > 100) {
return `function () { /* omitted long function */ }`;
}
}

return stringify(value);
}, 2);
}

merge(obj = {}, omit = []) {
const omissions = [
'node',
Expand Down
4 changes: 2 additions & 2 deletions src/Module.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ module.exports = class extends ChainedMap {

defaultRule(name) {
if (!this.defaultRules.has(name)) {
this.defaultRules.set(name, new Rule(this));
this.defaultRules.set(name, new Rule(this, name));
}

return this.defaultRules.get(name);
}

rule(name) {
if (!this.rules.has(name)) {
this.rules.set(name, new Rule(this));
this.rules.set(name, new Rule(this, name));
}

return this.rules.get(name);
Expand Down
17 changes: 15 additions & 2 deletions src/Plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ const ChainedMap = require('./ChainedMap');
const Orderable = require('./Orderable');

module.exports = Orderable(class extends ChainedMap {
constructor(parent) {
constructor(parent, name) {
super(parent);
this.name = name;
this.extend(['init']);

this.init((Plugin, args = []) => new Plugin(...args));
Expand Down Expand Up @@ -34,7 +35,19 @@ module.exports = Orderable(class extends ChainedMap {

toConfig() {
const init = this.get('init');
const plugin = this.get('plugin');
const constructorName = plugin.__expression
? `(${plugin.__expression})`
: plugin.name;

return init(this.get('plugin'), this.get('args'));
const config = init(this.get('plugin'), this.get('args'));

Object.defineProperties(config, {
__pluginName: { value: this.name },
__pluginArgs: { value: this.get('args') },
__pluginConstructorName: { value: constructorName }
});

return config;
}
});
2 changes: 1 addition & 1 deletion src/Resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = class extends ChainedMap {

plugin(name) {
if (!this.plugins.has(name)) {
this.plugins.set(name, new Plugin(this));
this.plugins.set(name, new Plugin(this, name));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test for this one to test/Resolve.js? :-)

}

return this.plugins.get(name);
Expand Down
23 changes: 19 additions & 4 deletions src/Rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ const ChainedSet = require('./ChainedSet');
const Use = require('./Use');

module.exports = class Rule extends ChainedMap {
constructor(parent) {
constructor(parent, name) {
super(parent);
this.name = name;
this.names = [];

let rule = this;
while (rule instanceof Rule) {
this.names.unshift(rule.name);
rule = rule.parent;
}

this.uses = new ChainedMap(this);
this.include = new ChainedSet(this);
this.exclude = new ChainedSet(this);
Expand All @@ -23,15 +32,15 @@ module.exports = class Rule extends ChainedMap {

use(name) {
if (!this.uses.has(name)) {
this.uses.set(name, new Use(this));
this.uses.set(name, new Use(this, name));
}

return this.uses.get(name);
}

oneOf(name) {
if (!this.oneOfs.has(name)) {
this.oneOfs.set(name, new Rule(this));
this.oneOfs.set(name, new Rule(this, name));
}

return this.oneOfs.get(name);
Expand All @@ -46,12 +55,18 @@ module.exports = class Rule extends ChainedMap {
}

toConfig() {
return this.clean(Object.assign(this.entries() || {}, {
const config = this.clean(Object.assign(this.entries() || {}, {
include: this.include.values(),
exclude: this.exclude.values(),
oneOf: this.oneOfs.values().map(oneOf => oneOf.toConfig()),
use: this.uses.values().map(use => use.toConfig())
}));

Object.defineProperties(config, {
__ruleNames: { value: this.names }
});

return config;
}

merge(obj, omit = []) {
Expand Down
12 changes: 10 additions & 2 deletions src/Use.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const Orderable = require('./Orderable');
const merge = require('deepmerge');

module.exports = Orderable(class extends ChainedMap {
constructor(parent) {
constructor(parent, name) {
super(parent);
this.name = name;
this.extend(['loader', 'options']);
}

Expand All @@ -26,6 +27,13 @@ module.exports = Orderable(class extends ChainedMap {
}

toConfig() {
return this.clean(this.entries() || {});
const config = this.clean(this.entries() || {});

Object.defineProperties(config, {
__useName: { value: this.name },
__ruleNames: { value: this.parent && this.parent.names }
});

return config;
}
});
Loading