Skip to content

Commit

Permalink
feat(typescript): process TypeScript files
Browse files Browse the repository at this point in the history
To support TypeScript, mostly we just needed to use the `typescript` parser plugin when the file being processed is TypeScript.

BREAKING CHANGE: This expands the set of file extensions that babel-codemod will look for to include `.ts`, `.tsx`, `.es`, `.es6`, and `.mjs` in addition to `.js` and `.jsx`.

Closes #137
  • Loading branch information
eventualbuddha committed Jun 20, 2018
1 parent 9e44e2e commit 00ae598
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 63 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# babel-codemod

babel-codemod rewrites JavaScript using babel plugins.
babel-codemod rewrites JavaScript and TypeScript using babel plugins.

## Install

Expand All @@ -21,10 +21,13 @@ The primary interface is as a command line tool, usually run like so:
```sh
$ codemod --plugin transform-module-name \
path/to/file.js \
another/file.js
another/file.js \
a/directory
```

This will re-write the files `path/to/file.js` and `another/file.js` by transforming them with the babel plugin `transform-module-name`. Multiple plugins may be specified, and multiple files or directories may be re-written at once.
This will re-write the files `path/to/file.js`, `another/file.js`, and any supported files found in `a/directory` by transforming them with the babel plugin `transform-module-name`. Multiple plugins may be specified, and multiple files or directories may be re-written at once.

Note that TypeScript support is provided by babel and therefore may not completely support all valid TypeScript code. If you encounter an issue, consider looking for it in the [babel issues labeled `area: typescript`](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22area%3A+typescript%22+) before filing an issue with babel-codemod.

