feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,8 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -0,0 +1,3 @@
/dist/
/docs/
/sample/__testfixtures__/

View File

@@ -0,0 +1,27 @@
name: test
on:
workflow_call:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Run test
run: yarn test

View File

@@ -0,0 +1 @@
12.22.11

View File

@@ -0,0 +1,116 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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).

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,47 @@
# 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 `master`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. 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).

View File

@@ -0,0 +1,21 @@
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.

View File

@@ -0,0 +1,629 @@
# jscodeshift [![Support Ukraine](https://img.shields.io/badge/Support-Ukraine-FFD500?style=flat&labelColor=005BBB)](https://opensource.fb.com/support-ukraine) [![Build Status](https://travis-ci.org/facebook/jscodeshift.svg?branch=master)](https://travis-ci.org/facebook/jscodeshift)
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][]:
```
$ npm install -g jscodeshift
```
This will install the runner as `jscodeshift`.
## VSCode Debugger
[Configure VSCode to debug codemods](#vscode-debugging)
## Usage (CLI)
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
--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 transformPath = '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 do 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/blob/master/def/).
### 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
```
### Passing options to [recast]
You may want to change some of the output settings (like setting `'` instead of `"`).
This can be done by passing config options to [recast].
```js
.toSource({quote: 'single'}); // sets strings to use single quotes in transformed code.
```
You can also pass options to recast's `parse` method by passing an object to
jscodeshift as second argument:
```js
jscodeshift(source, {...})
```
More on config options [here](https://github.com/benjamn/recast/blob/52a7ec3eaaa37e78436841ed8afc948033a86252/lib/options.js#L61)
### Unit Testing
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.
```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.
```
{
// 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/

View File

@@ -0,0 +1,176 @@
#!/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 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: 'js',
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);
}

View File

@@ -0,0 +1,12 @@
#!/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');

View File

@@ -0,0 +1,472 @@
/**
* 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;

View File

@@ -0,0 +1,318 @@
/**
* 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 chalk = require('chalk');
const fs = require('graceful-fs');
const path = require('path');
const http = require('http');
const https = require('https');
const temp = require('temp');
const ignores = require('./ignoreFiles');
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(chalk.white.bgGreen(' OKK ') + msg);
},
nochange(msg, verbose) {
verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' NOC ') + msg);
},
skip(msg, verbose) {
verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' SKIP ') + msg);
},
error(msg, verbose) {
verbose >= 0 && bufferedWrite(chalk.white.bgRed(' ERR ') + msg);
},
};
function report({file, msg}) {
bufferedWrite(lineBreak(`${chalk.white.bgBlue(' 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'+
chalk.red(fileStats.error + ' errors\n')+
chalk.yellow(fileStats.nochange + ' unmodified\n')+
chalk.yellow(fileStats.skip + ' skipped\n')+
chalk.green(fileStats.ok + ' ok\n')
);
}
function showStats(stats) {
const names = Object.keys(stats).sort();
if (names.length) {
process.stdout.write(chalk.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 (ignores.shouldIgnore(file)) {
// ignoring the file
resolve([]);
} else {
resolve([file]);
}
})
}))
).then(concatAll);
}
function run(transformFile, paths, options) {
let usedRemoteScript = false;
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.ignorePattern);
ignores.addFromFile(options.ignoreConfig);
if (/^http/.test(transformFile)) {
usedRemoteScript = true;
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);
temp.open({ prefix: 'jscodeshift', suffix: ext }, (err, info) => {
if (err) return reject(err);
fs.write(info.fd, contents, function (err) {
if (err) return reject(err);
fs.close(info.fd, function(err) {
if (err) return reject(err);
transform(info.path).then(resolve, reject);
});
});
});
})
})
.on('error', (e) => {
reject(e);
});
});
} else if (!fs.existsSync(transformFile)) {
process.stderr.write(
chalk.white.bgRed('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(
chalk.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);
}
}
if (usedRemoteScript) {
temp.cleanupSync();
}
return Object.assign({
stats: statsCounter,
timeElapsed: timeElapsed
}, fileCounters);
})
);
}
}
exports.run = run;

View File

@@ -0,0 +1,208 @@
/**
* 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')({
babelrc: false,
presets,
plugins: [
require('@babel/plugin-proposal-class-properties').default,
require('@babel/plugin-proposal-nullish-coalescing-operator').default,
require('@babel/plugin-proposal-optional-chaining').default,
require('@babel/plugin-transform-modules-commonjs').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();
}
);
}

View File

@@ -0,0 +1,273 @@
/**
* 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);
},
};
},
};

View File

@@ -0,0 +1,221 @@
/**
* 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;

View File

@@ -0,0 +1,187 @@
/**
* 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);

View File

@@ -0,0 +1,201 @@
/**
* 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;
}
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;

View File

@@ -0,0 +1,13 @@
/**
* 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 = {
Node: require('./Node'),
JSXElement: require('./JSXElement'),
VariableDeclarator: require('./VariableDeclarator'),
};

View File

@@ -0,0 +1,183 @@
/**
* 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 collections = require('./collections');
const getParser = require('./getParser');
const matchNode = require('./matchNode');
const recast = require('recast');
const template = require('./template');
const Node = recast.types.namedTypes.Node;
const NodePath = recast.types.NodePath;
// Register all built-in collections
for (var name in collections) {
collections[name].register();
}
/**
* Main entry point to the tool. The function accepts multiple different kinds
* of arguments as a convenience. In particular the function accepts either
*
* - a string containing source code
* The string is parsed with Recast
* - a single AST node
* - a single node path
* - an array of nodes
* - an array of node paths
*
* @exports jscodeshift
* @param {Node|NodePath|Array|string} source
* @param {Object} options Options to pass to Recast when passing source code
* @return {Collection}
*/
function core(source, options) {
return typeof source === 'string' ?
fromSource(source, options) :
fromAST(source);
}
/**
* Returns a collection from a node, node path, array of nodes or array of node
* paths.
*
* @ignore
* @param {Node|NodePath|Array} source
* @return {Collection}
*/
function fromAST(ast) {
if (Array.isArray(ast)) {
if (ast[0] instanceof NodePath || ast.length === 0) {
return Collection.fromPaths(ast);
} else if (Node.check(ast[0])) {
return Collection.fromNodes(ast);
}
} else {
if (ast instanceof NodePath) {
return Collection.fromPaths([ast]);
} else if (Node.check(ast)) {
return Collection.fromNodes([ast]);
}
}
throw new TypeError(
'Received an unexpected value ' + Object.prototype.toString.call(ast)
);
}
function fromSource(source, options) {
if (!options) {
options = {};
}
if (!options.parser) {
options.parser = getParser();
}
return fromAST(recast.parse(source, options));
}
/**
* Utility function to match a node against a pattern.
* @augments core
* @static
* @param {Node|NodePath|Object} path
* @parma {Object} filter
* @return boolean
*/
function match(path, filter) {
if (!(path instanceof NodePath)) {
if (typeof path.get === 'function') {
path = path.get();
} else {
path = {value: path};
}
}
return matchNode(path.value, filter);
}
const plugins = [];
/**
* Utility function for registering plugins.
*
* Plugins are simple functions that are passed the core jscodeshift instance.
* They should extend jscodeshift by calling `registerMethods`, etc.
* This method guards against repeated registrations (the plugin callback will only be called once).
*
* @augments core
* @static
* @param {Function} plugin
*/
function use(plugin) {
if (plugins.indexOf(plugin) === -1) {
plugins.push(plugin);
plugin(core);
}
}
/**
* Returns a version of the core jscodeshift function "bound" to a specific
* parser.
*
* @augments core
* @static
*/
function withParser(parser) {
if (typeof parser === 'string') {
parser = getParser(parser);
}
const newCore = function(source, options) {
if (options && !options.parser) {
options.parser = parser;
} else {
options = {parser};
}
return core(source, options);
};
return enrichCore(newCore, parser);
}
/**
* The ast-types library
* @external astTypes
* @see {@link https://github.com/benjamn/ast-types}
*/
function enrichCore(core, parser) {
// add builders and types to the function for simple access
Object.assign(core, recast.types.namedTypes);
Object.assign(core, recast.types.builders);
core.registerMethods = Collection.registerMethods;
/**
* @augments core
* @type external:astTypes
*/
core.types = recast.types;
core.match = match;
core.template = template(parser);
// add mappings and filters to function
core.filters = {};
core.mappings = {};
for (const name in collections) {
if (collections[name].filters) {
core.filters[name] = collections[name].filters;
}
if (collections[name].mappings) {
core.mappings[name] = collections[name].mappings;
}
}
core.use = use;
core.withParser = withParser;
return core;
}
module.exports = enrichCore(core, getParser());

View File

@@ -0,0 +1,25 @@
/**
* 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';
module.exports = function getParser(parserName, options) {
switch (parserName) {
case 'babylon':
return require('../parser/babylon')(options);
case 'flow':
return require('../parser/flow')(options);
case 'ts':
return require('../parser/ts')(options);
case 'tsx':
return require('../parser/tsx')(options);
case 'babel':
default:
return require('../parser/babel5Compat')(options);
}
};

View File

@@ -0,0 +1,68 @@
'use strict';
const fs = require('fs');
const mm = require('micromatch');
const matchers = [];
/**
* Add glob patterns to ignore matched files and folders.
* Creates glob patterns to approximate gitignore patterns.
* @param {String} val - the glob or gitignore-style pattern to ignore
* @see {@linkplain https://git-scm.com/docs/gitignore#_pattern_format}
*/
function addIgnorePattern(val) {
if (val && typeof val === 'string' && val[0] !== '#') {
let pattern = val;
if (pattern.indexOf('/') === -1) {
matchers.push('**/' + pattern);
} else if (pattern[pattern.length-1] === '/') {
matchers.push('**/' + pattern + '**');
matchers.push(pattern + '**');
}
matchers.push(pattern);
}
}
/**
* Adds ignore patterns directly from function input
* @param {String|Array<String>} input - the ignore patterns
*/
function addIgnoreFromInput(input) {
let patterns = [];
if (input) {
patterns = patterns.concat(input);
}
patterns.forEach(addIgnorePattern);
}
/**
* Adds ignore patterns by reading files
* @param {String|Array<String>} input - the paths to the ignore config files
*/
function addIgnoreFromFile(input) {
let lines = [];
let files = [];
if (input) {
files = files.concat(input);
}
files.forEach(function(config) {
const stats = fs.statSync(config);
if (stats.isFile()) {
const content = fs.readFileSync(config, 'utf8');
lines = lines.concat(content.split(/\r?\n/));
}
});
lines.forEach(addIgnorePattern);
}
function shouldIgnore(path) {
const matched = matchers.length ? mm.isMatch(path, matchers, { dot:true }) : false;
return matched;
}
exports.add = addIgnoreFromInput;
exports.addFromFile = addIgnoreFromFile;
exports.shouldIgnore = shouldIgnore;

View File

@@ -0,0 +1,40 @@
/**
* 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 hasOwn =
Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
/**
* Checks whether needle is a strict subset of haystack.
*
* @param {*} haystack Value to test.
* @param {*} needle Test function or value to look for in `haystack`.
* @return {bool}
*/
function matchNode(haystack, needle) {
if (typeof needle === 'function') {
return needle(haystack);
}
if (isNode(needle) && isNode(haystack)) {
return Object.keys(needle).every(function(property) {
return (
hasOwn(haystack, property) &&
matchNode(haystack[property], needle[property])
);
});
}
return haystack === needle;
}
function isNode(value) {
return typeof value === 'object' && value;
}
module.exports = matchNode;

View File

@@ -0,0 +1,173 @@
/**
* 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 recast = require('recast');
const builders = recast.types.builders;
const types = recast.types.namedTypes;
function splice(arr, element, replacement) {
arr.splice.apply(arr, [arr.indexOf(element), 1].concat(replacement));
}
function cleanLocation(node) {
delete node.start;
delete node.end;
delete node.loc;
return node;
}
function ensureStatement(node) {
return types.Statement.check(node) ?
// Removing the location information seems to ensure that the node is
// correctly reprinted with a trailing semicolon
cleanLocation(node) :
builders.expressionStatement(node);
}
function getVistor(varNames, nodes) {
return {
visitIdentifier: function(path) {
this.traverse(path);
const node = path.node;
const parent = path.parent.node;
// If this identifier is not one of our generated ones, do nothing
const varIndex = varNames.indexOf(node.name);
if (varIndex === -1) {
return;
}
let replacement = nodes[varIndex];
nodes[varIndex] = null;
// If the replacement is an array, we need to explode the nodes in context
if (Array.isArray(replacement)) {
if (types.Function.check(parent) &&
parent.params.indexOf(node) > -1) {
// Function parameters: function foo(${bar}) {}
splice(parent.params, node, replacement);
} else if (types.VariableDeclarator.check(parent)) {
// Variable declarations: var foo = ${bar}, baz = 42;
splice(
path.parent.parent.node.declarations,
parent,
replacement
);
} else if (types.ArrayExpression.check(parent)) {
// Arrays: var foo = [${bar}, baz];
splice(parent.elements, node, replacement);
} else if (types.Property.check(parent) && parent.shorthand) {
// Objects: var foo = {${bar}, baz: 42};
splice(
path.parent.parent.node.properties,
parent,
replacement
);
} else if (types.CallExpression.check(parent) &&
parent.arguments.indexOf(node) > -1) {
// Function call arguments: foo(${bar}, baz)
splice(parent.arguments, node, replacement);
} else if (types.ExpressionStatement.check(parent)) {
// Generic sequence of statements: { ${foo}; bar; }
path.parent.replace.apply(
path.parent,
replacement.map(ensureStatement)
);
} else {
// Every else, let recast take care of it
path.replace.apply(path, replacement);
}
} else if (types.ExpressionStatement.check(parent)) {
path.parent.replace(ensureStatement(replacement));
} else {
path.replace(replacement);
}
}
};
}
function replaceNodes(src, varNames, nodes, parser) {
const ast = recast.parse(src, {parser});
recast.visit(ast, getVistor(varNames, nodes));
return ast;
}
let varNameCounter = 0;
function getUniqueVarName() {
return `$jscodeshift${varNameCounter++}$`;
}
module.exports = function withParser(parser) {
function statements(template/*, ...nodes*/) {
template = Array.from(template);
const nodes = Array.from(arguments).slice(1);
const varNames = nodes.map(() => getUniqueVarName());
const src = template.reduce(
(result, elem, i) => result + varNames[i - 1] + elem
);
return replaceNodes(
src,
varNames,
nodes,
parser
).program.body;
}
function statement(/*template, ...nodes*/) {
return statements.apply(null, arguments)[0];
}
function expression(template/*, ...nodes*/) {
// wrap code in `(...)` to force evaluation as expression
template = Array.from(template);
if (template.length > 0) {
template[0] = '(' + template[0];
template[template.length - 1] += ')';
}
const expression = statement.apply(
null,
[template].concat(Array.from(arguments).slice(1))
).expression;
// Remove added parens
if (expression.extra) {
expression.extra.parenthesized = false;
}
return expression;
}
function asyncExpression(template/*, ...nodes*/) {
template = Array.from(template);
if (template.length > 0) {
template[0] = 'async () => (' + template[0];
template[template.length - 1] += ')';
}
const expression = statement.apply(
null,
[template].concat(Array.from(arguments).slice(1))
).expression.body;
// Remove added parens
if (expression.extra) {
expression.extra.parenthesized = false;
}
return expression;
}
return {statements, statement, expression, asyncExpression};
}

