Skip to content

Commit

Permalink
feat(cli): adds CLI support for transforming a project from CJS to ESM
Browse files Browse the repository at this point in the history
  • Loading branch information
wessberg committed Sep 16, 2019
1 parent a88f6c8 commit b542834
Show file tree
Hide file tree
Showing 29 changed files with 663 additions and 35 deletions.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- SHADOW_SECTION_DESCRIPTION_SHORT_START -->

> A Custom Transformer for Typescript that transforms Node-style CommonJS to tree-shakeable ES Modules
> A CLI and Custom Transformer for Typescript that transforms Node-style CommonJS to tree-shakeable ES Modules
<!-- SHADOW_SECTION_DESCRIPTION_SHORT_END -->

Expand Down Expand Up @@ -125,6 +125,7 @@ As you can see, this transformer will attempt to produce code that generates as
- Clean, idiomatic output
- No wrappers
- Low-level implementation that can be used as the foundation for other tools such as Loaders, Plugins, CLIs, and Linters.
- CLI integration, enabling you to convert a project from CJS to ESM from the command line.

<!-- SHADOW_SECTION_FEATURE_IMAGE_START -->

Expand Down Expand Up @@ -343,6 +344,29 @@ const config = {
};
```

## CLI

You can also use this library as a CLI to convert your project files from using CommonJS to using ESM.
This is still considered somewhat experimental. If you have any issues, please submit an issue.

If you install `cjs-to-esm` globally, you'll have `cjstoesm` in your path. If you install it locally, you can run `npx cjstoesm`.

```
$ cjstoesm --help
Welcome to the CJS to ESM CLI!
Usage: cjstoesm [options] [command]
Options:
-h, --help output usage information
Commands:
transform [options] <input> <outDir> Transforms CJS to ESM modules based on the input glob
```

For example, you can run `cjstoesm transform "**/*.*" dist` to transform all files matched by the glob `**/*.*` and emit them to the folder `dit` from the current working directory.

## Options

You can provide options to the `cjsToEsm` Custom Transformer to configure its behavior:
Expand Down
5 changes: 5 additions & 0 deletions bin/cjstoesm
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
'use strict';

process.title = 'cjstoesm';
require("../dist/cli/index.js");
34 changes: 22 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"prebuild": "npm run clean:dist",
"build": "npm run rollup",
"build:built_in_module_map": "ts-node script/generate-built-in-module-map.ts",
"prewatch": "npm run clean:dist",
"watch": "npm run rollup -- --watch",
"rollup": "rollup -c rollup.config.js",
"preversion": "npm run lint && npm run build:built_in_module_map && NODE_ENV=production npm run build",
Expand All @@ -34,7 +35,11 @@
"custom transformer",
"treeshake"
],
"bin": {
"cjstoesm": "bin/cjstoesm"
},
"files": [
"bin/**/*.*",
"dist/**/*.*"
],
"contributors": [
Expand All @@ -50,29 +55,34 @@
],
"license": "MIT",
"devDependencies": {
"core-js": "3.1.4",
"core-js": "3.2.1",
"@wessberg/scaffold": "1.0.19",
"@wessberg/ts-config": "0.0.41",
"@wessberg/rollup-plugin-ts": "1.1.59",
"@wessberg/rollup-plugin-ts": "1.1.64",
"rollup-plugin-node-resolve": "5.2.0",
"rollup": "1.16.7",
"rollup-pluginutils": "2.8.1",
"ava": "2.2.0",
"standard-changelog": "2.0.11",
"ts-node": "8.3.0",
"tslint": "5.18.0",
"rollup": "1.21.4",
"rollup-pluginutils": "2.8.2",
"ava": "2.4.0",
"standard-changelog": "2.0.13",
"ts-node": "8.4.1",
"tslint": "5.20.0",
"prettier": "1.18.2",
"pretty-quick": "1.11.1",
"husky": "3.0.0",
"np": "5.0.3"
"husky": "3.0.5",
"np": "5.1.0",
"typescript": "^3.6.3"
},
"dependencies": {
"@types/node": "12.6.1",
"@types/node": "12.7.5",
"@types/reserved-words": "^0.1.0",
"@types/resolve": "0.0.8",
"@wessberg/stringutil": "1.0.18",
"chalk": "^2.4.2",
"commander": "^3.0.1",
"glob": "^7.1.4",
"inquirer": "^7.0.0",
"reserved-words": "^0.1.2",
"resolve": "1.11.1"
"resolve": "1.12.0"
},
"peerDependencies": {
"typescript": "^3.x"
Expand Down
52 changes: 37 additions & 15 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,46 @@ import ts from "@wessberg/rollup-plugin-ts";
import packageJson from "./package.json";
import {builtinModules} from "module";

export default {
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true
},
{
file: packageJson.module,
format: "esm",
sourcemap: true
}
],
const SHARED_OPTIONS = {
plugins: [
ts({
tsconfig: process.env.NODE_ENV === "production" ? "tsconfig.dist.json" : "tsconfig.json"
})
],
external: [...builtinModules, ...Object.keys(packageJson.dependencies), ...Object.keys(packageJson.devDependencies)]
external: [
...builtinModules,
...Object.keys(packageJson.dependencies),
...Object.keys(packageJson.devDependencies),
...Object.keys(packageJson.peerDependencies)
]
};

