Skip to content

Commit

Permalink
support loading ESM plugins from the CLI (#4265)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukas Taegert-Atkinson <lukastaegert@users.noreply.github.com>
  • Loading branch information
kzc and lukastaegert committed Nov 12, 2021
1 parent 2810269 commit 2d85884
Show file tree
Hide file tree
Showing 19 changed files with 151 additions and 20 deletions.
2 changes: 1 addition & 1 deletion build-plugins/esm-dynamic-import.ts
Expand Up @@ -13,7 +13,7 @@ export default function addBinShebangAndEsmImport(): Plugin {
name: 'esm-dynamic-import',
renderDynamicImport({ moduleId }) {
importFound = true;
if (moduleId.endsWith('loadConfigFile.ts')) {
if (moduleId.endsWith('commandPlugins.ts') || moduleId.endsWith('loadConfigFile.ts')) {
return { left: 'import(', right: ')' };
}
}
Expand Down
30 changes: 20 additions & 10 deletions cli/run/commandPlugins.ts
Expand Up @@ -3,40 +3,42 @@ import { InputOptions } from '../../src/rollup/types';
import { stdinPlugin } from './stdin';
import { waitForInputPlugin } from './waitForInput';

export function addCommandPluginsToInputOptions(
export async function addCommandPluginsToInputOptions(
inputOptions: InputOptions,
command: Record<string, unknown>
): void {
): Promise<void> {
if (command.stdin !== false) {
inputOptions.plugins!.push(stdinPlugin(command.stdin));
}
if (command.waitForBundleInput === true) {
inputOptions.plugins!.push(waitForInputPlugin());
}
addPluginsFromCommandOption(command.plugin, inputOptions);
await addPluginsFromCommandOption(command.plugin, inputOptions);
}

export function addPluginsFromCommandOption(
export async function addPluginsFromCommandOption(
commandPlugin: unknown,
inputOptions: InputOptions
): void {
): Promise<void> {
if (commandPlugin) {
const plugins = Array.isArray(commandPlugin) ? commandPlugin : [commandPlugin];
for (const plugin of plugins) {
if (/[={}]/.test(plugin)) {
// -p plugin=value
// -p "{transform(c,i){...}}"
loadAndRegisterPlugin(inputOptions, plugin);
await loadAndRegisterPlugin(inputOptions, plugin);
} else {
// split out plugins joined by commas
// -p node-resolve,commonjs,buble
plugin.split(',').forEach((plugin: string) => loadAndRegisterPlugin(inputOptions, plugin));
for (const p of plugin.split(',')) {
await loadAndRegisterPlugin(inputOptions, p);
}
}
}
}
}

function loadAndRegisterPlugin(inputOptions: InputOptions, pluginText: string): void {
async function loadAndRegisterPlugin(inputOptions: InputOptions, pluginText: string) {
let plugin: any = null;
let pluginArg: any = undefined;
if (pluginText[0] === '{') {
Expand All @@ -57,7 +59,7 @@ function loadAndRegisterPlugin(inputOptions: InputOptions, pluginText: string):
// Prefix order is significant - left has higher precedence.
for (const prefix of ['@rollup/plugin-', 'rollup-plugin-']) {
try {
plugin = require(prefix + pluginText);
plugin = await requireOrImport(prefix + pluginText);
break;
} catch {
// if this does not work, we try requiring the actual name below
Expand All @@ -67,7 +69,7 @@ function loadAndRegisterPlugin(inputOptions: InputOptions, pluginText: string):
if (!plugin) {
try {
if (pluginText[0] == '.') pluginText = path.resolve(pluginText);
plugin = require(pluginText);
plugin = await requireOrImport(pluginText);
} catch (err: any) {
throw new Error(`Cannot load plugin "${pluginText}": ${err.message}.`);
}
Expand Down Expand Up @@ -99,3 +101,11 @@ function getCamelizedPluginBaseName(pluginText: string): string {
.map((part, index) => (index === 0 || !part ? part : part[0].toUpperCase() + part.slice(1)))
.join('');
}

async function requireOrImport(pluginPath: string): Promise<any> {
try {
return require(pluginPath);
} catch {
return import(pluginPath);
}
}
2 changes: 1 addition & 1 deletion cli/run/index.ts
Expand Up @@ -92,5 +92,5 @@ async function getConfigs(
const { options, warnings } = await loadAndParseConfigFile(configFile, command);
return { options, warnings };
}
return loadConfigFromCommand(command);
return await loadConfigFromCommand(command);
}
11 changes: 6 additions & 5 deletions cli/run/loadConfigFile.ts
Expand Up @@ -27,11 +27,12 @@ export default async function loadAndParseConfigFile(
const configs = await loadConfigFile(fileName, commandOptions);
const warnings = batchWarnings();
try {
const normalizedConfigs = configs.map(config => {
const normalizedConfigs: MergedRollupOptions[] = [];
for (const config of configs) {
const options = mergeOptions(config, commandOptions, warnings.add);
addCommandPluginsToInputOptions(options, commandOptions);
return options;
});
await addCommandPluginsToInputOptions(options, commandOptions);
normalizedConfigs.push(options);
}
return { options: normalizedConfigs, warnings };
} catch (err) {
warnings.flush();
Expand Down Expand Up @@ -73,7 +74,7 @@ async function getDefaultFromTranspiledConfigFile(
plugins: [],
treeshake: false
};
addPluginsFromCommandOption(commandOptions.configPlugin, inputOptions);
await addPluginsFromCommandOption(commandOptions.configPlugin, inputOptions);
const bundle = await rollup.rollup(inputOptions);
if (!commandOptions.silent && warnings.count > 0) {
stderr(bold(`loaded ${relativeId(fileName)} with warnings`));
Expand Down
6 changes: 3 additions & 3 deletions cli/run/loadConfigFromCommand.ts
Expand Up @@ -4,15 +4,15 @@ import batchWarnings, { BatchWarnings } from './batchWarnings';
import { addCommandPluginsToInputOptions } from './commandPlugins';
import { stdinName } from './stdin';

export default function loadConfigFromCommand(command: Record<string, any>): {
export default async function loadConfigFromCommand(command: Record<string, any>): Promise<{
options: MergedRollupOptions[];
warnings: BatchWarnings;
} {
}> {
const warnings = batchWarnings();
if (!command.input && (command.stdin || !process.stdin.isTTY)) {
command.input = stdinName;
}
const options = mergeOptions({ input: [] }, command, warnings.add);
addCommandPluginsToInputOptions(options, command);
await addCommandPluginsToInputOptions(options, command);
return { options: [options], warnings };
}
6 changes: 6 additions & 0 deletions test/cli/samples/plugin/absolute-esm/_config.js
@@ -0,0 +1,6 @@
module.exports = {
description: 'ESM CLI --plugin /absolute/path',
minNodeVersion: 12,
skipIfWindows: true,
command: `echo 'console.log(1 ? 2 : 3);' | rollup -p "\`pwd\`/my-esm-plugin.mjs={comment: 'Absolute ESM'}"`
};
2 changes: 2 additions & 0 deletions test/cli/samples/plugin/absolute-esm/_expected.js
@@ -0,0 +1,2 @@
// Hello Absolute ESM
console.log(2 );
8 changes: 8 additions & 0 deletions test/cli/samples/plugin/absolute-esm/my-esm-plugin.mjs
@@ -0,0 +1,8 @@
export default function(options = {}) {
const {comment} = options;
return {
transform(code) {
return `// Hello ${comment}\n${code}`;
}
};
};
10 changes: 10 additions & 0 deletions test/cli/samples/plugin/advanced-esm/_config.js
@@ -0,0 +1,10 @@
module.exports = {
description: 'load an ESM-only rollup plugin from node_modules as well as CJS plugins',
minNodeVersion: 12,
skipIfWindows: true,

// The NodeJS resolution rules for ESM modules are more restrictive than CJS.
// Must copy the ESM plugin into the main node_modules in order to use and test it.

command: `rm -rf ../../../../../node_modules/rollup-plugin-esm-test && cp -rp node_modules/rollup-plugin-esm-test ../../../../../node_modules/ && rollup -c -p node-resolve,commonjs,esm-test -p "terser={mangle: false, output: {beautify: true, indent_level: 2}}"`
};
20 changes: 20 additions & 0 deletions test/cli/samples/plugin/advanced-esm/_expected/cjs.js
@@ -0,0 +1,20 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: !0
}), console.log("esm-test: node_modules/print/index.js");

console.log("esm-test: node_modules/foo/index.js");

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

console.log("esm-test: main.js"), new Foo(123).output(), exports.Bar = Foo;
18 changes: 18 additions & 0 deletions test/cli/samples/plugin/advanced-esm/_expected/es.js
@@ -0,0 +1,18 @@
console.log("esm-test: node_modules/print/index.js");

console.log("esm-test: node_modules/foo/index.js");

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

console.log("esm-test: main.js"), new Foo(123).output();

export { Foo as Bar };
4 changes: 4 additions & 0 deletions test/cli/samples/plugin/advanced-esm/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-esm/node_modules/foo/index.js

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

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

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

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-esm/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'
}
]
};
6 changes: 6 additions & 0 deletions test/cli/samples/plugin/relative-esm/_config.js
@@ -0,0 +1,6 @@
module.exports = {
description: 'ESM CLI --plugin ../relative/path',
minNodeVersion: 12,
skipIfWindows: true,
command: `echo 'console.log(1 ? 2 : 3);' | rollup -p "../absolute-esm/my-esm-plugin.mjs={comment: 'Relative ESM'}"`
};
2 changes: 2 additions & 0 deletions test/cli/samples/plugin/relative-esm/_expected.js
@@ -0,0 +1,2 @@
// Hello Relative ESM
console.log(2 );

0 comments on commit 2d85884

Please sign in to comment.