View File

@@ -0,0 +1,149 @@
/**
* 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 expect, describe, it */
'use strict';
const fs = require('fs');
const path = require('path');
function applyTransform(module, options, input, testOptions = {}) {
// Handle ES6 modules using default export for the transform
const transform = module.default ? module.default : module;
// Jest resets the module registry after each test, so we need to always get
// a fresh copy of jscodeshift on every test run.
let jscodeshift = require('./core');
if (testOptions.parser || module.parser) {
jscodeshift = jscodeshift.withParser(testOptions.parser || module.parser);
}
const output = transform(
input,
{
jscodeshift,
j: jscodeshift,
stats: () => {},
},
options || {}
);
return (output || '').trim();
}
exports.applyTransform = applyTransform;
function runSnapshotTest(module, options, input) {
const output = applyTransform(module, options, input);
expect(output).toMatchSnapshot();
return output;
}
exports.runSnapshotTest = runSnapshotTest;
function runInlineTest(module, options, input, expectedOutput, testOptions) {
const output = applyTransform(module, options, input, testOptions);
expect(output).toEqual(expectedOutput.trim());
return output;
}
exports.runInlineTest = runInlineTest;
function extensionForParser(parser) {
switch (parser) {
case 'ts':
case 'tsx':
return parser;
default:
return 'js'
}
}
/**
* Utility function to run a jscodeshift script within a unit test. This makes
* several assumptions about the environment:
*
* - `dirName` contains the name of the directory the test is located in. This
* should normally be passed via __dirname.
* - The test should be located in a subdirectory next to the transform itself.
* Commonly tests are located in a directory called __tests__.
* - `transformName` contains the filename of the transform being tested,
* excluding the .js extension.
* - `testFilePrefix` optionally contains the name of the file with the test
* data. If not specified, it defaults to the same value as `transformName`.
* This will be suffixed with ".input.js" for the input file and ".output.js"
* for the expected output. For example, if set to "foo", we will read the
* "foo.input.js" file, pass this to the transform, and expect its output to
* be equal to the contents of "foo.output.js".
* - Test data should be located in a directory called __testfixtures__
* alongside the transform and __tests__ directory.
*/
function runTest(dirName, transformName, options, testFilePrefix, testOptions = {}) {
if (!testFilePrefix) {
testFilePrefix = transformName;
}
const extension = extensionForParser(testOptions.parser)
const fixtureDir = path.join(dirName, '..', '__testfixtures__');
const inputPath = path.join(fixtureDir, testFilePrefix + `.input.${extension}`);
const source = fs.readFileSync(inputPath, 'utf8');
const expectedOutput = fs.readFileSync(
path.join(fixtureDir, testFilePrefix + `.output.${extension}`),
'utf8'
);
// Assumes transform is one level up from __tests__ directory
const module = require(path.join(dirName, '..', transformName));
runInlineTest(module, options, {
path: inputPath,
source
}, expectedOutput, testOptions);
}
exports.runTest = runTest;
/**
* Handles some boilerplate around defining a simple jest/Jasmine test for a
* jscodeshift transform.
*/
function defineTest(dirName, transformName, options, testFilePrefix, testOptions) {
const testName = testFilePrefix
? `transforms correctly using "${testFilePrefix}" data`
: 'transforms correctly';
describe(transformName, () => {
it(testName, () => {
runTest(dirName, transformName, options, testFilePrefix, testOptions);
});
});
}
exports.defineTest = defineTest;
function defineInlineTest(module, options, input, expectedOutput, testName) {
it(testName || 'transforms correctly', () => {
runInlineTest(module, options, {
source: input
}, expectedOutput);
});
}
exports.defineInlineTest = defineInlineTest;
function defineSnapshotTest(module, options, input, testName) {
it(testName || 'transforms correctly', () => {
runSnapshotTest(module, options, {
source: input
});
});
}
exports.defineSnapshotTest = defineSnapshotTest;
/**
* Handles file-loading boilerplates, using same defaults as defineTest
*/
function defineSnapshotTestFromFixture(dirName, module, options, testFilePrefix, testName, testOptions = {}) {
const extension = extensionForParser(testOptions.parser)
const fixtureDir = path.join(dirName, '..', '__testfixtures__');
const inputPath = path.join(fixtureDir, testFilePrefix + `.input.${extension}`);
const source = fs.readFileSync(inputPath, 'utf8');
defineSnapshotTest(module, options, source, testName)
}
exports.defineSnapshotTestFromFixture = defineSnapshotTestFromFixture;