export default [
{
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true
},
{
file: packageJson.module,
format: "esm",
sourcemap: true
}
],
...SHARED_OPTIONS
},
{
input: "src/cli/index.ts",
output: [
{
dir: "./dist/cli",
format: "cjs",
sourcemap: true
}
],
...SHARED_OPTIONS
}
];
37 changes: 37 additions & 0 deletions src/cli/command/create-command/create-command-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export type CommandOptionType = "string" | "number" | "boolean";
export type CommandArgType = "string" | "string[]";

export interface CommandOption {
shortHand?: string;
type: CommandOptionType;
defaultValue?: unknown;
description: string;
}

export interface CommandOptions {
[key: string]: CommandOption;
}

export interface CommandArg {
type: CommandArgType;
required: boolean;
}

export interface CommandArgs {
[key: string]: CommandArg;
}

export interface CreateCommandOptions {
name: string;
description: string;
args: CommandArgs;
options: CommandOptions;
isDefault: boolean;
}

export type CommandActionOptions<T extends CreateCommandOptions, U extends T["options"] = T["options"], J extends T["args"] = T["args"]> = {
[Key in keyof U]: U[Key]["type"] extends "number" ? number : U[Key]["type"] extends "boolean" ? boolean : string;
} &
{[Key in keyof J]: J[Key]["type"] extends "string[]" ? string[] : string};

export type CommandAction<T extends CreateCommandOptions> = (options: CommandActionOptions<T>) => void;
101 changes: 101 additions & 0 deletions src/cli/command/create-command/create-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import commander from "commander";
import {CommandAction, CommandActionOptions, CommandOptionType, CreateCommandOptions} from "./create-command-options";

// tslint:disable:no-any

/**
* Coerces the given option value into an acceptable data type
* @param {CommandOptionType} type
* @param {*} value
*/
function coerceOptionValue(
type: CommandOptionType,
value: unknown
): typeof type extends "boolean" ? boolean : typeof type extends "number" ? number : string {
switch (type) {
case "string":
if (value === null) return "null";
else if (value === undefined) return "undefined";
return String(value);

case "number":
if (typeof value === "number") return (value as unknown) as string;
else if (value === true) return (1 as unknown) as string;
else if (value === false) return (0 as unknown) as string;
return (parseFloat(value as string) as unknown) as string;
case "boolean":
if (value === "true" || value === "" || value === "1" || value === 1) return (true as unknown) as string;
else if (value === "false" || value === "0" || value === 0) return (false as unknown) as string;
return (Boolean(value) as unknown) as string;
}
}

