Skip to content

Commit

Permalink
feat(typescript-plugins): use babel to transpile .ts plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
eventualbuddha committed Jan 24, 2018
1 parent 9b89840 commit 79c144a
Show file tree
Hide file tree
Showing 16 changed files with 967 additions and 619 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ node_modules

# allow fixtures
!test/fixtures/**/*.js
test/fixtures/**/*-typescript.js

# allow JS config files
!*.config.js
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ There are [many, many existing plugins](https://yarnpkg.com/en/packages?q=babel-

### Transpiling using babel plugins

`babel-codemod` also supports non-standard/future language features that are not currently supported by the latest version of node. It does this by leveraging `babel-preset-env` which loads the [latest babel plugins](https://github.com/babel/babel/tree/master/packages/babel-preset-env#support-all-plugins-in-babel-that-are-considered-latest). This feature is on by default.
`babel-codemod` also supports non-standard/future language features that are not currently supported by the latest version of node. It does this by leveraging `@babel/preset-env` which loads the [latest babel plugins](https://github.com/babel/babel/tree/master/packages/babel-preset-env#support-all-plugins-in-babel-that-are-considered-latest). This feature is on by default.

This feature should support most use cases when writing plugins in advanced JavaScript syntax. However, if you are writing plugins with syntax that is beyond "latest", or you would like to use your own set of plugins and presets, you can pass in the `--find-babel-config` switch in combination with a local `.babelrc` file that lists the presets/plugins you want applied to your plugin code.

Expand All @@ -49,17 +49,17 @@ This feature should support most use cases when writing plugins in advanced Java
$ 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).
This requires that all babel plugins and presets be installed locally and are listed in your `.babelrc` file. `babel-codemod` uses `@babel/core` under the hood to accomplish this and all `.babelrc` [lookup rules apply](https://babeljs.io/docs/usage/babelrc/#lookup-behavior).

### Transpiling using 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`.
There is experimental support for running plugins written in TypeScript. This is on by default and works by using `@babel/preset-typescript` rather than the official TypeScript compiler. This feature may be removed in the future.

For example:

```sh
# Run a local plugin written with TypeScript.
$ codemod --require ts-node/register --plugin ./my-plugin.ts src/
$ codemod --plugin ./my-plugin.ts src/
```

## Contributing
Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"devDependencies": {
"@commitlint/cli": "^6.0.1",
"@commitlint/config-conventional": "^6.0.2",
"@types/babel-core": "^6.7.14",
"@types/babel-traverse": "^6.7.16",
"@types/babylon": "^6.7.15",
"@types/get-port": "^3.2.0",
Expand All @@ -48,6 +47,7 @@
"@types/mz": "0.0.32",
"@types/node": "^9.3.0",
"@types/rimraf": "2.0.2",
"@types/source-map-support": "^0.4.0",
"@types/tmp": "^0.0.33",
"commitlint": "^6.0.1",
"get-port": "^3.2.0",
Expand All @@ -59,21 +59,25 @@
"prettier-check": "^2.0.0",
"rimraf": "2.6.2",
"semantic-release": "^11.0.2",
"source-map-support": "^0.5.0",
"tslint": "^5.4.2",
"tslint-config-prettier": "^1.6.0",
"typescript": "^2.2.1"
},
"dependencies": {
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-register": "^6.26.0",
"@babel/core": "^7.0.0-beta.38",
"@babel/generator": "^7.0.0-beta.38",
"@babel/preset-env": "^7.0.0-beta.38",
"@babel/preset-typescript": "^7.0.0-beta.38",
"@babel/traverse": "^7.0.0-beta.38",
"@babel/types": "^7.0.0-beta.38",
"get-stream": "^3.0.0",
"glob": "^7.1.2",
"got": "^8.0.1",
"mz": "^2.7.0",
"pirates": "^3.0.2",
"recast": "^0.13.0",
"resolve": "^1.5.0",
"source-map-support": "^0.5.0",
"tmp": "^0.0.33",
"whatwg-url": "^6.4.0"
},
Expand Down
20 changes: 12 additions & 8 deletions src/Options.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import * as Babel from 'babel-core';
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 { install } from 'source-map-support';
import { PathPredicate } from './iterateSources';
import PluginLoader from './PluginLoader';
import RecastPlugin from './RecastPlugin';
import AstExplorerResolver from './resolvers/AstExplorerResolver';
import FileSystemResolver from './resolvers/FileSystemResolver';
import NetworkResolver from './resolvers/NetworkResolver';
import PackageResolver from './resolvers/PackageResolver';
import { BabelPlugin, RawBabelPlugin } from './TransformRunner';
import { disable, enable } from './transpile-requires';

export const DEFAULT_EXTENSIONS = new Set(['.js', '.jsx']);
export type ParseOptionsResult = Options | Error;
Expand Down Expand Up @@ -99,13 +102,14 @@ export default class Options {

loadBabelTranspile() {
if (this.transpilePlugins) {
let pluginOptions;
if (!this.findBabelConfig) {
pluginOptions = require('babel-preset-env').default();
pluginOptions.babelrc = false; // ignore babelrc file if present
}
enable(this.findBabelConfig);
install();
}
}

require('babel-register')(pluginOptions);
unloadBabelTranspile() {
if (this.transpilePlugins) {
disable();
}
}

Expand All @@ -120,7 +124,7 @@ export default class Options {
}

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

for (let plugin of await this.getPlugins()) {
let options =
Expand Down
50 changes: 50 additions & 0 deletions src/RecastPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { GeneratorOptions } from '@babel/generator';
import * as recast from 'recast';

const DEFAULT_OPTIONS = {
sourceType: 'module',
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
plugins: [
'flow',
'jsx',
'asyncGenerators',
'classProperties',
'doExpressions',
'exportExtensions',
'functionBind',
'functionSent',
'objectRestSpread',
'dynamicImport',
'decorators'
]
};

type ParseOptions = object;
type AST = object;

export default {
parserOverride(
code: string,
options: ParseOptions,
parse: (code: string, options: ParseOptions) => AST
): AST {
return recast.parse(code, {
parser: {
parse(code: string) {
return parse(code, DEFAULT_OPTIONS);
}
}
});
},

generatorOverride(
ast: AST,
options: GeneratorOptions,
code: string,
generate: (ast: AST, options: GeneratorOptions) => string
): string {
return recast.print(ast);
}
};
40 changes: 3 additions & 37 deletions src/TransformRunner.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as Babel from 'babel-core';
import { transform } from 'babel-core';
import { Visitor } from 'babel-traverse';
import * as babylon from 'babylon';
import { parse, print } from 'recast';
import * as Babel from '@babel/core';
import { transform } from '@babel/core';
import { Visitor } from '@babel/traverse';

export class Source {
constructor(readonly path: string, readonly content: string) {}
Expand Down Expand Up @@ -47,38 +45,6 @@ export default class TransformRunner {
return transform(source.content, {
filename: source.path,
babelrc: false,
parserOpts: {
parser(code: string) {
return parse(code, {
parser: {
parse(code: string) {
return babylon.parse(code, {
sourceType: 'module',
allowImportExportEverywhere: false, // consistent with espree
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
plugins: [
'flow',
'jsx',
'asyncGenerators',
'classProperties',
'doExpressions',
'exportExtensions',
'functionBind',
'functionSent',
'objectRestSpread',
'dynamicImport',
'decorators'
]
});
}
}
});
}
},
generatorOpts: {
generator: print
},
plugins: this.plugins
} as {}).code as string;
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default async function run(
options.loadRequires();
plugins = await options.getBabelPlugins();
} finally {
options.unloadBabelTranspile();
snapshot.restore();
}

Expand Down
5 changes: 2 additions & 3 deletions src/resolvers/FileSystemResolver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { stat } from 'mz/fs';
import { resolve } from 'path';
import { SUPPORTED_EXTENSIONS } from '../transpile-requires';
import Resolver from './Resolver';

const DEFAULT_PLUGIN_EXTENSIONS = new Set(['.js']);

async function isFile(path: string): Promise<boolean> {
try {
return (await stat(path)).isFile();
Expand All @@ -17,7 +16,7 @@ async function isFile(path: string): Promise<boolean> {
*/
export default class FileSystemResolver implements Resolver {
constructor(
private readonly optionalExtensions: Set<string> = DEFAULT_PLUGIN_EXTENSIONS
private readonly optionalExtensions: Set<string> = SUPPORTED_EXTENSIONS
) {}

private *enumerateCandidateSources(source: string): IterableIterator<string> {
Expand Down
2 changes: 1 addition & 1 deletion src/resolvers/NetworkResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class NetworkResolver implements Resolver {
throw new NetworkLoadError(response);
}

let filename = tmp();
let filename = tmp({ postfix: '.js' });
await writeFile(filename, response.body);
return filename;
}
Expand Down
56 changes: 56 additions & 0 deletions src/transpile-requires.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { transform } from '@babel/core';
import { extname } from 'path';
import { addHook } from 'pirates';

let useBabelrc = false;
let revert: (() => void) | null = null;

export const SUPPORTED_EXTENSIONS = new Set([
'.js',
'.jsx',
'.es',
'.es6',
'.mjs',
'.ts'
]);

export function hook(code: string, filename: string): string {
let ext = extname(filename);

if (!SUPPORTED_EXTENSIONS.has(ext)) {
throw new Error(`cannot load file type '${ext}': ${filename}`);
}

let options = {
filename,
babelrc: useBabelrc,
presets: [] as Array<string>,
sourceMaps: 'inline'
};

if (!useBabelrc) {
if (ext === '.ts') {
options.presets.push(require('@babel/preset-typescript').default());
}

options.presets.push(require('@babel/preset-env').default());
}

return transform(code, options).code as string;
}

export function enable(babelrc: boolean = false) {
disable();
useBabelrc = babelrc;
revert = addHook(hook, {
exts: Array.from(SUPPORTED_EXTENSIONS),
ignoreNodeModules: true
});
}

export function disable() {
if (revert) {
revert();
revert = null;
}
}
56 changes: 56 additions & 0 deletions test/cli/CLITest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,62 @@ describe('CLI', function() {
strictEqual(await readFile(afile, 'utf8'), '4 + 5;');
});

it('can load plugins written in TypeScript by default', async function() {
let afile = await createTemporaryFile('a-file.js', '3 + 4;');
let { status, stdout, stderr } = await runCodemodCLI([
afile,
'-p',
plugin('increment-typescript', '.ts')
]);

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 implicitly find plugins with .ts extensions', async function() {
let afile = await createTemporaryFile('a-file.js', '3 + 4;');
let { status, stdout, stderr } = await runCodemodCLI([
afile,
'-p',
plugin('increment-typescript', '')
]);

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('does not try to load TypeScript files when --no-transpile-plugins is set', async function() {
let afile = await createTemporaryFile('a-file.js', '3 + 4;');
try {
await runCodemodCLI([
afile,
'--no-transpile-plugins',
'-p',
plugin('increment-typescript', '')
]);
ok(false, 'this command should have failed');
} catch (err) {
ok(
/unable to resolve a plugin from source: .*increment-typescript/,
`error should complain about loading plugin: ${err.stack}`
);
}
});

it('can load plugins with multiple files with ES modules by default`', async function() {
let afile = await createTemporaryFile('a-file.js', '3 + 4;');
let { status, stdout, stderr } = await runCodemodCLI([
Expand Down
Loading

0 comments on commit 79c144a

Please sign in to comment.