View File

@@ -0,0 +1,27 @@
/**
* 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 = function(arrays) {
const result = new Set(arrays[0]);
let resultSize = result.length;
let i, value, valuesToCheck;
for (i = 1; i < arrays.length; i++) {
valuesToCheck = new Set(arrays[i]);
for (value of result) {
if (!valuesToCheck.has(value)) {
result.delete(value);
resultSize -= 1;
}
if (resultSize === 0) {
return [];
}
}
}
return Array.from(result);
};

View File

@@ -0,0 +1,21 @@
/**
* 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.
*/
/**
* This replicates lodash's once functionality for our purposes.
*/
module.exports = function(func) {
let called = false;
let result;
return function(...args) {
if (called) {
return result;
}
called = true;
return result = func.apply(this, args);
};
};

View File

@@ -0,0 +1,20 @@
/**
* 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 = function(arrays) {
const result = new Set(arrays[0]);
let i,j, array;
for (i = 1; i < arrays.length; i++) {
array = arrays[i];
for (j = 0; j < array.length; j++) {
result.add(array[j]);
}
}
return Array.from(result);
};

View File

@@ -0,0 +1,9 @@
/**
* 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');

View File

@@ -0,0 +1,64 @@
{
"name": "jscodeshift",
"version": "0.14.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": {
"test": "jest --bail",
"docs": "rm -rf docs && jsdoc -d docs -R README.md src/collections/* src/core.js src/Collection.js"
},
"bin": {
"jscodeshift": "./bin/jscodeshift.js"
},
"keywords": [
"codemod",
"recast",
"babel"
],
"author": "Felix Kling",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.13.16",
"@babel/parser": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@babel/plugin-transform-modules-commonjs": "^7.13.8",
"@babel/preset-flow": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@babel/register": "^7.13.16",
"babel-core": "^7.0.0-bridge.0",
"chalk": "^4.1.2",
"flow-parser": "0.*",
"graceful-fs": "^4.2.4",
"micromatch": "^4.0.4",
"neo-async": "^2.5.0",
"node-dir": "^0.1.17",
"recast": "^0.21.0",
"temp": "^0.8.4",
"write-file-atomic": "^2.3.0"
},
"peerDependencies": {
"@babel/preset-env": "^7.1.6"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.9.0",
"jest": "^26",
"jsdoc": "3.6.7",
"mkdirp": "^0.5.1"
},
"jest": {
"roots": [
"src",
"bin",
"parser",
"sample"
]
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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);
},
};
};

View File

@@ -0,0 +1,57 @@
/**
* 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);
},
};
};

View File

@@ -0,0 +1,34 @@
/**
* 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);
},
};
};

View File

@@ -0,0 +1,24 @@
/**
* 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);
},
};
};

View File

@@ -0,0 +1,44 @@
/**
* 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',
'bigInt',
'classPrivateMethods',
'classPrivateProperties',
'classProperties',
'decorators-legacy',
'doExpressions',
'dynamicImport',
'exportDefaultFrom',
'exportExtensions',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importMeta',
'nullishCoalescingOperator',
'numericSeparator',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
['pipelineOperator', { proposal: 'minimal' }],
'throwExpressions',
'typescript'
],
};

View File

@@ -0,0 +1,27 @@
/**
* 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);
},
};
};

View File

@@ -0,0 +1,472 @@
/**
* 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;

View File

@@ -0,0 +1,318 @@
/**
* 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 chalk = require('chalk');
const fs = require('graceful-fs');
const path = require('path');
const http = require('http');
const https = require('https');
const temp = require('temp');
const ignores = require('./ignoreFiles');
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(chalk.white.bgGreen(' OKK ') + msg);
},
nochange(msg, verbose) {
verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' NOC ') + msg);
},
skip(msg, verbose) {
verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' SKIP ') + msg);
},
error(msg, verbose) {
verbose >= 0 && bufferedWrite(chalk.white.bgRed(' ERR ') + msg);
},
};
function report({file, msg}) {
bufferedWrite(lineBreak(`${chalk.white.bgBlue(' 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'+
chalk.red(fileStats.error + ' errors\n')+
chalk.yellow(fileStats.nochange + ' unmodified\n')+
chalk.yellow(fileStats.skip + ' skipped\n')+
chalk.green(fileStats.ok + ' ok\n')
);
}
function showStats(stats) {
const names = Object.keys(stats).sort();
if (names.length) {
process.stdout.write(chalk.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 (ignores.shouldIgnore(file)) {
// ignoring the file
resolve([]);
} else {
resolve([file]);
}
})
}))
).then(concatAll);
}
function run(transformFile, paths, options) {
let usedRemoteScript = false;
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.ignorePattern);
ignores.addFromFile(options.ignoreConfig);
if (/^http/.test(transformFile)) {
usedRemoteScript = true;
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);
temp.open({ prefix: 'jscodeshift', suffix: ext }, (err, info) => {
if (err) return reject(err);
fs.write(info.fd, contents, function (err) {
if (err) return reject(err);
fs.close(info.fd, function(err) {
if (err) return reject(err);
transform(info.path).then(resolve, reject);
});
});
});
})
})
.on('error', (e) => {
reject(e);
});
});
} else if (!fs.existsSync(transformFile)) {
process.stderr.write(
chalk.white.bgRed('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(
chalk.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);
}
}
if (usedRemoteScript) {
temp.cleanupSync();
}
return Object.assign({
stats: statsCounter,
timeElapsed: timeElapsed
}, fileCounters);
})
);
}
}
exports.run = run;

View File

@@ -0,0 +1,208 @@
/**
* 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')({
babelrc: false,
presets,
plugins: [
require('@babel/plugin-proposal-class-properties').default,
require('@babel/plugin-proposal-nullish-coalescing-operator').default,
require('@babel/plugin-proposal-optional-chaining').default,
require('@babel/plugin-transform-modules-commonjs').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();
}
);
}

View File

@@ -0,0 +1,273 @@
/**
* 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);
},
};
},
};

View File

@@ -0,0 +1,221 @@
/**
* 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;

View File

@@ -0,0 +1,187 @@
/**
* 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);

View File

@@ -0,0 +1,201 @@
/**
* 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;
}
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;

View File

@@ -0,0 +1,13 @@
/**
* 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 = {
Node: require('./Node'),
JSXElement: require('./JSXElement'),
VariableDeclarator: require('./VariableDeclarator'),
};

View File

@@ -0,0 +1,183 @@
/**
* 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 collections = require('./collections');
const getParser = require('./getParser');
const matchNode = require('./matchNode');
const recast = require('recast');
const template = require('./template');
const Node = recast.types.namedTypes.Node;
const NodePath = recast.types.NodePath;
// Register all built-in collections
for (var name in collections) {
collections[name].register();
}
/**
* Main entry point to the tool. The function accepts multiple different kinds
* of arguments as a convenience. In particular the function accepts either
*
* - a string containing source code
* The string is parsed with Recast
* - a single AST node
* - a single node path
* - an array of nodes
* - an array of node paths
*
* @exports jscodeshift
* @param {Node|NodePath|Array|string} source
* @param {Object} options Options to pass to Recast when passing source code
* @return {Collection}
*/
function core(source, options) {
return typeof source === 'string' ?
fromSource(source, options) :
fromAST(source);
}
/**
* Returns a collection from a node, node path, array of nodes or array of node
* paths.
*
* @ignore
* @param {Node|NodePath|Array} source
* @return {Collection}
*/
function fromAST(ast) {
if (Array.isArray(ast)) {
if (ast[0] instanceof NodePath || ast.length === 0) {
return Collection.fromPaths(ast);
} else if (Node.check(ast[0])) {
return Collection.fromNodes(ast);
}
} else {
if (ast instanceof NodePath) {
return Collection.fromPaths([ast]);
} else if (Node.check(ast)) {
return Collection.fromNodes([ast]);
}
}
throw new TypeError(
'Received an unexpected value ' + Object.prototype.toString.call(ast)
);
}
function fromSource(source, options) {
if (!options) {
options = {};
}
if (!options.parser) {
options.parser = getParser();
}
return fromAST(recast.parse(source, options));
}
/**
* Utility function to match a node against a pattern.
* @augments core
* @static
* @param {Node|NodePath|Object} path
* @parma {Object} filter
* @return boolean
*/
function match(path, filter) {
if (!(path instanceof NodePath)) {
if (typeof path.get === 'function') {
path = path.get();
} else {
path = {value: path};
}
}
return matchNode(path.value, filter);
}
const plugins = [];
/**
* Utility function for registering plugins.
*
* Plugins are simple functions that are passed the core jscodeshift instance.
* They should extend jscodeshift by calling `registerMethods`, etc.
* This method guards against repeated registrations (the plugin callback will only be called once).
*
* @augments core
* @static
* @param {Function} plugin
*/
function use(plugin) {
if (plugins.indexOf(plugin) === -1) {
plugins.push(plugin);
plugin(core);
}
}
/**
* Returns a version of the core jscodeshift function "bound" to a specific
* parser.
*
* @augments core
* @static
*/
function withParser(parser) {
if (typeof parser === 'string') {
parser = getParser(parser);
}
const newCore = function(source, options) {
if (options && !options.parser) {
options.parser = parser;
} else {
options = {parser};
}
return core(source, options);
};
return enrichCore(newCore, parser);
}
/**
* The ast-types library
* @external astTypes
* @see {@link https://github.com/benjamn/ast-types}
*/
function enrichCore(core, parser) {
// add builders and types to the function for simple access
Object.assign(core, recast.types.namedTypes);
Object.assign(core, recast.types.builders);
core.registerMethods = Collection.registerMethods;
/**
* @augments core
* @type external:astTypes
*/
core.types = recast.types;
core.match = match;
core.template = template(parser);
// add mappings and filters to function
core.filters = {};
core.mappings = {};
for (const name in collections) {
if (collections[name].filters) {
core.filters[name] = collections[name].filters;
}
if (collections[name].mappings) {
core.mappings[name] = collections[name].mappings;
}
}
core.use = use;
core.withParser = withParser;
return core;
}
module.exports = enrichCore(core, getParser());

