Showing preview only (336K chars total). Download the full file or copy to clipboard to get everything.
Repository: facebook/jscodeshift
Branch: main
Commit: f1f0b9f0a44c
Files: 98
Total size: 311.4 KB
Directory structure:
gitextract_10vd5d0w/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── cr.yml
│ ├── pull_request.yml
│ ├── push.yml
│ └── test.yml
├── .gitignore
├── .node-version
├── .npmignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│ ├── __tests__/
│ │ └── jscodeshift-test.js
│ ├── jscodeshift.js
│ └── jscodeshift.sh
├── index.js
├── package.json
├── parser/
│ ├── __tests__/
│ │ ├── .eslintrc
│ │ ├── __snapshots__/
│ │ │ └── tsx-test.js.snap
│ │ └── tsx-test.js
│ ├── babel5Compat.js
│ ├── babylon.js
│ ├── flow.js
│ ├── ts.js
│ ├── tsOptions.js
│ └── tsx.js
├── recipes/
│ └── retain-first-comment.md
├── sample/
│ ├── __testfixtures__/
│ │ ├── reverse-identifiers.input.js
│ │ ├── reverse-identifiers.output.js
│ │ └── typescript/
│ │ ├── reverse-identifiers.input.ts
│ │ └── reverse-identifiers.output.ts
│ ├── __tests__/
│ │ ├── __snapshots__/
│ │ │ └── reverse-identifiers-test.js.snap
│ │ └── reverse-identifiers-test.js
│ └── reverse-identifiers.js
├── src/
│ ├── Collection.js
│ ├── Runner.js
│ ├── Worker.js
│ ├── __testfixtures__/
│ │ ├── test-async-transform.input.js
│ │ ├── test-async-transform.js
│ │ ├── test-async-transform.output.js
│ │ ├── test-sync-transform.input.js
│ │ ├── test-sync-transform.js
│ │ └── test-sync-transform.output.js
│ ├── __tests__/
│ │ ├── .eslintrc
│ │ ├── Collection-test.js
│ │ ├── Worker-test.js
│ │ ├── __snapshots__/
│ │ │ └── testUtils-test.js.snap
│ │ ├── argsParser-test.js
│ │ ├── core-test.js
│ │ ├── matchNode-test.js
│ │ ├── template-test.js
│ │ ├── testUtils-test.js
│ │ └── ts-decorator-auto-accessor-test.js
│ ├── argsParser.js
│ ├── collections/
│ │ ├── ImportDeclaration.js
│ │ ├── JSXElement.js
│ │ ├── Node.js
│ │ ├── VariableDeclarator.js
│ │ ├── __tests__/
│ │ │ ├── .eslintrc
│ │ │ ├── .jshintrc
│ │ │ ├── ImportDeclaration-test.js
│ │ │ ├── JSXElement-test.js
│ │ │ ├── Node-test.js
│ │ │ └── VariableDeclarator-test.js
│ │ └── index.js
│ ├── core.js
│ ├── getParser.js
│ ├── ignoreFiles.js
│ ├── matchNode.js
│ ├── template.js
│ ├── testUtils.js
│ └── utils/
│ ├── __tests__/
│ │ ├── intersection-test.js
│ │ ├── once-test.js
│ │ └── union-test.js
│ ├── intersection.js
│ ├── once.js
│ └── union.js
├── utils/
│ ├── requirePackage.js
│ └── testUtils.js
└── website/
├── .astro/
│ ├── settings.json
│ └── types.d.ts
├── README.md
├── astro.config.mjs
├── package.json
├── src/
│ ├── content/
│ │ ├── config.ts
│ │ └── docs/
│ │ ├── build/
│ │ │ ├── api-reference.mdx
│ │ │ └── ast-grammar.mdx
│ │ ├── index.mdx
│ │ ├── overview/
│ │ │ └── introduction.mdx
│ │ └── run/
│ │ └── cli.mdx
│ └── env.d.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .eslintignore
================================================
/dist/
/docs/
/sample/__testfixtures__/
================================================
FILE: .eslintrc
================================================
{
"extends": "eslint:recommended",
"parser": "@babel/eslint-parser",
"rules": {
"comma-dangle": 0,
"no-underscore-dangle": 0,
"quotes": [2, "single", "avoid-escape"],
"strict": 0
},
"env": {
"jest": true,
"node": true,
"es6": true
},
"globals": {
"Promise": true
}
}
================================================
FILE: .gitattributes
================================================
bin/jscodeshift.js eol=lf
================================================
FILE: .github/workflows/cr.yml
================================================
name: Continuous Releases
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: 20.x
cache: yarn
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Preview
run: npx pkg-pr-new@0.0.24 publish --compact
================================================
FILE: .github/workflows/pull_request.yml
================================================
name: pull_request
on:
pull_request:
branches: [main]
jobs:
call-test:
uses: ./.github/workflows/test.yml
================================================
FILE: .github/workflows/push.yml
================================================
name: push
on:
push:
branches: [main]
jobs:
call-test:
uses: ./.github/workflows/test.yml
release:
needs: call-test
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: yarn
- run: yarn
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: yarn release
title: Publish <version>
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: yarn
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Run test
run: yarn test
================================================
FILE: .gitignore
================================================
/dist
node_modules/
================================================
FILE: .node-version
================================================
16.20.2
================================================
FILE: .npmignore
================================================
/docs/
/test/
/sample/
/recipes/
.gitignore
.eslintrc
.eslintrc.yaml
.jshintrc
.module-cache
__tests__
yarn.lock
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## 17.3.0
### Minor Changes
- 6c2ff57: Bumps recast to allow parsing of Typescript type arguments on tagged template literals
## 17.2.0
### Minor Changes
- e5fe5be: Bumps recast to resolve a bug where JSX elements are wrapped in two pairs of parenthesis
## 17.1.2
### Patch Changes
- 8f60fbf: Enable async tranformers in test utils.
All notable changes to this project will be documented in this file.
## [17.1.1] 2024-10-31
### Fixed
- Republished with `temp` dependency properly removed (#638, thanks @trivikr for reporting)
## [17.1.0] 2024-10-30
### Added
- `pkg.pr.new` will now be used to build an npm pakage for each commit to the repo, allowing you to more easily test changes or use new features before an official release is cut. (#622, @Aslemammad)
### Changed
- Replaced `temp` library with `tmp` (#633, @r4zendev)
### Fixed
- Removed old `docs` command from `package.json` since the new docs are in the `website` folder, which has instructions in its README.
## [17.0.0] 2024-08-06
We needed to go [from v0.x to a major release](https://github.com/facebook/jscodeshift/issues/593), and it may as well happen now. jscodeshift has been around for nine years though, so going to v1.0.0 didn't feel quite right. I've instead promoted the minor version number to a major version number, similar to what React did when it went from 0.14 to 15.0.
### Fixed
- Node.js 16 is now explicitly specified as the minimum required version. It was _already_ implicitly required due to some dependencies requiring it, but this wasn't explicitly specified until now. (#607, @trivikr)
### Added
- A new [jscodeshift website](https://jscodeshift.com/) has launched, thanks to the team at [Codemod](https://codemod.com/). (#592, @mohab-sameh with some tweaks by @morinokami)
- Added collection functions for import declarations, including finding imports and inserting new imports (#617, @danieldelcore)
### Changed
- Enabled TypeScript `importAttributes` (#585, @benasher44) and `decoratorAutoAccessors` (#594, @syi0808) plugins
- Removed or replaced various outdated and unused dependencies (#601, #605, #609, #613, @trivikr)
- Started using Corepack to manage Yarn version (#599, @trivikr)
## [0.16.1] 2024-06-25
### Fixed
- Removed old `babel-core` dependency that was unused but caused security scanners to flag vulnerabilities.
## [0.16.0] 2024-06-18
### Added
- Added a `--gitignore` flag to avoid transforming any files listed in `.gitignore` (#508, @ElonVolo)
### Changed
- Updated various dependencies to latest version (#588, @ElonVolo)
## [0.15.2] 2024-02-21
### Fixed
- Process all supported extensions by default (#584, @trivikr)
## [0.15.1] 2023-10-28
### Changed
- Upgraded to recast 0.23.3 (#564, @ashsearle)
- Enable `@babel/plugin-proposal-private-methods` in worker (#568, @sibelius)
- Upgraded Babel packages (#570, @dartess)
### Fixed
- Respect extensions cli option when filtering individual files (#562, @robcmills)
- Fixed unit test after #562 broke them (#575, @ElonVolo)
## [0.15.0] 2023-05-07
### Changed
- Upgraded to recast 0.23.1 (#544, @ryanrhee)
- Make @babel/preset-env optional (#480, @SimenB)
### Fixed
- Force LF line ending in bin/jscodeshift.sh (#555, @jakeboone02)
- Use transform's exported parser in testUitls (#528, @CrispyBacon12)
- Ensure jscodeshift doesn't load Babel config file (#460, @raon0211)
## [0.14.0] 2022-10-04
### Added
- Added a `defineSnapshotTestFromFixture` test util (#471, @shriuken)
- Added `renameTo` filters for Babel 6+ node types (#412 and #504, @elonvolo and @henryqdineen)
- Added `childNodesOfType` to JSX traversal methods (#415, @j13huang)
### Changed
- Bumped dependency versions
- Allow arguments in `--help` to be listed in an order other than alphabetically, so they can instead be grouped thematically (#507, @elonvolo)
- Allow the `j` shortcut in test utils (#515, @no23reason)
## [0.13.1] 2022-01-10
### Changed
- Switched from `colors` to `chalk` to mitigate a security vulnerability in `colors@1.4.1`.
## [0.13.0] 2021-06-26
### Added
- Added a `--fail-on-error` flag to return a `1` error code when errors were found (#416, @marcodejongh)
- Created `template.asyncExpression` (#405, @jedwards1211)
### Changed
- Removed lodash dependency from tsx parser (#432, @JHilker and @robyoder)
## [0.12.0] 2021-04-21
### Changed
- Allow transform to be a Promise (#237, @rektide)
- Support newer TypeScript syntax by upgrading to newer Babel parser (#410, @wdoug and @mfeckie)
## [0.11.0] 2020-09-01
### Changed
- Updated `recast` to latest
## [0.10.0] 2020-06-01
### Changed
- Updated `flow-parser` to latest, and enabled Flow Enums parsing by default when using Flow parser
## [0.8.0] 2020-05-03
### Changed
- Dropped support for Node versions 6 and 8
## [0.7.0] 2019-12-11
## Added
- Added jest snapshot utils (#297, @dogoku)
### Changed
- Moved from BSD to MIT license
### Fixed
- No longer throw an error when calling jscodeshift on a non-existent path (#334, @threepointone)
- Preserve the original file extension in remote files (#317, @samselikoff)
## [0.6.4] 2019-04-30
### Changed
- Allow writing tests in TypeScript ([PR #308](https://github.com/facebook/jscodeshift/pull/308))
- Better handling of `.gitingore` files: Ignore comments and support `\r\n` line breaks ([PR #306](https://github.com/facebook/jscodeshift/pull/306))
## [0.6.3] 2019-01-18
### Fixed
- Don't throw an error when jscodeshift processes an empty set of files (#295,
@skovhus).
- `renameTo` should not rename class properties (#296, @henryqdineen).
- Custom/unknown CLI parameters are parsed as JSON, just like nomnom used to
do.
## [0.6.2] 2018-12-05
### Changed
- `@babel/register`/`@babel/preset-env` is configured to not transpile any
language features that the running Node process supports. That means if you use
features in your transform code supported by the Node version you are running,
they will be left as is. Most of ES2015 is actually supported since Node v6.
- Do not transpile object rest/spread in transform code if supported by running
Node version.
### Fixed
- Presets and plugins passed to `@babel/register` are now properly named and
loaded.
## [0.6.1] 2018-12-04
### Added
- Tranform files can be written in Typescript. If the file extension of the
transform file is `.ts` or `.tsx`, `@babel/preset-typescript` is used to
convert them. This requires the `--babel` option to be set (which it is by
default). ( #287 , @brieb )
### Changed
- The preset and plugins for converting the transform file itself via babeljs
have been updated to work with babel v7. This included removing
`babel-preset-es2015` and `babel-preset-stage-1` in favor of
`@babel/preset-env`. Only `@babel/proposal-class-properties` and
`@babel/proposal-object-rest-spread` are enabled as experimental features. If
you want to use other's in your transform file, please create a PR.
### Fixed
- Typescript parses use `@babel/parser` instead of Babylon ( #291, @elliottsj )
### Bumped
- `micromatch` => v3.1.10, which doesn't (indirectly) depend on `randomatic` <
v3 anymore (see #292).
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to jscodeshift
We want to make contributing to this project as easy and transparent as
possible.
## Code of Conduct
The code of conduct is described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
## Our Development Process
The majority of development on jscodeshift will occur through GitHub. Accordingly,
the process for contributing will follow standard GitHub protocol.
## Pull Requests
We actively welcome your pull requests.
1. Fork the repo and create your branch from `main`.
1. Run `corepack enable` to set up `yarn` package manager.
1. If you've added code that should be tested, add tests.
1. If you've changed APIs, update the documentation.
1. Ensure the test suite passes.
1. Make sure your code lints.
1. If you haven't already, complete the Contributor License Agreement ("CLA").
## Contributor License Agreement ("CLA")
In order to accept your pull request, we need you to submit a CLA. You only need
to do this once to work on any of Facebook's open source projects.
Complete your CLA here: <https://code.facebook.com/cla>
## Issues
We use GitHub issues to track public bugs. Please ensure your description is
clear and has sufficient instructions to be able to reproduce the issue.
Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
disclosure of security bugs. In those cases, please go through the process
outlined on that page and do not file a public issue.
## Coding Style
* Use semicolons;
* Commas last,
* 2 spaces for indentation (no tabs)
* Prefer `'` over `"`
* `'use strict';`
* 80 character line length
* "Attractive"
### License
jscodeshift is [MIT licensed](./LICENSE).
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# [jscodeshift](https://jscodeshift.com/) [](https://opensource.fb.com/support-ukraine) [](https://github.com/facebook/jscodeshift/actions/workflows/test.yml?query=branch%3Amain)
jscodeshift is a toolkit for running codemods over multiple JavaScript or
TypeScript files.
It provides:
- A runner, which executes the provided transform for each file passed to it.
It also outputs a summary of how many files have (not) been transformed.
- A wrapper around [recast][], providing a different API. Recast is an
AST-to-AST transform tool and also tries to preserve the style of original code
as much as possible.
## Install
Get jscodeshift from [npm](https://www.npmjs.com/package/jscodeshift):
```
$ npm install -g jscodeshift
```
This will install the runner as `jscodeshift`.
## VSCode Debugger
[Configure VSCode to debug codemods](#vscode-debugging)
## Usage (CLI)
See [the website](https://jscodeshift.com/) for full documentation.
The CLI provides the following options:
```
$ jscodeshift --help
Usage: jscodeshift [OPTION]... PATH...
or: jscodeshift [OPTION]... -t TRANSFORM_PATH PATH...
or: jscodeshift [OPTION]... -t URL PATH...
or: jscodeshift [OPTION]... --stdin < file_list.txt
Apply transform logic in TRANSFORM_PATH (recursively) to every PATH.
If --stdin is set, each line of the standard input is used as a path.
Options:
"..." behind an option means that it can be supplied multiple times.
All options are also passed to the transformer, which means you can supply custom options that are not listed here.
--(no-)babel apply babeljs to the transform file
(default: true)
-c, --cpus=N start at most N child processes to process source files
(default: max(all - 1, 1))
-d, --(no-)dry dry run (no changes are made to files)
(default: false)
--extensions=EXT transform files with these file extensions (comma separated list)
(default: js)
-h, --help print this help and exit
--ignore-config=FILE ... ignore files if they match patterns sourced from a configuration file (e.g. a .gitignore)
--ignore-pattern=GLOB ... ignore files that match a provided glob expression
--(no-)gitignore adds entries the current directory's .gitignore file
(default: false)
--parser=babel|babylon|flow|ts|tsx the parser to use for parsing the source files
(default: babel)
--parser-config=FILE path to a JSON file containing a custom parser configuration for flow or babylon
-p, --(no-)print print transformed files to stdout, useful for development
(default: false)
--(no-)run-in-band run serially in the current process
(default: false)
-s, --(no-)silent do not write to stdout or stderr
(default: false)
--(no-)stdin read file/directory list from stdin
(default: false)
-t, --transform=FILE path to the transform file. Can be either a local path or url
(default: ./transform.js)
-v, --verbose=0|1|2 show more information about the transform process
(default: 0)
--version print version and exit
--fail-on-error return a 1 exit code when errors were found during execution of codemods
```
This passes the source of all passed through the transform module specified
with `-t` or `--transform` (defaults to `transform.js` in the current
directory). The next section explains the structure of the transform module.
## Usage (JS)
```js
const {run: jscodeshift} = require('jscodeshift/src/Runner')
const path = require('node:path');
const transformPath = path.resolve('transform.js')
const paths = ['foo.js', 'bar']
const options = {
dry: true,
print: true,
verbose: 1,
// ...
}
const res = await jscodeshift(transformPath, paths, options)
console.log(res)
/*
{
stats: {},
timeElapsed: '0.001',
error: 0,
ok: 0,
nochange: 0,
skip: 0
}
*/
```
## Transform module
The transform is simply a module that exports a function of the form:
```js
module.exports = function(fileInfo, api, options) {
// transform `fileInfo.source` here
// ...
// return changed source
return source;
};
```
As of v0.6.1, this module can also be written in TypeScript.
### Arguments
#### `fileInfo`
Holds information about the currently processed file.
Property | Description
------------|------------
path | File path
source | File content
#### `api`
This object exposes the `jscodeshift` library and helper functions from the
runner.
Property | Description
------------|------------
jscodeshift | A reference to the jscodeshift library
stats | A function to collect statistics during `--dry` runs
report | Prints the passed string to stdout
`jscodeshift` is a reference to the wrapper around recast and provides a
jQuery-like API to navigate and transform the AST. Here is a quick example,
a more detailed description can be found below.
```js
/**
* This replaces every occurrence of variable "foo".
*/
module.exports = function(fileInfo, api, options) {
return api.jscodeshift(fileInfo.source)
.findVariableDeclarators('foo')
.renameTo('bar')
.toSource();
}
```
**Note:** This API is exposed for convenience, but you don't have to use it.
You can use any tool to modify the source.
`stats` is a function that only works when the `--dry` options is set. It accepts
a string, and will simply count how often it was called with that value.
At the end, the CLI will report those values. This can be useful while
developing the transform, e.g. to find out how often a certain construct
appears in the source(s).
**`report`** allows you to print arbitrary strings to stdout. This can be
useful when other tools consume the output of jscodeshift. The reason to not
directly use `process.stdout` in transform code is to avoid mangled output when
many files are processed.
#### `options`
Contains all options that have been passed to runner. This allows you to pass
additional options to the transform. For example, if the CLI is called with
```
$ jscodeshift -t myTransforms fileA fileB --foo=bar
```
`options` would contain `{foo: 'bar'}`.
### Return value
The return value of the function determines the status of the transformation:
- If a string is returned and it is different from passed source, the
transform is considered to be successful.
- If a string is returned but it's the same as the source, the transform
is considered to be unsuccessful.
- If nothing is returned, the file is not supposed to be transformed (which is
ok).
The CLI provides a summary of the transformation at the end. You can get more
detailed information by setting the `-v` option to `1` or `2`.
You can collect even more stats via the `stats` function as explained above.
### Parser
The transform file 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"`, `"flow"`, `"ts"`, or `"tsx"`,
or it can be a parser object that is compatible with recast and follows the estree spec.
__Example: specifying parser type string in the transform file__
```js
module.exports = function transformer(file, api, options) {
const j = api.jscodeshift;
const rootSource = j(file.source);
// whatever other code...
return rootSource.toSource();
}
// use the flow parser
module.exports.parser = 'flow';
```
__Example: specifying a custom parser object in the transform file__
```js
module.exports = function transformer(file, api, options) {
const j = api.jscodeshift;
const rootSource = j(file.source);
// whatever other code...
return rootSource.toSource();
}
module.exports.parser = {
parse: function(source) {
// return estree compatible AST
},
};
```
### Example output
```text
$ jscodeshift -t myTransform.js src
Processing 10 files...
Spawning 2 workers with 5 files each...
All workers done.
Results: 0 errors 2 unmodified 3 skipped 5 ok
```
## The jscodeshift API
As already mentioned, jscodeshift also provides a wrapper around [recast][].
In order to properly use the jscodeshift API, one has to understand the basic
building blocks of recast (and ASTs) as well.
### Core Concepts
#### AST nodes
An AST node is a plain JavaScript object with a specific set of fields, in
accordance with the [Mozilla Parser API][]. The primary way to identify nodes
is via their `type`.
For example, string literals are represented via `Literal` nodes, which
have the structure
```js
// "foo"
{
type: 'Literal',
value: 'foo',
raw: '"foo"'
}
```
It's OK to not know the structure of every AST node type.
The [(esprima) AST explorer][ast-explorer] is an online tool to inspect the AST
for a given piece of JS code.
#### Path objects
Recast itself relies heavily on [ast-types][] which defines methods to traverse
the AST, access node fields and build new nodes. ast-types wraps every AST node
into a *path object*. Paths contain meta-information and helper methods to
process AST nodes.
For example, the child-parent relationship between two nodes is not explicitly
defined. Given a plain AST node, it is not possible to traverse the tree *up*.
Given a path object however, the parent can be traversed to via `path.parent`.
For more information about the path object API, please have a look at
[ast-types][].
#### Builders
To make creating AST nodes a bit simpler and "safer", ast-types defines a couple
of *builder methods*, which are also exposed on `jscodeshift`.
For example, the following creates an AST equivalent to `foo(bar)`:
```js
// inside a module transform
var j = jscodeshift;
// foo(bar);
var ast = j.callExpression(
j.identifier('foo'),
[j.identifier('bar')]
);
```
The signature of each builder function is best learned by having a look at the
[definition files](https://github.com/benjamn/ast-types/tree/master/src/def)
or in the babel/types [docs](https://babeljs.io/docs/en/babel-types).
### Collections and Traversal
In order to transform the AST, you have to traverse it and find the nodes that
need to be changed. jscodeshift is built around the idea of **collections** of
paths and thus provides a different way of processing an AST than recast or
ast-types.
A collection has methods to process the nodes inside a collection, often
resulting in a new collection. This results in a fluent interface, which can
make the transform more readable.
Collections are "typed" which means that the type of a collection is the
"lowest" type all AST nodes in the collection have in common. That means you
cannot call a method for a `FunctionExpression` collection on an `Identifier`
collection.
Here is an example of how one would find/traverse all `Identifier` nodes with
jscodeshift and with recast:
```js
// recast
var ast = recast.parse(src);
recast.visit(ast, {
visitIdentifier: function(path) {
// do something with path
return false;
}
});
// jscodeshift
jscodeshift(src)
.find(jscodeshift.Identifier)
.forEach(function(path) {
// do something with path
});
```
To learn about the provided methods, have a look at the
[Collection.js](src/Collection.js) and its [extensions](src/collections/).
### Extensibility
jscodeshift provides an API to extend collections. By moving common operators
into helper functions (which can be stored separately in other modules), a
transform can be made more readable.
There are two types of extensions: generic extensions and type-specific
extensions. **Generic extensions** are applicable to all collections. As such,
they typically don't access specific node data, but rather traverse the AST from
the nodes in the collection. **Type-specific** extensions work only on specific
node types and are not callable on differently typed collections.
#### Examples
```js
// Adding a method to all Identifiers
jscodeshift.registerMethods({
logNames: function() {
return this.forEach(function(path) {
console.log(path.node.name);
});
}
}, jscodeshift.Identifier);
// Adding a method to all collections
jscodeshift.registerMethods({
findIdentifiers: function() {
return this.find(jscodeshift.Identifier);
}
});
jscodeshift(ast).findIdentifiers().logNames();
jscodeshift(ast).logNames(); // error, unless `ast` only consists of Identifier nodes
```
### Ignoring files and directories
Sometimes there are files and directories that you want to avoid running transforms on. For example, the node_modules/ directory, where the project's installed local npm packages reside, can introduce bugs if any files in it are accidentally transformed by jscodeshift.
The simplest way to avoid many of these unwanted transforms is to pass jscodeshift the __--gitignore__ flag, which uses the glob patterns specified in your project’s .gitignore file to avoid transforming anything in directories such as node_modules/, dist/, etc. In most cases anything you want git to ignore you proabably are also going to want jscodeshift to ignore as well. _Please note that the .gitignore file use will be taken from the current working directory from which jscodeshift is being run._
```
jscodeshift --gitignore mytransform.js
```
For more custom ignore functionality, the __--ignore-pattern__ and the __--ignore-config__ arguments can be used.
__--ignore-pattern__ takes a [.gitignore format](https://git-scm.com/docs/gitignore#_pattern_format) glob pattern that specifies file and directory patterns to ignore
```
jscodeshift --ignore-pattern="js_configuration_files/**/*” mytransform.js
// More than one ignore
jscodeshift --ignore-pattern="first_ignored_dir/**/*” -—ignore-pattern="second_ignored_dir/**/*” mytransform.js
```
__--ignore-config__ takes one or more paths to files containing lines with [.gitignore format](https://git-scm.com/docs/gitignore#_pattern_format) glob patterns.
```
// note: .gitignore is a random made-up filename extension for this example
jscodeshift --ignore-config="MyIgnoreFile.gitignore" mytransform.js
// More than one ignore file
jscodeshift --ignore-pattern="first_ignore_file.gitignore” --ignore-pattern="second_ignore_file.gitignore” mytransform.js
```
### Passing options to [recast]
You may want to change some of the output settings (like setting `'` instead of `"` or
changing the end-of-line terminator to `\r\n` for Windows files).
This can be done by passing config options to [recast].
```js
.toSource({quote: 'single'}); // sets strings to use single quotes in transformed code.
// or
.toSource({lineTerminator: '\r\n'}); // sets EOL to use CRLF.
```
You can also pass options to recast's `parse` method by passing an object to
jscodeshift as second argument:
```js
jscodeshift(source, {...})
```
For more details on recast config options, see [here](https://github.com/benjamn/recast/blob/master/lib/options.ts).
### Unit Testing
jscodeshift comes with a simple utility to allow easy unit testing with [Jest](https://facebook.github.io/jest/), without having to write a lot of boilerplate code. This utility makes some assumptions in order to reduce the amount of configuration required:
- The test is located in a subdirectory under the directory the transform itself is located in (eg. `__tests__`)
- Test fixtures are located in a `__testfixtures__` directory
This results in a directory structure like this:
```
/MyTransform.js
/__tests__/MyTransform-test.js
/__testfixtures__/MyTransform.input.js
/__testfixtures__/MyTransform.output.js
```
A simple example of unit tests is bundled in the [sample directory](sample).
The `testUtils` module exposes a number of useful helpers for unit testing.
#### `defineTest`
Defines a Jest/Jasmine test for a jscodeshift transform which depends on fixtures
```js
jest.autoMockOff();
const defineTest = require('jscodeshift/dist/testUtils').defineTest;
defineTest(__dirname, 'MyTransform');
```
An alternate fixture filename can be provided as the fourth argument to `defineTest`.
This also means that multiple test fixtures can be provided:
```js
defineTest(__dirname, 'MyTransform', null, 'FirstFixture');
defineTest(__dirname, 'MyTransform', null, 'SecondFixture');
```
This will run two tests:
- `__testfixtures__/FirstFixture.input.js`
- `__testfixtures__/SecondFixture.input.js`
#### `defineInlineTest`
Defines a Jest/Jasmine test suite for a jscodeshift transform which accepts inline values
This is a more flexible alternative to `defineTest`, as this allows to also provide options to your transform
```js
const defineInlineTest = require('jscodeshift/dist/testUtils').defineInlineTest;
const transform = require('../myTransform');
const transformOptions = {};
defineInlineTest(transform, transformOptions, 'input', 'expected output', 'test name (optional)');
```
#### `defineSnapshotTest`
Similar to `defineInlineTest` but instead of requiring an output value, it uses Jest's `toMatchSnapshot()`
```js
const defineSnapshotTest = require('jscodeshift/dist/testUtils').defineSnapshotTest;
const transform = require('../myTransform');
const transformOptions = {};
defineSnapshotTest(transform, transformOptions, 'input', 'test name (optional)');
```
For more information on snapshots, check out [Jest's docs](https://jestjs.io/docs/en/snapshot-testing)
#### `defineSnapshotTestFromFixture`
Similar to `defineSnapshotTest` but will load the file using same file-directory defaults as `defineTest`
```js
const defineSnapshotTestDefault = require('jscodeshift/dist/testUtils').defineSnapshotTestDefault;
const transform = require('../myTransform');
const transformOptions = {};
defineSnapshotTestFromFixture(__dirname, transform, transformOptions, 'FirstFixture', 'test name (optional)');
```
#### `applyTransform`
Executes your transform using the options and the input given and returns the result.
This function is used internally by the other helpers, but it can prove useful in other cases.
(bear in mind the `transform` module can be asynchronous. In that case, `applyTransform` will return a `Promise` with the transformed code. Otherwise, it will directly return the transformed code as a `string`).
```js
const applyTransform = require('jscodeshift/dist/testUtils').applyTransform;
const transform = require('../myTransform');
const transformOptions = {};
const output = applyTransform(transform, transformOptions, 'input');
```
#### ES modules
If you're authoring your transforms and tests using ES modules, make sure to import the transform's parser (if specified) in your tests:
```js
// MyTransform.js
export const parser = 'flow'
export default function MyTransform(fileInfo, api, options) {
// ...
}
```
```js
// __tests__/MyTransform-test.js
import { defineInlineTest } from 'jscodeshift/dist/testUtils
import * as transform from '../MyTransform
console.log(transform.parser) // 'flow'
defineInlineTest(transform, /* ... */)
```
### Example Codemods
- [react-codemod](https://github.com/reactjs/react-codemod) - React codemod scripts to update React APIs.
- [js-codemod](https://github.com/cpojer/js-codemod/) - Codemod scripts to transform code to next generation JS.
- [js-transforms](https://github.com/jhgg/js-transforms) - Some documented codemod experiments to help you learn.
- [fix-js](https://github.com/anshckr/fix-js) - Codemods to fix some ESLint issues
### Local Documentation Server
To update docs in `/docs`, use `npm run docs`.
To view these docs locally, use `npx http-server ./docs`
## VSCode Debugging
It's recommended that you set up your codemod project to all debugging via the VSCode IDE. When you open your project in VSCode, add the following configuration to your launch.json debugging configuration.
```jsonc
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Debug Transform",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceRoot}/node_modules/.bin/jscodeshift",
"stopOnEntry": false,
"args": ["--dry", "--print", "-t", "${input:transformFile}", "--parser", "${input:parser}", "--run-in-band", "${file}"],
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"console": "internalConsole",
"sourceMaps": true,
"outFiles": []
},
{
"name": "Debug All JSCodeshift Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand",
"--testPathPattern=${fileBasenameNoExtension}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
],
"inputs": [
{
"type": "pickString",
"id": "parser",
"description": "jscodeshift parser",
"options": [
"babel",
"babylon",
"flow",
"ts",
"tsx",
],
"default": "babel"
},
{
"type": "promptString",
"id": "transformFile",
"description": "jscodeshift transform file",
"default": "transform.js"
}
]
}
```
Once this has been added to the configuration
1. Install jscodeshift as a package if you haven't done so already by running the command **npm install --save jscodeshift**. The debug configuration will not work otherwise.
2. Once the jscodeshift local package has been installed, go to the VSCode file tree and select the file on which you want to run the transform. For example, if you wanted to run codemod transforms of foo.js file, you would click on the entry for foo.js file in your project tree.
3. Select "Debug Transform" from the debugging menu's options menu.
4. Click the **"Start Debugging"** button on the VSCode debugger.
5. You will be then prompted for the name of jscodeshift transform file. Enter in the name of the transform file to use. If no name is given it will default to **transform.js**
6. Select the parser to use from the presented selection list of parsers. The transform will otherwise default to using the **babel** parser.
7. The transform will then be run, stopping at any breakpoints that have been set.
8. If there are no errors and the transform is complete, then the results of the transform will be printed in the VSCode debugging console. The file with the contents that have been transformed will not be changed, as the debug configuration makes use the jscodeshift **--dry** option.
### Recipes
- [Retain leading comment(s) in file when replacing/removing first statement](recipes/retain-first-comment.md)
[npm]: https://www.npmjs.com/
[Mozilla Parser API]: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API
[recast]: https://github.com/benjamn/recast
[ast-types]: https://github.com/benjamn/ast-types
[ast-explorer]: http://astexplorer.net/
================================================
FILE: bin/__tests__/jscodeshift-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*global jest, jasmine, xdescribe, it, expect, beforeEach*/
/*eslint camelcase: 0, no-unused-vars: 0*/
jest.autoMockOff();
// Increase default timeout (5000ms) for Travis
jest.setTimeout(600000); // 10 minutes
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const testUtils = require('../../utils/testUtils');
const createTransformWith = testUtils.createTransformWith;
const createTempFileWith = testUtils.createTempFileWith;
function readFile(path) {
return fs.readFileSync(path).toString();
}
function run(args, stdin, cwd) {
return new Promise(resolve => {
const jscodeshift = child_process.spawn(
path.join(__dirname, '../jscodeshift.sh'),
args,
{
cwd,
}
);
let stdout = '';
let stderr = '';
jscodeshift.stdout.on('data', data => stdout += data);
jscodeshift.stderr.on('data', data => stderr += data);
jscodeshift.on('close', () => resolve([stdout, stderr]));
if (stdin) {
jscodeshift.stdin.write(stdin);
}
jscodeshift.stdin.end();
});
}
describe('jscodeshift CLI', () => {
it('calls the transform and file information', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const sourceB = createTempFileWith('b\n', 'sourceB', '.js');
const sourceC = createTempFileWith('c', 'sourceC', '.js');
const transformA = createTransformWith(
'return "transform" + fileInfo.source;'
);
const transformB = createTransformWith(
'return fileInfo.path;'
);
return Promise.all([
run(['-t', transformA, sourceA, sourceB]).then(
out => {
expect(out[1]).toBe('');
expect(readFile(sourceA)).toBe('transforma');
expect(readFile(sourceB)).toBe('transformb\n');
}
),
run(['-t', transformB, sourceC]).then(
out => {
expect(out[1]).toBe('');
expect(readFile(sourceC)).toBe(sourceC);
}
)
]);
});
it('takes file list from stdin if --stdin is set', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const sourceB = createTempFileWith('b\n', 'sourceB', '.js');
const sourceC = createTempFileWith('c', 'sourceC', '.js');
const transformA = createTransformWith(
'return "transform" + fileInfo.source;'
);
return run(['--stdin', '-t', transformA], [sourceA, sourceB, sourceC].join('\n')).then(
out => {
expect(out[1]).toBe('');
expect(readFile(sourceA)).toBe('transforma');
expect(readFile(sourceB)).toBe('transformb\n');
expect(readFile(sourceC)).toBe('transformc');
}
);
});
it('does not transform files in a dry run', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith(
'return "transform" + fileInfo.source;'
);
return run(['-t', transform, '-d', sourceA]).then(
() => {
expect(readFile(sourceA).toString()).toBe('a');
}
);
});
describe('Babel', () => {
// Verifiers that ES6 features are supported either natively or via Babel
it('supports ES6 features in transform files', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith(
'const a = 42; return a;'
);
return Promise.all([
run(['-t', transform, sourceA]).then(
() => {
expect(readFile(sourceA).toString())
.toEqual('42');
}
),
]);
});
// Verifies that spread is supported, either natively over via Babel
it('supports property spread in transform files', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith(
'const a = {...{foo: 42}, bar: 21}; return a.foo;'
);
return Promise.all([
run(['-t', transform, sourceA]).then(
() => {
expect(readFile(sourceA).toString())
.toEqual('42');
}
),
]);
});
it('supports class properties in transform files', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith(`
return (class {
x = 42;
}).toString();
`);
return Promise.all([
run(['-t', transform, sourceA]).then(
() => {
expect(readFile(sourceA).toString())
.toMatch(/\(this,\s*['"]x['"]/);
}
),
]);
});
it('supports flow type annotations in transform files', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith(
'return (function() { "use strict"; const a: number = 42; }).toString();'
);
return Promise.all([
run(['-t', transform, sourceA]).then(
() => {
expect(readFile(sourceA).toString())
.toMatch(/a\s*=\s*42/);
}
),
]);
});
it('supports Typescript type annotations in transform files', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith(
'return (function() { "use strict"; function foo(x: string): x is string {}}).toString();',
'.ts'
);
return Promise.all([
run(['-t', transform, sourceA]).then(
args => {
expect(readFile(sourceA).toString())
.toMatch(/function\s+foo\(x\)\s*{}/);
}
),
]);
});
it('transpiles imported Typescript files in transform files', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const helper = createTempFileWith(
'module.exports = function(x: string): x is string {};',
undefined,
'.ts'
);
const transform = createTransformWith(
`return require('${helper}').toString();`,
'.ts'
);
return Promise.all([
run(['-t', transform, sourceA]).then(
args => {
expect(readFile(sourceA).toString())
.toMatch(/function\s*\(x\)\s*{}/);
}
),
]);
});
});
it('passes jscodeshift and stats the transform function', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith([
' return String(',
' typeof api.jscodeshift === "function" &&',
' typeof api.stats === "function"',
' );',
].join('\n'));
return run(['-t', transform, sourceA]).then(
() => {
expect(readFile(sourceA).toString()).toBe('true');
}
);
});
it('passes options along to the transform', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith('return options.foo;');
return run(['-t', transform, '--foo=42', sourceA]).then(
() => {
expect(readFile(sourceA).toString()).toBe('42');
}
);
});
it('does not stall with too many files', () => {
const sources = [];
for (let i = 0; i < 100; i++) {
sources.push(createTempFileWith('a'));
}
const transform = createTransformWith('');
return run(['-t', transform, '--foo=42'].concat(sources)).then(
() => {
expect(true).toBe(true);
}
);
});
describe('ignoring', () => {
const transform = createTransformWith(
'return "transform" + fileInfo.source;'
);
let sources = [];
beforeEach(() => {
sources = [];
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const testIgnoreFile = createTempFileWith('a', 'a-test', '.js');
sources.push(sourceA);
sources.push(testIgnoreFile);
// sources.push(createTempFileWith('b', 'src/lib/b.js'));
});
it('supports basic glob', () => {
const pattern = '*-test.js';
return run(['-t', transform, '--ignore-pattern', pattern].concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('transforma');
expect(readFile(sources[1]).toString()).toBe('a');
}
);
});
it('supports filename match', () => {
const pattern = 'sourceA.js';
return run(['-t', transform, '--ignore-pattern', pattern].concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('a');
expect(readFile(sources[1]).toString()).toBe('transforma');
}
);
});
it('accepts a list of patterns', () => {
const patterns = ['--ignore-pattern', 'sourceA.js', '--ignore-pattern', '*-test.js'];
return run(['-t', transform].concat(patterns).concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('a');
expect(readFile(sources[1]).toString()).toBe('a');
}
);
});
it('sources ignore patterns from configuration file', () => {
const patterns = ['sub/dir/', '*-test.js'];
const gitignore = createTempFileWith(patterns.join('\n'), '.gitignore');
sources.push(createTempFileWith('subfile', 'sub/dir/file.js'));
return run(['-t', transform, '--ignore-config', gitignore].concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('transforma');
expect(readFile(sources[1]).toString()).toBe('a');
expect(readFile(sources[2]).toString()).toBe('subfile');
}
);
});
it('sources ignore patterns from configuration file', () => {
const patterns = ['sub/dir/', '*-test.js'];
const gitignore = createTempFileWith(patterns.join('\n'), '.gitignore');
sources.push(createTempFileWith('subfile', 'sub/dir/file.js'));
return run(['-t', transform, '--ignore-config', gitignore].concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('transforma');
expect(readFile(sources[1]).toString()).toBe('a');
expect(readFile(sources[2]).toString()).toBe('subfile');
}
);
});
it('sources ignore patterns from a root directory\'s .gitignore file', () => {
// This test is a little different from the one above only in that we have
// to simulate automatically hitting up the .gitignore file from the current
// directory that the codeshift process is running from
const patterns = ['sub/dir/', '*-test.js'];
const gitignore = createTempFileWith(patterns.join('\n'), '.gitignore');
sources.push(createTempFileWith('subfile', 'sub/dir/file.js'));
// Make the temp directory with our test files the current working directory
let currPath = process.cwd();
let tempDirPath = path.dirname(sources[0]);
process.chdir(tempDirPath);;
return run(['-t', transform, '--gitignore'].concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('transforma');
expect(readFile(sources[1]).toString()).toBe('a');
expect(readFile(sources[2]).toString()).toBe('subfile');
}
)
.catch((err) => {
console.log(err);
})
.finally(() => {
process.chdir(currPath);
})
});
it('accepts a list of configuration files', () => {
const gitignore = createTempFileWith(['sub/dir/'].join('\n'), '.gitignore');
const eslintignore = createTempFileWith(['**/*test.js', 'sourceA.js'].join('\n'), '.eslintignore');
const configs = ['--ignore-config', gitignore, '--ignore-config', eslintignore];
sources.push(createTempFileWith('subfile', 'sub/dir/file.js'));
return run(['-t', transform].concat(configs).concat(sources)).then(
() => {
expect(readFile(sources[0]).toString()).toBe('a');
expect(readFile(sources[1]).toString()).toBe('a');
expect(readFile(sources[2]).toString()).toBe('subfile');
}
);
});
});
describe('output', () => {
it('shows workers info and stats at the end by default', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith('return null;');
return run(['-t', transform, sourceA]).then(
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: ');
}
);
});
it('does not ouput anything in silent mode', () => {
const sourceA = createTempFileWith('a', 'sourceA', '.js');
const transform = createTransformWith('return null;');
return run(['-t', transform, '-s', sourceA]).then(
out => {
expect(out[0]).toEqual('');
}
);
});
});
xdescribe('--parser=ts', () => {
it('parses TypeScript sources', () => {
const source = createTempFileWith('type Foo = string | string[];', 'source', '.ts');
const transform = createTransformWith(
'api.jscodeshift(fileInfo.source)\n { return "changed" };'
);
return run([
'-t', transform,
'--parser', 'ts',
'--run-in-band',
source,
]).then(
out => {
expect(out[0]).not.toContain('Transformation error');
expect(readFile(source)).toEqual('changed');
}
);
});
});
describe('--parser-config', () => {
it('allows custom parser settings to be passed', () => {
// @decorators before export are not supported in the current default
// config
const source = createTempFileWith('@foo\nexport class Bar {}', 'source', '.js');
const parserConfig = createTempFileWith(JSON.stringify({
sourceType: 'module',
tokens: true,
plugins: [
['decorators', {decoratorsBeforeExport: true}],
],
}));
const transform = createTransformWith(
'api.jscodeshift(fileInfo.source)\n { return "changed" };'
);
return run([
'-t', transform,
'--parser-config', parserConfig,
'--parser', 'babylon',
'--run-in-band',
source,
]).then(
out => {
expect(out[0]).not.toContain('Transformation error');
expect(readFile(source)).toEqual('changed');
}
);
});
});
});
================================================
FILE: bin/jscodeshift.js
================================================
#!/usr/bin/env node
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Runner = require('../src/Runner.js');
const fs = require('fs');
const path = require('path');
const pkg = require('../package.json');
const { DEFAULT_EXTENSIONS } = require('@babel/core');
const defaultExtensions = DEFAULT_EXTENSIONS.concat(['ts', 'tsx']).map(
(ext) => (ext.startsWith('.') ? ext.substring(1) : ext)
).sort().join(',');
const parser = require('../src/argsParser')
.options({
transform: {
display_index: 15,
abbr: 't',
default: './transform.js',
help: 'path to the transform file. Can be either a local path or url',
metavar: 'FILE',
required: true
},
cpus: {
display_index: 1,
abbr: 'c',
help: 'start at most N child processes to process source files',
defaultHelp: 'max(all - 1, 1)',
metavar: 'N',
process: Number,
},
verbose: {
display_index: 16,
abbr: 'v',
choices: [0, 1, 2],
default: 0,
help: 'show more information about the transform process',
metavar: 'N',
process: Number,
},
dry: {
display_index: 2,
abbr: 'd',
flag: true,
default: false,
help: 'dry run (no changes are made to files)'
},
print: {
display_index: 11,
abbr: 'p',
flag: true,
default: false,
help: 'print transformed files to stdout, useful for development'
},
babel: {
display_index: 0,
flag: true,
default: true,
help: 'apply babeljs to the transform file'
},
extensions: {
display_index: 3,
default: defaultExtensions,
help: 'transform files with these file extensions (comma separated list)',
metavar: 'EXT',
},
ignorePattern: {
display_index: 7,
full: 'ignore-pattern',
list: true,
help: 'ignore files that match a provided glob expression',
metavar: 'GLOB',
},
ignoreConfig: {
display_index: 6,
full: 'ignore-config',
list: true,
help: 'ignore files if they match patterns sourced from a configuration file (e.g. a .gitignore)',
metavar: 'FILE'
},
gitignore: {
display_index: 8,
flag: true,
default: false,
help: 'adds entries the current directory\'s .gitignore file',
},
runInBand: {
display_index: 12,
flag: true,
default: false,
full: 'run-in-band',
help: 'run serially in the current process'
},
silent: {
display_index: 13,
abbr: 's',
flag: true,
default: false,
help: 'do not write to stdout or stderr'
},
parser: {
display_index: 9,
choices: ['babel', 'babylon', 'flow', 'ts', 'tsx'],
default: 'babel',
help: 'the parser to use for parsing the source files'
},
parserConfig: {
display_index: 10,
full: 'parser-config',
help: 'path to a JSON file containing a custom parser configuration for flow or babylon',
metavar: 'FILE',
process: file => JSON.parse(fs.readFileSync(file)),
},
failOnError: {
display_index: 4,
flag: true,
help: 'Return a non-zero code when there are errors',
full: 'fail-on-error',
default: false,
},
version: {
display_index: 17,
help: 'print version and exit',
callback: function() {
const requirePackage = require('../utils/requirePackage');
return [
`jscodeshift: ${pkg.version}`,
` - babel: ${require('@babel/core').version}`,
` - babylon: ${requirePackage('@babel/parser').version}`,
` - flow: ${requirePackage('flow-parser').version}`,
` - recast: ${requirePackage('recast').version}\n`,
].join('\n');
},
},
stdin: {
display_index: 14,
help: 'read file/directory list from stdin',
flag: true,
default: false,
},
});
let options, positionalArguments;
try {
({options, positionalArguments} = parser.parse());
if (positionalArguments.length === 0 && !options.stdin) {
process.stderr.write(
'Error: You have to provide at least one file/directory to transform.' +
'\n\n---\n\n' +
parser.getHelpText()
);
process.exit(1);
}
} catch(e) {
const exitCode = e.exitCode === undefined ? 1 : e.exitCode;
(exitCode ? process.stderr : process.stdout).write(e.message);
process.exit(exitCode);
}
function run(paths, options) {
Runner.run(
/^https?/.test(options.transform) ? options.transform : path.resolve(options.transform),
paths,
options
);
}
if (options.stdin) {
let buffer = '';
process.stdin.on('data', data => buffer += data);
process.stdin.on('end', () => run(buffer.split('\n'), options));
} else {
run(positionalArguments, options);
}
================================================
FILE: bin/jscodeshift.sh
================================================
#!/usr/bin/env node
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
require('./jscodeshift.js');
================================================
FILE: index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = require('./src/core');
================================================
FILE: package.json
================================================
{
"name": "jscodeshift",
"version": "17.3.0",
"description": "A toolkit for JavaScript codemods",
"repository": {
"type": "git",
"url": "https://github.com/facebook/jscodeshift.git"
},
"bugs": "https://github.com/facebook/jscodeshift/issues",
"main": "index.js",
"scripts": {
"clean": "rm -rf dist/",
"prepare": "yarn clean && cp -R src/ dist/",
"test": "jest --bail",
"release": "changeset publish"
},
"bin": {
"jscodeshift": "./bin/jscodeshift.js"
},
"keywords": [
"codemod",
"recast",
"babel"
],
"author": "Felix Kling",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.24.7",
"@babel/parser": "^7.24.7",
"@babel/plugin-transform-class-properties": "^7.24.7",
"@babel/plugin-transform-modules-commonjs": "^7.24.7",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
"@babel/plugin-transform-optional-chaining": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
"@babel/preset-flow": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@babel/register": "^7.24.6",
"flow-parser": "0.*",
"graceful-fs": "^4.2.4",
"micromatch": "^4.0.7",
"neo-async": "^2.5.0",
"picocolors": "^1.0.1",
"recast": "^0.23.11",
"tmp": "^0.2.3",
"write-file-atomic": "^5.0.1"
},
"peerDependencies": {
"@babel/preset-env": "^7.1.6"
},
"peerDependenciesMeta": {
"@babel/preset-env": {
"optional": true
}
},
"devDependencies": {
"@babel/eslint-parser": "^7.24.7",
"@changesets/cli": "^2.27.8",
"eslint": "8.56.0",
"jest": "^29.7.0",
"jsdoc": "^4.0.3"
},
"jest": {
"roots": [
"src",
"bin",
"parser",
"sample"
]
},
"engines": {
"node": ">=16"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
================================================
FILE: parser/__tests__/.eslintrc
================================================
{
"globals": {
"jest": true
},
"env": {
"jasmine": true
}
}
================================================
FILE: parser/__tests__/__snapshots__/tsx-test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tsxParser parse extends the ts config with jsx support 1`] = `
[
""mock content";",
{
"allowImportExportEverywhere": true,
"allowReturnOutsideFunction": true,
"plugins": [
"jsx",
"asyncGenerators",
"decoratorAutoAccessors",
"bigInt",
"classPrivateMethods",
"classPrivateProperties",
"classProperties",
"decorators-legacy",
"doExpressions",
"dynamicImport",
"exportDefaultFrom",
"exportExtensions",
"exportNamespaceFrom",
"functionBind",
"functionSent",
"importAttributes",
"importMeta",
"nullishCoalescingOperator",
"numericSeparator",
"objectRestSpread",
"optionalCatchBinding",
"optionalChaining",
[
"pipelineOperator",
{
"proposal": "minimal",
},
],
"throwExpressions",
"typescript",
],
"sourceType": "module",
"startLine": 1,
"tokens": true,
},
]
`;
================================================
FILE: parser/__tests__/tsx-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*global jest, describe, it, expect*/
'use strict';
jest.mock('@babel/parser')
const babylon = require('@babel/parser');
const tsxParser = require('../tsx');
describe('tsxParser', function() {
describe('parse', function() {
it('extends the ts config with jsx support', function() {
const parser = tsxParser();
parser.parse('"mock content";');
expect(babylon.parse).toHaveBeenCalledTimes(1);
expect(babylon.parse.mock.calls[0]).toMatchSnapshot();
});
});
});
================================================
FILE: parser/babel5Compat.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const babylon = require('@babel/parser');
// These are the options that were the default of the Babel5 parse function
// see https://github.com/babel/babel/blob/5.x/packages/babel/src/api/node.js#L81
const options = {
sourceType: 'module',
allowHashBang: true,
ecmaVersion: Infinity,
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
startLine: 1,
tokens: true,
plugins: [
'estree',
'jsx',
'asyncGenerators',
'classProperties',
'doExpressions',
'exportExtensions',
'functionBind',
'functionSent',
'objectRestSpread',
'dynamicImport',
'nullishCoalescingOperator',
'optionalChaining',
['decorators', {decoratorsBeforeExport: false}],
],
};
/**
* Wrapper to set default options. Doesn't accept custom options because in that
* case babylon should be used instead.
*/
module.exports = function() {
return {
parse(code) {
return babylon.parse(code, options);
},
};
};
================================================
FILE: parser/babylon.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const babylon = require('@babel/parser');
const defaultOptions = {
sourceType: 'module',
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
startLine: 1,
tokens: true,
plugins: [
['flow', {all: true}],
'flowComments',
'jsx',
'asyncGenerators',
'bigInt',
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
['decorators', {decoratorsBeforeExport: false}],
'doExpressions',
'dynamicImport',
'exportDefaultFrom',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importMeta',
'logicalAssignment',
'nullishCoalescingOperator',
'numericSeparator',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
['pipelineOperator', {proposal: 'minimal'}],
'throwExpressions',
],
};
/**
* Wrapper to set default options
*/
module.exports = function(options=defaultOptions) {
return {
parse(code) {
return babylon.parse(code, options);
},
};
};
================================================
FILE: parser/flow.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const flowParser = require('flow-parser');
const defaultOptions = {
enums: true,
esproposal_class_instance_fields: true,
esproposal_class_static_fields: true,
esproposal_decorators: true,
esproposal_export_star_as: true,
esproposal_optional_chaining: true,
esproposal_nullish_coalescing: true,
tokens: true,
types: true,
};
/**
* Wrapper to set default options
*/
module.exports = function(options=defaultOptions) {
return {
parse(code) {
return flowParser.parse(code, options);
},
};
};
================================================
FILE: parser/ts.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const babylon = require('@babel/parser');
const options = require('./tsOptions');
/**
* Doesn't accept custom options because babylon should be used directly in
* that case.
*/
module.exports = function() {
return {
parse(code) {
return babylon.parse(code, options);
},
};
};
================================================
FILE: parser/tsOptions.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
/**
* Options shared by the TypeScript and TSX parsers.
*/
module.exports = {
sourceType: 'module',
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
startLine: 1,
tokens: true,
plugins: [
'asyncGenerators',
'decoratorAutoAccessors',
'bigInt',
'classPrivateMethods',
'classPrivateProperties',
'classProperties',
'decorators-legacy',
'doExpressions',
'dynamicImport',
'exportDefaultFrom',
'exportExtensions',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importAttributes',
'importMeta',
'nullishCoalescingOperator',
'numericSeparator',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
['pipelineOperator', { proposal: 'minimal' }],
'throwExpressions',
'typescript'
],
};
================================================
FILE: parser/tsx.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const babylon = require('@babel/parser');
const baseOptions = require('./tsOptions');
const options = Object.assign({}, baseOptions);
options.plugins = ['jsx'].concat(baseOptions.plugins);
/**
* Doesn't accept custom options because babylon should be used directly in
* that case.
*/
module.exports = function() {
return {
parse(code) {
return babylon.parse(code, options);
},
};
};
================================================
FILE: recipes/retain-first-comment.md
================================================
# Retain comment on first line
## Problem
When removing or replacing the first statement in a file, it is possible for [leading comments at the top of the file to be removed](https://github.com/facebook/jscodeshift/issues/44).
## Solution
To retain the leading comments during a transformation, the `comments` array on the statement's `node` _must_ be copied to the next statement's `node` that will be at the top of the file.
## Examples
#### Bad
##### Transform
```javascript
export default function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source)
.find(j.VariableDeclaration)
.replaceWith(
j.expressionStatement(j.callExpression(
j.identifier('foo'), []
)
)
)
.toSource();
};
```
##### In
```javascript
// Comment on first line
const firstStatement = require('some-module');
```
##### Out
```javascript
foo();
```
#### Good
##### Transform
```javascript
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
const getFirstNode = () => root.find(j.Program).get('body', 0).node;
// Save the comments attached to the first node
const firstNode = getFirstNode();
const { comments } = firstNode;
root.find(j.VariableDeclaration).replaceWith(
j.expressionStatement(j.callExpression(
j.identifier('foo'),
[]
))
);
// If the first node has been modified or deleted, reattach the comments
const firstNode2 = getFirstNode();
if (firstNode2 !== firstNode) {
firstNode2.comments = comments;
}
return root.toSource();
};
```
##### In
```javascript
// Comment on first line
const firstStatement = require('some-module');
```
##### Out
```javascript
// Comment on first line
foo();
```
================================================
FILE: sample/__testfixtures__/reverse-identifiers.input.js
================================================
var firstWord = 'Hello ';
var secondWord = 'world';
var message = firstWord + secondWord;
class Foo {
@decorated
*bar() { }
}
================================================
FILE: sample/__testfixtures__/reverse-identifiers.output.js
================================================
var droWtsrif = 'Hello ';
var droWdnoces = 'world';
var egassem = droWtsrif + droWdnoces;
class ooF {
@detaroced
*rab() { }
}
================================================
FILE: sample/__testfixtures__/typescript/reverse-identifiers.input.ts
================================================
import type { Stream } from 'stream';
const firstWord = 'Hello ';
const secondWord = 'world';
const message = firstWord + secondWord;
const getMessage = (): string => message
================================================
FILE: sample/__testfixtures__/typescript/reverse-identifiers.output.ts
================================================
import type { maertS } from 'stream';
const droWtsrif = 'Hello ';
const droWdnoces = 'world';
const egassem = droWtsrif + droWdnoces;
const egasseMteg = (): string => egassem
================================================
FILE: sample/__tests__/__snapshots__/reverse-identifiers-test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transforms correctly 1`] = `
"var droWtsrif = 'Hello ';
var droWdnoces = 'world';
var egassem = droWtsrif + droWdnoces;
class ooF {
@detaroced
*rab() { }
}"
`;
================================================
FILE: sample/__tests__/reverse-identifiers-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* An example of writing a unit test for a jscodeshift script using the
* `defineTest` helper bundled with jscodeshift. This will run the
* reverse-identifiers.js transform with the input specified in the
* reverse-identifiers-input file, and expect the output to be the same as that
* in reverse-identifiers-output.
*/
'use strict';
jest.autoMockOff();
const defineTest = require('../../src/testUtils').defineTest;
const defineInlineTest = require('../../src/testUtils').defineInlineTest;
const defineSnapshotTestFromFixture = require('../../src/testUtils').defineSnapshotTestFromFixture;
const transform = require('../reverse-identifiers');
defineTest(__dirname, 'reverse-identifiers');
defineTest(__dirname, 'reverse-identifiers', null, 'typescript/reverse-identifiers', { parser: 'ts' });
describe('reverse-identifiers', () => {
defineInlineTest(transform, {}, `
var firstWord = 'Hello ';
var secondWord = 'world';
var message = firstWord + secondWord;`,`
var droWtsrif = 'Hello ';
var droWdnoces = 'world';
var egassem = droWtsrif + droWdnoces;
`);
defineInlineTest(transform, {},
'function aFunction() {};',
'function noitcnuFa() {};',
'Reverses function names'
);
});
// the snapshot output of this file should be the same as reverse-identifiers.output.js
defineSnapshotTestFromFixture(__dirname, transform, {}, 'reverse-identifiers');
================================================
FILE: sample/reverse-identifiers.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Example jscodeshift transformer. Simply reverses the names of all
* identifiers.
*/
function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source)
.find(j.Identifier)
.replaceWith(
p => j.identifier(p.node.name.split('').reverse().join(''))
)
.toSource();
}
module.exports = transformer;
================================================
FILE: src/Collection.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const assert = require('assert');
const intersection = require('./utils/intersection');
const recast = require('recast');
const union = require('./utils/union');
const astTypes = recast.types;
var types = astTypes.namedTypes;
const NodePath = astTypes.NodePath;
const Node = types.Node;
/**
* This represents a generic collection of node paths. It only has a generic
* API to access and process the elements of the list. It doesn't know anything
* about AST types.
*
* @mixes traversalMethods
* @mixes mutationMethods
* @mixes transformMethods
* @mixes globalMethods
*/
class Collection {
/**
* @param {Array} paths An array of AST paths
* @param {Collection} parent A parent collection
* @param {Array} types An array of types all the paths in the collection
* have in common. If not passed, it will be inferred from the paths.
* @return {Collection}
*/
constructor(paths, parent, types) {
assert.ok(Array.isArray(paths), 'Collection is passed an array');
assert.ok(
paths.every(p => p instanceof NodePath),
'Array contains only paths'
);
this._parent = parent;
this.__paths = paths;
if (types && !Array.isArray(types)) {
types = _toTypeArray(types);
} else if (!types || Array.isArray(types) && types.length === 0) {
types = _inferTypes(paths);
}
this._types = types.length === 0 ? _defaultType : types;
}
/**
* Returns a new collection containing the nodes for which the callback
* returns true.
*
* @param {function} callback
* @return {Collection}
*/
filter(callback) {
return new this.constructor(this.__paths.filter(callback), this);
}
/**
* Executes callback for each node/path in the collection.
*
* @param {function} callback
* @return {Collection} The collection itself
*/
forEach(callback) {
this.__paths.forEach(
(path, i, paths) => callback.call(path, path, i, paths)
);
return this;
}
/**
* Tests whether at-least one path passes the test implemented by the provided callback.
*
* @param {function} callback
* @return {boolean}
*/
some(callback) {
return this.__paths.some(
(path, i, paths) => callback.call(path, path, i, paths)
);
}
/**
* Tests whether all paths pass the test implemented by the provided callback.
*
* @param {function} callback
* @return {boolean}
*/
every(callback) {
return this.__paths.every(
(path, i, paths) => callback.call(path, path, i, paths)
);
}
/**
* Executes the callback for every path in the collection and returns a new
* collection from the return values (which must be paths).
*
* The callback can return null to indicate to exclude the element from the
* new collection.
*
* If an array is returned, the array will be flattened into the result
* collection.
*
* @param {function} callback
* @param {Type} type Force the new collection to be of a specific type
*/
map(callback, type) {
const paths = [];
this.forEach(function(path) {
/*jshint eqnull:true*/
let result = callback.apply(path, arguments);
if (result == null) return;
if (!Array.isArray(result)) {
result = [result];
}
for (let i = 0; i < result.length; i++) {
if (paths.indexOf(result[i]) === -1) {
paths.push(result[i]);
}
}
});
return fromPaths(paths, this, type);
}
/**
* Returns the number of elements in this collection.
*
* @return {number}
*/
size() {
return this.__paths.length;
}
/**
* Returns the number of elements in this collection.
*
* @return {number}
*/
get length() {
return this.__paths.length;
}
/**
* Returns an array of AST nodes in this collection.
*
* @return {Array}
*/
nodes() {
return this.__paths.map(p => p.value);
}
paths() {
return this.__paths;
}
getAST() {
if (this._parent) {
return this._parent.getAST();
}
return this.__paths;
}
toSource(options) {
if (this._parent) {
return this._parent.toSource(options);
}
if (this.__paths.length === 1) {
return recast.print(this.__paths[0], options).code;
} else {
return this.__paths.map(p => recast.print(p, options).code);
}
}
/**
* Returns a new collection containing only the element at position index.
*
* In case of a negative index, the element is taken from the end:
*
* .at(0) - first element
* .at(-1) - last element
*
* @param {number} index
* @return {Collection}
*/
at(index) {
return fromPaths(
this.__paths.slice(
index,
index === -1 ? undefined : index + 1
),
this
);
}
/**
* Proxies to NodePath#get of the first path.
*
* @param {string|number} ...fields
*/
get() {
const path = this.__paths[0];
if (!path) {
throw Error(
'You cannot call "get" on a collection with no paths. ' +
'Instead, check the "length" property first to verify at least 1 path exists.'
);
}
return path.get.apply(path, arguments);
}
/**
* Returns the type(s) of the collection. This is only used for unit tests,
* I don't think other consumers would need it.
*
* @return {Array<string>}
*/
getTypes() {
return this._types;
}
/**
* Returns true if this collection has the type 'type'.
*
* @param {Type} type
* @return {boolean}
*/
isOfType(type) {
return !!type && this._types.indexOf(type.toString()) > -1;
}
}
/**
* Given a set of paths, this infers the common types of all paths.
* @private
* @param {Array} paths An array of paths.
* @return {Type} type An AST type
*/
function _inferTypes(paths) {
let _types = [];
if (paths.length > 0 && Node.check(paths[0].node)) {
const nodeType = types[paths[0].node.type];
const sameType = paths.length === 1 ||
paths.every(path => nodeType.check(path.node));
if (sameType) {
_types = [nodeType.toString()].concat(
astTypes.getSupertypeNames(nodeType.toString())
);
} else {
// try to find a common type
_types = intersection(
paths.map(path => astTypes.getSupertypeNames(path.node.type))
);
}
}
return _types;
}
function _toTypeArray(value) {
value = !Array.isArray(value) ? [value] : value;
value = value.map(v => v.toString());
if (value.length > 1) {
return union(
[value].concat(intersection(value.map(_getSupertypeNames)))
);
} else {
return value.concat(_getSupertypeNames(value[0]));
}
}
function _getSupertypeNames(type) {
try {
return astTypes.getSupertypeNames(type);
} catch(error) {
if (error.message === '') {
// Likely the case that the passed type wasn't found in the definition
// list. Maybe a typo. ast-types doesn't throw a useful error in that
// case :(
throw new Error(
'"' + type + '" is not a known AST node type. Maybe a typo?'
);
}
throw error;
}
}
/**
* Creates a new collection from an array of node paths.
*
* If type is passed, it will create a typed collection if such a collection
* exists. The nodes or path values must be of the same type.
*
* Otherwise it will try to infer the type from the path list. If every
* element has the same type, a typed collection is created (if it exists),
* otherwise, a generic collection will be created.
*
* @ignore
* @param {Array} paths An array of paths
* @param {Collection} parent A parent collection
* @param {Type} type An AST type
* @return {Collection}
*/
function fromPaths(paths, parent, type) {
assert.ok(
paths.every(n => n instanceof NodePath),
'Every element in the array should be a NodePath'
);
return new Collection(paths, parent, type);
}
/**
* Creates a new collection from an array of nodes. This is a convenience
* method which converts the nodes to node paths first and calls
*
* Collections.fromPaths(paths, parent, type)
*
* @ignore
* @param {Array} nodes An array of AST nodes
* @param {Collection} parent A parent collection
* @param {Type} type An AST type
* @return {Collection}
*/
function fromNodes(nodes, parent, type) {
assert.ok(
nodes.every(n => Node.check(n)),
'Every element in the array should be a Node'
);
return fromPaths(
nodes.map(n => new NodePath(n)),
parent,
type
);
}
const CPt = Collection.prototype;
/**
* This function adds the provided methods to the prototype of the corresponding
* typed collection. If no type is passed, the methods are added to
* Collection.prototype and are available for all collections.
*
* @param {Object} methods Methods to add to the prototype
* @param {Type=} type Optional type to add the methods to
*/
function registerMethods(methods, type) {
for (const methodName in methods) {
if (!methods.hasOwnProperty(methodName)) {
return;
}
if (hasConflictingRegistration(methodName, type)) {
let msg = `There is a conflicting registration for method with name "${methodName}".\nYou tried to register an additional method with `;
if (type) {
msg += `type "${type.toString()}".`
} else {
msg += 'universal type.'
}
msg += '\nThere are existing registrations for that method with ';
const conflictingRegistrations = CPt[methodName].typedRegistrations;
if (conflictingRegistrations) {
msg += `type ${Object.keys(conflictingRegistrations).join(', ')}.`;
} else {
msg += 'universal type.';
}
throw Error(msg);
}
if (!type) {
CPt[methodName] = methods[methodName];
} else {
type = type.toString();
if (!CPt.hasOwnProperty(methodName)) {
installTypedMethod(methodName);
}
var registrations = CPt[methodName].typedRegistrations;
registrations[type] = methods[methodName];
astTypes.getSupertypeNames(type).forEach(function (name) {
registrations[name] = false;
});
}
}
}
function installTypedMethod(methodName) {
if (CPt.hasOwnProperty(methodName)) {
throw new Error(`Internal Error: "${methodName}" method is already installed`);
}
const registrations = {};
function typedMethod() {
const types = Object.keys(registrations);
for (let i = 0; i < types.length; i++) {
const currentType = types[i];
if (registrations[currentType] && this.isOfType(currentType)) {
return registrations[currentType].apply(this, arguments);
}
}
throw Error(
`You have a collection of type [${this.getTypes()}]. ` +
`"${methodName}" is only defined for one of [${types.join('|')}].`
);
}
typedMethod.typedRegistrations = registrations;
CPt[methodName] = typedMethod;
}
function hasConflictingRegistration(methodName, type) {
if (!type) {
return CPt.hasOwnProperty(methodName);
}
if (!CPt.hasOwnProperty(methodName)) {
return false;
}
const registrations = CPt[methodName] && CPt[methodName].typedRegistrations;
if (!registrations) {
return true;
}
type = type.toString();
if (registrations.hasOwnProperty(type)) {
return true;
}
return astTypes.getSupertypeNames(type.toString()).some(function (name) {
return !!registrations[name];
});
}
var _defaultType = [];
/**
* Sets the default collection type. In case a collection is created form an
* empty set of paths and no type is specified, we return a collection of this
* type.
*
* @ignore
* @param {Type} type
*/
function setDefaultCollectionType(type) {
_defaultType = _toTypeArray(type);
}
exports.fromPaths = fromPaths;
exports.fromNodes = fromNodes;
exports.registerMethods = registerMethods;
exports.hasConflictingRegistration = hasConflictingRegistration;
exports.setDefaultCollectionType = setDefaultCollectionType;
================================================
FILE: src/Runner.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const child_process = require('child_process');
const pc = require('picocolors');
const fs = require('graceful-fs');
const path = require('path');
const http = require('http');
const https = require('https');
const ignores = require('./ignoreFiles');
const tmp = require('tmp');
tmp.setGracefulCleanup();
const availableCpus = Math.max(require('os').cpus().length - 1, 1);
const CHUNK_SIZE = 50;
function lineBreak(str) {
return /\n$/.test(str) ? str : str + '\n';
}
const bufferedWrite = (function() {
const buffer = [];
let buffering = false;
process.stdout.on('drain', () => {
if (!buffering) return;
while (buffer.length > 0 && process.stdout.write(buffer.shift()) !== false);
if (buffer.length === 0) {
buffering = false;
}
});
return function write(msg) {
if (buffering) {
buffer.push(msg);
}
if (process.stdout.write(msg) === false) {
buffering = true;
}
};
}());
const log = {
ok(msg, verbose) {
verbose >= 2 && bufferedWrite(pc.bgGreen(pc.white(' OKK ')) + msg);
},
nochange(msg, verbose) {
verbose >= 1 && bufferedWrite(pc.bgYellow(pc.white(' NOC ')) + msg);
},
skip(msg, verbose) {
verbose >= 1 && bufferedWrite(pc.bgYellow(pc.white(' SKIP ')) + msg);
},
error(msg, verbose) {
verbose >= 0 && bufferedWrite(pc.bgRed(pc.white(' ERR ')) + msg);
},
};
function report({file, msg}) {
bufferedWrite(lineBreak(`${pc.bgBlue(pc.white(' REP '))}${file} ${msg}`));
}
function concatAll(arrays) {
const result = [];
for (const array of arrays) {
for (const element of array) {
result.push(element);
}
}
return result;
}
function showFileStats(fileStats) {
process.stdout.write(
'Results: \n'+
pc.red(fileStats.error + ' errors\n')+
pc.yellow(fileStats.nochange + ' unmodified\n')+
pc.yellow(fileStats.skip + ' skipped\n')+
pc.green(fileStats.ok + ' ok\n')
);
}
function showStats(stats) {
const names = Object.keys(stats).sort();
if (names.length) {
process.stdout.write(pc.blue('Stats: \n'));
}
names.forEach(name => process.stdout.write(name + ': ' + stats[name] + '\n'));
}
function dirFiles (dir, callback, acc) {
// acc stores files found so far and counts remaining paths to be processed
acc = acc || { files: [], remaining: 1 };
function done() {
// decrement count and return if there are no more paths left to process
if (!--acc.remaining) {
callback(acc.files);
}
}
fs.readdir(dir, (err, files) => {
// if dir does not exist or is not a directory, bail
// (this should not happen as long as calls do the necessary checks)
if (err) throw err;
acc.remaining += files.length;
files.forEach(file => {
let name = path.join(dir, file);
fs.stat(name, (err, stats) => {
if (err) {
// probably a symlink issue
process.stdout.write(
'Skipping path "' + name + '" which does not exist.\n'
);
done();
} else if (ignores.shouldIgnore(name)) {
// ignore the path
done();
} else if (stats.isDirectory()) {
dirFiles(name + '/', callback, acc);
} else {
acc.files.push(name);
done();
}
});
});
done();
});
}
function getAllFiles(paths, filter) {
return Promise.all(
paths.map(file => new Promise(resolve => {
fs.lstat(file, (err, stat) => {
if (err) {
process.stderr.write('Skipping path ' + file + ' which does not exist. \n');
resolve([]);
return;
}
if (stat.isDirectory()) {
dirFiles(
file,
list => resolve(list.filter(filter))
);
} else if (!filter(file) || ignores.shouldIgnore(file)) {
// ignoring the file
resolve([]);
} else {
resolve([file]);
}
})
}))
).then(concatAll);
}
function run(transformFile, paths, options) {
const cpus = options.cpus ? Math.min(availableCpus, options.cpus) : availableCpus;
const extensions =
options.extensions && options.extensions.split(',').map(ext => '.' + ext);
const fileCounters = {error: 0, ok: 0, nochange: 0, skip: 0};
const statsCounter = {};
const startTime = process.hrtime();
ignores.add(options.ignoreSet);
ignores.add(options.ignorePattern);
ignores.addFromFile(options.ignoreConfig);
if (options.gitignore) {
let currDirectory = process.cwd();
let gitIgnorePath = path.join(currDirectory, '.gitignore');
ignores.addFromFile(gitIgnorePath);
}
if (/^http/.test(transformFile)) {
return new Promise((resolve, reject) => {
// call the correct `http` or `https` implementation
(transformFile.indexOf('https') !== 0 ? http : https).get(transformFile, (res) => {
let contents = '';
res
.on('data', (d) => {
contents += d.toString();
})
.on('end', () => {
const ext = path.extname(transformFile);
tmp.file({ prefix: 'jscodeshift', postfix: ext }, (err, path, fd) => {
if (err) return reject(err);
fs.write(fd, contents, function (err) {
if (err) return reject(err);
fs.close(fd, function(err) {
if (err) return reject(err);
transform(path).then(resolve, reject);
});
});
});
})
})
.on('error', (e) => {
reject(e);
});
});
} else if (!fs.existsSync(transformFile)) {
process.stderr.write(
pc.bgRed(pc.white('ERROR')) + ' Transform file ' + transformFile + ' does not exist \n'
);
return;
} else {
return transform(transformFile);
}
function transform(transformFile) {
return getAllFiles(
paths,
name => !extensions || extensions.indexOf(path.extname(name)) != -1
).then(files => {
const numFiles = files.length;
if (numFiles === 0) {
process.stdout.write('No files selected, nothing to do. \n');
return [];
}
const processes = options.runInBand ? 1 : Math.min(numFiles, cpus);
const chunkSize = processes > 1 ?
Math.min(Math.ceil(numFiles / processes), CHUNK_SIZE) :
numFiles;
let index = 0;
// return the next chunk of work for a free worker
function next() {
if (!options.silent && !options.runInBand && index < numFiles) {
process.stdout.write(
'Sending ' +
Math.min(chunkSize, numFiles-index) +
' files to free worker...\n'
);
}
return files.slice(index, index += chunkSize);
}
if (!options.silent) {
process.stdout.write('Processing ' + files.length + ' files... \n');
if (!options.runInBand) {
process.stdout.write(
'Spawning ' + processes +' workers...\n'
);
}
if (options.dry) {
process.stdout.write(
pc.green('Running in dry mode, no files will be written! \n')
);
}
}
const args = [transformFile, options.babel ? 'babel' : 'no-babel'];
const workers = [];
for (let i = 0; i < processes; i++) {
workers.push(options.runInBand ?
require('./Worker')(args) :
child_process.fork(require.resolve('./Worker'), args)
);
}
return workers.map(child => {
child.send({files: next(), options});
child.on('message', message => {
switch (message.action) {
case 'status':
fileCounters[message.status] += 1;
log[message.status](lineBreak(message.msg), options.verbose);
break;
case 'update':
if (!statsCounter[message.name]) {
statsCounter[message.name] = 0;
}
statsCounter[message.name] += message.quantity;
break;
case 'free':
child.send({files: next(), options});
break;
case 'report':
report(message);
break;
}
});
return new Promise(resolve => child.on('disconnect', resolve));
});
})
.then(pendingWorkers =>
Promise.all(pendingWorkers).then(() => {
const endTime = process.hrtime(startTime);
const timeElapsed = (endTime[0] + endTime[1]/1e9).toFixed(3);
if (!options.silent) {
process.stdout.write('All done. \n');
showFileStats(fileCounters);
showStats(statsCounter);
process.stdout.write(
'Time elapsed: ' + timeElapsed + 'seconds \n'
);
if (options.failOnError && fileCounters.error > 0) {
process.exit(1);
}
}
return Object.assign({
stats: statsCounter,
timeElapsed: timeElapsed
}, fileCounters);
})
);
}
}
exports.run = run;
================================================
FILE: src/Worker.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const EventEmitter = require('events').EventEmitter;
const async = require('neo-async');
const fs = require('graceful-fs');
const writeFileAtomic = require('write-file-atomic');
const { DEFAULT_EXTENSIONS } = require('@babel/core');
const getParser = require('./getParser');
const jscodeshift = require('./core');
let presetEnv;
try {
presetEnv = require('@babel/preset-env');
} catch (_) {}
let emitter;
let finish;
let notify;
let transform;
let parserFromTransform;
if (module.parent) {
emitter = new EventEmitter();
emitter.send = (data) => { run(data); };
finish = () => { emitter.emit('disconnect'); };
notify = (data) => { emitter.emit('message', data); };
module.exports = (args) => {
setup(args[0], args[1]);
return emitter;
};
} else {
finish = () => setImmediate(() => process.disconnect());
notify = (data) => { process.send(data); };
process.on('message', (data) => { run(data); });
setup(process.argv[2], process.argv[3]);
}
function prepareJscodeshift(options) {
const parser = parserFromTransform ||
getParser(options.parser, options.parserConfig);
return jscodeshift.withParser(parser);
}
function setup(tr, babel) {
if (babel === 'babel') {
const presets = [];
if (presetEnv) {
presets.push([
presetEnv.default,
{targets: {node: true}},
]);
}
presets.push(
/\.tsx?$/.test(tr) ?
require('@babel/preset-typescript').default :
require('@babel/preset-flow').default
);
require('@babel/register')({
configFile: false,
babelrc: false,
presets,
plugins: [
require('@babel/plugin-transform-class-properties').default,
require('@babel/plugin-transform-nullish-coalescing-operator').default,
require('@babel/plugin-transform-optional-chaining').default,
require('@babel/plugin-transform-modules-commonjs').default,
require('@babel/plugin-transform-private-methods').default,
],
extensions: [...DEFAULT_EXTENSIONS, '.ts', '.tsx'],
// By default, babel register only compiles things inside the current working directory.
// https://github.com/babel/babel/blob/2a4f16236656178e84b05b8915aab9261c55782c/packages/babel-register/src/node.js#L140-L157
ignore: [
// Ignore parser related files
/@babel\/parser/,
/\/flow-parser\//,
/\/recast\//,
/\/ast-types\//,
],
});
}
const module = require(tr);
transform = typeof module.default === 'function' ?
module.default :
module;
if (module.parser) {
parserFromTransform = typeof module.parser === 'string' ?
getParser(module.parser) :
module.parser;
}
}
function free() {
notify({action: 'free'});
}
function updateStatus(status, file, msg) {
msg = msg ? file + ' ' + msg : file;
notify({action: 'status', status: status, msg: msg});
}
function report(file, msg) {
notify({action: 'report', file, msg});
}
function empty() {}
function stats(name, quantity) {
quantity = typeof quantity !== 'number' ? 1 : quantity;
notify({action: 'update', name: name, quantity: quantity});
}
function trimStackTrace(trace) {
if (!trace) {
return '';
}
// Remove this file from the stack trace of an error thrown in the transformer
const lines = trace.split('\n');
const result = [];
lines.every(function(line) {
if (line.indexOf(__filename) === -1) {
result.push(line);
return true;
}
});
return result.join('\n');
}
function run(data) {
const files = data.files;
const options = data.options || {};
if (!files.length) {
finish();
return;
}
async.each(
files,
function(file, callback) {
fs.readFile(file, async function(err, source) {
if (err) {
updateStatus('error', file, 'File error: ' + err);
callback();
return;
}
source = source.toString();
try {
const jscodeshift = prepareJscodeshift(options);
const out = await transform(
{
path: file,
source: source,
},
{
j: jscodeshift,
jscodeshift: jscodeshift,
stats: options.dry ? stats : empty,
report: msg => report(file, msg),
},
options
);
if (!out || out === source) {
updateStatus(out ? 'nochange' : 'skip', file);
callback();
return;
}
if (options.print) {
console.log(out); // eslint-disable-line no-console
}
if (!options.dry) {
writeFileAtomic(file, out, function(err) {
if (err) {
updateStatus('error', file, 'File writer error: ' + err);
} else {
updateStatus('ok', file);
}
callback();
});
} else {
updateStatus('ok', file);
callback();
}
} catch(err) {
updateStatus(
'error',
file,
'Transformation error ('+ err.message.replace(/\n/g, ' ') + ')\n' + trimStackTrace(err.stack)
);
callback();
}
});
},
function(err) {
if (err) {
updateStatus('error', '', 'This should never be shown!');
}
free();
}
);
}
================================================
FILE: src/__testfixtures__/test-async-transform.input.js
================================================
export const sum = (a, b) => a + b;
================================================
FILE: src/__testfixtures__/test-async-transform.js
================================================
const synchronousTestTransform = (fileInfo, api, options) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(api.jscodeshift(fileInfo.source)
.findVariableDeclarators('sum')
.renameTo('addition')
.toSource());
}, 100);
});
}
module.exports = synchronousTestTransform;
================================================
FILE: src/__testfixtures__/test-async-transform.output.js
================================================
export const addition = (a, b) => a + b;
================================================
FILE: src/__testfixtures__/test-sync-transform.input.js
================================================
export const sum = (a, b) => a + b;
================================================
FILE: src/__testfixtures__/test-sync-transform.js
================================================
const synchronousTestTransform = (fileInfo, api, options) => {
return api.jscodeshift(fileInfo.source)
.findVariableDeclarators('sum')
.renameTo('addition')
.toSource();
}
module.exports = synchronousTestTransform;
================================================
FILE: src/__testfixtures__/test-sync-transform.output.js
================================================
export const addition = (a, b) => a + b;
================================================
FILE: src/__tests__/.eslintrc
================================================
{
"globals": {
"jest": true
},
"env": {
"jasmine": true
}
}
================================================
FILE: src/__tests__/Collection-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
describe('Collection API', function() {
let nodes;
let Collection;
let recast;
let NodePath;
let types;
let b;
beforeEach(function() {
jest.resetModules();
Collection = require('../Collection');
recast = require('recast');
NodePath = recast.types.NodePath;
types = recast.types.namedTypes;
b = recast.types.builders;
nodes = [b.identifier('foo'), b.identifier('bar')];
});
describe('Instantiation', function() {
it('should create a collection from an array of nodes', function() {
expect(Collection.fromNodes(nodes).getTypes()).toContain('Identifier');
});
it('should create a collection from an array of paths', function() {
const paths = [
new NodePath(b.identifier('foo')),
new NodePath(b.identifier('bar')),
];
expect(Collection.fromPaths(paths).getTypes()).toContain('Identifier');
});
it('accepts an empty array as input', function() {
const values = [];
expect(() => Collection.fromPaths(values)).not.toThrow();
expect(() => Collection.fromNodes(values)).not.toThrow();
});
it('throws if it is passed an array of mixed values', function() {
const values = [
new NodePath(b.identifier('foo')),
b.identifier('bar'),
];
expect(() => Collection.fromPaths(values)).toThrow();
expect(() => Collection.fromNodes(values)).toThrow();
});
it('returns a collection of the closest common type', function() {
let nodes = [
b.identifier('foo'),
b.sequenceExpression([]),
];
expect(Collection.fromNodes(nodes).getTypes())
.toContain('Expression');
nodes = [
b.identifier('foo'),
b.blockStatement([]),
];
expect(Collection.fromNodes(nodes).getTypes())
.toContain('Node');
});
});
describe('Method extensions', function() {
it('handles method extensions for types', function() {
const Collection = require('../Collection');
const getNames = jest.fn(function() {
expect(this.nodes()).toEqual(nodes);
});
Collection.registerMethods({getNames: getNames}, types.Identifier);
const collection = Collection.fromNodes(nodes);
expect(collection.getNames).toBeDefined();
collection.getNames();
expect(getNames).toBeCalled();
});
it('throws if a method is called for the wrong node type', function() {
const Collection = require('../Collection');
const getNames = jest.fn();
Collection.registerMethods({getNames: getNames}, types.Identifier);
const collection = Collection.fromNodes([
b.blockStatement([])
]);
expect(() => collection.getNames()).toThrow();
});
it('adds "global" methods to all types', function() {
const Collection = require('../Collection');
const getNames = jest.fn();
Collection.registerMethods({getNames: getNames});
expect(Collection.fromNodes([b.blockStatement([])]).getNames).toBeDefined();
expect(Collection.fromNodes(nodes).getNames).toBeDefined();
expect(Collection.fromNodes([]).getNames).toBeDefined();
});
it('handles type inheritance chains', function() {
const Collection = require('../Collection');
const nodeMethod = function() {};
const identifierMethod = function() {};
Collection.registerMethods({nodeMethod: nodeMethod}, types.Node);
Collection.registerMethods(
{identifierMethod: identifierMethod},
types.Identifier
);
const collection = Collection.fromNodes([b.identifier('foo')]);
expect(() => collection.identifierMethod()).not.toThrow();
expect(() => collection.nodeMethod()).not.toThrow();
});
it('handles type inheritance with multiple parents', function() {
Collection.registerMethods(
{expressionMethod: function() {}},
types.Expression
);
const collection = Collection.fromNodes([
b.functionExpression(null, [], b.blockStatement([]))
]);
expect(() => collection.expressionMethod()).not.toThrow();
});
it('allows multiple registrations for non-conflicting types', function () {
Collection.registerMethods(
{foo: function () {}},
types.FunctionExpression
);
Collection.registerMethods(
{foo: function () {}},
types.BinaryExpression
);
const collection = Collection.fromNodes([
b.functionExpression(null, [], b.blockStatement([])),
b.functionExpression(null, [], b.blockStatement([])),
b.binaryExpression('+', b.identifier('a'), b.identifier('b'))
]);
function typeFilter(type) {
return function (path) {
return type.check(path.value);
};
}
// allowed if collection contents match one of the registered types.
collection.filter(typeFilter(types.BinaryExpression)).foo();
collection.filter(typeFilter(types.FunctionExpression)).foo();
// not allowed if there is mixed types (even though all types match one function or the other).
expect(function () {
collection.foo();
}).toThrow();
});
describe('hasConflictingRegistration', function () {
function register(methodName, type) {
const methods = {};
methods[methodName] = function () {};
if (!types[type]) {
throw new Error(type + ' is not a valid type');
}
Collection.registerMethods(methods, types[type]);
}
it('true if supertype is registered', function () {
register('supertypeIsRegistered', 'Expression');
expect(Collection.hasConflictingRegistration('supertypeIsRegistered', 'FunctionExpression')).toBe(true);
});
it('true if subtype is registered', function () {
register('subtypeIsRegistered', 'FunctionExpression');
expect(Collection.hasConflictingRegistration('subtypeIsRegistered', 'Expression')).toBe(true);
});
it('false if only a sibling type is registered', function () {
register('siblingIsRegistered', 'FunctionExpression');
expect(Collection.hasConflictingRegistration('siblingIsRegistered', 'BinaryExpression')).toBe(false);
});
});
});
describe('Processing functions', function() {
describe('filter', function() {
it('lets you filter with custom logic', function() {
const filter = jest.fn(function(path) {
return path.value.name === 'foo';
});
const fooVariables = Collection.fromNodes(nodes).filter(filter);
expect(filter.mock.calls.length).toBe(2);
expect(fooVariables.length).toBe(1);
});
});
describe('forEach', function() {
it('lets you iterate over each element of an collection', function() {
const each = jest.fn();
Collection.fromNodes(nodes).forEach(each);
expect(each.mock.calls.length).toBe(2);
expect(each.mock.calls[0][0].value).toBe(nodes[0]);
expect(each.mock.calls[1][0].value).toBe(nodes[1]);
});
it('returns the collection itself', function() {
const fVariables = Collection.fromNodes(nodes);
const result = fVariables.forEach(function(){});
expect(result).toBe(fVariables);
});
});
describe('some', function() {
it('lets you test each element of a collection and stops when one passes the test', function() {
const each = jest.fn().mockImplementation(() => true);
Collection.fromNodes(nodes).some(each);
expect(each.mock.calls.length).toBe(1);
expect(each.mock.calls[0][0].value).toBe(nodes[0]);
});
it('returns true if at least one element passes the test', function() {
const result = Collection.fromNodes(nodes).some((_, i) => i === 1);
expect(result).toBe(true);
});
it('returns false if no elements pass the test', function() {
const result = Collection.fromNodes(nodes).some(() => false);
expect(result).toBe(false);
});
});
describe('every', function() {
it('lets you test each element of a collection and stops when one fails the test', function() {
const each = jest.fn().mockImplementation(() => false);
Collection.fromNodes(nodes).every(each);
expect(each.mock.calls.length).toBe(1);
expect(each.mock.calls[0][0].value).toBe(nodes[0]);
});
it('returns true if all elements pass the test', function() {
const result = Collection.fromNodes(nodes).every(() => true);
expect(result).toBe(true);
});
it('returns false if at least one element does not pass the test', function() {
const result = Collection.fromNodes(nodes).every((_, i) => i === 1);
expect(result).toBe(false);
});
});
describe('map', function() {
it('returns a new collection with mapped values', function() {
const root = Collection.fromNodes(nodes);
const mapped = root.map((_, i) => new NodePath(nodes[+!i]));
expect(root).not.toBe(mapped);
expect(mapped.length).toBe(2);
expect(mapped.nodes()[0]).toBe(nodes[1]);
expect(mapped.nodes()[1]).toBe(nodes[0]);
});
it('dedupes elements', function() {
const path = new NodePath(nodes[0]);
const root = Collection.fromNodes(nodes);
const mapped = root.map(() => path);
expect(root).not.toBe(mapped);
expect(mapped.length).toBe(1);
expect(mapped.paths()[0]).toBe(path);
});
});
describe('at', function() {
it('should work with positive indecies', function() {
const root = Collection.fromNodes(nodes);
expect(root.at(0).nodes()[0]).toEqual(nodes[0]);
expect(root.at(1).nodes()[0]).toEqual(nodes[1]);
});
it('should work with negative indecies', function() {
const root = Collection.fromNodes(nodes);
expect(root.at(-1).nodes()[0]).toEqual(nodes[nodes.length - 1]);
expect(root.at(-2).nodes()[0]).toEqual(nodes[nodes.length - 2]);
});
});
describe('get', function() {
it('should throw descriptive error when no paths are present', function() {
const root = Collection.fromNodes([]);
expect(() => root.get()).toThrowError(/cannot call "get" on a collection with no paths/);
});
});
});
});
================================================
FILE: src/__tests__/Worker-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const testUtils = require('../../utils/testUtils');
const createTransformWith = testUtils.createTransformWith;
const createTempFileWith = testUtils.createTempFileWith;
const getFileContent = testUtils.getFileContent;
describe('Worker API', () => {
it('transforms files', done => {
const worker = require('../Worker');
const transformPath =
createTransformWith('return fileInfo.source + " changed";');
const sourcePath = createTempFileWith('foo');
const emitter = worker([transformPath]);
emitter.send({files: [sourcePath]});
emitter.once('message', (data) => {
expect(data.status).toBe('ok');
expect(data.msg).toBe(sourcePath);
expect(getFileContent(sourcePath)).toBe('foo changed');
done();
});
});
it('passes j as argument', done => {
const worker = require('../Worker');
const transformPath = createTempFileWith(
`module.exports = function (file, api) {
return api.j(file.source).toSource() + ' changed';
}`
);
const sourcePath = createTempFileWith('const x = 10;');
const emitter = worker([transformPath]);
emitter.send({files: [sourcePath]});
emitter.once('message', (data) => {
expect(data.status).toBe('ok');
expect(getFileContent(sourcePath)).toBe(
'const x = 10;' + ' changed'
);
done();
});
});
describe('custom parser', () => {
function getTransformForParser(parser) {
return createTempFileWith(
`function transform(fileInfo, api) {
api.jscodeshift(fileInfo.source);
return "changed";
}
${parser ? `transform.parser = '${parser}';` : ''}
module.exports = transform;
`
);
}
function getSourceFile() {
// This code cannot be parsed by Babel v5
return createTempFileWith(
'const x = (a: Object, b: string): void => {}'
);
}
it('errors if new flow type code is parsed with babel v5', done => {
const worker = require('../Worker');
const transformPath = createTransformWith(
'api.jscodeshift(fileInfo.source); return "changed";'
);
const sourcePath = getSourceFile();
const emitter = worker([transformPath]);
emitter.send({files: [sourcePath]});
emitter.once('message', (data) => {
expect(data.status).toBe('error');
expect(data.msg).toMatch('SyntaxError');
done();
});
});
['flow', 'babylon'].forEach(parser => {
it(`uses ${parser} if configured as such`, done => {
const worker = require('../Worker');
const transformPath = getTransformForParser(parser);
const sourcePath = getSourceFile();
const emitter = worker([transformPath]);
emitter.send({files: [sourcePath]});
emitter.once('message', (data) => {
expect(data.status).toBe('ok');
expect(getFileContent(sourcePath)).toBe('changed');
done();
});
});
});
['babylon', 'flow', 'tsx'].forEach(parser => {
it(`can parse JSX with ${parser}`, done => {
const worker = require('../Worker');
const transformPath = getTransformForParser(parser);
const sourcePath = createTempFileWith(
'var component = <div>{foobar}</div>;'
);
const emitter = worker([transformPath]);
emitter.send({files: [sourcePath]});
emitter.once('message', (data) => {
expect(data.status).toBe('ok');
expect(getFileContent(sourcePath)).toBe('changed');
done();
});
});
});
it('can parse enums with flow', done => {
const worker = require('../Worker');
const transformPath = getTransformForParser('flow');
const sourcePath = createTempFileWith(
'enum E {A, B}'
);
const emitter = worker([transformPath]);
emitter.send({files: [sourcePath]});
emitter.once('message', (data) => {
expect(data.status).toBe('ok');
expect(getFileContent(sourcePath)).toBe('changed');
done();
});
});
});
});
================================================
FILE: src/__tests__/__snapshots__/testUtils-test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`testUtils async should run async defineSnapshotTest 1`] = `"export const addition = (a, b) => a + b;"`;
exports[`testUtils async should run async defineSnapshotTestFromFixture 1`] = `"export const addition = (a, b) => a + b;"`;
exports[`testUtils async should run snapshot test 1`] = `"export const addition = (a, b) => a + b;"`;
exports[`testUtils synchronous should run snapshot test 1`] = `"export const addition = (a, b) => a + b;"`;
exports[`testUtils synchronous should run sync defineSnapshotTest 1`] = `"export const addition = (a, b) => a + b;"`;
exports[`testUtils synchronous should run sync defineSnapshotTestFromFixture 1`] = `"export const addition = (a, b) => a + b;"`;
================================================
FILE: src/__tests__/argsParser-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*global jest, describe, it, expect, beforeEach*/
'use strict';
const argsParser = require('../argsParser');
describe('argsParser', function() {
it('prints the help text', function() {
const parser = argsParser.options({});
let exception;
try {
parser.parse(['--help']);
} catch(e) {
exception = e;
}
expect(exception.exitCode).toEqual(0);
expect(exception.message).toEqual(parser.getHelpText());
});
it('parsers positional arguments', function() {
const parser = argsParser.options({});
const {positionalArguments} = parser.parse(['foo', 'bar']);
expect(positionalArguments).toEqual(['foo', 'bar']);
});
it('parsers mixed options, flags and positional arguments', function() {
const parser = argsParser.options({
foo: {},
bar: {
flag: true,
},
bay: {
flag: true,
},
baz: {
default: 'zab',
},
});
expect(parser.parse(['arg1', '--foo=1', 'arg2', '--bar', '--bay=1', 'arg3', 'arg4']))
.toEqual({
options: {
foo: '1',
bar: true,
bay: true,
baz: 'zab',
},
positionalArguments: ['arg1', 'arg2', 'arg3', 'arg4'],
});
});
describe('options', function() {
function test(testCases) {
for (const testName in testCases) {
const testCase = testCases[testName];
const parser = argsParser.options(testCase.options);
it(testName + ' (space separated values)', function() {
const parse = () => parser.parse(
Array.prototype.concat.apply([], testCase.args)
);
if (typeof testCase.expected === 'string') {
expect(parse).toThrowError(testCase.expected);
} else {
expect(parse()).toEqual(testCase.expected);
}
});
it(testName + ' (= separated values)', function() {
const parse = () => parser.parse(
testCase.args.map(args => args.join('='))
);
if (typeof testCase.expected === 'string') {
expect(parse).toThrowError(testCase.expected);
} else {
expect(parse()).toEqual(testCase.expected);
}
});
}
}
test({
'understands separate arg name and short option names': {
options: {
foo: {
full: 'another-foo',
},
bar: {
abbr: 'b',
},
},
args: [['--another-foo', 'oof'], ['-b', 'rab']],
expected: {
options: {
foo: 'oof',
bar: 'rab',
},
positionalArguments: []
},
},
'understands default values': {
options: {
foo: {},
bar: {
default: 'rab',
},
baz: {
abbr: 'b',
default: 'zab',
},
},
args: [['--foo', 'oof']],
expected: {
options: {
foo: 'oof',
bar: 'rab',
baz: 'zab',
},
positionalArguments: []
},
},
'allows preprocessing values': {
options: {
foo: {},
bar: {
default: 456,
},
bay: {
process: Number,
},
baz: {
abbr: 'b',
process: v => v+v,
},
},
args: [['--foo', '123'], ['--bay', '789'], ['-b', 'zab']],
expected: {
options: {
foo: '123',
bar: 456,
bay: 789,
baz: 'zabzab',
},
positionalArguments: []
},
},
'understands lists': {
options: {
foo: {
list: true,
},
bar: {
list: true,
},
baz: {},
},
args: [
['--foo', 'oof1'],
['--baz', 'zab1'],
['--foo', 'oof2'],
['--baz', 'zab2'],
],
expected: {
options: {
foo: ['oof1', 'oof2'],
bar: [],
baz: 'zab2',
},
positionalArguments: []
},
},
'errors when an option does not have a value (1)': {
options: {
foo: {},
bar: {
abbr: 'b',
},
baz: {},
},
args: [
['--foo', 'oof'],
['-b'],
['--baz', 'zab'],
],
expected: '--bar requires a value',
},
'errors when an option does not have a value (2)': {
options: {
foo: {},
bar: {
abbr: 'b',
},
baz: {},
},
args: [
['--foo', 'oof'],
['--bar'],
['--baz', 'zab'],
],
expected: '--bar requires a value',
},
'errors when an option does not have a value (3)': {
options: {
foo: {
default: 'oof',
},
},
args: [['--foo']],
expected: '--foo requires a value',
},
'understands choices': {
options: {
foo: {
choices: ['oof'],
},
},
args: [
['--foo', 'oof'],
],
expected: {
options: {
foo: 'oof',
},
positionalArguments: []
},
},
'errors if choice does not match': {
options: {
foo: {
choices: ['oof'],
},
bar: {
choices: ['rab1', 'rab2'],
},
},
args: [
['--foo', 'oof'],
['--bar', 'rab'],
],
expected: '--bar must be one of the values rab1,rab2',
},
'accepts unkown options': {
options: {},
args: [
['--foo'],
['--bar'],
['--bay', 'yab'],
['foo'],
['--b', 'zab1'],
['--foo', 'oof'],
['--b', 'zab2'],
['bar'],
],
expected: {
options: {
foo: 'oof',
bar: true,
bay: 'yab',
b: ['zab1', 'zab2'],
},
positionalArguments: ['foo', 'bar'],
},
},
'parses unkown options as JSON': {
options: {},
args: [
['--foo', '{"foo": "bar"}'],
],
expected: {
options: {
foo: {foo: 'bar'},
},
positionalArguments: [],
},
},
});
});
describe('flags', function() {
const parser = argsParser.options({
foo: {
abbr: 'f',
full: 'foo',
flag: true,
},
bar: {
full: 'another-bar',
flag: true,
default: false,
},
});
it('sets values to true of specified', function() {
expect(parser.parse(['--foo', '--another-bar', 'foo', 'bar']))
.toEqual({
options: {
foo: true,
bar: true,
},
positionalArguments: ['foo', 'bar'],
});
});
it('understands short options', function() {
expect(parser.parse(['-f', '--another-bar', 'f', 'bar']))
.toEqual({
options: {
foo: true,
bar: true,
},
positionalArguments: ['f', 'bar'],
});
});
it('sets default value if not specified', function() {
expect(parser.parse(['f', 'bar']))
.toEqual({
options: {
bar: false,
},
positionalArguments: ['f', 'bar'],
});
});
it('accepts flag=0 and flag=1 (undocumented)', function() {
expect(parser.parse(['--foo=0', '--another-bar=1']))
.toEqual({
options: {
foo: false,
bar: true,
},
positionalArguments: [],
});
expect(parser.parse(['-f=0']))
.toEqual({
options: {
foo: false,
bar: false,
},
positionalArguments: [],
});
});
it('understands --no-prefixes', function() {
expect(parser.parse(['--no-foo', '--no-another-bar']))
.toEqual({
options: {
foo: false,
bar: false,
},
positionalArguments: [],
});
});
});
});
================================================
FILE: src/__tests__/core-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
/*global jest, describe, it, expect*/
const core = require('../core');
const recast = require('recast');
const b = recast.types.builders;
const NodePath = recast.types.NodePath;
describe('core API', function() {
it('returns a Collection from a source string', function() {
expect(core('var foo;').constructor.name ).toContain('Collection');
});
it('returns a Collection from an AST node', function() {
const node = b.identifier('foo');
expect(core(node).constructor.name).toContain('Collection');
});
it('returns a Collection from an array of AST nodes', function() {
const node = b.identifier('foo');
expect(core([node]).constructor.name).toContain('Collection');
});
it('returns a Collection from a path', function() {
const path = new NodePath(b.identifier('foo'));
expect(core(path).constructor.name).toContain('Collection');
});
it('returns a Collection from an array of paths', function() {
const path = new NodePath(b.identifier('foo'));
expect(core([path]).constructor.name).toContain('Collection');
});
it('returns a Collection from an empty array', function() {
expect(core([]).constructor.name).toContain('Collection');
});
it('throws if it gets an invalid value', function() {
expect(() => core(42)).toThrow();
expect(() => core({})).toThrow();
});
it('returns the source as is if nothing was modified', function () {
const source = '\nvar foo;\n';
expect(core(source).toSource()).toEqual(source);
});
it('plugins are called with core', function (done) {
core.use(function (j) {
expect(j).toBe(core);
done();
});
});
it('plugins are only registered once', function () {
let ct = 0;
function plugin() {
ct++;
}
core.use(plugin);
core.use(plugin);
expect(ct).toBe(1);
});
});
================================================
FILE: src/__tests__/matchNode-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*global jest, describe, it, expect, beforeEach*/
'use strict';
const matchNode = require('../matchNode');
describe('matchNode', function() {
beforeEach(function() {
expect.extend({
toMatchNode: function(haystack, needle) {
const result = {};
result.pass = matchNode(haystack, needle);
return result;
}
});
})
it('matches null and undefined', function() {
expect(null).toMatchNode(null);
expect(null).not.toMatchNode(undefined);
expect(undefined).toMatchNode(undefined);
expect(undefined).not.toMatchNode(null);
});
it('matches scalars', function() {
expect('foo').toMatchNode('foo');
expect('foo').not.toMatchNode('bar');
expect('123').not.toMatchNode(123);
expect(123).toMatchNode(123);
expect(123).not.toMatchNode(456);
expect(123).not.toMatchNode('123');
expect(true).toMatchNode(true);
expect(true).not.toMatchNode(false);
expect(true).not.toMatchNode('true');
});
it('matches arrays', function() {
expect([1, 2, 3]).toMatchNode([1, 2, 3]);
expect([1, 2, 3]).not.toMatchNode([4, 5, 6]);
expect([[1, 2, 3], 'foo']).toMatchNode([[1, 2, 3], 'foo']);
expect([[1, 2, 3], 'foo']).not.toMatchNode([[456], 'foo']);
expect([[1, 2, 3], 'foo']).not.toMatchNode([[1, 2, 3], 'bar']);
expect([1, 2, 3, 4]).toMatchNode([1, 2, 3]);
expect([1, 2, 3]).not.toMatchNode([1, 2, 3, 4]);
});
it('matches objects', function() {
expect({}).toMatchNode({});
expect({name: 'foo'}).toMatchNode({name: 'foo'});
expect({name: 'foo'}).not.toMatchNode({name: 'bar'});
expect({name: 'foo', value: {name: 'bar'}})
.toMatchNode({name: 'foo', value: {name: 'bar'}});
expect({name: 'foo', value: {name: 'bar'}})
.not.toMatchNode({name: 'foo', value: {name: 'baz'}});
expect({name: 'foo', value: 'bar'}).toMatchNode({name: 'foo'});
expect({name: 'foo'}).not.toMatchNode({name: 'foo', value: 'bar'});
expect(Object.create({name: 'foo'})).not.toMatchNode({name: 'foo'});
expect({}).toMatchNode(Object.create({name: 'foo'}));
});
it('matches with a function', function() {
const haystack = {name: 'foo'};
const needle = jest.fn();
needle.mockReturnValue(true);
expect(haystack).toMatchNode(needle);
expect(needle).toBeCalledWith(haystack);
needle.mockReturnValue(false);
expect(haystack).not.toMatchNode(needle);
});
it('matches nested value with a function', function() {
const haystack = {name: 'foo', value: 'bar'};
const needle = {
name: jest.fn(),
value: jest.fn(),
};
needle.name.mockReturnValue(true);
needle.value.mockReturnValue(true);
expect(haystack).toMatchNode(needle);
expect(needle.name).toBeCalledWith(haystack.name);
expect(needle.value).toBeCalledWith(haystack.value);
needle.name.mockReturnValue(false);
needle.value.mockReturnValue(true);
expect(haystack).not.toMatchNode(needle);
needle.name.mockReturnValue(true);
needle.value.mockReturnValue(false);
expect(haystack).not.toMatchNode(needle);
});
});
================================================
FILE: src/__tests__/template-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
/*global jest, describe, it, expect, beforeEach*/
describe('Templates', () => {
let statements;
let statement;
let expression;
let jscodeshift;
beforeEach(() => {
jest.resetModules();
jscodeshift = require('../core');
const template = jscodeshift.template;
expression = template.expression;
statement = template.statement;
statements = template.statements;
});
it('interpolates expression nodes with source code', () => {
let input =
`var foo = bar;
if(bar) {
console.log(42);
}`;
let expected =
`var foo = alert(bar);
if(alert(bar)) {
console.log(42);
}`;
expect(
jscodeshift(input)
.find('Identifier', {name: 'bar'})
.replaceWith(path => expression`alert(${path.node})`)
.toSource()
).toEqual(expected);
});
it('interpolates statement nodes with source code', () => {
let input =
`for (var i = 0; i < 10; i++) {
console.log(i);
console.log(i / 2);
}`;
let expected =
`var i = 0;
while (i < 10) {
console.log(i);
console.log(i / 2);
i++;
}`;
expect(
jscodeshift(input)
.find('ForStatement')
.replaceWith(
p => statements`
${p.node.init};
while (${p.node.test}) {
${p.node.body.body}
${p.node.update};
}`
)
.toSource()
).toEqual(expected);
});
it('can be used with a different parser', () => {
const parser = require('../../parser/flow')();
const template = require('../template')(parser);
const node = {type: 'Literal', value: 41};
expect(
jscodeshift(template.expression`1 + ${node}`, {parser}).toSource()
).toEqual('1 + 41');
});
it('handles out-of-order traversal', () => {
const input = 'var x';
const expected = 'class X extends a {f(b) {}}';
const a = jscodeshift.identifier('a');
const b = jscodeshift.identifier('b');
const classDecl = statement`
class X extends ${a} {f(${b}) {}}
`;
expect(
jscodeshift(input)
.find('VariableDeclaration')
.replaceWith(classDecl)
.toSource()
)
.toEqual(expected);
});
it('correctly parses expressions without any interpolation', () => {
const expected = 'function() {}';
expect(
jscodeshift(
expression`function() {}`
)
.toSource()
)
.toEqual(expected);
});
for (const parser of ['babel', 'babylon', 'flow', 'ts', 'tsx']) {
it(`asyncExpression correctly parses expressions with await -- ${parser}`, () => {
const expected = '{\n bar: await baz\n}'
const j = jscodeshift.withParser(parser)
expect(j(j.template.asyncExpression`{\n bar: await baz\n}`).toSource()).toEqual(expected)
})
}
describe('explode arrays', () => {
it('explodes arrays in function definitions', () => {
let input = 'var foo = [a, b];';
let expected = 'var foo = function foo(a, b, c) {};';
expect(
jscodeshift(input)
.find('ArrayExpression')
.replaceWith(
p => expression`function foo(${p.node.elements}, c) {}`
)
.toSource()
)
.toEqual(expected);
expected = 'var foo = function(a, b, c) {};';
expect(
jscodeshift(input)
.find('ArrayExpression')
.replaceWith(
p => expression`function(${p.node.elements}, c) {}`
)
.toSource()
)
.toEqual(expected);
expected = 'var foo = (a, b) => {};';
expect(
jscodeshift(input)
.find('ArrayExpression')
.replaceWith(
p => expression`${p.node.elements} => {}`
)
.toSource()
)
.toEqual(expected);
expected = 'var foo = (a, b, c) => {};';
expect(
jscodeshift(input)
.find('ArrayExpression')
.replaceWith(
p => expression`(${p.node.elements}, c) => {}`
)
.toSource()
)
.toEqual(expected);
});
it('explodes arrays in variable declarations', () => {
let input = 'var foo = [a, b];';
let expected = 'var foo, a, b;';
expect(
jscodeshift(input)
.find('VariableDeclaration')
// Need to use a block here because the arrow doesn't seem to be
// compiled with a line break after the return statement. Can't repro
// outside here though
.replaceWith(p => {
const node = p.node.declarations[0];
return statement`var ${node.id}, ${node.init.elements};`;
})
.toSource()
)
.toEqual(expected);
});
it('explodes arrays in array expressions', () => {
let input = 'var foo = [a, b];';
let expected = 'var foo = [a, b, c];';
expect(
jscodeshift(input)
.find('ArrayExpression')
.replaceWith(p => expression`[${p.node.elements}, c]`)
.toSource()
)
.toEqual(expected);
});
it('explodes arrays in object expressions', () => {
let input = 'var foo = {a, b};';
let expected = /var foo = \{\s*a,\s*b,\s*c: 42\s*};/;
expect(
jscodeshift(input)
.find('ObjectExpression')
.replaceWith(p => expression`{${p.node.properties}, c: 42}`)
.toSource()
)
.toMatch(expected);
});
it('explodes arrays in call expressions', () => {
let input = 'var foo = [a, b];';
let expected = 'var foo = bar(a, b, c);';
expect(
jscodeshift(input)
.find('ArrayExpression')
.replaceWith(
p => expression`bar(${p.node.elements}, c)`
)
.toSource()
)
.toEqual(expected);
});
});
});
================================================
FILE: src/__tests__/testUtils-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const testSyncTransform = require('../__testfixtures__/test-sync-transform');
const testAsyncTransform = require('../__testfixtures__/test-async-transform');
const testUtils = require('../testUtils');
const testInputSource = 'export const sum = (a, b) => a + b;';
const expectedInlineOutput = 'export const addition = (a, b) => a + b;';
const getModuleToTransform = () => {
const moduleToTransformPath = path.join(__dirname, '..', '__testfixtures__', 'test-sync-transform.input.js');
const source = fs.readFileSync(moduleToTransformPath, 'utf8');
return {
path: moduleToTransformPath,
source,
}
}
describe('testUtils', () => {
describe('synchronous', () => {
it('should apply transformation', () => {
const moduleToTransform = getModuleToTransform();
const transformedCode = testUtils.applyTransform(testSyncTransform, null, moduleToTransform);
expect(transformedCode).not.toMatch(/sum/);
expect(transformedCode).toMatch(/addition/);
});
it('should run test', () => {
testUtils.runTest(
__dirname,
path.join('__testfixtures__',
'test-sync-transform'),
null,
'test-sync-transform'
);
});
it ('should run snapshot test', () => {
const moduleToTransform = getModuleToTransform();
testUtils.runSnapshotTest(testSyncTransform, null, moduleToTransform);
});
it('should run inline test', () => {
const moduleToTransform = getModuleToTransform();
testUtils.runInlineTest(testSyncTransform, null, moduleToTransform, expectedInlineOutput);
});
testUtils.defineTest(
__dirname,
path.join('__testfixtures__', 'test-sync-transform'),
null,
'test-sync-transform'
);
testUtils.defineInlineTest(
testSyncTransform,
null,
testInputSource,
expectedInlineOutput,
'should run sync defineInlineTest'
);
testUtils.defineSnapshotTest(
testSyncTransform,
null,
testInputSource,
'should run sync defineSnapshotTest'
);
testUtils.defineSnapshotTestFromFixture(
__dirname,
testSyncTransform,
null,
'test-sync-transform',
'should run sync defineSnapshotTestFromFixture'
);
});
describe('async', () => {
it('should apply transformation', async () => {
const moduleToTransform = getModuleToTransform();
const transformedCode = await testUtils.applyTransform(testAsyncTransform, null, moduleToTransform);
expect(transformedCode).not.toMatch(/sum/);
expect(transformedCode).toMatch(/addition/);
});
it('should run test', () => {
return testUtils.runTest(__dirname, path.join('__testfixtures__', 'test-async-transform'), null, 'test-async-transform');
});
it ('should run snapshot test', () => {
const moduleToTransform = getModuleToTransform();
return testUtils.runSnapshotTest(testAsyncTransform, null, moduleToTransform);
});
it('should run inline test', () => {
const moduleToTransform = getModuleToTransform();
return testUtils.runInlineTest(testAsyncTransform, null, moduleToTransform, expectedInlineOutput);
});
testUtils.defineTest(
__dirname,
path.join('__testfixtures__', 'test-async-transform'),
null,
'test-async-transform'
);
testUtils.defineInlineTest(
testAsyncTransform,
null,
testInputSource,
expectedInlineOutput,
'should run async defineInlineTest'
);
testUtils.defineSnapshotTest(
testAsyncTransform,
null,
testInputSource,
'should run async defineSnapshotTest'
);
testUtils.defineSnapshotTestFromFixture(
__dirname,
testSyncTransform,
null,
'test-async-transform',
'should run async defineSnapshotTestFromFixture'
);
});
});
================================================
FILE: src/__tests__/ts-decorator-auto-accessor-test.js
================================================
'use strict';
function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source).toSource();
}
transformer.parser = 'ts';
jest.autoMockOff();
const defineInlineTest = require('../../src/testUtils').defineInlineTest;
describe('should be parse typescript decoratorAutoAccessors correctly', function () {
defineInlineTest(
transformer,
{},
'export class Test {\n' +
' public accessor myValue = 10;\n' +
'}\n',
'export class Test {\n' +
' public accessor myValue = 10;\n' +
'}',
'ts-decorator-auto-accessor',
);
});
================================================
FILE: src/argsParser.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
function throwError(exitCode, message, helpText) {
const error = new Error(
helpText ? `${message}\n\n---\n\n${helpText}` : message
);
error.exitCode = exitCode;
throw error;
}
function formatOption(option) {
let text = ' ';
text += option.abbr ? `-${option.abbr}, ` : ' ';
text += `--${option.flag ? '(no-)' : ''}${option.full}`;
if (option.choices) {
text += `=${option.choices.join('|')}`;
} else if (option.metavar) {
text += `=${option.metavar}`;
}
if (option.list) {
text += ' ...';
}
if (option.defaultHelp || option.default !== undefined || option.help) {
text += ' ';
if (text.length < 32) {
text += ' '.repeat(32 - text.length);
}
const textLength = text.length;
if (option.help) {
text += option.help;
}
if (option.defaultHelp || option.default !== undefined) {
if (option.help) {
text += '\n';
}
text += `${' '.repeat(textLength)}(default: ${option.defaultHelp || option.default})`;
}
}
return text;
}
function getHelpText(options) {
const opts = Object.keys(options)
.map(k => options[k])
.sort((a,b) => a.display_index - b.display_index);
const text = `
Usage: jscodeshift [OPTION]... PATH...
or: jscodeshift [OPTION]... -t TRANSFORM_PATH PATH...
or: jscodeshift [OPTION]... -t URL PATH...
or: jscodeshift [OPTION]... --stdin < file_list.txt
Apply transform logic in TRANSFORM_PATH (recursively) to every PATH.
If --stdin is set, each line of the standard input is used as a path.
Options:
"..." behind an option means that it can be supplied multiple times.
All options are also passed to the transformer, which means you can supply custom options that are not listed here.
${opts.map(formatOption).join('\n')}
`;
return text.trimLeft();
}
function validateOptions(parsedOptions, options) {
const errors = [];
for (const optionName in options) {
const option = options[optionName];
if (option.choices && !option.choices.includes(parsedOptions[optionName])) {
errors.push(
`Error: --${option.full} must be one of the values ${option.choices.join(',')}`
);
}
}
if (errors.length > 0) {
throwError(
1,
errors.join('\n'),
getHelpText(options)
);
}
}
function prepareOptions(options) {
options.help = {
display_index: 5,
abbr: 'h',
help: 'print this help and exit',
callback() {
return getHelpText(options);
},
};
const preparedOptions = {};
for (const optionName of Object.keys(options)) {
const option = options[optionName];
if (!option.full) {
option.full = optionName;
}
option.key = optionName;
preparedOptions['--'+option.full] = option;
if (option.abbr) {
preparedOptions['-'+option.abbr] = option;
}
if (option.flag) {
preparedOptions['--no-'+option.full] = option;
}
}
return preparedOptions;
}
function isOption(value) {
return /^--?/.test(value);
}
function parse(options, args=process.argv.slice(2)) {
const missingValue = Symbol();
const preparedOptions = prepareOptions(options);
const parsedOptions = {};
const positionalArguments = [];
for (const optionName in options) {
const option = options[optionName];
if (option.default !== undefined) {
parsedOptions[optionName] = option.default;
} else if (option.list) {
parsedOptions[optionName] = [];
}
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (isOption(arg)) {
let optionName = arg;
let value = null;
let option = null;
if (optionName.includes('=')) {
const index = arg.indexOf('=');
optionName = arg.slice(0, index);
value = arg.slice(index+1);
}
if (preparedOptions.hasOwnProperty(optionName)) {
option = preparedOptions[optionName];
} else {
// Unknown options are just "passed along".
// The logic is as follows:
// - If an option is encountered without a value, it's treated
// as a flag
// - If the option has a value, it's initialized with that value
// - If the option has been seen before, it's converted to a list
// If the previous value was true (i.e. a flag), that value is
// discarded.
const realOptionName = optionName.replace(/^--?(no-)?/, '');
const isList = parsedOptions.hasOwnProperty(realOptionName) &&
parsedOptions[realOptionName] !== true;
option = {
key: realOptionName,
full: realOptionName,
flag: !parsedOptions.hasOwnProperty(realOptionName) &&
value === null &&
isOption(args[i+1]),
list: isList,
process(value) {
// Try to parse values as JSON to be compatible with nomnom
try {
return JSON.parse(value);
} catch(_e) {}
return value;
},
};
if (isList) {
const currentValue = parsedOptions[realOptionName];
if (!Array.isArray(currentValue)) {
parsedOptions[realOptionName] = currentValue === true ?
[] :
[currentValue];
}
}
}
if (option.callback) {
throwError(0, option.callback());
} else if (option.flag) {
if (optionName.startsWith('--no-')) {
value = false;
} else if (value !== null) {
value = value === '1';
} else {
value = true;
}
parsedOptions[option.key] = value;
} else {
if (value === null && i < args.length - 1 && !isOption(args[i+1])) {
// consume next value
value = args[i+1];
i += 1;
}
if (value !== null) {
if (option.process) {
value = option.process(value);
}
if (option.list) {
parsedOptions[option.key].push(value);
} else {
parsedOptions[option.key] = value;
}
} else {
parsedOptions[option.key] = missingValue;
}
}
} else {
positionalArguments.push(/^\d+$/.test(arg) ? Number(arg) : arg);
}
}
for (const optionName in parsedOptions) {
if (parsedOptions[optionName] === missingValue) {
throwError(
1,
`Missing value: --${options[optionName].full} requires a value`,
getHelpText(options)
);
}
}
const result = {
positionalArguments,
options: parsedOptions,
};
validateOptions(parsedOptions, options);
return result;
}
module.exports = {
/**
* `options` is an object of objects. Each option can have the following
* properties:
*
* - full: The name of the option to be used in the command line (if
* different than the property name.
* - abbr: The short version of the option, a single character
* - flag: Whether the option takes an argument or not.
* - default: The default value to use if option is not supplied
* - choices: Restrict possible values to these values
* - help: The help text to print
* - metavar: Value placeholder to use in the help
* - callback: If option is supplied, call this function and exit
* - process: Pre-process value before returning it
*/
options(options) {
return {
parse(args) {
return parse(options, args);
},
getHelpText() {
return getHelpText(options);
},
};
},
};
================================================
FILE: src/collections/ImportDeclaration.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
"use strict";
const Collection = require("../Collection");
const NodeCollection = require("./Node");
const assert = require("assert");
const once = require("../utils/once");
const recast = require("recast");
const types = recast.types.namedTypes;
const globalMethods = {
/**
* Inserts an ImportDeclaration at the top of the AST
*
* @param {string} sourcePath
* @param {Array} specifiers
*/
insertImportDeclaration: function (sourcePath, specifiers) {
assert.ok(
sourcePath && typeof sourcePath === "string",
"insertImportDeclaration(...) needs a source path"
);
assert.ok(
specifiers && Array.isArray(specifiers),
"insertImportDeclaration(...) needs an array of specifiers"
);
if (this.hasImportDeclaration(sourcePath)) {
return this;
}
const importDeclaration = recast.types.builders.importDeclaration(
specifiers,
recast.types.builders.stringLiteral(sourcePath)
);
return this.forEach((path) => {
if (path.value.type === "Program") {
path.value.body.unshift(importDeclaration);
}
});
},
/**
* Finds all ImportDeclarations optionally filtered by name
*
* @param {string} sourcePath
* @return {Collection}
*/
findImportDeclarations: function (sourcePath) {
assert.ok(
sourcePath && typeof sourcePath === "string",
"findImportDeclarations(...) needs a source path"
);
return this.find(types.ImportDeclaration, {
source: { value: sourcePath },
});
},
/**
* Determines if the collection has an ImportDeclaration with the given sourcePath
*
* @param {string} sourcePath
* @returns {boolean}
*/
hasImportDeclaration: function (sourcePath) {
assert.ok(
sourcePath && typeof sourcePath === "string",
"findImportDeclarations(...) needs a source path"
);
return this.findImportDeclarations(sourcePath).length > 0;
},
/**
* Renames all ImportDeclarations with the given name
*
* @param {string} sourcePath
* @param {string} newSourcePath
* @return {Collection}
*/
renameImportDeclaration: function (sourcePath, newSourcePath) {
assert.ok(
sourcePath && typeof sourcePath === "string",
"renameImportDeclaration(...) needs a name to look for"
);
assert.ok(
newSourcePath && typeof newSourcePath === "string",
"renameImportDeclaration(...) needs a new name to rename to"
);
return this.findImportDeclarations(sourcePath).forEach((path) => {
path.value.source.value = newSourcePath;
});
},
};
function register() {
NodeCollection.register();
Collection.registerMethods(globalMethods, types.Node);
}
exports.register = once(register);
================================================
FILE: src/collections/JSXElement.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('../Collection');
const NodeCollection = require('./Node');
const assert = require('assert');
const once = require('../utils/once');
const recast = require('recast');
const requiresModule = require('./VariableDeclarator').filters.requiresModule;
const types = recast.types.namedTypes;
const JSXElement = types.JSXElement;
const JSXAttribute = types.JSXAttribute;
const Literal = types.Literal;
/**
* Contains filter methods and mutation methods for processing JSXElements.
* @mixin
*/
const globalMethods = {
/**
* Finds all JSXElements optionally filtered by name
*
* @param {string} name
* @return {Collection}
*/
findJSXElements: function(name) {
const nameFilter = name && {openingElement: {name: {name: name}}};
return this.find(JSXElement, nameFilter);
},
/**
* Finds all JSXElements by module name. Given
*
* var Bar = require('Foo');
* <Bar />
*
* findJSXElementsByModuleName('Foo') will find <Bar />, without having to
* know the variable name.
*/
findJSXElementsByModuleName: function(moduleName) {
assert.ok(
moduleName && typeof moduleName === 'string',
'findJSXElementsByModuleName(...) needs a name to look for'
);
return this.find(types.VariableDeclarator)
.filter(requiresModule(moduleName))
.map(function(path) {
const id = path.value.id.name;
if (id) {
return Collection.fromPaths([path])
.closestScope()
.findJSXElements(id)
.paths();
}
});
}
};
const filterMethods = {
/**
* Filter method for attributes.
*
* @param {Object} attributeFilter
* @return {function}
*/
hasAttributes: function(attributeFilter) {
const attributeNames = Object.keys(attributeFilter);
return function filter(path) {
if (!JSXElement.check(path.value)) {
return false;
}
const elementAttributes = Object.create(null);
path.value.openingElement.attributes.forEach(function(attr) {
if (!JSXAttribute.check(attr) ||
!(attr.name.name in attributeFilter)) {
return;
}
elementAttributes[attr.name.name] = attr;
});
return attributeNames.every(function(name) {
if (!(name in elementAttributes) ){
return false;
}
const value = elementAttributes[name].value;
const expected = attributeFilter[name];
// Only when value is truthy access it's properties
const actual = !value
? value
: Literal.check(value)
? value.value
: value.expression;
if (typeof expected === 'function') {
return expected(actual);
}
// Literal attribute values are always strings
return String(expected) === actual;
});
};
},
/**
* Filter elements which contain a specific child type
*
* @param {string} name
* @return {function}
*/
hasChildren: function(name) {
return function filter(path) {
return JSXElement.check(path.value) &&
path.value.children.some(
child => JSXElement.check(child) &&
child.openingElement.name.name === name
);
};
}
};
/**
* @mixin
*/
const traversalMethods = {
/**
* Returns all child nodes, including literals and expressions.
*
* @return {Collection}
*/
childNodes: function() {
const paths = [];
this.forEach(function(path) {
const children = path.get('children');
const l = children.value.length;
for (let i = 0; i < l; i++) {
paths.push(children.get(i));
}
});
return Collection.fromPaths(paths, this);
},
/**
* Returns all children that are JSXElements.
*
* @return {JSXElementCollection}
*/
childElements: function() {
const paths = [];
this.forEach(function(path) {
const children = path.get('children');
const l = children.value.length;
for (let i = 0; i < l; i++) {
if (types.JSXElement.check(children.value[i])) {
paths.push(children.get(i));
}
}
});
return Collection.fromPaths(paths, this, JSXElement);
},
/**
* Returns all children that are of jsxElementType.
*
* @return {Collection<jsxElementType>}
*/
childNodesOfType: function(jsxChildElementType) {
const paths = [];
this.forEach(function(path) {
const children = path.get('children');
const l = children.value.length;
for (let i = 0; i < l; i++) {
if (jsxChildElementType.check(children.value[i])) {
paths.push(children.get(i));
}
}
});
return Collection.fromPaths(paths, this, jsxChildElementType);
},
};
const mappingMethods = {
/**
* Given a JSXElement, returns its "root" name. E.g. it would return "Foo" for
* both <Foo /> and <Foo.Bar />.
*
* @param {NodePath} path
* @return {string}
*/
getRootName: function(path) {
let name = path.value.openingElement.name;
while (types.JSXMemberExpression.check(name)) {
name = name.object;
}
return name && name.name || null;
}
};
function register() {
NodeCollection.register();
Collection.registerMethods(globalMethods, types.Node);
Collection.registerMethods(traversalMethods, JSXElement);
}
exports.register = once(register);
exports.filters = filterMethods;
exports.mappings = mappingMethods;
================================================
FILE: src/collections/Node.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('../Collection');
const matchNode = require('../matchNode');
const once = require('../utils/once');
const recast = require('recast');
const Node = recast.types.namedTypes.Node;
var types = recast.types.namedTypes;
/**
* @mixin
*/
const traversalMethods = {
/**
* Find nodes of a specific type within the nodes of this collection.
*
* @param {type}
* @param {filter}
* @return {Collection}
*/
find: function(type, filter) {
const paths = [];
const visitorMethodName = 'visit' + type;
const visitor = {};
function visit(path) {
/*jshint validthis:true */
if (!filter || matchNode(path.value, filter)) {
paths.push(path);
}
this.traverse(path);
}
this.__paths.forEach(function(p, i) {
const self = this;
visitor[visitorMethodName] = function(path) {
if (self.__paths[i] === path) {
this.traverse(path);
} else {
return visit.call(this, path);
}
};
recast.visit(p, visitor);
}, this);
return Collection.fromPaths(paths, this, type);
},
/**
* Returns a collection containing the paths that create the scope of the
* currently selected paths. Dedupes the paths.
*
* @return {Collection}
*/
closestScope: function() {
return this.map(path => path.scope && path.scope.path);
},
/**
* Traverse the AST up and finds the closest node of the provided type.
*
* @param {Collection}
* @param {filter}
* @return {Collection}
*/
closest: function(type, filter) {
return this.map(function(path) {
let parent = path.parent;
while (
parent &&
!(
type.check(parent.value) &&
(!filter || matchNode(parent.value, filter))
)
) {
parent = parent.parent;
}
return parent || null;
});
},
/**
* Finds the declaration for each selected path. Useful for member expressions
* or JSXElements. Expects a callback function that maps each path to the name
* to look for.
*
* If the callback returns a falsey value, the element is skipped.
*
* @param {function} nameGetter
*
* @return {Collection}
*/
getVariableDeclarators: function(nameGetter) {
return this.map(function(path) {
/*jshint curly:false*/
let scope = path.scope;
if (!scope) return;
const name = nameGetter.apply(path, arguments);
if (!name) return;
scope = scope.lookup(name);
if (!scope) return;
const bindings = scope.getBindings()[name];
if (!bindings) return;
const decl = Collection.fromPaths(bindings)
.closest(types.VariableDeclarator);
if (decl.length === 1) {
return decl.paths()[0];
}
}, types.VariableDeclarator);
},
};
function toArray(value) {
return Array.isArray(value) ? value : [value];
}
/**
* @mixin
*/
const mutationMethods = {
/**
* Simply replaces the selected nodes with the provided node. If a function
* is provided it is executed for every node and the node is replaced with the
* functions return value.
*
* @param {Node|Array<Node>|function} nodes
* @return {Collection}
*/
replaceWith: function(nodes) {
return this.forEach(function(path, i) {
const newNodes =
(typeof nodes === 'function') ? nodes.call(path, path, i) : nodes;
path.replace.apply(path, toArray(newNodes));
});
},
/**
* Inserts a new node before the current one.
*
* @param {Node|Array<Node>|function} insert
* @return {Collection}
*/
insertBefore: function(insert) {
return this.forEach(function(path, i) {
const newNodes =
(typeof insert === 'function') ? insert.call(path, path, i) : insert;
path.insertBefore.apply(path, toArray(newNodes));
});
},
/**
* Inserts a new node after the current one.
*
* @param {Node|Array<Node>|function} insert
* @return {Collection}
*/
insertAfter: function(insert) {
return this.forEach(function(path, i) {
const newNodes =
(typeof insert === 'function') ? insert.call(path, path, i) : insert;
path.insertAfter.apply(path, toArray(newNodes));
});
},
remove: function() {
return this.forEach(path => path.prune());
}
};
function register() {
Collection.registerMethods(traversalMethods, Node);
Collection.registerMethods(mutationMethods, Node);
Collection.setDefaultCollectionType(Node);
}
exports.register = once(register);
================================================
FILE: src/collections/VariableDeclarator.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('../Collection');
const NodeCollection = require('./Node');
const once = require('../utils/once');
const recast = require('recast');
const astNodesAreEquivalent = recast.types.astNodesAreEquivalent;
const b = recast.types.builders;
var types = recast.types.namedTypes;
const VariableDeclarator = recast.types.namedTypes.VariableDeclarator;
/**
* @mixin
*/
const globalMethods = {
/**
* Finds all variable declarators, optionally filtered by name.
*
* @param {string} name
* @return {Collection}
*/
findVariableDeclarators: function(name) {
const filter = name ? {id: {name: name}} : null;
return this.find(VariableDeclarator, filter);
}
};
const filterMethods = {
/**
* Returns a function that returns true if the provided path is a variable
* declarator and requires one of the specified module names.
*
* @param {string|Array} names A module name or an array of module names
* @return {Function}
*/
requiresModule: function(names) {
if (names && !Array.isArray(names)) {
names = [names];
}
const requireIdentifier = b.identifier('require');
return function(path) {
const node = path.value;
if (!VariableDeclarator.check(node) ||
!types.CallExpression.check(node.init) ||
!astNodesAreEquivalent(node.init.callee, requireIdentifier)) {
return false;
}
return !names ||
names.some(
n => astNodesAreEquivalent(node.init.arguments[0], b.literal(n))
);
};
}
};
/**
* @mixin
*/
const transformMethods = {
/**
* Renames a variable and all its occurrences.
*
* @param {string} newName
* @return {Collection}
*/
renameTo: function(newName) {
// TODO: Include JSXElements
return this.forEach(function(path) {
const node = path.value;
const oldName = node.id.name;
const rootScope = path.scope;
const rootPath = rootScope.path;
Collection.fromPaths([rootPath])
.find(types.Identifier, {name: oldName})
.filter(function(path) { // ignore non-variables
const parent = path.parent.node;
if (
types.MemberExpression.check(parent) &&
parent.property === path.node &&
!parent.computed
) {
// obj.oldName
return false;
}
if (
types.Property.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// { oldName: 3 }
return false;
}
if (
types.ObjectProperty.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// { oldName: 3 }
return false;
}
if (
types.ObjectMethod.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// { oldName() {} }
return false;
}
if (
types.MethodDefinition.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// class A { oldName() {} }
return false;
}
if (
types.ClassMethod.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// class A { oldName() {} }
return false;
}
if (
types.ClassProperty.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// class A { oldName = 3 }
return false;
}
if (
types.JSXAttribute.check(parent) &&
parent.name === path.node &&
!parent.computed
) {
// <Foo oldName={oldName} />
return false;
}
if (
(types.JSXOpeningElement.check(parent) || types.JSXClosingElement.check(parent)) &&
parent.name === path.node &&
/^[a-z]/.test(path.node.name)
) {
// <oldName></oldName>
return false;
}
return true;
})
.forEach(function(path) {
let scope = path.scope;
while (scope && scope !== rootScope) {
if (scope.declares(oldName)) {
return;
}
scope = scope.parent;
}
if (scope) { // identifier must refer to declared variable
// It may look like we filtered out properties,
// but the filter only ignored property "keys", not "value"s
// In shorthand properties, "key" and "value" both have an
// Identifier with the same structure.
const parent = path.parent.node;
if (
types.Property.check(parent) &&
parent.shorthand &&
!parent.method
) {
path.parent.get('shorthand').replace(false);
}
path.get('name').replace(newName);
}
});
});
}
};
function register() {
NodeCollection.register();
Collection.registerMethods(globalMethods);
Collection.registerMethods(transformMethods, VariableDeclarator);
}
exports.register = once(register);
exports.filters = filterMethods;
================================================
FILE: src/collections/__tests__/.eslintrc
================================================
{
"globals": {
"jest": true
},
"env": {
"jasmine": true
}
}
================================================
FILE: src/collections/__tests__/.jshintrc
================================================
{
"predef": ["jest", "describe", "beforeEach" , "xit", "it", "expect"],
"node": true,
"esnext": true
}
================================================
FILE: src/collections/__tests__/ImportDeclaration-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
"use strict";
const getParser = require("./../../getParser");
describe("ImportDeclaration API", function () {
let nodes;
let Collection;
let ImportDeclarationCollection;
let recast;
let types;
let b;
beforeEach(function () {
jest.resetModules();
Collection = require("../../Collection");
ImportDeclarationCollection = require("../ImportDeclaration");
recast = require("recast");
types = recast.types.namedTypes;
b = recast.types.builders;
ImportDeclarationCollection.register();
nodes = [
recast.parse(
[
'import FooBar from "XYZ";',
'import Foo, { Bar, Baz } from "@meta/foo";',
'import { Bar as Burger } from "@meta/bar";',
].join("\n"),
{ parser: getParser() }
).program,
];
});
describe("Traversal", function () {
describe("hasImportDeclaration", function () {
it("returns true if an ImportDeclaration exists", function () {
const hasImport =
Collection.fromNodes(nodes).hasImportDeclaration("XYZ");
expect(hasImport).toBe(true);
});
it("returns false if an ImportDeclaration does not exist", function () {
const hasImport =
Collection.fromNodes(nodes).hasImportDeclaration("ABC");
expect(hasImport).toBe(false);
});
});
describe("findImportDeclarations", function () {
it("lets us find ImportDeclarations by source path conveniently", function () {
const imports =
Collection.fromNodes(nodes).findImportDeclarations("XYZ");
expect(imports.length).toBe(1);
});
it("returns an empty ImportDeclarationCollection if no ImportDeclarations are found", function () {
const imports =
Collection.fromNodes(nodes).findImportDeclarations("Foo");
expect(imports.length).toBe(0);
});
});
describe("renameImportDeclaration", function () {
it("renames an ImportDeclaration with the given sourcePath", function () {
Collection.fromNodes(nodes).renameImportDeclaration("XYZ", "ABC");
{
const imports =
Collection.fromNodes(nodes).findImportDeclarations("ABC");
expect(imports.length).toBe(1);
}
{
const imports =
Collection.fromNodes(nodes).findImportDeclarations("XYZ");
expect(imports.length).toBe(0);
}
});
it("throws if sourcePath is not provided", function () {
expect(function () {
Collection.fromNodes(nodes).renameImportDeclaration();
}).toThrow();
});
it("throws if newSourcePath is not provided", function () {
expect(function () {
Collection.fromNodes(nodes).renameImportDeclaration("XYZ");
}).toThrow();
});
});
describe("insertImportDeclaration", function () {
it("inserts an ImportDeclaration into the AST", function () {
Collection.fromNodes(nodes).insertImportDeclaration("@foo/bar", [
b.importDefaultSpecifier(b.identifier("Foo")),
b.importSpecifier(b.identifier("ABC")),
b.importSpecifier(b.identifier("123")),
]);
const imports =
Collection.fromNodes(nodes).findImportDeclarations("@foo/bar");
expect(imports.length).toBe(1);
const importSpecifiers = imports.paths()[0].value.specifiers;
expect(importSpecifiers.length).toBe(3);
});
it("does not insert duplicate ImportDeclarations", function () {
Collection.fromNodes(nodes).insertImportDeclaration("@foo/baz", [
b.importDefaultSpecifier(b.identifier("Foo")),
b.importSpecifier(b.identifier("ABC")),
]);
Collection.fromNodes(nodes).insertImportDeclaration("@foo/baz", [
b.importDefaultSpecifier(b.identifier("Foo")),
b.importSpecifier(b.identifier("ABC")),
]);
const imports =
Collection.fromNodes(nodes).findImportDeclarations("@foo/baz");
expect(imports.length).toBe(1);
});
it("throws if importDeclaration is not provided", function () {
expect(function () {
Collection.fromNodes(nodes).insertImportDeclaration();
}).toThrow();
});
});
});
});
================================================
FILE: src/collections/__tests__/JSXElement-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const getParser = require('./../../getParser');
describe('JSXCollection API', function() {
let nodes;
let Collection;
let JSXElementCollection;
let recast;
let types;
let b;
beforeEach(function() {
jest.resetModules();
Collection = require('../../Collection');
JSXElementCollection = require('../JSXElement');
recast = require('recast');
types = recast.types.namedTypes;
b = recast.types.builders;
JSXElementCollection.register();
nodes = [recast.parse([
'var FooBar = require("XYZ");',
'<FooBar foo="bar" bar="foo">',
' <Child id="1" foo="bar" baz>',
' <Child />',
' <Baz.Bar />',
' </Child>',
' <Child id="2" foo="baz" baz/>',
' {"foo"}',
'</FooBar>'
].join('\n'), {parser: getParser()}).program];
});
describe('Traversal', function() {
it('returns a non empty JSXCollection', function() {
const jsx = Collection.fromNodes(nodes).find(types.JSXElement);
expect(jsx.getTypes()).toContain('JSXElement');
expect(jsx.length).toBeGreaterThan(0);
});
it('lets us find JSXElements by name conveniently', function() {
const jsx = Collection.fromNodes(nodes).findJSXElements('Child');
expect(jsx.length).toBe(3);
});
it('finds JSXElements by module name', function() {
const jsx = Collection.fromNodes(nodes).findJSXElementsByModuleName('XYZ');
expect(jsx.length).toBe(1);
});
it('returns the child nodes of an JSXElement', function() {
const children =
Collection.fromNodes(nodes)
.findJSXElements('FooBar')
.childNodes();
expect(children.length).toBe(7);
expect(children.getTypes()).toContain('Expression');
});
it('returns the child JSXElements of an JSXElement', function() {
const children =
Collection.fromNodes(nodes)
.findJSXElements('FooBar')
.childElements();
expect(children.length).toBe(2);
expect(children.getTypes()).toContain('JSXElement');
});
it('returns the child element types of an JSXElement', function() {
const children =
Collection.fromNodes(nodes)
.findJSXElements('FooBar')
.childNodesOfType(types.JSXExpressionContainer);
expect(children.length).toBe(1);
expect(children.getTypes()).toContain('JSXExpressionContainer');
});
it('returns a properly typed collection even if empty', function() {
const children =
Collection.fromNodes([])
.findJSXElements('Foo')
.childElements();
expect(children.length).toBe(0);
expect(children.getTypes()).toContain('JSXElement');
});
});
describe('Filtering', function() {
it('filters elements by attributes', function() {
const jsx = Collection.fromNodes(nodes)
.findJSXElements()
.filter(JSXElementCollection.filters.hasAttributes({foo: 'bar'}));
expect(jsx.length).toBe(2);
});
it('accepts callback functions as attribute filters', function() {
const jsx = Collection.fromNodes(nodes)
.findJSXElements()
.filter(JSXElementCollection.filters.hasAttributes(
{
foo: v => ['bar', 'baz'].indexOf(v) > -1,
baz: v => v === null
}
));
expect(jsx.length).toBe(2);
});
it('filters elements by children', function() {
const jsx = Collection.fromNodes(nodes)
.findJSXElements()
.filter(JSXElementCollection.filters.hasChildren('Child'));
expect(jsx.length).toBe(2);
});
});
describe('Mappings', function() {
it('gets the root names of JSXElements', function() {
const names = Collection.fromNodes(nodes)
.findJSXElements()
.paths().map(JSXElementCollection.mappings.getRootName);
expect(names.indexOf('FooBar') > -1).toBe(true);
expect(names.indexOf('Child') > -1).toBe(true);
expect(names.indexOf('Baz') > -1).toBe(true);
});
});
describe('Mutation', function() {
it('handles insertions before children correctly', function() {
const childElement = b.jsxElement(
b.jsxOpeningElement(b.jsxIdentifier('Bar'), [], true)
);
const newChildElement = b.jsxElement(
b.jsxOpeningElement(b.jsxIdentifier('Baz'), [], true)
);
const literal = b.literal('\n ');
const ast = b.jsxElement(
b.jsxOpeningElement(b.jsxIdentifier('Foo')),
b.jsxClosingElement(b.jsxIdentifier('Foo')),
[literal, childElement, literal, childElement, b.literal('\n')]
);
Collection.fromNodes([ast])
.childElements().at(1).insertBefore(newChildElement);
expect(ast.children.length).toBe(6);
expect(ast.children[3]).toBe(newChildElement);
});
});
});
================================================
FILE: src/collections/__tests__/Node-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
describe('Collection API', function() {
let ast;
let Collection;
let NodeCollection;
let recast;
let types;
let b;
beforeEach(function() {
jest.resetModules();
Collection = require('../../Collection');
NodeCollection = require('../Node');
recast = require('recast');
types = recast.types.namedTypes;
b = recast.types.builders;
NodeCollection.register();
ast = b.program([
b.variableDeclaration(
'var',
[b.variableDeclarator(b.identifier('foo'), null)]
),
b.functionDeclaration(
b.identifier('f'),
[],
b.blockStatement([
b.variableDeclaration(
'var',
[b.variableDeclarator(b.identifier('bar'), null)]
),
b.variableDeclaration(
'var',
[b.variableDeclarator(b.identifier('baz'), null)]
)
])
),
]);
});
describe('Traversal', function() {
describe('find', function() {
it('finds nodes by type', function() {
const ast = b.sequenceExpression([ // eslint-disable-line no-shadow
b.identifier('foo'),
b.literal('asd'),
b.identifier('bar'),
]);
const vars = Collection.fromNodes([ast]).find(types.Identifier);
expect(vars.length).toBe(2);
});
it('doesn\'t find the nodes in the collection itself', function() {
const nodes = [
b.identifier('foo'),
b.literal('asd'),
b.identifier('bar'),
];
const vars = Collection.fromNodes(nodes).find(types.Identifier);
expect(vars.length).toBe(0);
});
it('finds nodes by type and properties', function() {
const ast = b.sequenceExpression([ // eslint-disable-line no-shadow
b.identifier('foo'),
b.literal('asd'),
b.identifier('bar'),
]);
const vars = Collection.fromNodes([ast])
.find(types.Identifier, {name: 'bar'});
expect(vars.length).toBe(1);
expect(vars.nodes()[0]).toBe(ast.expressions[2]);
});
it('handles chained find calls properly', function() {
const vars = Collection.fromNodes([ast])
.find(types.FunctionDeclaration)
.find(types.VariableDeclarator, {id: {name: 'bar'}});
expect(vars.length).toBe(1);
expect(vars.nodes()[0]).toBe(
ast.body[1].body.body[0].declarations[0]
);
});
it('handles multi chain find calls properly', function() {
const functionBody = ast.body[1].body.body;
const functionDeclarations = Collection.fromNodes([ast])
.find(types.FunctionDeclaration);
const bar = functionDeclarations
.find(types.VariableDeclarator, {id: {name: 'bar'}});
const baz = functionDeclarations
.find(types.VariableDeclarator, {id: {name: 'baz'}});
expect(bar.length).toBe(1);
expect(bar.nodes()[0]).toBe(functionBody[0].declarations[0]);
expect(baz.length).toBe(1);
expect(baz.nodes()[0]).toBe(functionBody[1].declarations[0]);
});
});
describe('closestScope', function() {
it('gets the closest scope', function() {
const functionDeclaration = ast.body[1];
const scopes = Collection.fromNodes([ast])
.find(types.Identifier)
.closestScope();
expect(scopes.nodes()[0]).toBe(ast);
expect(scopes.nodes()[1]).toBe(functionDeclaration);
});
});
describe('closest', function() {
let decl;
beforeEach(()=> {
decl = b.functionDeclaration(
b.identifier('foo'),
[],
b.blockStatement([
b.functionDeclaration(
b.identifier('bar'),
[],
b.blockStatement([
b.returnStatement(
b.literal(3)
)
])
),
])
);
});
it('finds closest node (up the tree) of the given type', function() {
const functionDeclaration = ast.body[1];
decl = Collection.fromNodes([ast])
.find(types.Identifier)
.closest(types.FunctionDeclaration);
expect(decl.length).toBe(1);
expect(decl.nodes()[0]).toBe(functionDeclaration);
});
it('allows to filter nodes by pattern', function() {
const literals = Collection.fromNodes([decl])
.find(types.Literal);
expect(literals.get(0).node.value).toBe(3);
const closest = literals.closest(
types.FunctionDeclaration,
{id: {name: 'foo'}}
);
expect(closest.get(0).node.id.name).toBe('foo');
});
it('allows to filter nodes with a filter function', function() {
const literals = Collection.fromNodes([decl])
.find(types.Literal);
expect(literals.get(0).node.value).toBe(3);
const closest = literals.closest(
types.FunctionDeclaration,
(node) => node.id && node.id.name === 'foo'
);
expect(closest.get(0).node.id.name).toBe('foo');
});
it('fails when filter evaluates as false', function() {
const literals = Collection.fromNodes([decl])
.find(types.Literal);
expect(literals.get(0).node.value).toBe(3);
const closest = literals.closest(
types.FunctionDeclaration,
(node) => node.id && node.id.name === 'blue'
);
expect(closest.nodes().length).toBe(0);
});
});
describe('getVariableDeclarators', function() {
it('gets the variable declarators for each selected path', function() {
const variableDeclarator =
b.variableDeclarator(b.identifier('foo'), null);
const program = b.program([
b.variableDeclaration('var', [variableDeclarator]),
b.expressionStatement(b.identifier('foo')),
b.expressionStatement(b.identifier('bar'))
]);
const decl = Collection.fromNodes([program])
.find(types.Identifier)
.getVariableDeclarators(p => p.value.name);
expect(decl.length).toBe(1);
expect(decl.nodes()[0]).toBe(variableDeclarator);
});
});
});
describe('Mutation', function() {
describe('replaceWith', function() {
it('handles simple AST node replacement', function() {
const ast = b.sequenceExpression([ // eslint-disable-line no-shadow
b.identifier('foo'),
b.literal('asd'),
b.identifier('bar'),
]);
const newNode = b.identifier('xyz');
const S = Collection.fromNodes([ast]);
S.find(types.Identifier, {name: 'bar'})
.replaceWith(newNode);
expect(S.nodes()[0].expressions[2]).toBe(newNode);
});
it('accepts an array as replacement', function() {
const ast = b.sequenceExpression([ // eslint-disable-line no-shadow
b.identifier('foo'),
b.literal('asd'),
b.identifier('bar'),
]);
const newNode1 = b.identifier('xyz');
const newNode2 = b.identifier('jkl');
const S = Collection.fromNodes([ast]);
S.find(types.Identifier, {name: 'bar'})
.replaceWith([newNode1, newNode2]);
expect(S.nodes()[0].expressions[2]).toBe(newNode1);
expect(S.nodes()[0].expressions[3]).toBe(newNode2);
});
it('accepts a function as replacement ', function() {
const ast = b.sequenceExpression([ // eslint-disable-line no-shadow
b.identifier('foo'),
b.literal('asd'),
b.identifier('bar'),
]);
const expectedArgs = [b.identifier('foo'), b.identifier('bar')];
const receivedArgs = [];
const replaceFunction =
jest.fn(function(path, i) {
// We have to keep a reference to the argument before it gets
// replaced
receivedArgs.push(path.value);
return b.identifier(path.value.name + i);
});
const S = Collection.fromNodes([ast]);
S.find(types.Identifier)
.replaceWith(replaceFunction);
expect(replaceFunction.mock.calls.length).toBe(2);
expect(receivedArgs).toEqual(expectedArgs);
// baz is properly replaced
expect(S.nodes()[0].expressions[0]).toEqual(b.identifier('foo0'));
// baz1 is properly replaced
expect(S.nodes()[0].expressions[2]).toEqual(b.identifier('bar1'));
});
});
describe('insertBefore', function() {
it('inserts a new node before the current one', function() {
const ast = b.variableDeclaration( // eslint-disable-line no-shadow
'var',
[b.variableDeclarator(b.identifier('foo'), null)]
);
const one = b.variableDeclarator(b.identifier('one'), null);
Collection.fromNodes([ast])
.find(types.VariableDeclarator)
.insertBefore(one);
expect(ast.declarations.length).toBe(2);
expect(ast.declarations[0]).toBe(one);
});
it('accepts an array of nodes', function() {
const ast = b.variableDeclaration( // eslint-disable-line no-shadow
'var',
[b.variableDeclarator(b.identifier('foo'), null)]
);
const one = b.variableDeclarator(b.identifier('one'), null);
const two = b.variableDeclarator(b.identifier('two'), null);
Collection.fromNodes([ast])
.find(types.VariableDeclarator)
.insertBefore([one, two]);
expect(ast.declarations.length).toBe(3);
expect(ast.declarations[0]).toBe(one);
expect(ast.declarations[1]).toBe(two);
});
it('accepts a function', function() {
const x = b.identifier('x');
const foo = b.identifier('foo');
const bar = b.identifier('bar');
const ast = b.sequenceExpression([foo, bar]); // eslint-disable-line no-shadow
Collection.fromNodes([ast])
.find(types.Identifier)
.insertBefore(function() {
return x;
});
expect(ast.expressions.length).toBe(4);
expect(ast.expressions).toEqual([x, foo, x, bar]);
expect(ast.expressions[0]).toBe(x);
expect(ast.expressions[2]).toBe(x);
});
});
describe('insertAfter', function() {
it('inserts a new node after the current one', function() {
const ast = b.variableDeclaration( // eslint-disable-line no-shadow
'var',
[b.variableDeclarator(b.identifier('foo'), null)]
);
const one = b.variableDeclarator(b.identifier('one'), null);
Collection.fromNodes([ast])
.find(types.VariableDeclarator)
.insertAfter(one);
expect(ast.declarations.length).toBe(2);
expect(ast.declarations[1]).toBe(one);
});
it('accepts an array of nodes', function() {
const ast = b.variableDeclaration( // eslint-disable-line no-shadow
'var',
[b.variableDeclarator(b.identifier('foo'), null)]
);
const one = b.variableDeclarator(b.identifier('one'), null);
const two = b.variableDeclarator(b.identifier('two'), null);
Collection.fromNodes([ast])
.find(types.VariableDeclarator)
.insertAfter([one, two]);
expect(ast.declarations.length).toBe(3);
expect(ast.declarations[1]).toBe(one);
expect(ast.declarations[2]).toBe(two);
});
it('accepts a function', function() {
const x = b.identifier('x');
const foo = b.identifier('foo');
const bar = b.identifier('bar');
const ast = b.sequenceExpression([foo, bar]); // eslint-disable-line no-shadow
Collection.fromNodes([ast])
.find(types.Identifier)
.insertAfter(function() {
return x;
});
expect(ast.expressions.length).toBe(4);
expect(ast.expressions).toEqual([foo, x, bar, x]);
expect(ast.expressions[1]).toBe(x);
expect(ast.expressions[3]).toBe(x);
});
});
describe('removes', function() {
it('removes a node if it is part of the body of a statement', function() {
const x = b.expressionStatement(b.identifier('x'));
const y = b.expressionStatement(b.identifier('y'));
const ast = b.program([x, y]); // eslint-disable-line no-shadow
Collection.fromNodes([ast])
.find(types.Identifier, {name: 'x'})
.remove();
expect(ast.body.length).toBe(1);
expect(ast.body[0]).toBe(y);
});
it('removes a node if it is a function param', function() {
const x = b.identifier('x');
const y = b.identifier('y');
const ast = b.arrowFunctionExpression( // eslint-disable-line no-shadow
[x, b.identifier('z')],
y
);
Collection.fromNodes([ast])
.find(types.Identifier, {name: 'z'})
.remove();
expect(ast.params.length).toBe(1);
expect(ast.params[0]).toBe(x);
expect(ast.body).toBe(y);
});
});
});
});
================================================
FILE: src/collections/__tests__/VariableDeclarator-test.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const getParser = require('./../../getParser');
const recast = require('recast');
const types = recast.types.namedTypes;
describe('VariableDeclarators', function() {
let nodes;
let Collection;
let VariableDeclaratorCollection;
beforeEach(function() {
jest.resetModules();
Collection = require('../../Collection');
VariableDeclaratorCollection = require('../VariableDeclarator');
VariableDeclaratorCollection.register();
nodes = [recast.parse([
'var foo = 42;',
'var bar = require("module");',
'var baz = require("module2");',
'function first() {',
' var x = bar;',
' bar.someMethod();',
' func1(bar);',
'}',
'function func1(bar) {',
' var bar = 21;',
'}',
'foo.bar();',
'foo[bar]();',
'bar.foo();',
'function second() {',
' var blah;',
' var obj = {',
' blah: 4,',
' blah() {},',
' };',
' obj.blah = 3;',
' class A {',
' blah = 10',
' blah() {}',
' }',
'}',
'class Foo { @decorator\n*stuff() {} }',
gitextract_10vd5d0w/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── cr.yml
│ ├── pull_request.yml
│ ├── push.yml
│ └── test.yml
├── .gitignore
├── .node-version
├── .npmignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│ ├── __tests__/
│ │ └── jscodeshift-test.js
│ ├── jscodeshift.js
│ └── jscodeshift.sh
├── index.js
├── package.json
├── parser/
│ ├── __tests__/
│ │ ├── .eslintrc
│ │ ├── __snapshots__/
│ │ │ └── tsx-test.js.snap
│ │ └── tsx-test.js
│ ├── babel5Compat.js
│ ├── babylon.js
│ ├── flow.js
│ ├── ts.js
│ ├── tsOptions.js
│ └── tsx.js
├── recipes/
│ └── retain-first-comment.md
├── sample/
│ ├── __testfixtures__/
│ │ ├── reverse-identifiers.input.js
│ │ ├── reverse-identifiers.output.js
│ │ └── typescript/
│ │ ├── reverse-identifiers.input.ts
│ │ └── reverse-identifiers.output.ts
│ ├── __tests__/
│ │ ├── __snapshots__/
│ │ │ └── reverse-identifiers-test.js.snap
│ │ └── reverse-identifiers-test.js
│ └── reverse-identifiers.js
├── src/
│ ├── Collection.js
│ ├── Runner.js
│ ├── Worker.js
│ ├── __testfixtures__/
│ │ ├── test-async-transform.input.js
│ │ ├── test-async-transform.js
│ │ ├── test-async-transform.output.js
│ │ ├── test-sync-transform.input.js
│ │ ├── test-sync-transform.js
│ │ └── test-sync-transform.output.js
│ ├── __tests__/
│ │ ├── .eslintrc
│ │ ├── Collection-test.js
│ │ ├── Worker-test.js
│ │ ├── __snapshots__/
│ │ │ └── testUtils-test.js.snap
│ │ ├── argsParser-test.js
│ │ ├── core-test.js
│ │ ├── matchNode-test.js
│ │ ├── template-test.js
│ │ ├── testUtils-test.js
│ │ └── ts-decorator-auto-accessor-test.js
│ ├── argsParser.js
│ ├── collections/
│ │ ├── ImportDeclaration.js
│ │ ├── JSXElement.js
│ │ ├── Node.js
│ │ ├── VariableDeclarator.js
│ │ ├── __tests__/
│ │ │ ├── .eslintrc
│ │ │ ├── .jshintrc
│ │ │ ├── ImportDeclaration-test.js
│ │ │ ├── JSXElement-test.js
│ │ │ ├── Node-test.js
│ │ │ └── VariableDeclarator-test.js
│ │ └── index.js
│ ├── core.js
│ ├── getParser.js
│ ├── ignoreFiles.js
│ ├── matchNode.js
│ ├── template.js
│ ├── testUtils.js
│ └── utils/
│ ├── __tests__/
│ │ ├── intersection-test.js
│ │ ├── once-test.js
│ │ └── union-test.js
│ ├── intersection.js
│ ├── once.js
│ └── union.js
├── utils/
│ ├── requirePackage.js
│ └── testUtils.js
└── website/
├── .astro/
│ ├── settings.json
│ └── types.d.ts
├── README.md
├── astro.config.mjs
├── package.json
├── src/
│ ├── content/
│ │ ├── config.ts
│ │ └── docs/
│ │ ├── build/
│ │ │ ├── api-reference.mdx
│ │ │ └── ast-grammar.mdx
│ │ ├── index.mdx
│ │ ├── overview/
│ │ │ └── introduction.mdx
│ │ └── run/
│ │ └── cli.mdx
│ └── env.d.ts
└── tsconfig.json
SYMBOL INDEX (135 symbols across 32 files)
FILE: bin/__tests__/jscodeshift-test.js
function readFile (line 25) | function readFile(path) {
function run (line 29) | function run(args, stdin, cwd) {
FILE: bin/jscodeshift.js
function run (line 168) | function run(paths, options) {
FILE: parser/babel5Compat.js
method parse (line 46) | parse(code) {
FILE: parser/babylon.js
method parse (line 53) | parse(code) {
FILE: parser/flow.js
method parse (line 30) | parse(code) {
FILE: parser/ts.js
method parse (line 20) | parse(code) {
FILE: parser/tsx.js
method parse (line 23) | parse(code) {
FILE: sample/__testfixtures__/reverse-identifiers.input.js
class Foo (line 5) | class Foo {
method bar (line 6) | @decorated
FILE: sample/__testfixtures__/reverse-identifiers.output.js
class ooF (line 5) | class ooF {
method rab (line 6) | @detaroced
FILE: sample/reverse-identifiers.js
function transformer (line 12) | function transformer(file, api) {
FILE: src/Collection.js
class Collection (line 31) | class Collection {
method constructor (line 40) | constructor(paths, parent, types) {
method filter (line 63) | filter(callback) {
method forEach (line 73) | forEach(callback) {
method some (line 86) | some(callback) {
method every (line 98) | every(callback) {
method map (line 117) | map(callback, type) {
method size (line 140) | size() {
method length (line 149) | get length() {
method nodes (line 158) | nodes() {
method paths (line 162) | paths() {
method getAST (line 166) | getAST() {
method toSource (line 173) | toSource(options) {
method at (line 195) | at(index) {
method get (line 210) | get() {
method getTypes (line 227) | getTypes() {
method isOfType (line 237) | isOfType(type) {
function _inferTypes (line 248) | function _inferTypes(paths) {
function _toTypeArray (line 271) | function _toTypeArray(value) {
function _getSupertypeNames (line 283) | function _getSupertypeNames(type) {
function fromPaths (line 315) | function fromPaths(paths, parent, type) {
function fromNodes (line 336) | function fromNodes(nodes, parent, type) {
function registerMethods (line 358) | function registerMethods(methods, type) {
function installTypedMethod (line 400) | function installTypedMethod(methodName) {
function hasConflictingRegistration (line 428) | function hasConflictingRegistration(methodName, type) {
function setDefaultCollectionType (line 464) | function setDefaultCollectionType(type) {
FILE: src/Runner.js
constant CHUNK_SIZE (line 23) | const CHUNK_SIZE = 50;
function lineBreak (line 25) | function lineBreak(str) {
method ok (line 51) | ok(msg, verbose) {
method nochange (line 54) | nochange(msg, verbose) {
method skip (line 57) | skip(msg, verbose) {
method error (line 60) | error(msg, verbose) {
function report (line 65) | function report({file, msg}) {
function concatAll (line 69) | function concatAll(arrays) {
function showFileStats (line 79) | function showFileStats(fileStats) {
function showStats (line 89) | function showStats(stats) {
function dirFiles (line 97) | function dirFiles (dir, callback, acc) {
function getAllFiles (line 138) | function getAllFiles(paths, filter) {
function run (line 164) | function run(transformFile, paths, options) {
FILE: src/Worker.js
function prepareJscodeshift (line 48) | function prepareJscodeshift(options) {
function setup (line 54) | function setup(tr, babel) {
function free (line 104) | function free() {
function updateStatus (line 108) | function updateStatus(status, file, msg) {
function report (line 113) | function report(file, msg) {
function empty (line 117) | function empty() {}
function stats (line 119) | function stats(name, quantity) {
function trimStackTrace (line 124) | function trimStackTrace(trace) {
function run (line 140) | function run(data) {
FILE: src/__tests__/Collection-test.js
function typeFilter (line 160) | function typeFilter(type) {
function register (line 177) | function register(methodName, type) {
FILE: src/__tests__/Worker-test.js
function getTransformForParser (line 55) | function getTransformForParser(parser) {
function getSourceFile (line 66) | function getSourceFile() {
FILE: src/__tests__/argsParser-test.js
function test (line 59) | function test(testCases) {
FILE: src/__tests__/core-test.js
function plugin (line 67) | function plugin() {
FILE: src/__tests__/ts-decorator-auto-accessor-test.js
function transformer (line 3) | function transformer(file, api) {
FILE: src/argsParser.js
function throwError (line 8) | function throwError(exitCode, message, helpText) {
function formatOption (line 16) | function formatOption(option) {
function getHelpText (line 48) | function getHelpText(options) {
function validateOptions (line 71) | function validateOptions(parsedOptions, options) {
function prepareOptions (line 90) | function prepareOptions(options) {
function isOption (line 121) | function isOption(value) {
function parse (line 125) | function parse(options, args=process.argv.slice(2)) {
method options (line 263) | options(options) {
FILE: src/collections/ImportDeclaration.js
function register (line 108) | function register() {
FILE: src/collections/JSXElement.js
function register (line 213) | function register() {
FILE: src/collections/Node.js
function visit (line 37) | function visit(path) {
function toArray (line 123) | function toArray(value) {
function register (line 181) | function register() {
FILE: src/collections/VariableDeclarator.js
function register (line 203) | function register() {
FILE: src/core.js
function core (line 42) | function core(source, options) {
function fromAST (line 56) | function fromAST(ast) {
function fromSource (line 75) | function fromSource(source, options) {
function match (line 93) | function match(path, filter) {
function use (line 117) | function use(plugin) {
function withParser (line 131) | function withParser(parser) {
function enrichCore (line 154) | function enrichCore(core, parser) {
FILE: src/ignoreFiles.js
function addIgnorePattern (line 14) | function addIgnorePattern(val) {
function addIgnoreFromInput (line 31) | function addIgnoreFromInput(input) {
function addIgnoreFromFile (line 43) | function addIgnoreFromFile(input) {
function shouldIgnore (line 61) | function shouldIgnore(path) {
FILE: src/matchNode.js
function matchNode (line 21) | function matchNode(haystack, needle) {
function isNode (line 36) | function isNode(value) {
FILE: src/template.js
function splice (line 16) | function splice(arr, element, replacement) {
function cleanLocation (line 20) | function cleanLocation(node) {
function ensureStatement (line 27) | function ensureStatement(node) {
function getVistor (line 35) | function getVistor(varNames, nodes) {
function replaceNodes (line 98) | function replaceNodes(src, varNames, nodes, parser) {
function getUniqueVarName (line 105) | function getUniqueVarName() {
function statements (line 111) | function statements(template/*, ...nodes*/) {
function statement (line 127) | function statement(/*template, ...nodes*/) {
function expression (line 131) | function expression(template/*, ...nodes*/) {
function asyncExpression (line 152) | function asyncExpression(template/*, ...nodes*/) {
FILE: src/testUtils.js
function applyTransform (line 15) | function applyTransform(module, options, input, testOptions = {}) {
function runSnapshotTest (line 45) | function runSnapshotTest(module, options, input) {
function runInlineTest (line 58) | function runInlineTest(module, options, input, expectedOutput, testOptio...
function extensionForParser (line 72) | function extensionForParser(parser) {
function runTest (line 101) | function runTest(dirName, transformName, options, testFilePrefix, testOp...
function defineTest (line 129) | function defineTest(dirName, transformName, options, testFilePrefix, tes...
function defineInlineTest (line 142) | function defineInlineTest(module, options, input, expectedOutput, testNa...
function defineSnapshotTest (line 152) | function defineSnapshotTest(module, options, input, testName) {
function defineSnapshotTestFromFixture (line 165) | function defineSnapshotTestFromFixture(dirName, module, options, testFil...
FILE: src/utils/__tests__/intersection-test.js
function test (line 12) | function test(testCases) {
FILE: src/utils/__tests__/union-test.js
function test (line 12) | function test(testCases) {
FILE: utils/testUtils.js
function renameFileTo (line 15) | function renameFileTo(oldPath, newFilename, extension = '') {
function createTempFileWith (line 23) | function createTempFileWith(content, filename, extension) {
function createTransformWith (line 37) | function createTransformWith(content, ext='.js') {
function getFileContent (line 46) | function getFileContent(filePath) {
FILE: website/.astro/types.d.ts
type Render (line 2) | interface Render {
type Render (line 12) | interface Render {
type Flatten (line 22) | type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
type CollectionKey (line 24) | type CollectionKey = keyof AnyEntryMap;
type CollectionEntry (line 25) | type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
type ContentCollectionKey (line 27) | type ContentCollectionKey = keyof ContentEntryMap;
type DataCollectionKey (line 28) | type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf (line 30) | type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug (line 31) | type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
type ReturnTypeOrOriginal (line 132) | type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R...
type InferEntrySchema (line 133) | type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod')...
type ContentEntryMap (line 137) | type ContentEntryMap = {
type DataEntryMap (line 178) | type DataEntryMap = {};
type AnyEntryMap (line 180) | type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ContentConfig (line 182) | type ContentConfig = typeof import("../src/content/config.js");
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (339K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 271,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@3.0.3/schema.json\",\n \"changelog\": \"@changesets/cli/changelog\",\n \""
},
{
"path": ".editorconfig",
"chars": 131,
"preview": "root = true\n\n[*]\nend_of_line = lf\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitesp"
},
{
"path": ".eslintignore",
"chars": 40,
"preview": "/dist/\n/docs/\n/sample/__testfixtures__/\n"
},
{
"path": ".eslintrc",
"chars": 315,
"preview": "{\n \"extends\": \"eslint:recommended\",\n \"parser\": \"@babel/eslint-parser\",\n \"rules\": {\n \"comma-dangle\": 0,\n \"no-und"
},
{
"path": ".gitattributes",
"chars": 26,
"preview": "bin/jscodeshift.js eol=lf\n"
},
{
"path": ".github/workflows/cr.yml",
"chars": 444,
"preview": "name: Continuous Releases\n\non:\n push:\n pull_request:\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n - us"
},
{
"path": ".github/workflows/pull_request.yml",
"chars": 120,
"preview": "name: pull_request\n\non:\n pull_request:\n branches: [main]\n\njobs:\n call-test:\n uses: ./.github/workflows/test.yml\n"
},
{
"path": ".github/workflows/push.yml",
"chars": 698,
"preview": "name: push\n\non:\n push:\n branches: [main]\n\njobs:\n call-test:\n uses: ./.github/workflows/test.yml\n release:\n n"
},
{
"path": ".github/workflows/test.yml",
"chars": 485,
"preview": "name: test\n\non:\n workflow_call:\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n strategy:\n matrix:\n node-ve"
},
{
"path": ".gitignore",
"chars": 20,
"preview": "/dist\nnode_modules/\n"
},
{
"path": ".node-version",
"chars": 8,
"preview": "16.20.2\n"
},
{
"path": ".npmignore",
"chars": 113,
"preview": "/docs/\n/test/\n/sample/\n/recipes/\n.gitignore\n.eslintrc\n.eslintrc.yaml\n.jshintrc\n.module-cache\n__tests__\nyarn.lock\n"
},
{
"path": "CHANGELOG.md",
"chars": 7172,
"preview": "# Changelog\n\n## 17.3.0\n\n### Minor Changes\n\n- 6c2ff57: Bumps recast to allow parsing of Typescript type arguments on tagg"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 249,
"preview": "# Code of Conduct\n\nFacebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read"
},
{
"path": "CONTRIBUTING.md",
"chars": 1657,
"preview": "# Contributing to jscodeshift\nWe want to make contributing to this project as easy and transparent as\npossible.\n\n## Code"
},
{
"path": "LICENSE",
"chars": 1086,
"preview": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any pers"
},
{
"path": "README.md",
"chars": 24003,
"preview": "# [jscodeshift](https://jscodeshift.com/) [ Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "bin/jscodeshift.js",
"chars": 4982,
"preview": "#!/usr/bin/env node\n\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under th"
},
{
"path": "bin/jscodeshift.sh",
"chars": 255,
"preview": "#!/usr/bin/env node\n\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under th"
},
{
"path": "index.js",
"chars": 231,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "package.json",
"chars": 1967,
"preview": "{\n \"name\": \"jscodeshift\",\n \"version\": \"17.3.0\",\n \"description\": \"A toolkit for JavaScript codemods\",\n \"repository\": "
},
{
"path": "parser/__tests__/.eslintrc",
"chars": 76,
"preview": "{\n \"globals\": {\n \"jest\": true\n },\n \"env\": {\n \"jasmine\": true\n }\n}\n"
},
{
"path": "parser/__tests__/__snapshots__/tsx-test.js.snap",
"chars": 1031,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`tsxParser parse extends the ts config with jsx support 1`] = `\n[\n "
},
{
"path": "parser/__tests__/tsx-test.js",
"chars": 689,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "parser/babel5Compat.js",
"chars": 1181,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "parser/babylon.js",
"chars": 1218,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "parser/flow.js",
"chars": 734,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "parser/ts.js",
"chars": 504,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "parser/tsOptions.js",
"chars": 1033,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "parser/tsx.js",
"chars": 612,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "recipes/retain-first-comment.md",
"chars": 1789,
"preview": "# Retain comment on first line\n\n## Problem\n\nWhen removing or replacing the first statement in a file, it is possible for"
},
{
"path": "sample/__testfixtures__/reverse-identifiers.input.js",
"chars": 131,
"preview": "var firstWord = 'Hello ';\nvar secondWord = 'world';\nvar message = firstWord + secondWord;\n\nclass Foo {\n @decorated\n *b"
},
{
"path": "sample/__testfixtures__/reverse-identifiers.output.js",
"chars": 131,
"preview": "var droWtsrif = 'Hello ';\nvar droWdnoces = 'world';\nvar egassem = droWtsrif + droWdnoces;\n\nclass ooF {\n @detaroced\n *r"
},
{
"path": "sample/__testfixtures__/typescript/reverse-identifiers.input.ts",
"chars": 177,
"preview": "import type { Stream } from 'stream';\n\nconst firstWord = 'Hello ';\nconst secondWord = 'world';\nconst message = firstWord"
},
{
"path": "sample/__testfixtures__/typescript/reverse-identifiers.output.ts",
"chars": 177,
"preview": "import type { maertS } from 'stream';\n\nconst droWtsrif = 'Hello ';\nconst droWdnoces = 'world';\nconst egassem = droWtsrif"
},
{
"path": "sample/__tests__/__snapshots__/reverse-identifiers-test.js.snap",
"chars": 218,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`transforms correctly 1`] = `\n\"var droWtsrif = 'Hello ';\nvar droWdno"
},
{
"path": "sample/__tests__/reverse-identifiers-test.js",
"chars": 1568,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "sample/reverse-identifiers.js",
"chars": 535,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/Collection.js",
"chars": 12139,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/Runner.js",
"chars": 9405,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/Worker.js",
"chars": 5611,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/__testfixtures__/test-async-transform.input.js",
"chars": 36,
"preview": "export const sum = (a, b) => a + b;\n"
},
{
"path": "src/__testfixtures__/test-async-transform.js",
"chars": 323,
"preview": "const synchronousTestTransform = (fileInfo, api, options) => {\n return new Promise(resolve => {\n setTimeout(() => {\n"
},
{
"path": "src/__testfixtures__/test-async-transform.output.js",
"chars": 41,
"preview": "export const addition = (a, b) => a + b;\n"
},
{
"path": "src/__testfixtures__/test-sync-transform.input.js",
"chars": 36,
"preview": "export const sum = (a, b) => a + b;\n"
},
{
"path": "src/__testfixtures__/test-sync-transform.js",
"chars": 229,
"preview": "const synchronousTestTransform = (fileInfo, api, options) => {\n return api.jscodeshift(fileInfo.source)\n .findVariab"
},
{
"path": "src/__testfixtures__/test-sync-transform.output.js",
"chars": 41,
"preview": "export const addition = (a, b) => a + b;\n"
},
{
"path": "src/__tests__/.eslintrc",
"chars": 76,
"preview": "{\n \"globals\": {\n \"jest\": true\n },\n \"env\": {\n \"jasmine\": true\n }\n}\n"
},
{
"path": "src/__tests__/Collection-test.js",
"chars": 10622,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/__tests__/Worker-test.js",
"chars": 4309,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/__tests__/__snapshots__/testUtils-test.js.snap",
"chars": 743,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`testUtils async should run async defineSnapshotTest 1`] = `\"export "
},
{
"path": "src/__tests__/argsParser-test.js",
"chars": 8558,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/__tests__/core-test.js",
"chars": 2053,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/__tests__/matchNode-test.js",
"chars": 3295,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/__tests__/template-test.js",
"chars": 5972,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/__tests__/testUtils-test.js",
"chars": 4117,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/__tests__/ts-decorator-auto-accessor-test.js",
"chars": 579,
"preview": "'use strict';\n\nfunction transformer(file, api) {\n const j = api.jscodeshift;\n\n return j(file.source).toSource();\n}\n\ntr"
},
{
"path": "src/argsParser.js",
"chars": 7727,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/collections/ImportDeclaration.js",
"chars": 2931,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/collections/JSXElement.js",
"chars": 5667,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/collections/Node.js",
"chars": 4733,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/collections/VariableDeclarator.js",
"chars": 5593,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/collections/__tests__/.eslintrc",
"chars": 76,
"preview": "{\n \"globals\": {\n \"jest\": true\n },\n \"env\": {\n \"jasmine\": true\n }\n}\n"
},
{
"path": "src/collections/__tests__/.jshintrc",
"chars": 109,
"preview": "{\n \"predef\": [\"jest\", \"describe\", \"beforeEach\" , \"xit\", \"it\", \"expect\"],\n \"node\": true,\n \"esnext\": true\n}\n"
},
{
"path": "src/collections/__tests__/ImportDeclaration-test.js",
"chars": 4469,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/collections/__tests__/JSXElement-test.js",
"chars": 5020,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/collections/__tests__/Node-test.js",
"chars": 13328,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/collections/__tests__/VariableDeclarator-test.js",
"chars": 7970,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/collections/index.js",
"chars": 387,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/core.js",
"chars": 4476,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/getParser.js",
"chars": 648,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/ignoreFiles.js",
"chars": 1771,
"preview": "'use strict';\n\nconst fs = require('fs');\nconst mm = require('micromatch');\n\nconst matchers = [];\n\n/**\n * Add glob patter"
},
{
"path": "src/matchNode.js",
"chars": 971,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/template.js",
"chars": 4907,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "src/testUtils.js",
"chars": 6006,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/utils/__tests__/intersection-test.js",
"chars": 797,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/utils/__tests__/once-test.js",
"chars": 559,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/utils/__tests__/union-test.js",
"chars": 765,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/utils/intersection.js",
"chars": 639,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/utils/once.js",
"chars": 478,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "src/utils/union.js",
"chars": 457,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "utils/requirePackage.js",
"chars": 595,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "utils/testUtils.js",
"chars": 1400,
"preview": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found "
},
{
"path": "website/.astro/settings.json",
"chars": 63,
"preview": "{\n \"_variables\": {\n \"lastUpdateCheck\": 1719853949463\n }\n}\n"
},
{
"path": "website/.astro/types.d.ts",
"chars": 5705,
"preview": "declare module 'astro:content' {\n interface Render {\n '.mdx': Promise<{\n Content: import('astro').MarkdownInsta"
},
{
"path": "website/README.md",
"chars": 1553,
"preview": "# jscodeshift docs\n\n[](https://starlight."
},
{
"path": "website/astro.config.mjs",
"chars": 900,
"preview": "import {defineConfig} from 'astro/config';\nimport starlight from '@astrojs/starlight';\n\n// https://astro.build/config\nex"
},
{
"path": "website/package.json",
"chars": 323,
"preview": "{\n \"name\": \"docs\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"scripts\": {\n \"dev\": \"astro dev\",\n \"start\": \"astro"
},
{
"path": "website/src/content/config.ts",
"chars": 185,
"preview": "import {defineCollection} from 'astro:content';\nimport {docsSchema} from '@astrojs/starlight/schema';\n\nexport const coll"
},
{
"path": "website/src/content/docs/build/api-reference.mdx",
"chars": 10280,
"preview": "---\ntitle: API Reference\n---\n\nimport {Steps, LinkCard, Card, CardGrid} from '@astrojs/starlight/components';\n\n`jscodeshi"
},
{
"path": "website/src/content/docs/build/ast-grammar.mdx",
"chars": 72264,
"preview": "---\ntitle: AST Grammar\n---\n\njscodeshift provides 278 node types which are mapped to their corresponding node type in `as"
},
{
"path": "website/src/content/docs/index.mdx",
"chars": 394,
"preview": "---\ntitle: Docs\ndescription: Get started with building and running jscodeshift codemods.\ntemplate: doc\nhero:\n tagline: "
},
{
"path": "website/src/content/docs/overview/introduction.mdx",
"chars": 1711,
"preview": "---\ntitle: Introduction\n---\n\nimport { Steps, LinkCard, Card, CardGrid } from '@astrojs/starlight/components';\n\n`jscodesh"
},
{
"path": "website/src/content/docs/run/cli.mdx",
"chars": 3437,
"preview": "---\ntitle: CLI Reference\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThe `jscodeshift` command-line int"
},
{
"path": "website/src/env.d.ts",
"chars": 85,
"preview": "/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />\n"
},
{
"path": "website/tsconfig.json",
"chars": 42,
"preview": "{\n \"extends\": \"astro/tsconfigs/strict\"\n}\n"
}
]
About this extraction
This page contains the full source code of the facebook/jscodeshift GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (311.4 KB), approximately 78.9k tokens, and a symbol index with 135 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.