diff --git a/README.md b/README.md index 393b9f40..be9f71d4 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,17 @@ $ codemod --find-babel-config --plugin ./my-plugin.js src/ This requires that all babel plugins and presets be installed locally and are listed in your `.babelrc` file. `babel-codemod` uses `babel-register` under the hood too accomplish this and all `.babelrc` [lookup rules apply](https://babeljs.io/docs/usage/babelrc/#lookup-behavior). ### Transpiling using TypeScript +`babel-codemod` supports plugins written in TypeScript. -There is currently an [open issue](https://github.com/square/babel-codemod/issues/51) for supporting plugins written in typescript. In the interim, you can take the same approach using `--require` along with `ts-node/register`. +Unlike babel transpilation described above, this feature is off by default and **requires that TypeScript be installed locally**. This feature uses `ts-node/register` under the hood so [all rules](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) regarding existence and placement of `tsconfig.json` apply. + +To opt-in to TypeScript transpilation of your plugins, simply pass the `--transpile-ts-plugins` option. For example: ```sh # Run a local plugin written with TypeScript. -$ codemod --require ts-node/register --plugin ./my-plugin.ts src/ +$ codemod --transpile-ts-plugins --plugin ./my-plugin.ts src/ ``` ## Contributing diff --git a/package.json b/package.json index d1b25056..d9a74385 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "recast": "^0.13.0", "resolve": "^1.5.0", "tmp": "^0.0.33", - "whatwg-url": "^6.4.0" + "whatwg-url": "^6.4.0", + "ts-node": "^4.1.0" }, "engines": { "node": ">=6.0.0" diff --git a/src/Options.ts b/src/Options.ts index bb0f98d6..0e9d0c1c 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -41,16 +41,26 @@ export default class Options { readonly requires: Array, readonly transpilePlugins: boolean, readonly findBabelConfig: boolean, + readonly transpilePluginsWithTypescript: boolean, readonly ignore: PathPredicate, readonly stdio: boolean, readonly help: boolean, readonly dry: boolean - ) {} + ) { + const defaultFileSystemExtensions = new Set(['.js']); + if (transpilePluginsWithTypescript) { + { + defaultFileSystemExtensions.add('.ts'); + } + } - private pluginLoader = new PluginLoader([ - new FileSystemResolver(), - new PackageResolver() - ]); + this.pluginLoader = new PluginLoader([ + new FileSystemResolver(defaultFileSystemExtensions), + new PackageResolver() + ]); + } + + private pluginLoader: PluginLoader; private remotePluginLoader = new PluginLoader([ new AstExplorerResolver(), @@ -109,6 +119,18 @@ export default class Options { } } + loadTypescriptTranspile() { + try { + require.resolve('typescript'); + } catch (e) { + throw new Error( + 'Typescript is not installed locally. You must installed Typescript locally in order to transpile plugins written in Typescript' + ); + } + + require('ts-node').register(); + } + async getPlugin(name: string): Promise { for (let plugin of await this.getPlugins()) { if (plugin.declaredName === name || plugin.inferredName === name) { @@ -164,6 +186,7 @@ export default class Options { let requires: Array = []; let findBabelConfig = false; let transpilePlugins = true; + let transpilePluginsWithTypescript = false; let stdio = false; let help = false; let dry = false; @@ -222,6 +245,10 @@ export default class Options { findBabelConfig = arg === '--find-babel-config'; break; + case '--transpile-ts-plugins': + transpilePluginsWithTypescript = true; + break; + case '--extensions': i++; extensions = new Set( @@ -267,6 +294,7 @@ export default class Options { requires, transpilePlugins, findBabelConfig, + transpilePluginsWithTypescript, ignore, stdio, help, diff --git a/src/index.ts b/src/index.ts index 4d8dd171..bdc3f10a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ OPTIONS --[no-]transpile-plugins Transpile plugins to enable future syntax (default: on). --[no-]find-babel-config Run plugins through babel plugins/presets specified in local .babelrc file instead of babel-preset-env (default: off). + --transpile-ts-plugins Transpile plugins to enable typescript syntax. Requires typescript to be installed local to project. (default: off). -s, --stdio Read source from stdin and print to stdout. -h, --help Show this help message. -d, --dry Run plugins without modifying files on disk. @@ -79,8 +80,14 @@ export default async function run( return 0; } + if(options.transpilePluginsWithTypescript){ + options.loadTypescriptTranspile(); + } + + options.loadBabelTranspile(); + options.loadRequires(); let plugins = await options.getBabelPlugins(); diff --git a/test/cli/CLITest.ts b/test/cli/CLITest.ts index 42e21e81..519d35df 100644 --- a/test/cli/CLITest.ts +++ b/test/cli/CLITest.ts @@ -229,4 +229,87 @@ describe('CLI', function() { await server.stop(); } }); + + it('can load plugins written in Typescript', async function() { + let afile = await createTemporaryFile('a-file.js', '3 + 4;'); + let { status, stdout, stderr } = await runCodemodCLI([ + afile, + '-p', + plugin('typescript/increment-typescript', '.ts'), + '--transpile-ts-plugins' + ]); + + deepEqual( + { status, stdout, stderr }, + { + status: 0, + stdout: `${afile}\n1 file(s), 1 modified, 0 errors\n`, + stderr: '' + } + ); + strictEqual(await readFile(afile, 'utf8'), '4 + 5;'); + }); + + it('can load plugins written in Typescript without ts extension', async function() { + let afile = await createTemporaryFile('a-file.js', '3 + 4;'); + let { status, stdout, stderr } = await runCodemodCLI([ + afile, + '-p', + plugin('typescript/increment-typescript', ''), + '--transpile-ts-plugins' + ]); + + deepEqual( + { status, stdout, stderr }, + { + status: 0, + stdout: `${afile}\n1 file(s), 1 modified, 0 errors\n`, + stderr: '' + } + ); + strictEqual(await readFile(afile, 'utf8'), '4 + 5;'); + }); + + it('can load plugins with multiple files written in Typescript', async function() { + let afile = await createTemporaryFile('a-file.js', '3 + 4;'); + let { status, stdout, stderr } = await runCodemodCLI([ + afile, + '-p', + plugin('typescript/increment-export-default-multiple/index', '.ts'), + '--transpile-ts-plugins' + ]); + + deepEqual( + { status, stdout, stderr }, + { + status: 0, + stdout: `${afile}\n1 file(s), 1 modified, 0 errors\n`, + stderr: '' + } + ); + strictEqual(await readFile(afile, 'utf8'), '4 + 5;'); + }); + + it('can load plugins with multiple files written in Typescript and Javascript', async function() { + let afile = await createTemporaryFile('a-file.js', '3 + 4;'); + let { status, stdout, stderr } = await runCodemodCLI([ + afile, + '-p', + plugin( + 'typescript/increment-export-default-multiple/increment-export-index', + '.ts' + ), + '--transpile-ts-plugins' + ]); + + deepEqual( + { status, stdout, stderr }, + { + status: 0, + stdout: `${afile}\n1 file(s), 1 modified, 0 errors\n`, + stderr: '' + } + ); + strictEqual(await readFile(afile, 'utf8'), '4 + 5;'); + }); }); diff --git a/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-default-js.js b/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-default-js.js new file mode 100644 index 00000000..3a04ca07 --- /dev/null +++ b/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-default-js.js @@ -0,0 +1,3 @@ +export function incrementValue(x) { + return x + 1; +} diff --git a/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-default.ts b/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-default.ts new file mode 100644 index 00000000..e3855618 --- /dev/null +++ b/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-default.ts @@ -0,0 +1,3 @@ +export function incrementValue(x: number): number { + return x + 1; +} diff --git a/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-index.ts b/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-index.ts new file mode 100644 index 00000000..1a85e16d --- /dev/null +++ b/test/fixtures/plugin/typescript/increment-export-default-multiple/increment-export-index.ts @@ -0,0 +1,13 @@ +import { incrementValue } from './increment-export-default-js'; + +/* tslint:disable */ +export default function() { + return { + visitor: { + NumericLiteral(path) { + let value: number = path.node.value; + path.node.value = incrementValue(value); + } + } + }; +} diff --git a/test/fixtures/plugin/typescript/increment-export-default-multiple/index.ts b/test/fixtures/plugin/typescript/increment-export-default-multiple/index.ts new file mode 100644 index 00000000..90f8072e --- /dev/null +++ b/test/fixtures/plugin/typescript/increment-export-default-multiple/index.ts @@ -0,0 +1,13 @@ +import {incrementValue} from './increment-export-default'; + +/* tslint:disable */ +export default function() { + return { + visitor: { + NumericLiteral(path) { + let value: number = path.node.value; + path.node.value = incrementValue(value); + } + } + }; +} diff --git a/test/fixtures/plugin/typescript/increment-typescript.ts b/test/fixtures/plugin/typescript/increment-typescript.ts new file mode 100644 index 00000000..9f62aa02 --- /dev/null +++ b/test/fixtures/plugin/typescript/increment-typescript.ts @@ -0,0 +1,15 @@ +function incrementValue(x: number): number { + return x + 1; +} + +/* tslint:disable */ +export default function() { + return { + visitor: { + NumericLiteral(path) { + let value: number = path.node.value; + path.node.value = incrementValue(value); + } + } + }; +} diff --git a/test/helpers/plugin.ts b/test/helpers/plugin.ts index 4efe1721..2629f188 100644 --- a/test/helpers/plugin.ts +++ b/test/helpers/plugin.ts @@ -1,5 +1,5 @@ import { join } from 'path'; -export default function plugin(name: string): string { - return join(__dirname, `../fixtures/plugin/${name}.js`); +export default function plugin(name: string, ext: string = '.js'): string { + return join(__dirname, `../fixtures/plugin/${name}${ext}`); } diff --git a/tsconfig.json b/tsconfig.json index da33e489..4a75bbb6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,13 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "noImplicitAny": false, - "sourceMap": true, - "strictNullChecks": true, - "declaration": true - } -} \ No newline at end of file + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": false, + "sourceMap": true, + "strictNullChecks": true, + "declaration": true + }, + "exclude": [ + "./test/fixtures/**/*.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 76836635..96292679 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,7 +14,7 @@ lodash.pick "4.4.0" meow "3.7.0" -"@commitlint/config-conventional@6.0.2": +"@commitlint/config-conventional@^6.0.2": version "6.0.2" resolved "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-6.0.2.tgz#8ef87a6facb75b3377b2760b0e91097f8ec64db4" @@ -273,6 +273,14 @@ "@types/glob" "*" "@types/node" "*" +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + "@types/tmp@^0.0.33": version "0.0.33" resolved "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz#1073c4bc824754ae3d10cfab88ab0237ba964e4d" @@ -388,7 +396,7 @@ array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" -arrify@^1.0.1: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -1283,7 +1291,7 @@ diff@3.3.1: version "3.3.1" resolved "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" -diff@^3.2.0: +diff@^3.1.0, diff@^3.2.0: version "3.4.0" resolved "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" @@ -1684,6 +1692,12 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" +homedir-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + hosted-git-info@^2.1.4, hosted-git-info@^2.4.2: version "2.5.0" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" @@ -2204,6 +2218,10 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +make-error@^1.1.1: + version "1.3.2" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.2.tgz#8762ffad2444dd8ff1f7c819629fa28e24fea1c4" + map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -2521,6 +2539,10 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + parse-url@^1.3.0: version "1.3.11" resolved "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz#57c15428ab8a892b1f43869645c711d0e144b554" @@ -3132,7 +3154,7 @@ strip-indent@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" -strip-json-comments@~2.0.1: +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -3239,6 +3261,30 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +ts-node@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-4.1.0.tgz#36d9529c7b90bb993306c408cd07f7743de20712" + dependencies: + arrify "^1.0.0" + chalk "^2.3.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.0" + tsconfig "^7.0.0" + v8flags "^3.0.0" + yn "^2.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + tslib@^1.7.1, tslib@^1.8.0: version "1.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz#6946af2d1d651a7b1863b531d6e5afa41aa44eac" @@ -3330,6 +3376,12 @@ uuid@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +v8flags@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b" + dependencies: + homedir-polyfill "^1.0.1" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -3407,3 +3459,7 @@ yargs@~3.10.0: cliui "^2.1.0" decamelize "^1.0.0" window-size "0.1.0" + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"