View File

@@ -0,0 +1,25 @@
/**
* 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';
module.exports = function getParser(parserName, options) {
switch (parserName) {
case 'babylon':
return require('../parser/babylon')(options);
case 'flow':
return require('../parser/flow')(options);
case 'ts':
return require('../parser/ts')(options);
case 'tsx':
return require('../parser/tsx')(options);
case 'babel':
default:
return require('../parser/babel5Compat')(options);
}
};

View File

@@ -0,0 +1,68 @@
'use strict';
const fs = require('fs');
const mm = require('micromatch');
const matchers = [];
/**
* Add glob patterns to ignore matched files and folders.
* Creates glob patterns to approximate gitignore patterns.
* @param {String} val - the glob or gitignore-style pattern to ignore
* @see {@linkplain https://git-scm.com/docs/gitignore#_pattern_format}
*/
function addIgnorePattern(val) {
if (val && typeof val === 'string' && val[0] !== '#') {
let pattern = val;
if (pattern.indexOf('/') === -1) {
matchers.push('**/' + pattern);
} else if (pattern[pattern.length-1] === '/') {
matchers.push('**/' + pattern + '**');
matchers.push(pattern + '**');
}
matchers.push(pattern);
}
}
/**
* Adds ignore patterns directly from function input
* @param {String|Array<String>} input - the ignore patterns
*/
function addIgnoreFromInput(input) {
let patterns = [];
if (input) {
patterns = patterns.concat(input);
}
patterns.forEach(addIgnorePattern);
}
/**
* Adds ignore patterns by reading files
* @param {String|Array<String>} input - the paths to the ignore config files
*/
function addIgnoreFromFile(input) {
let lines = [];
let files = [];
if (input) {
files = files.concat(input);
}
files.forEach(function(config) {
const stats = fs.statSync(config);
if (stats.isFile()) {
const content = fs.readFileSync(config, 'utf8');
lines = lines.concat(content.split(/\r?\n/));
}
});
lines.forEach(addIgnorePattern);
}
function shouldIgnore(path) {
const matched = matchers.length ? mm.isMatch(path, matchers, { dot:true }) : false;
return matched;
}
exports.add = addIgnoreFromInput;
exports.addFromFile = addIgnoreFromFile;
exports.shouldIgnore = shouldIgnore;