/**
* Formats the given option flags
* @param {string} shortHand
* @param {string} longHand
* @returns {string}
*/
function formatOptionFlags(shortHand: string | undefined, longHand: string): string {
const formattedLongHand = `${longHand} [arg]`;
return shortHand != null ? `-${shortHand}, --${formattedLongHand}` : `--${formattedLongHand}`;
}

/**
* Formats the given command name, along with its arguments
* @param {T} options
* @returns {string}
*/
function formatCommandNameWithArgs<T extends CreateCommandOptions>(options: T): string {
const formattedArgs = Object.entries(options.args)
.map(([argName, {type, required}]) => {
const left = required ? `<` : `[`;
const right = required ? ">" : `]`;
if (type === "string[]") {
return `${left}${argName}...${right}`;
} else {
return `${left}${argName}${right}`;
}
})
.join(" ");
return `${options.name} ${formattedArgs}`;
}

/**
* Creates a new command
* @param {T} options
* @param {CommandAction<T>} action
*/
export function createCommand<T extends CreateCommandOptions>(options: T, action: CommandAction<T>): void {
// Add the command to the program
const result = commander
.command(formatCommandNameWithArgs(options), {
isDefault: options.isDefault
})
.description(options.description);

// Add options to the command
Object.entries(options.options).forEach(([longhand, {shortHand, description, type, defaultValue}]) => {
result.option(formatOptionFlags(shortHand, longhand), description, coerceOptionValue.bind(null, type), defaultValue);
});
// Add the action to the command
result.action((...args: unknown[]) => {
const argKeys = Object.keys(options.args);
const optionKeys = Object.keys(options.options);
const actionOptions = {} as CommandActionOptions<T>;
for (let i = 0; i < args.length; i++) {
if (argKeys[i] == null) continue;
actionOptions[argKeys[i] as keyof typeof actionOptions] = args[i] as any;
}

// Take the last argument
const lastArg = args[args.length - 1];
// Apply all option values
for (const key of optionKeys) {
actionOptions[key as keyof typeof actionOptions] = (lastArg as any)[key];
}

// Invoke the action
action(actionOptions);
});
}
10 changes: 10 additions & 0 deletions src/cli/command/finalize-commands/finalize-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import commander from "commander";

commander.parse(process.argv);

// Show help if no arguments are given
if (commander.args.length === 0) {
commander.help(text => {
return `Welcome to the CJS to ESM CLI!\n\n` + text;
});
}
17 changes: 17 additions & 0 deletions src/cli/command/shared/shared-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const SHARED_OPTIONS = {
debug: {
shortHand: "d",
type: "boolean",
description: "Whether to print debug information"
},
verbose: {
shortHand: "v",
type: "boolean",
description: "Whether to print verbose information"
},
silent: {
shortHand: "s",
type: "boolean",
description: "Whether to not print anything"
}
} as const;
1 change: 1 addition & 0 deletions src/cli/command/transform/transform-command-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TRANSFORM_COMMAND_OPTIONS = {} as const;
40 changes: 40 additions & 0 deletions src/cli/command/transform/transform-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {createCommand} from "../create-command/create-command";
import {TRANSFORM_COMMAND_OPTIONS} from "./transform-command-options";
import {generateTaskOptions} from "../../task/generate-task-options/generate-task-options";
import {SHARED_OPTIONS} from "../shared/shared-options";

createCommand(
{
name: "transform",
description: `Transforms CJS to ESM modules based on the input glob`,
isDefault: true,
args: {
input: {
type: "string",
required: true,
description: "A glob for all the files that should be transformed"
},
outDir: {
type: "string",
required: true,
description: `The directory to write the transformed files to.`
}
},
options: {
...SHARED_OPTIONS,
...TRANSFORM_COMMAND_OPTIONS
}
},
async args => {
// Load the task
const {transformTask} = await import("../../task/transform/transform-task");
const taskOptions = await generateTaskOptions(args);

// Execute it
await transformTask({
...taskOptions,
input: args.input,
outDir: args.outDir
});
}
);

0 comments on commit b542834

Please sign in to comment.