Skip to content

Commit

Permalink
fix: ensure plugin option association (#9)
Browse files Browse the repository at this point in the history
Previously the basename of the resolved plugin file would be used as the plugin name when applying options. However, that meant that if the file was called "index.js" in the plugin package, i.e. named "my-codemod", it was distributed with, the plugin options would have to be given to a plugin named "index" rather than "my-codemod".
  • Loading branch information
eventualbuddha authored Mar 30, 2017
1 parent cee1915 commit 8ec00a6
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 36 deletions.
104 changes: 79 additions & 25 deletions src/Options.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,53 @@
import * as Babel from 'babel-core';
import { existsSync, readFileSync } from 'fs';
import { hasMagic as hasGlob, sync as globSync } from 'glob';
import { basename, extname, resolve } from 'path';
import { sync as resolveSync } from 'resolve';
import { PathPredicate } from './iterateSources';
import { Plugin } from './TransformRunner';
import { BabelPlugin, RawBabelPlugin } from './TransformRunner';

export const DEFAULT_EXTENSIONS = new Set(['.js', '.jsx']);
export type ParseOptionsResult = Options | Error;

export default class Options {
private plugins?: Array<Plugin>;
export class Plugin {
readonly declaredName?: string;

constructor(
readonly rawPlugin: RawBabelPlugin,
readonly inferredName: string,
readonly path?: string,
) {
let instance = rawPlugin(Babel);

if (instance.name) {
this.declaredName = instance.name;
}
}

static load(path: string, inferredName: string) {
let exports = require(path);
let plugin;

if (exports.default) {
plugin = exports.default;
} else {
plugin = exports;
}

let rawPlugin = plugin;

return new Plugin(
rawPlugin,
inferredName,
path
);
}
}

export default class Options {
constructor(
readonly sourcePaths: Array<string>,
readonly pluginFilePaths: Array<string>,
readonly plugins: Array<Plugin>,
readonly pluginOptions: Map<string, object>,
readonly extensions: Set<string>,
readonly requires: Array<string>,
Expand All @@ -23,10 +57,6 @@ export default class Options {
) {}

getPlugins(): Array<Plugin> {
if (!this.plugins) {
this.plugins = this.loadPlugins();
}

return this.plugins;
}

Expand All @@ -36,30 +66,53 @@ export default class Options {
}
}

private loadPlugins(): Array<Plugin> {
return this.pluginFilePaths.map(pluginFilePath => {
let name = basename(pluginFilePath, extname(pluginFilePath));
let options = this.pluginOptions.get(name);
let exports = require(pluginFilePath);
let plugin;

if (exports.default) {
plugin = exports.default;
} else {
plugin = exports;
getPlugin(name: string): Plugin | null {
for (let plugin of this.plugins) {
if (plugin.declaredName === name || plugin.inferredName === name) {
return plugin;
}
}

return null;
}

getBabelPlugins(): Array<BabelPlugin> {
let result: Array<BabelPlugin> = [];

for (let plugin of this.plugins) {
let options = plugin.declaredName &&
this.pluginOptions.get(plugin.declaredName) ||
this.pluginOptions.get(plugin.inferredName);

if (options) {
return [plugin, options];
result.push([plugin.rawPlugin, options]);
} else {
return plugin;
result.push(plugin.rawPlugin);
}
});
}

return result;
}

getBabelPlugin(name: string): BabelPlugin | null {
let plugin = this.getPlugin(name);

if (!plugin) {
return null;
}

let options = this.pluginOptions.get(name);

if (options) {
return [plugin.rawPlugin, options];
} else {
return plugin.rawPlugin;
}
}

static parse(args: Array<string>): ParseOptionsResult {
let sourcePaths: Array<string> = [];
let pluginFilePaths: Array<string> = [];
let plugins: Array<Plugin> = [];
let pluginOptions: Map<string, object> = new Map();
let extensions = DEFAULT_EXTENSIONS;
let ignore = (path: string, basename: string, root: string) => basename[0] === '.';
Expand All @@ -74,7 +127,8 @@ export default class Options {
case '-p':
case '--plugin':
i++;
pluginFilePaths.push(getRequirableModulePath(args[i]));
let path = args[i];
plugins.push(Plugin.load(getRequirableModulePath(path), basename(path, extname(path))));
break;

case '-o':
Expand Down Expand Up @@ -137,7 +191,7 @@ export default class Options {

return new Options(
sourcePaths,
pluginFilePaths,
plugins,
pluginOptions,
extensions,
requires,
Expand Down
8 changes: 4 additions & 4 deletions src/TransformRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export class SourceTransformResult {
) {}
}

export type Plugin =
((babel: typeof Babel) => { visitor: Visitor }) |
[(babel: typeof Babel) => { visitor: Visitor }, object];
export type RawBabelPlugin = (babel: typeof Babel) => { name?: string, visitor: Visitor };
export type RawBabelPluginWithOptions = [RawBabelPlugin, object];
export type BabelPlugin = RawBabelPlugin | RawBabelPluginWithOptions;

export type TransformRunnerDelegate = {
transformStart?: (runner: TransformRunner) => void;
Expand All @@ -33,7 +33,7 @@ export type TransformRunnerDelegate = {
export default class TransformRunner {
constructor(
readonly sources: IterableIterator<Source> | Array<Source>,
readonly plugins: Array<Plugin>,
readonly plugins: Array<BabelPlugin>,
private readonly delegate: TransformRunnerDelegate = {},
) {}

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default async function run(args: Array<string>) {

options.loadRequires();

let plugins = options.getPlugins();
let plugins = options.getBabelPlugins();
let runner: TransformRunner;

if (options.stdio) {
Expand Down
48 changes: 42 additions & 6 deletions test/OptionsTest.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { deepEqual, strictEqual } from 'assert';
import { inspect } from 'util';
import Options, { ParseOptionsResult } from '../src/Options';

describe('Options', function() {
it('has sensible defaults', function() {
let options = assertOptionsParsed(Options.parse([]));
deepEqual(options.extensions, new Set(['.js', '.jsx']));
deepEqual(options.pluginFilePaths, []);
deepEqual(options.plugins, []);
deepEqual(options.sourcePaths, []);
deepEqual(options.requires, []);
strictEqual(options.pluginOptions.size, 0);
Expand All @@ -22,11 +23,6 @@ describe('Options', function() {
strictEqual(error.message, 'unexpected option: --wtf');
});

it('allows existing file paths as plugins', function() {
let options = assertOptionsParsed(Options.parse(['--plugin', __filename]));
deepEqual(options.pluginFilePaths, [__filename]);
});

it('interprets non-option arguments as paths', function() {
let options = assertOptionsParsed(Options.parse(['src/', 'a.js']));
deepEqual(options.sourcePaths, ['src/', 'a.js']);
Expand All @@ -47,6 +43,46 @@ describe('Options', function() {
deepEqual(options.pluginOptions.get('my-plugin'), { foo: true });
});

it('associates plugin options based on declared name', function() {
let options = assertOptionsParsed(Options.parse([
'--plugin',
'./test/fixtures/plugin/index.js',
'--plugin-options',
'basic-plugin={"a": true}'
]));

// "basic-plugin" is declared in the plugin file
deepEqual(options.pluginOptions.get('basic-plugin'), { a: true });

let babelPlugin = options.getBabelPlugin('basic-plugin');

if (!Array.isArray(babelPlugin)) {
throw new Error(`expected plugin to be [plugin, options] tuple: ${inspect(babelPlugin)}`);
}

deepEqual(babelPlugin[1], { a: true });
});

it('associates plugin options based on inferred name', function() {
let options = assertOptionsParsed(Options.parse([
'--plugin',
'./test/fixtures/plugin/index.js',
'--plugin-options',
'index={"a": true}'
]));

// "index" is the name of the file
deepEqual(options.pluginOptions.get('index'), { a: true });

let babelPlugin = options.getBabelPlugin('index');

if (!Array.isArray(babelPlugin)) {
throw new Error(`expected plugin to be [plugin, options] tuple: ${inspect(babelPlugin)}`);
}

deepEqual(babelPlugin[1], { a: true });
});

it('can parse a JSON file for plugin options', function() {
// You wouldn't actually use package.json, but it's a convenient JSON file.
let options = assertOptionsParsed(Options.parse(['-o', '[email protected]']));
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function() {
return {
name: 'basic-plugin',
visitor: {}
}
};

0 comments on commit 8ec00a6

Please sign in to comment.