View File

@@ -0,0 +1,40 @@
/**
* 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 hasOwn =
Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
/**
* Checks whether needle is a strict subset of haystack.
*
* @param {*} haystack Value to test.
* @param {*} needle Test function or value to look for in `haystack`.
* @return {bool}
*/
function matchNode(haystack, needle) {
if (typeof needle === 'function') {
return needle(haystack);
}
if (isNode(needle) && isNode(haystack)) {
return Object.keys(needle).every(function(property) {
return (
hasOwn(haystack, property) &&
matchNode(haystack[property], needle[property])
);
});
}
return haystack === needle;
}
function isNode(value) {
return typeof value === 'object' && value;
}
module.exports = matchNode;

View File

@@ -0,0 +1,173 @@
/**
* 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 recast = require('recast');
const builders = recast.types.builders;
const types = recast.types.namedTypes;
function splice(arr, element, replacement) {
arr.splice.apply(arr, [arr.indexOf(element), 1].concat(replacement));
}
function cleanLocation(node) {
delete node.start;
delete node.end;
delete node.loc;
return node;
}
function ensureStatement(node) {
return types.Statement.check(node) ?
// Removing the location information seems to ensure that the node is
// correctly reprinted with a trailing semicolon
cleanLocation(node) :
builders.expressionStatement(node);
}
function getVistor(varNames, nodes) {
return {
visitIdentifier: function(path) {
this.traverse(path);
const node = path.node;
const parent = path.parent.node;
// If this identifier is not one of our generated ones, do nothing
const varIndex = varNames.indexOf(node.name);
if (varIndex === -1) {
return;
}
let replacement = nodes[varIndex];
nodes[varIndex] = null;
// If the replacement is an array, we need to explode the nodes in context
if (Array.isArray(replacement)) {
if (types.Function.check(parent) &&
parent.params.indexOf(node) > -1) {
// Function parameters: function foo(${bar}) {}
splice(parent.params, node, replacement);
} else if (types.VariableDeclarator.check(parent)) {
// Variable declarations: var foo = ${bar}, baz = 42;
splice(
path.parent.parent.node.declarations,
parent,
replacement
);
} else if (types.ArrayExpression.check(parent)) {
// Arrays: var foo = [${bar}, baz];
splice(parent.elements, node, replacement);
} else if (types.Property.check(parent) && parent.shorthand) {
// Objects: var foo = {${bar}, baz: 42};
splice(
path.parent.parent.node.properties,
parent,
replacement
);
} else if (types.CallExpression.check(parent) &&
parent.arguments.indexOf(node) > -1) {
// Function call arguments: foo(${bar}, baz)
splice(parent.arguments, node, replacement);
} else if (types.ExpressionStatement.check(parent)) {
// Generic sequence of statements: { ${foo}; bar; }
path.parent.replace.apply(
path.parent,
replacement.map(ensureStatement)
);
} else {
// Every else, let recast take care of it
path.replace.apply(path, replacement);
}
} else if (types.ExpressionStatement.check(parent)) {
path.parent.replace(ensureStatement(replacement));
} else {
path.replace(replacement);
}
}
};
}
function replaceNodes(src, varNames, nodes, parser) {
const ast = recast.parse(src, {parser});
recast.visit(ast, getVistor(varNames, nodes));
return ast;
}
let varNameCounter = 0;
function getUniqueVarName() {
return `$jscodeshift${varNameCounter++}$`;
}
module.exports = function withParser(parser) {
function statements(template/*, ...nodes*/) {
template = Array.from(template);
const nodes = Array.from(arguments).slice(1);
const varNames = nodes.map(() => getUniqueVarName());
const src = template.reduce(
(result, elem, i) => result + varNames[i - 1] + elem
);
return replaceNodes(
src,
varNames,
nodes,
parser
).program.body;
}
function statement(/*template, ...nodes*/) {
return statements.apply(null, arguments)[0];
}
function expression(template/*, ...nodes*/) {
// wrap code in `(...)` to force evaluation as expression
template = Array.from(template);
if (template.length > 0) {
template[0] = '(' + template[0];
template[template.length - 1] += ')';
}
const expression = statement.apply(
null,
[template].concat(Array.from(arguments).slice(1))
).expression;
// Remove added parens
if (expression.extra) {
expression.extra.parenthesized = false;
}
return expression;
}
function asyncExpression(template/*, ...nodes*/) {
template = Array.from(template);
if (template.length > 0) {
template[0] = 'async () => (' + template[0];
template[template.length - 1] += ')';
}
const expression = statement.apply(
null,
[template].concat(Array.from(arguments).slice(1))
).expression.body;
// Remove added parens
if (expression.extra) {
expression.extra.parenthesized = false;
}
return expression;
}
return {statements, statement, expression, asyncExpression};
}

