Skip to content

Commit

Permalink
introduce CLI --plugin support (#3379)
Browse files Browse the repository at this point in the history
* introduce CLI --plugin support

- provide plugin prefixes as required
- support new and old styles of plugin naming

Examples:

--plugin rollup-plugin-buble
--plugin @rollup/plugin-buble
--plugin buble
-p "@rollup/plugin-replace={DBG:true}"
-p node-resolve,commonjs -p "terser={output:{beautify:true}}"
-p "/absolute/path/to/plugin={A:1}"
-p "../relative/path/to/plugin={B:2}"

* Add documentation, fix error handling

* Fix markdown formatting

* Remove trailing slash

* Fix examples in documentation

Co-authored-by: Lukas Taegert-Atkinson <lukastaegert@users.noreply.github.com>
  • Loading branch information
kzc and lukastaegert committed Feb 28, 2020
1 parent 1b5f505 commit 07223eb
Show file tree
Hide file tree
Showing 26 changed files with 286 additions and 26 deletions.
1 change: 1 addition & 0 deletions cli/help.md
Expand Up @@ -16,6 +16,7 @@ Basic options:
-m, --sourcemap Generate sourcemap (`-m inline` for inline map)
-n, --name <name> Name for UMD export
-o, --file <output> Single output file (if absent, prints to stdout)
-p, --plugin <plugin> Use the plugin specified (may be repeated)
-v, --version Show version number
-w, --watch Watch files in bundle and rebuild on changes
--amd.id <id> ID for AMD module (default is anonymous)
Expand Down
16 changes: 4 additions & 12 deletions cli/run/build.ts
Expand Up @@ -13,13 +13,11 @@ export default function build(
outputOptions: OutputOptions[],
warnings: BatchWarnings,
silent = false
) {
): Promise<unknown> {
const useStdout = !outputOptions[0].file && !outputOptions[0].dir;

const start = Date.now();
const files = useStdout
? ['stdout']
: outputOptions.map(t => relativeId(t.file || t.dir!));
const files = useStdout ? ['stdout'] : outputOptions.map(t => relativeId(t.file || t.dir!));
if (!silent) {
let inputFiles: string | undefined;
if (typeof inputOptions.input === 'string') {
Expand Down Expand Up @@ -61,13 +59,11 @@ export default function build(
process.stdout.write('\n' + tc.cyan(tc.bold('//→ ' + file.fileName + ':')) + '\n');
process.stdout.write(source);
}
return null
return null;
});
}

return Promise.all(outputOptions.map(output => bundle.write(output))).then(
() => bundle
);
return Promise.all(outputOptions.map(output => bundle.write(output))).then(() => bundle);
})
.then((bundle: RollupBuild | null) => {
if (!silent) {
Expand All @@ -79,9 +75,5 @@ export default function build(
printTimings(bundle.getTimings());
}
}
})
.catch((err: Error) => {
warnings.flush();
handleError(err);
});
}
91 changes: 80 additions & 11 deletions cli/run/index.ts
@@ -1,6 +1,7 @@
import { realpathSync } from 'fs';
import * as path from 'path';
import relative from 'require-relative';
import { WarningHandler } from '../../src/rollup/types';
import { InputOptions, WarningHandler } from '../../src/rollup/types';
import mergeOptions, { GenericConfigObject } from '../../src/utils/mergeOptions';
import { getAliasName } from '../../src/utils/relativeId';
import { handleError } from '../logging';
Expand Down Expand Up @@ -107,18 +108,86 @@ async function execute(
} else {
for (const config of configs) {
const warnings = batchWarnings();
const { inputOptions, outputOptions, optionError } = mergeOptions({
command,
config,
defaultOnWarnHandler: warnings.add
});
if (optionError) {
(inputOptions.onwarn as WarningHandler)({ code: 'UNKNOWN_OPTION', message: optionError });
try {
const { inputOptions, outputOptions, optionError } = mergeOptions({
command,
config,
defaultOnWarnHandler: warnings.add
});
if (optionError) {
(inputOptions.onwarn as WarningHandler)({ code: 'UNKNOWN_OPTION', message: optionError });
}
if (command.stdin !== false) {
inputOptions.plugins!.push(stdinPlugin());
}
if (command.plugin) {
const plugins = Array.isArray(command.plugin) ? command.plugin : [command.plugin];
for (const plugin of plugins) {
if (/[={}]/.test(plugin)) {
// -p plugin=value
// -p "{transform(c,i){...}}"
loadAndRegisterPlugin(inputOptions, plugin);
} else {
// split out plugins joined by commas
// -p node-resolve,commonjs,buble
plugin
.split(',')
.forEach((plugin: string) => loadAndRegisterPlugin(inputOptions, plugin));
}
}
}
await build(inputOptions, outputOptions, warnings, command.silent);
} catch (err) {
warnings.flush();
handleError(err);
}
}
}
}