Plugins may also be loaded from remote URLs, including saved [AST Explorer](https://astexplorer.net/) URLs, using `--remote-plugin`. This feature should only be used as a convenience to load code that you or someone you trust wrote. It will run with your full user privileges, so please exercise caution!

Expand Down
26 changes: 19 additions & 7 deletions src/AllSyntaxPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as Babel from '@babel/core';
import { extname } from 'path';
import { BabelOptions, ParseOptions } from './BabelPluginTypes';
import { TypeScriptExtensions } from './extensions';

export const ALL_PLUGINS = [
'flow',
const BASIC_PLUGINS = [
'jsx',
'asyncGenerators',
'classProperties',
Expand All @@ -15,14 +16,25 @@ export const ALL_PLUGINS = [
'decorators'
];

function pluginsForFilename(filename: string): Array<string> {
let isTypeScript = TypeScriptExtensions.has(extname(filename));

return isTypeScript
? [...BASIC_PLUGINS, 'typescript']
: [...BASIC_PLUGINS, 'flow'];
}

export default function(babel: typeof Babel) {
return {
manipulateOptions(opts: BabelOptions, parserOpts: ParseOptions) {
for (let plugin of ALL_PLUGINS) {
if (plugin !== 'flow' || !opts.filename.endsWith('.ts')) {
parserOpts.plugins.push(plugin);
}
}
parserOpts.sourceType = 'module';
parserOpts.allowImportExportEverywhere = true;
parserOpts.allowReturnOutsideFunction = true;
parserOpts.allowSuperOutsideMethod = true;
parserOpts.plugins = [
...(parserOpts.plugins || []),
...pluginsForFilename(opts.filename)
];
}
};
}
6 changes: 5 additions & 1 deletion src/BabelPluginTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ export interface BabelOptions {
filename: string;
}
export interface ParseOptions {
plugins: Array<string>;
sourceType?: 'module' | 'script';
allowImportExportEverywhere?: boolean;
allowReturnOutsideFunction?: boolean;
allowSuperOutsideMethod?: boolean;
plugins?: Array<string>;
}
export type AST = object;

Expand Down
7 changes: 3 additions & 4 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { install } from 'source-map-support';
import AllSyntaxPlugin from './AllSyntaxPlugin';
import { BabelPlugin, RawBabelPlugin } from './BabelPluginTypes';
import BabelPrinterPlugin from './BabelPrinterPlugin';
import { TransformableExtensions } from './extensions';
import { PathPredicate } from './iterateSources';
import PluginLoader from './PluginLoader';
import PrettierPrinterPlugin from './PrettierPrinterPlugin';
Expand All @@ -14,8 +15,6 @@ import NetworkResolver from './resolvers/NetworkResolver';
import PackageResolver from './resolvers/PackageResolver';
import { disable, enable } from './transpile-requires';

export const DEFAULT_EXTENSIONS = new Set(['.js', '.jsx']);

export class Plugin {
readonly declaredName?: string;

Expand Down Expand Up @@ -50,7 +49,7 @@ export default class Config {
readonly remotePlugins: Array<string> = [],
readonly pluginOptions: Map<string, object> = new Map<string, object>(),
readonly printer: Printer = Printer.Recast,
readonly extensions: Set<string> = DEFAULT_EXTENSIONS,
readonly extensions: Set<string> = TransformableExtensions,
readonly requires: Array<string> = [],
readonly transpilePlugins: boolean = true,
readonly findBabelConfig: boolean = false,
Expand Down Expand Up @@ -187,7 +186,7 @@ export class ConfigBuilder {
private _remotePlugins?: Array<string>;
private _pluginOptions?: Map<string, object>;
private _printer?: Printer;
private _extensions: Set<string> = new Set(DEFAULT_EXTENSIONS);
private _extensions: Set<string> = new Set(TransformableExtensions);
private _requires?: Array<string>;
private _transpilePlugins?: boolean;
private _findBabelConfig?: boolean;
Expand Down
4 changes: 2 additions & 2 deletions src/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { hasMagic as hasGlob, sync as globSync } from 'glob';
import { resolve } from 'path';
import { sync as resolveSync } from 'resolve';
import Config, { ConfigBuilder, Printer } from './Config';
import { SUPPORTED_EXTENSIONS } from './transpile-requires';
import { RequireableExtensions } from './extensions';

export interface RunCommand {
kind: 'run';
Expand Down Expand Up @@ -157,7 +157,7 @@ function getRequirableModulePath(modulePath: string): string {
return resolve(modulePath);
}

for (let ext of SUPPORTED_EXTENSIONS) {
for (let ext of RequireableExtensions) {
if (existsSync(modulePath + ext)) {
return resolve(modulePath + ext);
}
Expand Down
11 changes: 1 addition & 10 deletions src/RecastPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import * as Babel from '@babel/core';
import { GeneratorOptions } from '@babel/generator';
import * as recast from 'recast';
import { ALL_PLUGINS } from './AllSyntaxPlugin';
import { AST, ParseOptions } from './BabelPluginTypes';

const DEFAULT_OPTIONS = {
sourceType: 'module',
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
plugins: ALL_PLUGINS
};

export function parse(
code: string,
options: ParseOptions,
Expand All @@ -20,7 +11,7 @@ export function parse(
return recast.parse(code, {
parser: {
parse(code: string) {
return parse(code, DEFAULT_OPTIONS);
return parse(code, options);
}
}
});
Expand Down
24 changes: 24 additions & 0 deletions src/extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function union<T>(...sets: Array<Set<T>>) {
return new Set(sets.reduce((result, set) => [...result, ...set], []));
}

export const TypeScriptExtensions = new Set(['.ts', '.tsx']);
export const JavaScriptExtensions = new Set([
'.js',
'.jsx',
'.mjs',
'.es',
'.es6'
]);
export const PluginExtensions = union(
TypeScriptExtensions,
JavaScriptExtensions
);
export const RequireableExtensions = union(
TypeScriptExtensions,
JavaScriptExtensions
);
export const TransformableExtensions = union(
TypeScriptExtensions,
JavaScriptExtensions
);
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ EXAMPLES
# Run with multiple plugins.
$ ${$0} -p ./a.js -p ./b.js some-file.js
# Transform TypeScript sources.
# ${$0} -p ./a.js my-typescript-file.ts a-component.tsx
# Run with a plugin in \`node_modules\` on stdin.
$ ${$0} -s -p babel-plugin-typecheck <<EOS
function add(a: number, b: number): number {
Expand Down
4 changes: 2 additions & 2 deletions src/resolvers/FileSystemResolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { stat } from 'mz/fs';
import { resolve } from 'path';
import { SUPPORTED_EXTENSIONS } from '../transpile-requires';
import { PluginExtensions } from '../extensions';
import Resolver from './Resolver';

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

private *enumerateCandidateSources(source: string): IterableIterator<string> {
Expand Down
16 changes: 4 additions & 12 deletions src/transpile-requires.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,15 @@ import { transform } from '@babel/core';
import { extname } from 'path';
import { addHook } from 'pirates';
import AllSyntaxPlugin from './AllSyntaxPlugin';
import { PluginExtensions, TypeScriptExtensions } from './extensions';

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)) {
if (!PluginExtensions.has(ext)) {
throw new Error(`cannot load file type '${ext}': ${filename}`);
}

Expand All @@ -31,7 +23,7 @@ export function hook(code: string, filename: string): string {
};

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

Expand All @@ -45,7 +37,7 @@ export function enable(babelrc: boolean = false) {
disable();
useBabelrc = babelrc;
revert = addHook(hook, {
exts: Array.from(SUPPORTED_EXTENSIONS),
exts: Array.from(PluginExtensions),
ignoreNodeModules: true
});
}
Expand Down
88 changes: 81 additions & 7 deletions test/cli/CLITest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ describe('CLI', function() {

it('processes all matching files in a directory', async function() {
let file1 = await createTemporaryFile('a-dir/file1.js', '3 + 4;');
let file2 = await createTemporaryFile('a-dir/file2.js', '0;');
let file2 = await createTemporaryFile('a-dir/file2.ts', '0;');
let file3 = await createTemporaryFile('a-dir/sub-dir/file3.jsx', '99;');
let ignored = await createTemporaryFile('a-dir/ignored.es6', '8;');
let ignored = await createTemporaryFile('a-dir/ignored.css', '* {}');
let { status, stdout, stderr } = await runCodemodCLI([
dirname(file1),
'-p',
Expand All @@ -115,28 +115,28 @@ describe('CLI', function() {
'4 + 5;',
'file1.js is processed'
);
strictEqual(await readFile(file2, 'utf8'), '1;', 'file2.js is processed');
strictEqual(await readFile(file2, 'utf8'), '1;', 'file2.ts is processed');
strictEqual(
await readFile(file3, 'utf8'),
'100;',
'file3.jsx in a sub-directory is processed'
);
strictEqual(
await readFile(ignored, 'utf8'),
'8;',
'ignored.es6 is ignored'
'* {}',
'ignored.css is ignored'
);
});

it('processes all matching files in a directory with custom extensions', async function() {
let ignored = await createTemporaryFile('a-dir/ignored.js', '3 + 4;');
let processed = await createTemporaryFile('a-dir/processed.es6', '0;');
let processed = await createTemporaryFile('a-dir/processed.myjs', '0;');
let { status, stdout, stderr } = await runCodemodCLI([
dirname(ignored),
'-p',
plugin('increment'),
'--extensions',
'.es6'
'.myjs'
]);

deepEqual(
Expand Down Expand Up @@ -400,4 +400,78 @@ describe('CLI', function() {

strictEqual(await readFile(file, 'utf8'), `var a = '';\n`);
});

it('can rewrite TypeScript files ending in `.ts`', async function() {
let afile = await createTemporaryFile(
'a-file.ts',
'type A = any;\nlet a = {} as any;'
);
let { status, stdout, stderr } = await runCodemodCLI([
afile,
'-p',
plugin('replace-any-with-object', '.ts')
]);

deepEqual(
{ status, stdout, stderr },
{
status: 0,
stdout: `${afile}\n1 file(s), 1 modified, 0 errors\n`,
stderr: ''
}
);

strictEqual(
await readFile(afile, 'utf8'),
'type A = object;\nlet a = {} as object;'
);
});

it('can rewrite TypeScript files ending in `.tsx`', async function() {
let afile = await createTemporaryFile(
'a-file.tsx',
'export default () => (<div/>);'
);
let { status, stdout, stderr } = await runCodemodCLI([afile]);

deepEqual(
{ status, stdout, stderr },
{
status: 0,
stdout: `${afile}\n1 file(s), 0 modified, 0 errors\n`,
stderr: ''
}
);

strictEqual(
await readFile(afile, 'utf8'),
'export default () => (<div/>);'
);
});

it('can rewrite TypeScript files with prettier', async function() {
let afile = await createTemporaryFile(
'a-file.ts',
'type A=any;\nlet a={} as any;'
);
let { status, stdout, stderr } = await runCodemodCLI([
afile,
'--printer',
'prettier'
]);

deepEqual(
{ status, stdout, stderr },
{
status: 0,
stdout: `${afile}\n1 file(s), 1 modified, 0 errors\n`,
stderr: ''
}
);

strictEqual(
await readFile(afile, 'utf8'),
'type A = any;\nlet a = {} as any;\n'
);
});
});
14 changes: 14 additions & 0 deletions test/fixtures/plugin/replace-any-with-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Babel from '@babel/core';
import { NodePath } from '@babel/traverse';

export default function(babel: typeof Babel) {
const { types: t } = babel;

return {
visitor: {
TSAnyKeyword(path: NodePath) {
path.replaceWith(t.tsObjectKeyword());
}
}
};
}
Loading

0 comments on commit 00ae598

Please sign in to comment.