View File

@@ -0,0 +1,149 @@
/**
* 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 expect, describe, it */
'use strict';
const fs = require('fs');
const path = require('path');
function applyTransform(module, options, input, testOptions = {}) {
// Handle ES6 modules using default export for the transform
const transform = module.default ? module.default : module;
// Jest resets the module registry after each test, so we need to always get
// a fresh copy of jscodeshift on every test run.
let jscodeshift = require('./core');
if (testOptions.parser || module.parser) {
jscodeshift = jscodeshift.withParser(testOptions.parser || module.parser);
}
const output = transform(
input,
{
jscodeshift,
j: jscodeshift,
stats: () => {},
},
options || {}
);
return (output || '').trim();
}
exports.applyTransform = applyTransform;
function runSnapshotTest(module, options, input) {
const output = applyTransform(module, options, input);
expect(output).toMatchSnapshot();
return output;
}
exports.runSnapshotTest = runSnapshotTest;
function runInlineTest(module, options, input, expectedOutput, testOptions) {
const output = applyTransform(module, options, input, testOptions);
expect(output).toEqual(expectedOutput.trim());
return output;
}
exports.runInlineTest = runInlineTest;
function extensionForParser(parser) {
switch (parser) {
case 'ts':
case 'tsx':
return parser;
default:
return 'js'
}
}
/**
* Utility function to run a jscodeshift script within a unit test. This makes
* several assumptions about the environment:
*
* - `dirName` contains the name of the directory the test is located in. This
* should normally be passed via __dirname.
* - The test should be located in a subdirectory next to the transform itself.
* Commonly tests are located in a directory called __tests__.
* - `transformName` contains the filename of the transform being tested,
* excluding the .js extension.
* - `testFilePrefix` optionally contains the name of the file with the test
* data. If not specified, it defaults to the same value as `transformName`.
* This will be suffixed with ".input.js" for the input file and ".output.js"
* for the expected output. For example, if set to "foo", we will read the
* "foo.input.js" file, pass this to the transform, and expect its output to
* be equal to the contents of "foo.output.js".
* - Test data should be located in a directory called __testfixtures__
* alongside the transform and __tests__ directory.
*/
function runTest(dirName, transformName, options, testFilePrefix, testOptions = {}) {
if (!testFilePrefix) {
testFilePrefix = transformName;
}
const extension = extensionForParser(testOptions.parser)
const fixtureDir = path.join(dirName, '..', '__testfixtures__');
const inputPath = path.join(fixtureDir, testFilePrefix + `.input.${extension}`);
const source = fs.readFileSync(inputPath, 'utf8');
const expectedOutput = fs.readFileSync(
path.join(fixtureDir, testFilePrefix + `.output.${extension}`),
'utf8'
);
// Assumes transform is one level up from __tests__ directory
const module = require(path.join(dirName, '..', transformName));
runInlineTest(module, options, {
path: inputPath,
source
}, expectedOutput, testOptions);
}
exports.runTest = runTest;
/**
* Handles some boilerplate around defining a simple jest/Jasmine test for a
* jscodeshift transform.
*/
function defineTest(dirName, transformName, options, testFilePrefix, testOptions) {
const testName = testFilePrefix
? `transforms correctly using "${testFilePrefix}" data`
: 'transforms correctly';
describe(transformName, () => {
it(testName, () => {
runTest(dirName, transformName, options, testFilePrefix, testOptions);
});
});
}
exports.defineTest = defineTest;
function defineInlineTest(module, options, input, expectedOutput, testName) {
it(testName || 'transforms correctly', () => {
runInlineTest(module, options, {
source: input
}, expectedOutput);
});
}
exports.defineInlineTest = defineInlineTest;
function defineSnapshotTest(module, options, input, testName) {
it(testName || 'transforms correctly', () => {
runSnapshotTest(module, options, {
source: input
});
});
}
exports.defineSnapshotTest = defineSnapshotTest;
/**
* Handles file-loading boilerplates, using same defaults as defineTest
*/
function defineSnapshotTestFromFixture(dirName, module, options, testFilePrefix, testName, testOptions = {}) {
const extension = extensionForParser(testOptions.parser)
const fixtureDir = path.join(dirName, '..', '__testfixtures__');
const inputPath = path.join(fixtureDir, testFilePrefix + `.input.${extension}`);
const source = fs.readFileSync(inputPath, 'utf8');
defineSnapshotTest(module, options, source, testName)
}
exports.defineSnapshotTestFromFixture = defineSnapshotTestFromFixture;

