Skip to content

Commit

Permalink
Add the possibility to select different parsers (closes #117)
Browse files Browse the repository at this point in the history
jscodeshift uses Babel v5 to parse source files. Since Babel v5 doesn't
get updated anymore, jscodeshift is unable to parse files that contain
more modern flow type annotation.

To solve this, jscodeshift now supports three parsers: Babel v5,
babylon and flow. The parser to use can be chosen by either

  - passing the command line option `parser`: `--parser=babelv5`,
    `--parser=babylon`, `--parser=flow`.
  - having the transform file export `parser`, with either the values
    `"babelv5"`, `"babylon"` or `"flow` to choose one of the built-int
    parsers, or export a parser object (e.g. `exports.parser = require('flow=parser'`)

    This allows transform to specify their own parser (as long as it is
    compatible with ESTree / recast), making them a bit more independent from
    jscodeshifts internals.

In addition to this change, `jscodeshift` now accepts a second argument,
`options` which is directly parsed passed to `recast.parse`. So if you
use `jscodeshift` programmatically , you can now pass your own parser
with

```
jscodeshift(source, {parser: require('myParser')})
```

**Note:** This is *not* a breaking change. Babel v5 still stays the
default parser, but we will likely use another parser (babylon or
flow) as default in the future.
  • Loading branch information
fkling committed Jun 20, 2016
1 parent 3cb7151 commit 327f985
Show file tree
Hide file tree
Showing 30 changed files with 699 additions and 255 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
},
"env": {
"node": true
},
"globals": {
"Promise": true
}
}
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/src/
/sample/
.gitignore
.eslintrc
.eslintrc.yaml
.jshintrc
.module-cache
__tests__
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ sudo: false
language: node_js
node_js:
- 4
- 5
- stable
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Options:
--ignore-config FILE Ignore files if they match patterns sourced from a configuration file (e.g., a .gitignore)
--run-in-band Run serially in the current process [false]
-s, --silent No output [false]
--parser The parser to use for parsing your source files (babel | babylon | flow) [babel]
--version print version and exit
```

This passes the source of all passed through the transform module specified
Expand Down Expand Up @@ -120,7 +122,7 @@ $ jscodeshift -t myTransforms fileA fileB --foo=bar
`options` would contain `{foo: 'bar'}`. jscodeshift uses [nomnom][] to parse
command line options.

#### Return value
### Return value

The return value of the function determines the status of the transformation:

Expand All @@ -136,7 +138,28 @@ detailed information by setting the `-v` option to `1` or `2`.

You can collect even more stats via the `stats` function as explained above.

### Example
### Parser

The transform can let jscodeshift know with which parser to parse the source
files (and features like templates).

To do that, the transform module can export `parser`, which can either be one
of the strings `"babel"`, `"babylon"`, or `"flow"`, or it can be a parser
object that is compatible with with recast.

For example:

```js
module.exports.parser = 'flow'; // use the flow parser
// or
module.exports.parser = {
parse: function(source) {
// return estree compatible AST
},
};
```

### Example output

```text
$ jscodeshift -t myTransform.js src
Expand Down Expand Up @@ -294,6 +317,13 @@ This can be done by passing config options to [recast].
.toSource({quote: 'single'}); // sets strings to use single quotes in transformed code.
```

You can also pass options to recast's `parse` method by passing an object to
jscodeshift as second argument:

```js
jscodeshift(source, {...})
```

More on config options [here](https://github.com/benjamn/recast/blob/52a7ec3eaaa37e78436841ed8afc948033a86252/lib/options.js#L61)

### Unit Testing
Expand Down
119 changes: 50 additions & 69 deletions bin/__tests__/jscodeshift-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@
*
*/

/*global jest, jasmine, describe, pit, expect*/
/*global jest, jasmine, describe, it, expect, beforeEach*/
/*eslint camelcase: 0, no-unused-vars: 0*/

jest.autoMockOff();

// Increase default timeout (5000ms) for Travis
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000; // 10 minutes

var child_process = require('child_process');
var fs = require('fs');
var path = require('path');
var temp = require('temp');
var mkdirp = require('mkdirp');
require('es6-promise').polyfill();
var testUtils = require('../../utils/testUtils');

var createTransformWith = testUtils.createTransformWith;
var createTempFileWith = testUtils.createTempFileWith;

function run(args, stdin, cwd) {
return new Promise(resolve => {
Expand All @@ -45,32 +48,8 @@ function run(args, stdin, cwd) {
}

describe('jscodeshift CLI', () => {
function createTempFileWith(content, filename) {
var info = temp.openSync();
var filePath = info.path;
fs.writeSync(info.fd, content);
fs.closeSync(info.fd);
if (filename) {
filePath = renameFileTo(filePath, filename);
}
return filePath;
}

function renameFileTo(oldPath, newFilename) {
var projectPath = path.dirname(oldPath);
var newPath = path.join(projectPath, newFilename);
mkdirp.sync(path.dirname(newPath));
fs.renameSync(oldPath, newPath);
return newPath;
}

function createTransformWith(content) {
return createTempFileWith(
'module.exports = function(fileInfo, api, options) { ' + content + ' }'
);
}

pit('calls the transform and file information', () => {
it('calls the transform and file information', () => {
var sourceA = createTempFileWith('a');
var sourceB = createTempFileWith('b\n');
var sourceC = createTempFileWith('c');
Expand All @@ -83,68 +62,70 @@ describe('jscodeshift CLI', () => {

return Promise.all([
run(['-t', transformA, sourceA, sourceB]).then(
([stdout, stderr]) => {
out => {
expect(out[1]).toBe('');
expect(fs.readFileSync(sourceA).toString()).toBe('transforma');
expect(fs.readFileSync(sourceB).toString()).toBe('transformb\n');
}
),
run(['-t', transformB, sourceC]).then(
([stdout, stderr]) => {
out => {
expect(out[1]).toBe('');
expect(fs.readFileSync(sourceC).toString()).toBe(sourceC);
}
)
]);
});

pit('does not transform files in a dry run', () => {
it('does not transform files in a dry run', () => {
var source = createTempFileWith('a');
var transform = createTransformWith(
'return "transform" + fileInfo.source;'
);
return run(['-t', transform, '-d', source]).then(
([stdout, stderr]) => {
() => {
expect(fs.readFileSync(source).toString()).toBe('a');
}
);
});

pit('loads transform files with Babel if not disabled', () => {
it('loads transform files with Babel if not disabled', () => {
var source = createTempFileWith('a');
var transform = createTransformWith(
'return (function() { "use strict"; const a = 42; }).toString();'
);
return Promise.all([
run(['-t', transform, source]).then(
([stdout, stderr]) => {
() => {
expect(fs.readFileSync(source).toString())
.toMatch(/var\s*a\s*=\s*42/);
}
),
run(['-t', transform, '--no-babel', source]).then(
([stdout, stderr]) => {
() => {
expect(fs.readFileSync(source).toString())
.toMatch(/const\s*a\s*=\s*42/);
}
),
]);
});

pit('ignores .babelrc files in the directories of the source files', () => {
it('ignores .babelrc files in the directories of the source files', () => {
var transform = createTransformWith(
'return (function() { "use strict"; const a = 42; }).toString();'
);
var babelrc = createTempFileWith(`{"ignore": ["${transform}"]}`, '.babelrc');
var source = createTempFileWith('a', 'source.js');

return run(['-t', transform, source]).then(
([stdout, stderr]) => {
() => {
expect(fs.readFileSync(source).toString())
.toMatch(/var\s*a\s*=\s*42/);
}
);
});

pit('passes jscodeshift and stats the transform function', () => {
it('passes jscodeshift and stats the transform function', () => {
var source = createTempFileWith('a');
var transform = createTransformWith([
' return String(',
Expand All @@ -153,30 +134,30 @@ describe('jscodeshift CLI', () => {
' );',
].join('\n'));
return run(['-t', transform, source]).then(
([stdout, stderr]) => {
() => {
expect(fs.readFileSync(source).toString()).toBe('true');
}
);
});

pit('passes options along to the transform', () => {
it('passes options along to the transform', () => {
var source = createTempFileWith('a');
var transform = createTransformWith('return options.foo;');
return run(['-t', transform, '--foo=42', source]).then(
([stdout, stderr]) => {
() => {
expect(fs.readFileSync(source).toString()).toBe('42');
}
);
});

pit('does not stall with too many files', () => {
it('does not stall with too many files', () => {
var sources = [];
for (var i = 0; i < 100; i++) {
sources.push(createTempFileWith('a'));
}
var transform = createTransformWith('');
return run(['-t', transform, '--foo=42'].concat(sources)).then(
([stdout, stderr]) => {
() => {
expect(true).toBe(true);
}
);
Expand All @@ -195,58 +176,58 @@ describe('jscodeshift CLI', () => {
// sources.push(createTempFileWith('b', 'src/lib/b.js'));
});

pit('supports basic glob', () => {
it('supports basic glob', () => {
var pattern = '*-test.js';
return run(['-t', transform, '--ignore-pattern', pattern, ...sources]).then(
([stdout, stderr]) => {
return run(['-t', transform, '--ignore-pattern', pattern].concat(sources)).then(
() => {
expect(fs.readFileSync(sources[0]).toString()).toBe('transforma');
expect(fs.readFileSync(sources[1]).toString()).toBe('a');
}
);
});

pit('supports filename match', () => {
it('supports filename match', () => {
var pattern = 'a.js';
return run(['-t', transform, '--ignore-pattern', pattern, ...sources]).then(
([stdout, stderr]) => {
return run(['-t', transform, '--ignore-pattern', pattern].concat(sources)).then(
() => {
expect(fs.readFileSync(sources[0]).toString()).toBe('a');
expect(fs.readFileSync(sources[1]).toString()).toBe('transforma');
}
);
});

pit('accepts a list of patterns', () => {
it('accepts a list of patterns', () => {
var patterns = ['--ignore-pattern', 'a.js', '--ignore-pattern', '*-test.js'];
return run(['-t', transform, ...patterns, ...sources]).then(
([stdout, stderr]) => {
return run(['-t', transform].concat(patterns).concat(sources)).then(
() => {
expect(fs.readFileSync(sources[0]).toString()).toBe('a');
expect(fs.readFileSync(sources[1]).toString()).toBe('a');
}
);
});

pit('sources ignore patterns from configuration file', () => {
it('sources ignore patterns from configuration file', () => {
var patterns = ['sub/dir/', '*-test.js'];
var gitignore = createTempFileWith(patterns.join('\n'), '.gitignore');
sources.push(createTempFileWith('subfile', 'sub/dir/file.js'));

return run(['-t', transform, '--ignore-config', gitignore, ...sources]).then(
([stdout, stderr]) => {
return run(['-t', transform, '--ignore-config', gitignore].concat(sources)).then(
() => {
expect(fs.readFileSync(sources[0]).toString()).toBe('transforma');
expect(fs.readFileSync(sources[1]).toString()).toBe('a');
expect(fs.readFileSync(sources[2]).toString()).toBe('subfile');
}
);
});

pit('accepts a list of configuration files', () => {
it('accepts a list of configuration files', () => {
var gitignore = createTempFileWith(['sub/dir/'].join('\n'), '.gitignore');
var eslintignore = createTempFileWith(['**/*test.js', 'a.js'].join('\n'), '.eslintignore');
var configs = ['--ignore-config', gitignore, '--ignore-config', eslintignore];
sources.push(createTempFileWith('subfile', 'sub/dir/file.js'));

return run(['-t', transform, ...configs, ...sources]).then(
([stdout, stderr]) => {
return run(['-t', transform].concat(configs).concat(sources)).then(
() => {
expect(fs.readFileSync(sources[0]).toString()).toBe('a');
expect(fs.readFileSync(sources[1]).toString()).toBe('a');
expect(fs.readFileSync(sources[2]).toString()).toBe('subfile');
Expand All @@ -256,27 +237,27 @@ describe('jscodeshift CLI', () => {
});

describe('output', () => {
pit('shows workers info and stats at the end by default', () => {
it('shows workers info and stats at the end by default', () => {
var source = createTempFileWith('a');
var transform = createTransformWith('return null;');
return run(['-t', transform, source]).then(
([stdout, stderr]) => {
expect(stdout).toContain('Processing 1 files...');
expect(stdout).toContain('Spawning 1 workers...');
expect(stdout).toContain('Sending 1 files to free worker...');
expect(stdout).toContain('All done.');
expect(stdout).toContain('Results: ');
expect(stdout).toContain('Time elapsed: ');
out => {
expect(out[0]).toContain('Processing 1 files...');
expect(out[0]).toContain('Spawning 1 workers...');
expect(out[0]).toContain('Sending 1 files to free worker...');
expect(out[0]).toContain('All done.');
expect(out[0]).toContain('Results: ');
expect(out[0]).toContain('Time elapsed: ');
}
);
});

pit('does not ouput anything in silent mode', () => {
it('does not ouput anything in silent mode', () => {
var source = createTempFileWith('a');
var transform = createTransformWith('return null;');
return run(['-t', transform, '-s', source]).then(
([stdout, stderr]) => {
expect(stdout).toEqual('');
out => {
expect(out[0]).toEqual('');
}
);
});
Expand Down
Loading

4 comments on commit 327f985

@benjamn
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work! Happy to help debug any problems that arise from this.

@evocateur
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm i jscodeshift now yields an ENOENT error attempting to install the babelv5 local fork. Perhaps the packaging when published didn't pick it up?

node v6.2.0
npm v3.9.6

@fkling
Copy link
Contributor Author

@fkling fkling commented on 327f985 Jun 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh :( I'll look into it

@fkling
Copy link
Contributor Author

@fkling fkling commented on 327f985 Jun 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not use file: URLs in the dependencies? (and publish to npm)

Please sign in to comment.