function loadAndRegisterPlugin(inputOptions: InputOptions, pluginText: string) {
let plugin: any = null;
let pluginArg: any = undefined;
if (pluginText[0] === '{') {
// -p "{transform(c,i){...}}"
plugin = new Function('return ' + pluginText);
} else {
const match = pluginText.match(/^([@.\/\\\w|^{}|-]+)(=(.*))?$/);
if (match) {
// -p plugin
// -p plugin=arg
pluginText = match[1];
pluginArg = new Function('return ' + match[3])();
} else {
throw new Error(`Invalid --plugin argument format: ${JSON.stringify(pluginText)}`);
}
if (!/^\.|^rollup-plugin-|[@\/\\]/.test(pluginText)) {
// Try using plugin prefix variations first if applicable.
// Prefix order is significant - left has higher precedence.
for (const prefix of ['@rollup/plugin-', 'rollup-plugin-']) {
try {
plugin = require(prefix + pluginText);
break;
} catch (ex) {
// if this does not work, we try requiring the actual name below
}
}
if (command.stdin !== false) {
inputOptions.plugins!.push(stdinPlugin());
}
if (!plugin) {
try {
if (pluginText[0] == '.') pluginText = path.resolve(pluginText);
plugin = require(pluginText);
} catch (ex) {
throw new Error(`Cannot load plugin "${pluginText}"`);
}
await build(inputOptions, outputOptions, warnings, command.silent);
}
}
if (typeof plugin === 'object' && pluginText in plugin) {
// some plugins do not use `export default` for their entry point.
// attempt to use the plugin name as the named import name.
plugin = plugin[pluginText];
}
inputOptions.plugins!.push(
typeof plugin === 'function' ? plugin.call(plugin, pluginArg) : plugin
);
}
4 changes: 2 additions & 2 deletions cli/run/loadConfigFile.ts
@@ -1,4 +1,4 @@
import path from 'path';
import * as path from 'path';
import tc from 'turbocolor';
import * as rollup from '../../src/node-entry';
import { RollupBuild, RollupOutput } from '../../src/rollup/types';
Expand Down Expand Up @@ -73,4 +73,4 @@ export default function loadConfigFile(
return Array.isArray(configs) ? configs : [configs];
});
});
}
}
42 changes: 42 additions & 0 deletions docs/01-command-line-reference.md
Expand Up @@ -220,6 +220,7 @@ Many options have command line equivalents. In those cases, any arguments passed
-m, --sourcemap Generate sourcemap (`-m inline` for inline map)
-n, --name <name> Name for UMD export
-o, --file <output> Single output file (if absent, prints to stdout)
-p, --plugin <plugin> Use the plugin specified (may be repeated)
-v, --version Show version number
-w, --watch Watch files in bundle and rebuild on changes
--amd.id <id> ID for AMD module (default is anonymous)
Expand Down Expand Up @@ -268,6 +269,47 @@ The flags listed below are only available via the command line interface. All ot

Print the help document.

#### `-p <plugin>`, `--plugin <plugin>`

Use the specified plugin. There are several ways to specify plugins here:

- Via a relative path:

```
rollup -i input.js -f es -p ./my-plugin.js
```

The file should export a plugin object or a function returning such an object.
- Via the name of a plugin that is installed in a local or global `node_modules` folder:

```
rollup -i input.js -f es -p @rollup/plugin-node-resolve
```

If the plugin name does not start with `rollup-plugin-` or `@rollup/plugin-`, Rollup will automatically try adding these prefixes:

```
rollup -i input.js -f es -p node-resolve
```

- Via an inline implementation:

```
rollup -i input.js -f es -p '{transform: (c, i) => `/* ${JSON.stringify(i)} */\n${c}`}'
```

If you want to load more than one plugin, you can repeat the option or supply a comma-separated list of names:

```
rollup -i input.js -f es -p node-resolve -p commonjs,json
```

By default, plugins that export functions will be called with no argument to create the plugin. You can however pass a custom argument as well:

```
rollup -i input.js -f es -p 'terser={output: {beautify: true, indent_level: 2}}'
```

#### `-v`/`--version`