View File

@@ -0,0 +1,27 @@
/**
* 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 = function(arrays) {
const result = new Set(arrays[0]);
let resultSize = result.length;
let i, value, valuesToCheck;
for (i = 1; i < arrays.length; i++) {
valuesToCheck = new Set(arrays[i]);
for (value of result) {
if (!valuesToCheck.has(value)) {
result.delete(value);
resultSize -= 1;
}
if (resultSize === 0) {
return [];
}
}
}
return Array.from(result);
};

View File

@@ -0,0 +1,21 @@
/**
* 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.
*/
/**
* This replicates lodash's once functionality for our purposes.
*/
module.exports = function(func) {
let called = false;
let result;
return function(...args) {
if (called) {
return result;
}
called = true;
return result = func.apply(this, args);
};
};

View File

@@ -0,0 +1,20 @@
/**
* 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 = function(arrays) {
const result = new Set(arrays[0]);
let i,j, array;
for (i = 1; i < arrays.length; i++) {
array = arrays[i];
for (j = 0; j < array.length; j++) {
result.add(array[j]);
}
}
return Array.from(result);
};

View File

@@ -0,0 +1,24 @@
/**
* 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 path = require('path');
module.exports = function requirePackage(name) {
const entry = require.resolve(name);
let dir = path.dirname(entry);
while (dir !== '/') {
try {
const pkg = require(path.join(dir, 'package.json'));
return pkg.name === name ? pkg : {};
} catch(error) {} // eslint-disable-line no-empty
dir = path.dirname(dir);
}
return {};
}

View File

@@ -0,0 +1,50 @@
/**
* 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 mkdirp = require('mkdirp');
const path = require('path');
const temp = require('temp');
function renameFileTo(oldPath, newFilename) {
const projectPath = path.dirname(oldPath);
const newPath = path.join(projectPath, newFilename);
mkdirp.sync(path.dirname(newPath));
fs.renameSync(oldPath, newPath);
return newPath;
}
function createTempFileWith(content, filename, extension) {
const info = temp.openSync({ suffix: extension });
let filePath = info.path;
fs.writeSync(info.fd, content);
fs.closeSync(info.fd);
if (filename) {
filePath = renameFileTo(filePath, filename);
}
return filePath;
}
exports.createTempFileWith = createTempFileWith;
// Test transform files need a js extension to work with @babel/register
// .ts or .tsx work as well
function createTransformWith(content, ext='.js') {
return createTempFileWith(
'module.exports = function(fileInfo, api, options) { ' + content + ' }',
undefined,
ext
);
}
exports.createTransformWith = createTransformWith;
function getFileContent(filePath) {
return fs.readFileSync(filePath).toString();
}
exports.getFileContent = getFileContent;