Print the installed version number.
Expand Down
2 changes: 2 additions & 0 deletions src/utils/mergeOptions.ts
Expand Up @@ -98,6 +98,7 @@ export const commandAliases: { [key: string]: string } = {
m: 'sourcemap',
n: 'name',
o: 'file',
p: 'plugin',
v: 'version',
w: 'watch'
};
Expand Down Expand Up @@ -158,6 +159,7 @@ export default function mergeOptions({
Object.keys(commandAliases),
'config',
'environment',
'plugin',
'silent',
'stdin'
),
Expand Down
5 changes: 5 additions & 0 deletions test/cli/samples/plugin/absolute/_config.js
@@ -0,0 +1,5 @@
module.exports = {
description: 'CLI --plugin /absolute/path',
skipIfWindows: true,
command: `echo 'console.log(VALUE);' | rollup -p "\`pwd\`/my-plugin={VALUE: 'absolute', ZZZ: 1}"`
};
1 change: 1 addition & 0 deletions test/cli/samples/plugin/absolute/_expected.js
@@ -0,0 +1 @@
console.log("absolute");
14 changes: 14 additions & 0 deletions test/cli/samples/plugin/absolute/my-plugin.js
@@ -0,0 +1,14 @@
module.exports = function(options) {
if (options === void 0) options = {};
return {
transform(code) {
// dumb search and replace for test purposes
for (var key in options) {
const rx = new RegExp(key, 'g');
const value = JSON.stringify(options[key]);
code = code.replace(rx, value);
}
return code;
}
};
};
5 changes: 5 additions & 0 deletions test/cli/samples/plugin/advanced/_config.js
@@ -0,0 +1,5 @@
module.exports = {
description: 'advanced CLI --plugin functionality with rollup config',
skipIfWindows: true,
command: `rollup -c -p node-resolve,commonjs -p "terser={output: {beautify: true, indent_level: 2}}"`
};
17 changes: 17 additions & 0 deletions test/cli/samples/plugin/advanced/_expected/cjs.js
@@ -0,0 +1,17 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: !0
});

var t = function() {
function t(t) {
this.x = t;
}
return t.prototype.output = function() {
var t;
t = this.x, console.log(t);
}, t;
}();

new t(123).output(), exports.Bar = t;
13 changes: 13 additions & 0 deletions test/cli/samples/plugin/advanced/_expected/es.js
@@ -0,0 +1,13 @@
var t = function() {
function t(t) {
this.x = t;
}
return t.prototype.output = function() {
var t;
t = this.x, console.log(t);
}, t;
}();

new t(123).output();

export { t as Bar };
4 changes: 4 additions & 0 deletions test/cli/samples/plugin/advanced/main.js
@@ -0,0 +1,4 @@
import {Foo} from "foo";
var foo = new Foo(123);
foo.output();
export {Foo as Bar};
10 changes: 10 additions & 0 deletions test/cli/samples/plugin/advanced/node_modules/foo/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions test/cli/samples/plugin/advanced/node_modules/print/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions test/cli/samples/plugin/advanced/rollup.config.js
@@ -0,0 +1,18 @@
const buble = require('rollup-plugin-buble');

export default {
input: 'main.js',
plugins: [
buble()
],
output: [
{
file: '_actual/cjs.js',
format: 'cjs'
},
{
file: '_actual/es.js',
format: 'esm'
}
]
};
4 changes: 4 additions & 0 deletions test/cli/samples/plugin/basic/_config.js
@@ -0,0 +1,4 @@
module.exports = {
description: 'basic CLI --plugin functionality',
command: `rollup main.js -f cjs --plugin rollup-plugin-buble`
};
14 changes: 14 additions & 0 deletions test/cli/samples/plugin/basic/_expected.js
@@ -0,0 +1,14 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var Bar = function Bar(x) {
this.x = value;
};
Bar.prototype.value = function value () {
return this.x;
};
var bar = new Bar(123);
console.log(bar.value());

exports.Bar = Bar;
10 changes: 10 additions & 0 deletions test/cli/samples/plugin/basic/main.js
@@ -0,0 +1,10 @@
export class Bar {
constructor(x) {
this.x = value;
}
value() {
return this.x;
}
}
var bar = new Bar(123);
console.log(bar.value());
10 changes: 10 additions & 0 deletions test/cli/samples/plugin/cannot-load/_config.js
@@ -0,0 +1,10 @@
const { assertStderrIncludes } = require('../../../../utils.js');

module.exports = {
description: 'unknown CLI --plugin results in an error',
skipIfWindows: true,
command: `echo "console.log(123);" | rollup --plugin foobar`,
error(err) {
assertStderrIncludes(err.message, '[!] Error: Cannot load plugin "foobar"');
}
};
10 changes: 10 additions & 0 deletions test/cli/samples/plugin/invalid-argument/_config.js
@@ -0,0 +1,10 @@
const { assertStderrIncludes } = require('../../../../utils.js');

module.exports = {
description: 'invalid CLI --plugin argument format',
skipIfWindows: true,
command: `echo "console.log(123);" | rollup --plugin 'foo bar'`,
error(err) {
assertStderrIncludes(err.message, '[!] Error: Invalid --plugin argument format: "foo bar"');
}
};
5 changes: 5 additions & 0 deletions test/cli/samples/plugin/object/_config.js
@@ -0,0 +1,5 @@
module.exports = {
description: 'CLI --plugin object',
skipIfWindows: true,
command: `echo 'console.log(42);' | rollup -f cjs -p '{transform: c => c + String.fromCharCode(10) + c}'`
};

0 comments on commit 07223eb

Please sign in to comment.