├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ ├── ci.yml
│ └── codeql.yml
├── .gitignore
├── .node-version
├── .prettierrc
├── .vscode
└── settings.json
├── CONTRIBUTING.md
├── README.md
├── codemod.code-workspace
├── commitlint.config.js
├── lerna.json
├── package.json
├── packages
├── cli
│ ├── .eslintignore
│ ├── CHANGELOG.md
│ ├── LICENSE.txt
│ ├── README.md
│ ├── __tests__
│ │ ├── cli
│ │ │ ├── cli.test.ts
│ │ │ ├── dry-run.test.ts
│ │ │ ├── errors.test.ts
│ │ │ ├── extensions.test.ts
│ │ │ ├── info.test.ts
│ │ │ ├── plugin-syntax.ts
│ │ │ ├── remote-plugin.test.ts
│ │ │ └── source-type.test.ts
│ │ ├── fixtures
│ │ │ ├── astexplorer
│ │ │ │ └── default.json
│ │ │ ├── babel-config
│ │ │ │ ├── babel.config.js
│ │ │ │ └── index.js
│ │ │ ├── glob-test
│ │ │ │ ├── abc.js
│ │ │ │ └── subdir
│ │ │ │ │ └── def.js
│ │ │ ├── plugin
│ │ │ │ ├── append-options-string.ts
│ │ │ │ ├── bad-plugin.js
│ │ │ │ ├── class-properties.ts
│ │ │ │ ├── generators.ts
│ │ │ │ ├── increment-export-default-multiple
│ │ │ │ │ ├── increment-export-default.js
│ │ │ │ │ └── increment-value.js
│ │ │ │ ├── increment-export-default.js
│ │ │ │ ├── increment-typescript.ts
│ │ │ │ ├── increment.js
│ │ │ │ ├── index.js
│ │ │ │ └── replace-any-with-object.ts
│ │ │ └── prettier
│ │ │ │ ├── defaults
│ │ │ │ ├── .prettierrc
│ │ │ │ └── index.jsx
│ │ │ │ └── with-config
│ │ │ │ ├── .prettierrc
│ │ │ │ └── index.js
│ │ ├── helpers
│ │ │ ├── TestServer.ts
│ │ │ ├── copyFixturesInto.ts
│ │ │ ├── createTemporaryDirectory.ts
│ │ │ ├── createTemporaryFile.ts
│ │ │ ├── plugin.ts
│ │ │ └── runCodemodCLI.ts
│ │ ├── integration.test.ts
│ │ └── unit
│ │ │ ├── Config.test.ts
│ │ │ ├── InlineTransformer.test.ts
│ │ │ ├── Options.test.ts
│ │ │ ├── TransformRunner.test.ts
│ │ │ ├── iterateSources.test.ts
│ │ │ └── resolvers
│ │ │ ├── AstExplorerResolver.test.ts
│ │ │ ├── FileSystemResolver.test.ts
│ │ │ └── NetworkResolver.test.ts
│ ├── bin
│ │ ├── codemod
│ │ └── codemod-dev
│ ├── examples
│ │ ├── convert-qunit-assert-expect-to-assert-async.ts
│ │ └── convert-static-class-to-named-exports.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── CLIEngine.ts
│ │ ├── Config.ts
│ │ ├── InlineTransformer.ts
│ │ ├── Options.ts
│ │ ├── PluginLoader.ts
│ │ ├── TransformRunner.ts
│ │ ├── Transformer.ts
│ │ ├── defineCodemod.ts
│ │ ├── extensions.ts
│ │ ├── index.ts
│ │ ├── iterateSources.ts
│ │ └── resolvers
│ │ │ ├── AstExplorerResolver.ts
│ │ │ ├── FileSystemResolver.ts
│ │ │ ├── NetworkResolver.ts
│ │ │ ├── PackageResolver.ts
│ │ │ └── Resolver.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── core
│ ├── .eslintignore
│ ├── CHANGELOG.md
│ ├── LICENSE.txt
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── AllSyntaxPlugin.ts
│ │ ├── BabelPluginTypes.ts
│ │ ├── RecastPlugin.ts
│ │ ├── __tests__
│ │ │ └── test.ts
│ │ ├── index.ts
│ │ └── transform.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── matchers
│ ├── .eslintignore
│ ├── CHANGELOG.md
│ ├── LICENSE.txt
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ ├── distributeAcrossSlices.test.ts
│ │ │ └── matchers.test.ts
│ │ ├── index.ts
│ │ ├── matchers
│ │ │ ├── Matcher.ts
│ │ │ ├── anyExpression.ts
│ │ │ ├── anyList.ts
│ │ │ ├── anyNode.ts
│ │ │ ├── anyNumber.ts
│ │ │ ├── anyStatement.ts
│ │ │ ├── anyString.ts
│ │ │ ├── anything.ts
│ │ │ ├── arrayOf.ts
│ │ │ ├── capture.ts
│ │ │ ├── containerOf.ts
│ │ │ ├── fromCapture.ts
│ │ │ ├── function.ts
│ │ │ ├── generated.ts
│ │ │ ├── index.ts
│ │ │ ├── oneOf.ts
│ │ │ ├── or.ts
│ │ │ ├── predicate.ts
│ │ │ ├── slice.ts
│ │ │ └── tupleOf.ts
│ │ └── utils
│ │ │ ├── distributeAcrossSlices.ts
│ │ │ ├── match.ts
│ │ │ └── matchPath.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── parser
│ ├── .eslintignore
│ ├── CHANGELOG.md
│ ├── LICENSE.txt
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ └── test.ts
│ │ ├── index.ts
│ │ └── options.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── rebuild-matchers
│ ├── .eslintignore
│ ├── CHANGELOG.md
│ ├── bin
│ │ └── rebuild
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ └── generated.test.ts
│ │ ├── rebuild.ts
│ │ └── utils
│ │ │ ├── ast.ts
│ │ │ ├── format.ts
│ │ │ ├── git.ts
│ │ │ └── rebuild.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
└── utils
│ ├── .eslintignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ ├── NodeTypes.ts
│ ├── __tests__
│ │ └── nodesEquivalent.test.ts
│ ├── builders.ts
│ ├── index.ts
│ ├── js.ts
│ └── nodesEquivalent.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.ts]
2 | indent_style = space
3 | indent_size = 2
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | packages/*/node_modules
3 | **/*.d.ts
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/eslint-recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:import/recommended",
8 | "plugin:import/typescript",
9 | "prettier"
10 | ],
11 | "plugins": ["@typescript-eslint", "import", "prettier"],
12 | "rules": {
13 | "prettier/prettier": ["error", { "singleQuote": true }],
14 | "@typescript-eslint/array-type": ["error", { "default": "generic" }],
15 | "@typescript-eslint/explicit-member-accessibility": "off",
16 | "@typescript-eslint/explicit-function-return-type": [
17 | "error",
18 | { "allowExpressions": true }
19 | ],
20 | "@typescript-eslint/no-use-before-define": [
21 | "error",
22 | { "functions": false }
23 | ],
24 | "@typescript-eslint/no-parameter-properties": "off",
25 | "@typescript-eslint/no-object-literal-type-assertion": "off",
26 | "@typescript-eslint/no-unused-vars": "error"
27 | },
28 | "settings": {
29 | "import/resolver": {
30 | "node": {
31 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
32 | }
33 | }
34 | },
35 | "overrides": [
36 | {
37 | "files": ["**/fixtures/**/*.ts"],
38 | "rules": {
39 | "no-console": "off"
40 | }
41 | },
42 | {
43 | "files": ["**/bin/*"],
44 | "env": {
45 | "node": true
46 | }
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [16.x, 18.x]
12 | package:
13 | - cli
14 | - core
15 | - matchers
16 | - parser
17 | - rebuild-matchers
18 | - utils
19 |
20 | env:
21 | CI: true
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - uses: pnpm/action-setup@v2
26 | with:
27 | version: 7
28 |
29 | - uses: actions/setup-node@v3
30 | with:
31 | node-version: ${{ matrix.node-version }}
32 | cache: 'pnpm'
33 |
34 | - name: Install Dependencies
35 | run: pnpm install --frozen-lockfile
36 |
37 | - name: Build
38 | working-directory: packages/${{ matrix.package }}
39 | run: pnpm build
40 |
41 | - name: Run Linter
42 | working-directory: packages/${{ matrix.package }}
43 | run: pnpm lint
44 |
45 | - name: Run Tests
46 | working-directory: packages/${{ matrix.package }}
47 | run: pnpm test
48 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | schedule:
9 | - cron: "54 14 * * 3"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ javascript ]
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v2
31 | with:
32 | languages: ${{ matrix.language }}
33 | queries: +security-and-quality
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v2
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v2
40 | with:
41 | category: "/language:${{ matrix.language }}"
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ignore pack
2 | *.tgz
3 |
4 | # ignore development
5 | node_modules
6 | *.log
7 |
8 | # ignore vim dotfiles
9 | *.swp
10 | *.swo
11 |
12 | # ignore idea directory
13 | .idea/
14 |
15 | # TypeScript build files
16 | build
17 | *.tsbuildinfo
18 |
19 | # allow fixtures
20 | !test/fixtures/**/*.js
21 | test/fixtures/**/*-typescript.js
22 |
23 | # allow JS config files
24 | !*.config.js
25 |
26 | # allow JS script files
27 | !script/_utils/runCommand.js
28 |
29 | # ignore tmp directory
30 | tmp
31 |
32 | # ignore custom build files
33 | packages/matchers/src/matchers.ts.expected
34 |
35 | # test coverage
36 | coverage
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 16.13.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.git": true,
4 | "**/.DS_Store": true,
5 | "**/*.js": { "when": "$(basename).ts" },
6 | "**/*.d.ts": { "when": "$(basename).ts" },
7 | "**/*.js.map": true
8 | },
9 | "typescript.tsdk": "node_modules/typescript/lib"
10 | }
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Much of the [prelude of the Babel contribution guide](https://github.com/babel/babel/blob/7.0/CONTRIBUTING.md#not-sure-where-to-start) also applies for this project, since it operates on and with Babel plugins. In particular:
4 |
5 | - [AST Explorer](http://astexplorer.net/#/scUfOmVOG5) is a very useful tool for examining ASTs and prototyping plugins.
6 | - [The Babel Plugin handbook](https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md#babel-plugin-handbook) is a great resource for understanding Babel plugins.
7 |
8 | This project is written with [TypeScript](https://www.typescriptlang.org/), a superset of JavaScript that allows explicit type annotations and includes a type checker.
9 |
10 | ## Developing
11 |
12 | codemod expects at least node 16 and pnpm. You can check each of these with `node -v` and `pnpm -v`. Look for instructions on installing node [here](https://nodejs.org) and pnpm [here](https://pnpm.io/).
13 |
14 | ### Setup
15 |
16 | ```sh
17 | $ git clone https://github.com/codemod-js/codemod
18 | $ cd codemod
19 | $ pnpm install
20 | ```
21 |
22 | Then make sure the tests pass:
23 |
24 | ```sh
25 | $ pnpm test
26 | ```
27 |
28 | ### Running linting/testing
29 |
30 | We use [ESLint](https://eslint.org/). To run ESLint on the project, run:
31 |
32 | ```sh
33 | $ pnpm lint
34 | ```
35 |
36 | To automatically fix some of the issues ESLint finds:
37 |
38 | ```sh
39 | $ pnpm lint:fix
40 | ```
41 |
42 | The tests in this project are written using [Jest](https://jestjs.io/) and, like the non-test code, are also written in TypeScript. To run the tests:
43 |
44 | ```sh
45 | $ pnpm test
46 | ```
47 |
48 | ## Submitting Changes
49 |
50 | We accept pull requests for bug fixes and improvements. For non-trivial changes it's usually a good idea to open an issue first to track the bug you'd like to fix or discuss the improvement you'd like to contribute.
51 |
52 | ### A good pull request…
53 |
54 | - **Is tested.** Any bugs fixed should have a test that fails without the fix. Any features added should have tests covering the expected usage and expected failures.
55 | - **Is documented.** Reference any existing issues that your pull request addresses. Provide a reasonable description in the pull request body. Documentation of expected usage for new features is required. Documentation is generally not needed for bug-fix pull requests, but sometimes a bug happened because an API was poorly understood. Consider improving the documentation such that a similar bug would not be introduced in the future.
56 | - **Is narrow in scope.** Pull requests that make broad, sweeping changes are generally a bad idea. Instead, larger refactors or improvements should be broken down into multiple smaller pull requests.
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codemod
2 |
3 | Code rewriting tools for automated refactors.
4 |
5 | ## Why Codemods?
6 |
7 | Sometimes big changes to your source code are required, and making such changes by hand is dangerous. For example, codemods can:
8 |
9 | - rename across a codebase safely
10 | - upgrade your uses of outdated APIs
11 | - perform complex automated refactors
12 | - much more!
13 |
14 | Since codemods are typically just code themselves, you can write one to do whatever you want.
15 |
16 | ## Getting Started
17 |
18 | Check out the docs for [@codemod/cli](packages/cli/README.md) for instructions on installing and using the `codemod` CLI tool.
19 |
20 | ## Repository Structure
21 |
22 | This repository is a monorepo, or multi-package repository. See the READMEs for the packages here:
23 |
24 | - [`@codemod/cli` README](packages/cli/README.md)
25 | - [`@codemod/core` README](packages/core/README.md)
26 | - [`@codemod/matchers` README](packages/matchers/README.md)
27 | - [`@codemod/parser` README](packages/parser/README.md)
28 |
29 | ## License
30 |
31 | Copyright 2017 Brian Donovan
32 |
33 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
34 |
35 | http://www.apache.org/licenses/LICENSE-2.0
36 |
37 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
38 |
--------------------------------------------------------------------------------
/codemod.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "packages/cli"
5 | },
6 | {
7 | "path": "packages/core"
8 | },
9 | {
10 | "path": "packages/matchers"
11 | },
12 | {
13 | "path": "packages/parser"
14 | },
15 | {
16 | "path": "packages/utils"
17 | },
18 | {
19 | "path": "packages/rebuild-matchers"
20 | }
21 | ],
22 | "settings": {
23 | "search.exclude": {
24 | "**/build": true,
25 | "**/coverage": true
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/*"],
3 | "version": "independent",
4 | "command": {
5 | "publish": {
6 | "conventionalCommits": true,
7 | "message": "chore(publish): publish new versions"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codemod",
3 | "private": true,
4 | "workspaces": {
5 | "packages": [
6 | "packages/*"
7 | ],
8 | "nohoist": [
9 | "**/@types/**"
10 | ]
11 | },
12 | "lint-staged": {
13 | "*.md": [
14 | "prettier --write"
15 | ],
16 | "*.ts": [
17 | "eslint --fix"
18 | ],
19 | "package.json": [
20 | "sort-package-json"
21 | ]
22 | },
23 | "resolutions": {
24 | "@babel/parser": "7.20.15"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^18.14.0",
28 | "@typescript-eslint/eslint-plugin": "^5.3.1",
29 | "@typescript-eslint/parser": "^5.3.1",
30 | "esbuild": "^0.13.13",
31 | "esbuild-runner": "^2.2.1",
32 | "eslint": "^8.2.0",
33 | "eslint-config-prettier": "^8.3.0",
34 | "eslint-plugin-import": "^2.27.5",
35 | "eslint-plugin-prettier": "^4.0.0",
36 | "husky": "^8.0.0",
37 | "jest": "^27.3.1",
38 | "lerna": "^4.0.0",
39 | "lint-staged": "^13.1.2",
40 | "prettier": "^2.4.1",
41 | "sort-package-json": "^1.53.1",
42 | "typescript": "^4.4.4"
43 | },
44 | "scripts": {
45 | "prepare": "husky install"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/cli/.eslintignore:
--------------------------------------------------------------------------------
1 | build
2 | coverage
3 | __tests__/fixtures
4 |
--------------------------------------------------------------------------------
/packages/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [3.3.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.2.0...@codemod/cli@3.3.0) (2023-02-18)
7 |
8 |
9 | ### Features
10 |
11 | * **cli:** add `defineCodemod` to ease creation ([86a62a1](https://github.com/codemod-js/codemod/commit/86a62a11d9f25f2e2e581ff6287ce885ce18f93a))
12 |
13 |
14 |
15 |
16 |
17 | # [3.2.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.1.1...@codemod/cli@3.2.0) (2023-02-17)
18 |
19 | ### Features
20 |
21 | - **cli:** add `--parser-plugins` option ([3593893](https://github.com/codemod-js/codemod/commit/3593893791c7e4e0e0c8cea31ea642b229c0bb8a))
22 |
23 | ## [3.1.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.0.1...@codemod/cli@3.1.0) (2021-11-14)
24 |
25 | - **feat:** improve gitignore handling ([e04c0c4](https://github.com/codemod-js/codemod/commit/e04c0c41d5cb3d86c6cdbbac932efcad968c73c9))
26 |
27 | ## [3.0.1](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.0.0...@codemod/cli@3.0.1) (2021-11-12)
28 |
29 | - **refactor:** use globby instead of a custom directory traversal (author: [NickHeiner](https://github.com/NickHeiner)) ([468092a](https://github.com/codemod-js/codemod/commit/468092afa532112ba2b126d949dcf0e38f0c2acd))
30 |
31 | ## [3.0.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.3.2...@codemod/cli@3.0.0) (2021-11-12)
32 |
33 | - remove underused options: `--printer`, `--babelrc`, `--find-babel-config` ([50a864d](https://github.com/codemod-js/codemod/commit/50a864df7344767a5c0e9e3ab990a0f4d05d634d))
34 | - update babel to v7.16.x ([1218bf9](https://github.com/codemod-js/codemod/commit/1218bf98145feaa8a692611152559aa6b46b9ba0))
35 |
36 | ## [2.1.16](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.13...@codemod/cli@2.1.16) (2020-04-05)
37 |
38 | ### Bug Fixes
39 |
40 | - **deps:** upgrade recast ([b6220f3](https://github.com/codemod-js/codemod/commit/b6220f3f26a41f4e58bdca7815bc8f6e9a820866))
41 | - update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d))
42 | - upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a))
43 | - upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102))
44 |
45 | ## [2.1.15](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.13...@codemod/cli@2.1.15) (2020-03-25)
46 |
47 | ### Bug Fixes
48 |
49 | - update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d))
50 | - upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a))
51 | - upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102))
52 |
53 | ## [2.1.11](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.10...@codemod/cli@2.1.11) (2019-10-16)
54 |
55 | ### Bug Fixes
56 |
57 | - **package:** remove unnecessary non-development dependency `jest` ([cf322a1](https://github.com/codemod-js/codemod/commit/cf322a1))
58 |
59 | ## [2.1.10](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.9...@codemod/cli@2.1.10) (2019-08-09)
60 |
61 | ### Bug Fixes
62 |
63 | - **license:** update outdated license files ([58e4b11](https://github.com/codemod-js/codemod/commit/58e4b11))
64 |
65 | ## [2.1.9](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.8...@codemod/cli@2.1.9) (2019-08-09)
66 |
67 | ### Bug Fixes
68 |
69 | - **package:** add "types" to package.json ([094d504](https://github.com/codemod-js/codemod/commit/094d504))
70 |
71 | ## [2.1.8](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.7...@codemod/cli@2.1.8) (2019-08-07)
72 |
73 | ### Bug Fixes
74 |
75 | - use `ParserOptions` from `@codemod/parser` ([09bc2b5](https://github.com/codemod-js/codemod/commit/09bc2b5))
76 |
77 | ## [2.1.7](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.6...@codemod/cli@2.1.7) (2019-08-02)
78 |
79 | ### Refactors
80 |
81 | - extract `@codemod/parser` and `@codemod/core` ([c43ecd52](https://github.com/codemod-js/codemod/commit/c43ecd52))
82 |
83 | ### Features
84 |
85 | - initial commit of [@codemod](https://github.com/codemod)/matchers ([84de839](https://github.com/codemod-js/codemod/commit/84de839))
86 |
87 | ## [2.1.6](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.5...@codemod/cli@2.1.6) (2019-07-31)
88 |
89 | ### Bug Fixes
90 |
91 | - update babel dependencies ([6984c7c](https://github.com/codemod-js/codemod/commit/6984c7c))
92 |
93 | ### Features
94 |
95 | - initial commit of `@codemod/matchers` ([84de839](https://github.com/codemod-js/codemod/commit/84de839))
96 |
--------------------------------------------------------------------------------
/packages/cli/README.md:
--------------------------------------------------------------------------------
1 | # codemod
2 |
3 | codemod rewrites JavaScript and TypeScript using babel plugins.
4 |
5 | ## Install
6 |
7 | Install from [npm](https://npmjs.com/):
8 |
9 | ```sh
10 | $ npm install @codemod/cli
11 | ```
12 |
13 | This will install the runner locally as `codemod`. This package requires node
14 | v16 or higher. This README assumes you've installed locally, but you can also
15 | install globally with `npm install -g @codemod/cli`.
16 |
17 | ## Usage
18 |
19 | The primary interface is as a command line tool, usually run like so:
20 |
21 | ```sh
22 | $ npx codemod --plugin transform-module-name \
23 | path/to/file.js \
24 | another/file.js \
25 | a/directory
26 | ```
27 |
28 | This will re-write the files `path/to/file.js`, `another/file.js`, and any supported files found in `a/directory` by transforming them with the babel plugin `transform-module-name`. Multiple plugins may be specified, and multiple files or directories may be re-written at once.
29 |
30 | Note that TypeScript support is provided by babel and therefore may not completely support all valid TypeScript code. If you encounter an issue, consider looking for it in the [babel issues labeled `area: typescript`](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22area%3A+typescript%22+) before filing an issue.
31 |
32 | Plugins may also be loaded from remote URLs, including saved [AST Explorer](https://astexplorer.net/) URLs, using `--remote-plugin`. This feature should only be used as a convenience to load code that you or someone you trust wrote. It will run with your full user privileges, so please exercise caution!
33 |
34 | ```sh
35 | $ npx codemod --remote-plugin URL …
36 | ```
37 |
38 | By default, `codemod` makes minimal changes to your source files by using [recast](https://github.com/benjamn/recast) to parse and print your code, retaining the original comments and formatting. If desired, you can reformat files using [Prettier](https://prettier.io/) or ESLint or whatever other tools you prefer after the fact.
39 |
40 | For more detailed options, run `npx codemod --help`.
41 |
42 | ## Writing a Plugin
43 |
44 | There are [many, many existing plugins](https://www.npmjs.com/search?q=babel-plugin) that you can use. However, if you need to write your own you should consult the [babel handbook](https://github.com/thejameskyle/babel-handbook). If you publish a plugin intended specifically as a codemod, consider using both the [`babel-plugin`](https://www.npmjs.com/search?q=babel-plugin) and [`babel-codemod`](https://www.npmjs.com/search?q=babel-codemod) keywords.
45 |
46 | `@codemod/cli` provides a few helpers to make writing codemod plugins easier. For example:
47 |
48 | ```ts
49 | /**
50 | * This codemod rewrites `A * A` to `A ** 2` for any expression `A`.
51 | */
52 | import { defineCodemod } from '@codemod/cli'
53 |
54 | // `m` is `@codemod/matchers`, a library of useful matchers
55 | // `t` is `@babel/types`, babel AST type predicates and builders
56 | export default defineCodemod(({ t, m }) => {
57 | // `operand` is a capture matcher that will be filled in by `multiplyBySelf`,
58 | // which is a binary expression matcher that matches any expression multiplied
59 | // by itself.
60 | const operand = m.capture(m.anyExpression())
61 | const multiplyBySelf = m.binaryExpression(
62 | '*',
63 | operand,
64 | m.fromCapture(operand)
65 | )
66 |
67 | return {
68 | visitor: {
69 | BinaryExpression(path) {
70 | m.match(multiplyBySelf, { operand }, path.node, ({ operand }) => {
71 | path.replaceWith(
72 | t.binaryExpression('**', operand, t.numericLiteral(2))
73 | )
74 | })
75 | },
76 | },
77 | }
78 | })
79 | ```
80 |
81 | ### Transpiling using Babel Plugins
82 |
83 | `codemod` supports parsing language features supported by a standard Babel or TypeScript build toolchain, similar to what a Create React App build pipeline can handle. Feel free to write your plugins using these language features–they'll be transpiled on the fly.
84 |
85 | ### Passing Options to Plugins
86 |
87 | You can pass a JSON object as options to a plugin:
88 |
89 | ```sh
90 | # Pass a JSON object literal
91 | $ npx codemod --plugin ./my-plugin.ts --plugin-options '{"opt": true}'
92 | # Pass a JSON object from a file
93 | $ npx codemod --plugin ./my-plugin.ts --plugin-options @opts.json
94 | ```
95 |
96 | ## Contributing
97 |
98 | See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information on setting up the project for development and on contributing to the project.
99 |
100 | ## License
101 |
102 | Copyright 2017 Brian Donovan
103 |
104 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
105 |
106 | http://www.apache.org/licenses/LICENSE-2.0
107 |
108 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
109 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/cli.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { dirname, resolve as pathResolve } from 'path'
3 | import createTemporaryFile, {
4 | createTemporaryFiles,
5 | } from '../helpers/createTemporaryFile'
6 | import plugin from '../helpers/plugin'
7 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
8 |
9 | it('respects globs', async function () {
10 | const pathInFixtures = (dirPath: string): string =>
11 | pathResolve(__dirname, '..', 'fixtures', 'glob-test', dirPath)
12 | const { status, stdout, stderr } = await runCodemodCLI([
13 | '--dry',
14 | pathInFixtures('**/*.js'),
15 | `!${pathInFixtures('omit.js')}`,
16 | ])
17 |
18 | expect(stderr).toEqual('')
19 | expect(stdout).toEqual(expect.stringContaining('abc.js'))
20 | expect(stdout).toEqual(expect.stringContaining('subdir/def.js'))
21 | expect(stdout).not.toEqual(expect.stringContaining('omit.js'))
22 | expect(status).toEqual(0)
23 | })
24 |
25 | it('can read from stdin and write to stdout given the --stdio flag', async function () {
26 | expect(await runCodemodCLI(['--stdio'], { stdin: '3+4' })).toEqual({
27 | status: 0,
28 | stdout: '3+4',
29 | stderr: '',
30 | })
31 | })
32 |
33 | it('reads from a file, processes with plugins, then writes to that file', async function () {
34 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
35 | expect(
36 | await runCodemodCLI([afile, '-p', plugin('increment')], {
37 | cwd: dirname(afile),
38 | })
39 | ).toEqual({
40 | status: 0,
41 | stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
42 | stderr: '',
43 | })
44 | expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
45 | })
46 |
47 | it('processes all matching files in a directory', async function () {
48 | const [file1, file2, file3, ignored] = await createTemporaryFiles(
49 | ['file1.js', '3 + 4;'],
50 | ['file2.ts', '0;'],
51 | ['sub-dir/file3.jsx', '99;'],
52 | ['ignored.css', '* {}']
53 | )
54 | expect(
55 | await runCodemodCLI([dirname(file1), '-p', plugin('increment')], {
56 | cwd: dirname(file1),
57 | })
58 | ).toEqual({
59 | status: 0,
60 | stdout: `file1.js\nfile2.ts\nsub-dir/file3.jsx\n3 file(s), 3 modified, 0 errors\n`,
61 | stderr: '',
62 | })
63 | expect(await fs.readFile(file1, 'utf8')).toEqual('4 + 5;')
64 | expect(await fs.readFile(file2, 'utf8')).toEqual('1;')
65 | expect(await fs.readFile(file3, 'utf8')).toEqual('100;')
66 | expect(await fs.readFile(ignored, 'utf8')).toEqual('* {}')
67 | })
68 |
69 | it('prints files not processed in dim colors', async function () {
70 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
71 | expect(await runCodemodCLI([afile], { cwd: dirname(afile) })).toEqual({
72 | status: 0,
73 | stdout: `a-file.js\n1 file(s), 0 modified, 0 errors\n`,
74 | stderr: '',
75 | })
76 | expect(await fs.readFile(afile, 'utf8')).toEqual('3 + 4;')
77 | })
78 |
79 | it('can rewrite TypeScript files ending in `.ts`', async function () {
80 | const afile = await createTemporaryFile(
81 | 'a-file.ts',
82 | 'type A = any;\nlet a = {} as any;'
83 | )
84 | expect(
85 | await runCodemodCLI(
86 | [afile, '-p', plugin('replace-any-with-object', '.ts')],
87 | { cwd: dirname(afile) }
88 | )
89 | ).toEqual({
90 | status: 0,
91 | stdout: `a-file.ts\n1 file(s), 1 modified, 0 errors\n`,
92 | stderr: '',
93 | })
94 |
95 | expect(await fs.readFile(afile, 'utf8')).toEqual(
96 | 'type A = object;\nlet a = {} as object;'
97 | )
98 | })
99 |
100 | it('can rewrite TypeScript files ending in `.tsx`', async function () {
101 | const afile = await createTemporaryFile(
102 | 'a-file.tsx',
103 | 'export default () => (
);'
104 | )
105 | expect(await runCodemodCLI([afile], { cwd: dirname(afile) })).toEqual({
106 | status: 0,
107 | stdout: `a-file.tsx\n1 file(s), 0 modified, 0 errors\n`,
108 | stderr: '',
109 | })
110 |
111 | expect(await fs.readFile(afile, 'utf8')).toEqual(
112 | 'export default () => ();'
113 | )
114 | })
115 |
116 | it('can pass options to a plugin without naming it', async function () {
117 | expect(
118 | await runCodemodCLI(
119 | [
120 | '--plugin',
121 | plugin('append-options-string', '.ts'),
122 | '--plugin-options',
123 | `${JSON.stringify({ a: 4 })}`,
124 | '--stdio',
125 | ],
126 | { stdin: '' }
127 | )
128 | ).toEqual({
129 | status: 0,
130 | stdout: `${JSON.stringify(JSON.stringify({ a: 4 }))};`,
131 | stderr: '',
132 | })
133 | })
134 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/dry-run.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { dirname } from 'path'
3 | import createTemporaryFile from '../helpers/createTemporaryFile'
4 | import plugin from '../helpers/plugin'
5 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
6 |
7 | it('processes files but does not replace their contents when using --dry', async function () {
8 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
9 | expect(
10 | await runCodemodCLI([afile, '-p', plugin('increment'), '--dry'], {
11 | cwd: dirname(afile),
12 | })
13 | ).toEqual({
14 | status: 0,
15 | stdout: `a-file.js\nDRY RUN: no files affected\n1 file(s), 1 modified, 0 errors\n`,
16 | stderr: '',
17 | })
18 | expect(await fs.readFile(afile, 'utf8')).toEqual('3 + 4;')
19 | })
20 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/errors.test.ts:
--------------------------------------------------------------------------------
1 | import createTemporaryFile from '../helpers/createTemporaryFile'
2 | import plugin from '../helpers/plugin'
3 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
4 |
5 | test('fails with an error when passing an invalid option', async function () {
6 | expect(await runCodemodCLI(['--not-a-real-option'])).toEqual({
7 | status: 1,
8 | stdout: '',
9 | stderr: expect.stringContaining(
10 | 'ERROR: unexpected option: --not-a-real-option'
11 | ),
12 | })
13 | })
14 |
15 | test('fails with an error when a plugin throws an exception', async function () {
16 | expect(
17 | await runCodemodCLI(['--plugin', plugin('bad-plugin'), '--stdio'], {
18 | stdin: '3+4',
19 | })
20 | ).toEqual({
21 | status: 1,
22 | stdout: '',
23 | stderr: expect.stringContaining('I am a bad plugin'),
24 | })
25 | })
26 |
27 | it.skip('does not try to load TypeScript files when --no-transpile-plugins is set', async function () {
28 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
29 | expect(
30 | await runCodemodCLI([
31 | afile,
32 | '--no-transpile-plugins',
33 | '-p',
34 | plugin('increment-typescript', ''),
35 | ])
36 | ).toEqual({
37 | status: 255,
38 | stdout: '',
39 | stderr: expect.stringContaining('SyntaxError'),
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/extensions.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { dirname } from 'path'
3 | import createTemporaryFile, {
4 | createTemporaryFiles,
5 | } from '../helpers/createTemporaryFile'
6 | import plugin from '../helpers/plugin'
7 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
8 |
9 | test('can load plugins written with ES modules by default', async function () {
10 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
11 | expect(
12 | await runCodemodCLI([afile, '-p', plugin('increment-export-default')], {
13 | cwd: dirname(afile),
14 | })
15 | ).toEqual({
16 | status: 0,
17 | stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
18 | stderr: '',
19 | })
20 | expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
21 | })
22 |
23 | test('can load plugins written in TypeScript by default', async function () {
24 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
25 | expect(
26 | await runCodemodCLI([afile, '-p', plugin('increment-typescript', '.ts')], {
27 | cwd: dirname(afile),
28 | })
29 | ).toEqual({
30 | status: 0,
31 | stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
32 | stderr: '',
33 | })
34 | expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
35 | })
36 |
37 | test('can implicitly find plugins with .ts extensions', async function () {
38 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
39 | expect(
40 | await runCodemodCLI([afile, '-p', plugin('increment-typescript', '')], {
41 | cwd: dirname(afile),
42 | })
43 | ).toEqual({
44 | status: 0,
45 | stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
46 | stderr: '',
47 | })
48 | expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
49 | })
50 |
51 | test('can load plugins with multiple files with ES modules by default`', async function () {
52 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
53 | expect(
54 | await runCodemodCLI(
55 | [
56 | afile,
57 | '-p',
58 | plugin('increment-export-default-multiple/increment-export-default'),
59 | ],
60 | { cwd: dirname(afile) }
61 | )
62 | ).toEqual({
63 | status: 0,
64 | stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
65 | stderr: '',
66 | })
67 | expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
68 | })
69 |
70 | test('processes all matching files in a directory with custom extensions', async function () {
71 | const [ignored, processed] = await createTemporaryFiles(
72 | ['ignored.js', '3 + 4;'],
73 | ['processed.myjs', '0;']
74 | )
75 | expect(
76 | await runCodemodCLI(
77 | [dirname(ignored), '-p', plugin('increment'), '--extensions', '.myjs'],
78 | { cwd: dirname(ignored) }
79 | )
80 | ).toEqual({
81 | status: 0,
82 | stdout: `processed.myjs\n1 file(s), 1 modified, 0 errors\n`,
83 | stderr: '',
84 | })
85 | expect(await fs.readFile(ignored, 'utf8')).toEqual('3 + 4;')
86 | expect(await fs.readFile(processed, 'utf8')).toEqual('1;')
87 | })
88 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/info.test.ts:
--------------------------------------------------------------------------------
1 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
2 |
3 | test('prints help', async function () {
4 | expect(await runCodemodCLI(['--help'])).toEqual({
5 | status: 0,
6 | stdout: expect.stringContaining('codemod [OPTIONS]'),
7 | stderr: '',
8 | })
9 | })
10 |
11 | test('prints the version', async function () {
12 | expect(await runCodemodCLI(['--version'])).toEqual({
13 | status: 0,
14 | stdout: expect.stringMatching(/\d+\.\d+\.\d+\s*$/),
15 | stderr: '',
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/plugin-syntax.ts:
--------------------------------------------------------------------------------
1 | import plugin from '../helpers/plugin'
2 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
3 |
4 | it('can load a plugin that uses class properties', async function () {
5 | expect(
6 | await runCodemodCLI(
7 | ['--plugin', plugin('class-properties', '.ts'), '--stdio'],
8 | { stdin: '' }
9 | )
10 | ).toEqual({
11 | status: 0,
12 | stdout: '',
13 | stderr: '',
14 | })
15 | })
16 |
17 | it('can load a plugin that uses generators', async function () {
18 | expect(
19 | await runCodemodCLI(['--plugin', plugin('generators', '.ts'), '--stdio'], {
20 | stdin: '',
21 | })
22 | ).toEqual({
23 | status: 0,
24 | stdout: '',
25 | stderr: '',
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/remote-plugin.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { dirname } from 'path'
3 | import createTemporaryFile from '../helpers/createTemporaryFile'
4 | import plugin from '../helpers/plugin'
5 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
6 | import { startServer } from '../helpers/TestServer'
7 |
8 | test('can load and run with a remote plugin', async () => {
9 | const afile = await createTemporaryFile('a-file.js', '3 + 4;')
10 | const server = await startServer(async (req, res) => {
11 | expect(req.url).toEqual('/plugin.js')
12 |
13 | res.end(
14 | await fs.readFile(plugin('increment-export-default'), {
15 | encoding: 'utf8',
16 | })
17 | )
18 | })
19 |
20 | try {
21 | const { status, stdout, stderr } = await runCodemodCLI(
22 | [afile, '--remote-plugin', server.requestURL('/plugin.js').toString()],
23 | { cwd: dirname(afile) }
24 | )
25 |
26 | expect({ status, stdout, stderr }).toEqual({
27 | status: 0,
28 | stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
29 | stderr: '',
30 | })
31 |
32 | expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
33 | } finally {
34 | await server.stop()
35 | }
36 | })
37 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/cli/source-type.test.ts:
--------------------------------------------------------------------------------
1 | import { dirname } from 'path'
2 | import createTemporaryFile, {
3 | createTemporaryFiles,
4 | } from '../helpers/createTemporaryFile'
5 | import { runCodemodCLI } from '../helpers/runCodemodCLI'
6 |
7 | it('can specify the source type as "script"', async function () {
8 | const afile = await createTemporaryFile(
9 | 'a-file.js',
10 | 'with (a) { b; }' // `with` statements aren't allowed in modules
11 | )
12 | expect(
13 | await runCodemodCLI([afile, '--source-type', 'script'], {
14 | cwd: dirname(afile),
15 | })
16 | ).toEqual({
17 | status: 0,
18 | stdout: `a-file.js\n1 file(s), 0 modified, 0 errors\n`,
19 | stderr: '',
20 | })
21 | })
22 |
23 | it('can specify the source type as "module"', async function () {
24 | const afile = await createTemporaryFile(
25 | 'a-file.js',
26 | 'import "./b-file"' // `import` statements aren't allowed in scripts
27 | )
28 | expect(
29 | await runCodemodCLI([afile, '--source-type', 'module'], {
30 | cwd: dirname(afile),
31 | })
32 | ).toEqual({
33 | status: 0,
34 | stdout: `a-file.js\n1 file(s), 0 modified, 0 errors\n`,
35 | stderr: '',
36 | })
37 | })
38 |
39 | it('can specify the source type as "unambiguous"', async function () {
40 | const [afile, bfile] = await createTemporaryFiles(
41 | [
42 | 'a-file.js',
43 | 'with (a) { b; }', // `with` statements aren't allowed in modules
44 | ],
45 | [
46 | 'b-file.js',
47 | 'import "./a-file"', // `import` statements aren't allowed in scripts
48 | ]
49 | )
50 | expect(
51 | await runCodemodCLI([afile, bfile, '--source-type', 'unambiguous'], {
52 | cwd: dirname(afile),
53 | })
54 | ).toEqual({
55 | status: 0,
56 | stdout: `a-file.js\nb-file.js\n2 file(s), 0 modified, 0 errors\n`,
57 | stderr: '',
58 | })
59 | })
60 |
61 | it('fails when given an invalid source type', async function () {
62 | expect(await runCodemodCLI(['--source-type', 'hypercard'])).toEqual({
63 | status: 1,
64 | stdout: '',
65 | stderr: expect.stringContaining(
66 | `ERROR: expected '--source-type' to be one of "module", "script", or "unambiguous" but got: "hypercard"`
67 | ),
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/astexplorer/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/5ece951309157948661ae732af89209715bfe532",
3 | "forks_url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/forks",
4 | "commits_url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/commits",
5 | "id": "68827467161b95ee48048ff906fab6d8",
6 | "git_pull_url": "https://gist.github.com/68827467161b95ee48048ff906fab6d8.git",
7 | "git_push_url": "https://gist.github.com/68827467161b95ee48048ff906fab6d8.git",
8 | "html_url": "https://gist.github.com/68827467161b95ee48048ff906fab6d8",
9 | "files": {
10 | "astexplorer.json": {
11 | "filename": "astexplorer.json",
12 | "type": "application/json",
13 | "language": "JSON",
14 | "raw_url": "https://gist.githubusercontent.com/astexplorer/68827467161b95ee48048ff906fab6d8/raw/cb6fddcb3f083959bd7a1eb5ad5162a4aae118a7/astexplorer.json",
15 | "size": 172,
16 | "truncated": false,
17 | "content": "{\n \"v\": 2,\n \"parserID\": \"babylon6\",\n \"toolID\": \"babelv6\",\n \"settings\": {\n \"babylon6\": {}\n },\n \"versions\": {\n \"babylon6\": \"6.18.0\",\n \"babelv6\": \"6.26.0\"\n }\n}"
18 | },
19 | "source.js": {
20 | "filename": "source.js",
21 | "type": "application/javascript",
22 | "language": "JavaScript",
23 | "raw_url": "https://gist.githubusercontent.com/astexplorer/68827467161b95ee48048ff906fab6d8/raw/6fd1298030902993fc1162089b648dd6cd37206d/source.js",
24 | "size": 476,
25 | "truncated": false,
26 | "content": "/**\n * Paste or drop some JavaScript here and explore\n * the syntax tree created by chosen parser.\n * You can use all the cool new features from ES6\n * and even more. Enjoy!\n */\n\nlet tips = [\n \"Click on any AST node with a '+' to expand it\",\n\n \"Hovering over a node highlights the \\\n corresponding part in the source code\",\n\n \"Shift click on an AST node expands the whole substree\"\n];\n\nfunction printTips() {\n tips.forEach((tip, i) => console.log(`Tip ${i}:` + tip));\n}\n"
27 | },
28 | "transform.js": {
29 | "filename": "transform.js",
30 | "type": "application/javascript",
31 | "language": "JavaScript",
32 | "raw_url": "https://gist.githubusercontent.com/astexplorer/68827467161b95ee48048ff906fab6d8/raw/610c084b3ef8b9d5d481f01098fb46ff80f7a570/transform.js",
33 | "size": 252,
34 | "truncated": false,
35 | "content": "export default function (babel) {\n const { types: t } = babel;\n \n return {\n name: \"ast-transform\", // not required\n visitor: {\n Identifier(path) {\n path.node.name = path.node.name.split('').reverse().join('');\n }\n }\n };\n}\n"
36 | }
37 | },
38 | "public": false,
39 | "created_at": "2018-01-03T14:16:56Z",
40 | "updated_at": "2018-01-03T14:19:43Z",
41 | "description": null,
42 | "comments": 0,
43 | "user": null,
44 | "comments_url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/comments",
45 | "owner": {
46 | "login": "astexplorer",
47 | "id": 24887483,
48 | "avatar_url": "https://avatars2.githubusercontent.com/u/24887483?v=4",
49 | "gravatar_id": "",
50 | "url": "https://api.github.com/users/astexplorer",
51 | "html_url": "https://github.com/astexplorer",
52 | "followers_url": "https://api.github.com/users/astexplorer/followers",
53 | "following_url": "https://api.github.com/users/astexplorer/following{/other_user}",
54 | "gists_url": "https://api.github.com/users/astexplorer/gists{/gist_id}",
55 | "starred_url": "https://api.github.com/users/astexplorer/starred{/owner}{/repo}",
56 | "subscriptions_url": "https://api.github.com/users/astexplorer/subscriptions",
57 | "organizations_url": "https://api.github.com/users/astexplorer/orgs",
58 | "repos_url": "https://api.github.com/users/astexplorer/repos",
59 | "events_url": "https://api.github.com/users/astexplorer/events{/privacy}",
60 | "received_events_url": "https://api.github.com/users/astexplorer/received_events",
61 | "type": "User",
62 | "site_admin": false
63 | },
64 | "forks": [],
65 | "history": [
66 | {
67 | "user": {
68 | "login": "astexplorer",
69 | "id": 24887483,
70 | "avatar_url": "https://avatars2.githubusercontent.com/u/24887483?v=4",
71 | "gravatar_id": "",
72 | "url": "https://api.github.com/users/astexplorer",
73 | "html_url": "https://github.com/astexplorer",
74 | "followers_url": "https://api.github.com/users/astexplorer/followers",
75 | "following_url": "https://api.github.com/users/astexplorer/following{/other_user}",
76 | "gists_url": "https://api.github.com/users/astexplorer/gists{/gist_id}",
77 | "starred_url": "https://api.github.com/users/astexplorer/starred{/owner}{/repo}",
78 | "subscriptions_url": "https://api.github.com/users/astexplorer/subscriptions",
79 | "organizations_url": "https://api.github.com/users/astexplorer/orgs",
80 | "repos_url": "https://api.github.com/users/astexplorer/repos",
81 | "events_url": "https://api.github.com/users/astexplorer/events{/privacy}",
82 | "received_events_url": "https://api.github.com/users/astexplorer/received_events",
83 | "type": "User",
84 | "site_admin": false
85 | },
86 | "version": "5ece951309157948661ae732af89209715bfe532",
87 | "committed_at": "2018-01-03T14:16:55Z",
88 | "change_status": { "total": 43, "additions": 43, "deletions": 0 },
89 | "url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/5ece951309157948661ae732af89209715bfe532"
90 | }
91 | ],
92 | "truncated": false
93 | }
94 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/babel-config/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = function (api) {
4 | api.cache(true)
5 | return {
6 | plugins: [
7 | {
8 | visitor: {
9 | NumericLiteral(path) {
10 | path.node.value = 42
11 | },
12 | },
13 | },
14 | ],
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/babel-config/index.js:
--------------------------------------------------------------------------------
1 | const a = 1
2 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/glob-test/abc.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codemod-js/codemod/8e8d90712be772ce69168281b0cd52e44c40f5e5/packages/cli/__tests__/fixtures/glob-test/abc.js
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/glob-test/subdir/def.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codemod-js/codemod/8e8d90712be772ce69168281b0cd52e44c40f5e5/packages/cli/__tests__/fixtures/glob-test/subdir/def.js
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/append-options-string.ts:
--------------------------------------------------------------------------------
1 | import { defineCodemod } from '../../../src'
2 |
3 | export default defineCodemod(({ t }, options) => ({
4 | visitor: {
5 | Program(path) {
6 | path.node.body.push(
7 | t.expressionStatement(t.stringLiteral(JSON.stringify(options)))
8 | )
9 | },
10 | },
11 | }))
12 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/bad-plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | return {
3 | visitor: {
4 | NumericLiteral(path) {
5 | throw new Error('I am a bad plugin')
6 | },
7 | },
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/class-properties.ts:
--------------------------------------------------------------------------------
1 | import { PluginObj } from '@babel/core'
2 |
3 | class Count {
4 | // This file exists to verify that class properties like ↓ can be loaded.
5 | count = 0
6 |
7 | incr(): void {
8 | this.count++
9 | }
10 | }
11 |
12 | export default function (): PluginObj {
13 | const debuggerCount = new Count()
14 |
15 | return {
16 | visitor: {
17 | DebuggerStatement(): void {
18 | debuggerCount.incr()
19 | },
20 | },
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/generators.ts:
--------------------------------------------------------------------------------
1 | import { PluginObj } from '@babel/core'
2 |
3 | // This file exists to verify that generator functions like ↓ can be loaded.
4 | function* counter(): IterableIterator {
5 | let i = 0
6 |
7 | while (true) {
8 | yield i++
9 | }
10 | }
11 |
12 | export default function (): PluginObj {
13 | const identifiers = counter()
14 |
15 | return {
16 | visitor: {
17 | Identifier(): void {
18 | console.log(identifiers.next())
19 | },
20 | },
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-export-default.js:
--------------------------------------------------------------------------------
1 | import incrementValue from './increment-value'
2 |
3 | export default function () {
4 | return {
5 | visitor: {
6 | NumericLiteral(path) {
7 | path.node.value = incrementValue(path.node.value)
8 | },
9 | },
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-value.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | export default function incrementValue(value) {
4 | return value + 1
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/increment-export-default.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return {
3 | visitor: {
4 | NumericLiteral(path) {
5 | path.node.value += 1
6 | },
7 | },
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/increment-typescript.ts:
--------------------------------------------------------------------------------
1 | import { defineCodemod } from '../../../src'
2 |
3 | enum IncrementValues {
4 | One = 1,
5 | Two = 2,
6 | }
7 |
8 | export default defineCodemod(() => ({
9 | visitor: {
10 | NumericLiteral(path) {
11 | path.node.value += IncrementValues.One
12 | },
13 | },
14 | }))
15 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/increment.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | return {
3 | visitor: {
4 | NumericLiteral(path) {
5 | path.node.value += 1
6 | },
7 | },
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | return {
3 | name: 'basic-plugin',
4 | visitor: {},
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/plugin/replace-any-with-object.ts:
--------------------------------------------------------------------------------
1 | import * as Babel from '@babel/core'
2 | import { NodePath } from '@babel/traverse'
3 | import { TSAnyKeyword } from '@babel/types'
4 |
5 | export default function (babel: typeof Babel): Babel.PluginObj {
6 | const { types: t } = babel
7 |
8 | return {
9 | visitor: {
10 | TSAnyKeyword(path: NodePath) {
11 | path.replaceWith(t.tsObjectKeyword())
12 | },
13 | },
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/prettier/defaults/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/prettier/defaults/index.jsx:
--------------------------------------------------------------------------------
1 | function HelloWorld({
2 | greeting = "hello",
3 | greeted = '"World"',
4 | silent = false,
5 | onMouseOver,
6 | }) {
7 | if (!greeting) {
8 | return null;
9 | }
10 |
11 | // TODO: Don't use random in render
12 | let num = Math.floor(Math.random() * 1e7)
13 | .toString()
14 | .replace(/\.\d+/gi, "");
15 |
16 | return (
17 |
22 |
23 | {greeting.slice(0, 1).toUpperCase() + greeting.slice(1).toLowerCase()}
24 |
25 | {greeting.endsWith(",") ? (
26 | " "
27 | ) : (
28 | ", "
29 | )}
30 | {greeted}
31 | {silent ? "." : "!"}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/prettier/with-config/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": true
4 | }
5 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/fixtures/prettier/with-config/index.js:
--------------------------------------------------------------------------------
1 | var a = '';
2 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/helpers/TestServer.ts:
--------------------------------------------------------------------------------
1 | import getPort = require('get-port')
2 | import { createServer, IncomingMessage, Server, ServerResponse } from 'http'
3 | import { URL } from 'url'
4 |
5 | export type RequestHandler = (req: IncomingMessage, res: ServerResponse) => void
6 |
7 | export class RealTestServer {
8 | private server: Server
9 |
10 | readonly protocol: string = 'http:'
11 | readonly hostname: string = '127.0.0.1'
12 |
13 | constructor(readonly port: number, readonly handler: RequestHandler) {
14 | this.server = createServer(handler)
15 | }
16 |
17 | async start(): Promise {
18 | return new Promise((resolve) => {
19 | this.server.once('listening', () => resolve())
20 | this.server.listen(this.port)
21 | })
22 | }
23 |
24 | async stop(): Promise {
25 | return new Promise((resolve) => {
26 | this.server.close(() => resolve())
27 | })
28 | }
29 |
30 | requestURL(path: string): URL {
31 | return new URL(`${this.protocol}//${this.hostname}:${this.port}${path}`)
32 | }
33 | }
34 |
35 | export async function startServer(
36 | handler: RequestHandler
37 | ): Promise {
38 | const port = await getPort()
39 | const server = new RealTestServer(port, handler)
40 | await server.start()
41 | return server
42 | }
43 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/helpers/copyFixturesInto.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { dirname, join, relative } from 'path'
3 | import { iterateSources } from '../../src/iterateSources'
4 | import mkdirp = require('make-dir')
5 |
6 | export default async function copyFixturesInto(
7 | fixture: string,
8 | destination: string
9 | ): Promise {
10 | const fixturePath = join('test/fixtures', fixture)
11 |
12 | for await (const file of iterateSources([fixturePath])) {
13 | const relativePath = relative(fixturePath, file.path)
14 | const destinationPath = join(destination, relativePath)
15 | await mkdirp(dirname(destinationPath))
16 | await fs.writeFile(destinationPath, file.content)
17 | }
18 |
19 | return destination
20 | }
21 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/helpers/createTemporaryDirectory.ts:
--------------------------------------------------------------------------------
1 | import tempy = require('tempy')
2 |
3 | export default async function createTemporaryDirectory(): Promise {
4 | return tempy.directory()
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/helpers/createTemporaryFile.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { dirname, join } from 'path'
3 | import tempy = require('tempy')
4 | import createTemporaryDirectory from './createTemporaryDirectory'
5 |
6 | export default async function createTemporaryFile(
7 | name: string,
8 | content: string
9 | ): Promise {
10 | const fullPath = tempy.file({ name })
11 | await fs.writeFile(fullPath, content, 'utf8')
12 | return fullPath
13 | }
14 |
15 | export async function createTemporaryFiles(
16 | ...files: Array<[name: string, content: string]>
17 | ): Promise> {
18 | const root = await createTemporaryDirectory()
19 |
20 | return Promise.all(
21 | files.map(async ([name, content]) => {
22 | const fullPath = join(root, name)
23 | await fs.mkdir(dirname(fullPath), { recursive: true })
24 | await fs.writeFile(fullPath, content)
25 | return fullPath
26 | })
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/helpers/plugin.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path'
2 |
3 | export default function plugin(name: string, ext = '.js'): string {
4 | return join(__dirname, `../fixtures/plugin/${name}${ext}`)
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/helpers/runCodemodCLI.ts:
--------------------------------------------------------------------------------
1 | import { spawn } from 'child_process'
2 | import { join } from 'path'
3 |
4 | export interface CLIResult {
5 | status: number
6 | stdout: string
7 | stderr: string
8 | }
9 |
10 | export async function runCodemodCLI(
11 | args: Array,
12 | { stdin = '', cwd }: { stdin?: string; cwd?: string } = {}
13 | ): Promise {
14 | const child = spawn(
15 | process.env.NODE ?? process.argv0,
16 | [join(__dirname, '../../bin/codemod'), ...args],
17 | {
18 | stdio: 'pipe',
19 | cwd,
20 | env: { CODEMOD_RUN_WITH_ESBUILD: '1' },
21 | }
22 | )
23 |
24 | child.stdin.end(stdin)
25 |
26 | let stdout = ''
27 | child.stdout.setEncoding('utf-8').on('readable', () => {
28 | stdout += child.stdout.read() ?? ''
29 | })
30 |
31 | let stderr = ''
32 | child.stderr.setEncoding('utf-8').on('readable', () => {
33 | stderr += child.stderr.read() ?? ''
34 | })
35 |
36 | return new Promise((resolve, reject) => {
37 | child
38 | .on('exit', (status) => {
39 | resolve({ status: status ?? 1, stdout, stderr })
40 | })
41 | .on('error', reject)
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/Config.test.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path'
2 | import { inspect } from 'util'
3 | import { Config, ConfigBuilder } from '../../src/Config'
4 |
5 | // TODO: move some of the babel plugin loading tests in here
6 |
7 | test('has sensible defaults', function () {
8 | const config = new Config()
9 | expect(config.extensions).toContain('.js')
10 | expect(config.extensions).toContain('.ts')
11 | expect(config.extensions).toContain('.jsx')
12 | expect(config.extensions).toContain('.tsx')
13 | expect(config.localPlugins).toEqual([])
14 | expect(config.sourcePaths).toEqual([])
15 | expect(config.requires).toEqual([])
16 | expect(config.pluginOptions.size).toEqual(0)
17 | expect(config.stdio).toEqual(false)
18 | })
19 |
20 | test('associates plugin options based on declared name', async function () {
21 | const config = new ConfigBuilder()
22 | .addLocalPlugin(join(__dirname, '../fixtures/plugin/index.js'))
23 | .setOptionsForPlugin({ a: true }, 'basic-plugin')
24 | .build()
25 |
26 | // "basic-plugin" is declared in the plugin file
27 | const babelPlugin = await config.getBabelPlugin('basic-plugin')
28 |
29 | if (!Array.isArray(babelPlugin)) {
30 | throw new Error(
31 | `expected plugin to be [plugin, options] tuple: ${inspect(babelPlugin)}`
32 | )
33 | }
34 |
35 | expect(babelPlugin[1]).toEqual({ a: true })
36 | })
37 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/InlineTransformer.test.ts:
--------------------------------------------------------------------------------
1 | import { PluginItem } from '@babel/core'
2 | import { NodePath } from '@babel/traverse'
3 | import { NumericLiteral, Program } from '@babel/types'
4 | import { join } from 'path'
5 | import { InlineTransformer } from '../../src/InlineTransformer'
6 |
7 | test('passes source through as-is when there are no plugins', async function () {
8 | const filepath = 'a.js'
9 | const content = 'a + b;'
10 | const transformer = new InlineTransformer([])
11 | const output = await transformer.transform(filepath, content)
12 |
13 | expect(output).toEqual(content)
14 | })
15 |
16 | test('transforms source using plugins', async function () {
17 | const filepath = 'a.js'
18 | const content = '3 + 4;'
19 | const transformer = new InlineTransformer([
20 | (): PluginItem => ({
21 | visitor: {
22 | NumericLiteral(path: NodePath): void {
23 | path.node.value++
24 | },
25 | },
26 | }),
27 | ])
28 | const output = await transformer.transform(filepath, content)
29 |
30 | expect(output).toEqual('4 + 5;')
31 | })
32 |
33 | test('does not include any plugins not specified explicitly', async function () {
34 | const filepath = 'a.js'
35 | const content = 'export default 0;'
36 | const transformer = new InlineTransformer([])
37 | const output = await transformer.transform(filepath, content)
38 |
39 | expect(output).toEqual('export default 0;')
40 | })
41 |
42 | test('allows running plugins with options', async function () {
43 | const filepath = 'a.js'
44 | const content = '3 + 4;'
45 | const transformer = new InlineTransformer([
46 | [
47 | (): PluginItem => ({
48 | visitor: {
49 | NumericLiteral(
50 | path: NodePath,
51 | state: { opts: { value?: number } }
52 | ) {
53 | if (state.opts.value === path.node.value) {
54 | path.node.value++
55 | }
56 | },
57 | },
58 | }),
59 | { value: 3 },
60 | ],
61 | ])
62 | const output = await transformer.transform(filepath, content)
63 |
64 | expect(output).toEqual('4 + 4;')
65 | })
66 |
67 | test('passes the filename', async function () {
68 | const filepath = 'a.js'
69 | const content = ''
70 | let filename: string | undefined
71 |
72 | const transformer = new InlineTransformer([
73 | (): PluginItem => ({
74 | visitor: {
75 | Program(
76 | path: NodePath,
77 | state: {
78 | file: { opts: { filename: string } }
79 | }
80 | ) {
81 | filename = state.file.opts.filename
82 | },
83 | },
84 | }),
85 | ])
86 |
87 | // Ignore the result since we only care about arguments to the visitor.
88 | await transformer.transform(filepath, content)
89 |
90 | expect(filename).toEqual(join(process.cwd(), 'a.js'))
91 | })
92 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/Options.test.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from 'crypto'
2 | import { readFile } from 'fs/promises'
3 | import { join } from 'path'
4 | import { inspect } from 'util'
5 | import { Config } from '../../src/Config'
6 | import { Options, Command } from '../../src/Options'
7 |
8 | test('has sensible defaults', function () {
9 | const config = getRunConfig(new Options([]).parse())
10 | expect(config.extensions).toContain('.js')
11 | expect(config.extensions).toContain('.ts')
12 | expect(config.extensions).toContain('.jsx')
13 | expect(config.extensions).toContain('.tsx')
14 | expect(config.localPlugins).toEqual([])
15 | expect(config.sourcePaths).toEqual([])
16 | expect(config.requires).toEqual([])
17 | expect(config.pluginOptions.size).toEqual(0)
18 | expect(config.stdio).toEqual(false)
19 | })
20 |
21 | test('interprets `--help` as asking for help', function () {
22 | expect(new Options(['--help']).parse().kind).toEqual('help')
23 | })
24 |
25 | test('interprets `--version` as asking to print the version', function () {
26 | expect(new Options(['--version']).parse().kind).toEqual('version')
27 | })
28 |
29 | test('interprets `--extensions` as expected', function () {
30 | const config = getRunConfig(new Options(['--extensions', '.js,.ts']).parse())
31 | expect(config.extensions).toEqual(new Set(['.js', '.ts']))
32 | })
33 |
34 | test('--add-extension adds to the default extensions', function () {
35 | const config = getRunConfig(new Options(['--add-extension', '.myjs']).parse())
36 | expect(config.extensions.size > 1).toBeTruthy()
37 | expect(config.extensions).toContain('.myjs')
38 | })
39 |
40 | test('fails to parse unknown options', function () {
41 | expect(() => new Options(['--wtf']).parse()).toThrow(
42 | new Error('unexpected option: --wtf')
43 | )
44 | })
45 |
46 | test('interprets non-option arguments as paths', function () {
47 | const config = getRunConfig(new Options(['src/', 'a.js']).parse())
48 | expect(config.sourcePaths).toEqual(['src/', 'a.js'])
49 | })
50 |
51 | test('interprets `--stdio` as reading/writing stdin/stdout', function () {
52 | const config = getRunConfig(new Options(['--stdio']).parse())
53 | expect(config.stdio).toEqual(true)
54 | })
55 |
56 | test('can parse inline plugin options as JSON', function () {
57 | const config = getRunConfig(
58 | new Options(['-o', 'my-plugin={"foo": true}']).parse()
59 | )
60 | expect(config.pluginOptions.get('my-plugin')).toEqual({ foo: true })
61 | })
62 |
63 | test('associates plugin options based on declared name', async function () {
64 | const config = getRunConfig(
65 | new Options([
66 | '--plugin',
67 | join(__dirname, '../fixtures/plugin/index.js'),
68 | '--plugin-options',
69 | 'basic-plugin={"a": true}',
70 | ]).parse()
71 | )
72 |
73 | expect(config.pluginOptions.get('basic-plugin')).toEqual({ a: true })
74 | })
75 |
76 | test('assigns anonymous options to the most recent plugin', async function () {
77 | const config = getRunConfig(
78 | new Options([
79 | '--plugin',
80 | join(__dirname, '../fixtures/plugin/index.js'),
81 | '--plugin-options',
82 | '{"a": true}',
83 | ]).parse()
84 | )
85 |
86 | expect(
87 | config.pluginOptions.get(join(__dirname, '../fixtures/plugin/index.js'))
88 | ).toEqual({
89 | a: true,
90 | })
91 | })
92 |
93 | test('interprets `--require` as expected', async function () {
94 | const config = getRunConfig(new Options(['--require', 'tmp']).parse())
95 | const expectedTmpRequirePath = require.resolve('tmp')
96 |
97 | expect(config.requires).toHaveLength(1)
98 | const [actualTmpRequirePath] = config.requires
99 |
100 | // `require.resolve` and the `resolve` package are not guaranteed to return
101 | // the same path, so we compare the hashes of the files instead.
102 | const expectedTmpHash = createHash('md5')
103 | .update(await readFile(expectedTmpRequirePath))
104 | .digest('hex')
105 | const actualTmpHash = createHash('md5')
106 | .update(await readFile(actualTmpRequirePath))
107 | .digest('hex')
108 | expect(actualTmpHash).toEqual(expectedTmpHash)
109 | })
110 |
111 | test('associates plugin options based on inferred name', async function () {
112 | const config = getRunConfig(
113 | new Options([
114 | '--plugin',
115 | join(__dirname, '../fixtures/plugin/index.js'),
116 | '--plugin-options',
117 | 'index={"a": true}',
118 | ]).parse()
119 | )
120 |
121 | // "index" is the name of the file
122 | expect(config.pluginOptions.get('index')).toEqual({ a: true })
123 |
124 | const babelPlugin = await config.getBabelPlugin('index')
125 |
126 | if (!Array.isArray(babelPlugin)) {
127 | throw new Error(
128 | `expected plugin to be [plugin, options] tuple: ${inspect(babelPlugin)}`
129 | )
130 | }
131 |
132 | expect(babelPlugin[1]).toEqual({ a: true })
133 | })
134 |
135 | test('can parse a JSON file for plugin options', function () {
136 | // You wouldn't actually use package.json, but it's a convenient JSON file.
137 | const config = getRunConfig(
138 | new Options(['-o', 'my-plugin=@package.json']).parse()
139 | )
140 | const pluginOpts = config.pluginOptions.get('my-plugin')
141 | expect(pluginOpts && pluginOpts['name']).toEqual('@codemod/cli')
142 | })
143 |
144 | test('should set dry option', function () {
145 | const config = getRunConfig(new Options(['--dry']).parse())
146 | expect(config.dry).toEqual(true)
147 | })
148 |
149 | function getRunConfig(command: Command): Config {
150 | if (command.kind === 'run') {
151 | return command.config
152 | } else {
153 | throw new Error(`expected a run command but got: ${inspect(command)}`)
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/TransformRunner.test.ts:
--------------------------------------------------------------------------------
1 | import { deepEqual } from 'assert'
2 | import {
3 | TransformRunner,
4 | Source,
5 | SourceTransformResult,
6 | SourceTransformResultKind,
7 | } from '../../src/TransformRunner'
8 |
9 | async function run(
10 | runner: TransformRunner
11 | ): Promise> {
12 | const result: Array = []
13 |
14 | for await (const transformResult of runner.run()) {
15 | result.push(transformResult)
16 | }
17 |
18 | return result
19 | }
20 |
21 | async function* asyncIterable(elements: Iterable): AsyncGenerator {
22 | for (const element of elements) {
23 | yield element
24 | }
25 | }
26 |
27 | test('generates a result for each source by calling the transformer', async function () {
28 | const aSource = new Source('a.js', 'a;')
29 | const bSource = new Source('b.js', 'b;')
30 | const sources = asyncIterable([aSource, bSource])
31 | const runner = new TransformRunner(sources, {
32 | async transform(filepath: string, content: string): Promise {
33 | return content.toUpperCase()
34 | },
35 | })
36 |
37 | deepEqual(await run(runner), [
38 | {
39 | kind: SourceTransformResultKind.Transformed,
40 | source: aSource,
41 | output: 'A;',
42 | },
43 | {
44 | kind: SourceTransformResultKind.Transformed,
45 | source: bSource,
46 | output: 'B;',
47 | },
48 | ])
49 | })
50 |
51 | test('collects errors for each failed source transform', async function () {
52 | const source = new Source('fails.js', 'invalid syntax')
53 | const sources = asyncIterable([source])
54 | const runner = new TransformRunner(sources, {
55 | async transform(filepath: string, content: string): Promise {
56 | throw new Error(`unable to process ${filepath}: ${content}`)
57 | },
58 | })
59 |
60 | deepEqual(await run(runner), [
61 | {
62 | kind: SourceTransformResultKind.Error,
63 | source,
64 | error: new Error('unable to process fails.js: invalid syntax'),
65 | },
66 | ])
67 | })
68 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/iterateSources.test.ts:
--------------------------------------------------------------------------------
1 | import { dirname, join } from 'path'
2 | import { iterateSources } from '../../src/iterateSources'
3 | import createTemporaryDirectory from '../helpers/createTemporaryDirectory'
4 | import createTemporaryFile, {
5 | createTemporaryFiles,
6 | } from '../helpers/createTemporaryFile'
7 |
8 | async function asyncCollect(iter: AsyncIterable): Promise> {
9 | const result: Array = []
10 | for await (const element of iter) {
11 | result.push(element)
12 | }
13 | return result
14 | }
15 |
16 | test('empty directory', async () => {
17 | const dir = await createTemporaryDirectory()
18 | expect(await asyncCollect(iterateSources([dir]))).toEqual([])
19 | })
20 |
21 | test('single file', async () => {
22 | const file = await createTemporaryFile('a-file', 'with contents')
23 | expect(await asyncCollect(iterateSources([dirname(file)]))).toEqual([
24 | {
25 | path: file,
26 | content: 'with contents',
27 | },
28 | ])
29 | })
30 |
31 | test('selecting extensions', async () => {
32 | const [js] = await createTemporaryFiles(
33 | ['js-files/file.js', 'JS file'],
34 | ['ts-files/file.ts', 'TS file']
35 | )
36 | const root = dirname(dirname(js))
37 | expect(
38 | await asyncCollect(iterateSources([root], { extensions: new Set(['.js']) }))
39 | ).toEqual([
40 | {
41 | path: js,
42 | content: 'JS file',
43 | },
44 | ])
45 | })
46 |
47 | test('globbing', async () => {
48 | const paths = await createTemporaryFiles(
49 | ['main.ts', 'export default 0'],
50 | ['main.js', 'module.exports.default = 0'],
51 | ['subdir/index.test.ts', ''],
52 | ['subdir/utils.ts', ''],
53 | ['subdir/utils.test.ts', ''],
54 | ['subdir/.gitignore', 'ignored.test.ts'],
55 | ['subdir/ignored.test.ts', ''],
56 | ['.git/config', '']
57 | )
58 |
59 | expect(
60 | await asyncCollect(
61 | iterateSources(['**/*.test.ts'], { cwd: dirname(paths[0]) })
62 | )
63 | ).toEqual(
64 | paths
65 | .filter((path) => path.endsWith('.test.ts') && !path.includes('ignored'))
66 | .map((path) =>
67 | expect.objectContaining({
68 | path,
69 | })
70 | )
71 | )
72 | })
73 |
74 | test('gitignore', async () => {
75 | const [main] = await createTemporaryFiles(
76 | ['main.ts', 'export default 0'],
77 | ['.git/config', ''],
78 | ['.gitignore', '*.d.ts'],
79 | ['ignored.d.ts', ''],
80 | ['subdir/.gitignore', '*.js'],
81 | ['subdir/ignored-by-root.d.ts', ''],
82 | ['subdir/ignored-by-subdir.js', ''],
83 | ['subdir/subdir2/.gitignore', ''],
84 | ['subdir/subdir2/ignored-by-root.d.ts', ''],
85 | ['subdir/subdir2/ignored-by-subdir.js', '']
86 | )
87 |
88 | const root = dirname(main)
89 | expect(await asyncCollect(iterateSources([root]))).toEqual([
90 | { path: main, content: 'export default 0' },
91 | ])
92 |
93 | const subdir = join(root, 'subdir')
94 | expect(await asyncCollect(iterateSources([subdir]))).toEqual([])
95 | })
96 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/resolvers/AstExplorerResolver.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { join } from 'path'
3 | import { AstExplorerResolver } from '../../../src/resolvers/AstExplorerResolver'
4 | import { startServer } from '../../helpers/TestServer'
5 |
6 | test('normalizes a gist+commit editor URL into an API URL', async function () {
7 | const resolver = new AstExplorerResolver()
8 | const normalized = await resolver.normalize(
9 | 'https://astexplorer.net/#/gist/688274/5ece95'
10 | )
11 |
12 | expect(normalized).toEqual(
13 | 'https://astexplorer.net/api/v1/gist/688274/5ece95'
14 | )
15 | })
16 |
17 | test('normalizes http gist+commit editor URL to an https API URL', async function () {
18 | const resolver = new AstExplorerResolver()
19 | const normalized = await resolver.normalize(
20 | 'http://astexplorer.net/#/gist/b5b33c/f9ae8a'
21 | )
22 |
23 | expect(normalized).toEqual(
24 | 'https://astexplorer.net/api/v1/gist/b5b33c/f9ae8a'
25 | )
26 | })
27 |
28 | test('normalizes a gist-only editor URL into an API URL', async function () {
29 | const resolver = new AstExplorerResolver()
30 | const normalized = await resolver.normalize(
31 | 'https://astexplorer.net/#/gist/688274'
32 | )
33 |
34 | expect(normalized).toEqual('https://astexplorer.net/api/v1/gist/688274')
35 | })
36 |
37 | test('normalizes a gist+latest editor URL into an API URL', async function () {
38 | const resolver = new AstExplorerResolver()
39 | const normalized = await resolver.normalize(
40 | 'https://astexplorer.net/#/gist/688274/latest'
41 | )
42 |
43 | expect(normalized).toEqual(
44 | 'https://astexplorer.net/api/v1/gist/688274/latest'
45 | )
46 | })
47 |
48 | test('extracts the transform from the editor view', async function () {
49 | const result = await fs.readFile(
50 | join(__dirname, '../../fixtures/astexplorer/default.json'),
51 | { encoding: 'utf8' }
52 | )
53 | const server = await startServer((req, res) => {
54 | res.end(result)
55 | })
56 |
57 | try {
58 | const resolver = new AstExplorerResolver(server.requestURL('/'))
59 | expect(
60 | await fs.readFile(
61 | await resolver.resolve(server.requestURL('/#/gist/abc/def').toString()),
62 | { encoding: 'utf8' }
63 | )
64 | ).toEqual(JSON.parse(result).files['transform.js'].content)
65 | } finally {
66 | await server.stop()
67 | }
68 | })
69 |
70 | test('fails when returned data is not JSON', async function () {
71 | const server = await startServer((req, res) => {
72 | res.end('this is not JSON')
73 | })
74 | const url = server.requestURL('/')
75 |
76 | try {
77 | const resolver = new AstExplorerResolver(server.requestURL('/'))
78 |
79 | await expect(resolver.resolve(url.toString())).rejects.toThrowError(
80 | `data loaded from ${url} is not JSON: this is not JSON`
81 | )
82 | } finally {
83 | await server.stop()
84 | }
85 | })
86 |
87 | test('fails when files data is not present', async function () {
88 | const server = await startServer((req, res) => {
89 | res.end(JSON.stringify({}))
90 | })
91 |
92 | try {
93 | const resolver = new AstExplorerResolver(server.requestURL('/'))
94 |
95 | await expect(
96 | resolver.resolve(server.requestURL('/').toString())
97 | ).rejects.toThrowError(
98 | "'transform.js' could not be found, perhaps transform is disabled"
99 | )
100 | } finally {
101 | await server.stop()
102 | }
103 | })
104 |
105 | test('fails when transform.js is not present', async function () {
106 | const server = await startServer((req, res) => {
107 | res.end(JSON.stringify({ files: {} }))
108 | })
109 |
110 | try {
111 | const resolver = new AstExplorerResolver(server.requestURL('/'))
112 |
113 | await expect(
114 | resolver.resolve(server.requestURL('/').toString())
115 | ).rejects.toThrowError(
116 | "'transform.js' could not be found, perhaps transform is disabled"
117 | )
118 | } finally {
119 | await server.stop()
120 | }
121 | })
122 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/resolvers/FileSystemResolver.test.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path'
2 | import { FileSystemResolver } from '../../../src/resolvers/FileSystemResolver'
3 |
4 | test('can resolve any files that exist as-is', async function () {
5 | const resolver = new FileSystemResolver()
6 | expect(await resolver.canResolve(__filename)).toBeTruthy()
7 | expect(await resolver.resolve(__filename)).toEqual(__filename)
8 | })
9 |
10 | test('can resolve files by inferring an extension from a configurable set of extensions', async function () {
11 | const resolver = new FileSystemResolver(new Set(['.json']))
12 | const packageJsonWithoutExtension = join(__dirname, '../../../package')
13 | expect(await resolver.canResolve(packageJsonWithoutExtension)).toBeTruthy()
14 | expect(await resolver.resolve(packageJsonWithoutExtension)).toEqual(
15 | `${packageJsonWithoutExtension}.json`
16 | )
17 | })
18 |
19 | test('can resolve files by inferring an dot-less extension from a configurable set of extensions', async function () {
20 | const resolver = new FileSystemResolver(new Set(['json']))
21 | const packageJsonWithoutExtension = join(__dirname, '../../../package')
22 | expect(await resolver.canResolve(packageJsonWithoutExtension)).toBeTruthy()
23 | expect(await resolver.resolve(packageJsonWithoutExtension)).toEqual(
24 | `${packageJsonWithoutExtension}.json`
25 | )
26 | })
27 |
28 | test('fails to resolve a non-existent file', async function () {
29 | const resolver = new FileSystemResolver()
30 | expect(!(await resolver.canResolve('/this/file/is/not/there'))).toBeTruthy()
31 |
32 | await expect(
33 | resolver.resolve('/this/file/is/not/there')
34 | ).rejects.toThrowError(
35 | 'unable to resolve file from source: /this/file/is/not/there'
36 | )
37 | })
38 |
--------------------------------------------------------------------------------
/packages/cli/__tests__/unit/resolvers/NetworkResolver.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs'
2 | import { NetworkResolver } from '../../../src/resolvers/NetworkResolver'
3 | import { startServer } from '../../helpers/TestServer'
4 |
5 | test('can load data from a URL', async function () {
6 | const server = await startServer((req, res) => {
7 | res.end('here you go!')
8 | })
9 |
10 | try {
11 | const resolver = new NetworkResolver()
12 | const url = server.requestURL('/gimme')
13 |
14 | expect(await resolver.canResolve(url.toString())).toBeTruthy()
15 |
16 | const filename = await resolver.resolve(url.toString())
17 |
18 | expect(await fs.readFile(filename, { encoding: 'utf8' })).toEqual(
19 | 'here you go!'
20 | )
21 | } finally {
22 | await server.stop()
23 | }
24 | })
25 |
26 | test('only resolves absolute HTTP URLs', async function () {
27 | const resolver = new NetworkResolver()
28 |
29 | expect(await resolver.canResolve('http://example.com/')).toBeTruthy()
30 | expect(await resolver.canResolve('https://example.com/')).toBeTruthy()
31 | expect(await resolver.canResolve('/')).toBeFalsy()
32 | expect(
33 | await resolver.canResolve('afp://192.168.0.1/volume/folder/file.js')
34 | ).toBeFalsy()
35 | expect(await resolver.canResolve('data:,Hello%2C%20World!')).toBeFalsy()
36 | })
37 |
38 | test('follows redirects', async function () {
39 | const server = await startServer((req, res) => {
40 | if (req.url === '/') {
41 | res.writeHead(302, { Location: '/plugin' })
42 | res.end()
43 | } else if (req.url === '/plugin') {
44 | res.end('redirected successfully!')
45 | } else {
46 | res.writeHead(404)
47 | res.end()
48 | }
49 | })
50 |
51 | try {
52 | const resolver = new NetworkResolver()
53 | const filename = await resolver.resolve(server.requestURL('/').toString())
54 |
55 | expect(await fs.readFile(filename, { encoding: 'utf8' })).toEqual(
56 | 'redirected successfully!'
57 | )
58 | } finally {
59 | await server.stop()
60 | }
61 | })
62 |
63 | test('throws if it gets a non-200 response', async function () {
64 | const server = await startServer((req, res) => {
65 | res.statusCode = 400
66 | res.end()
67 | })
68 |
69 | try {
70 | const resolver = new NetworkResolver()
71 | const url = server.requestURL('/')
72 |
73 | await expect(resolver.resolve(url.toString())).rejects.toThrowError(
74 | 'failed to load plugin'
75 | )
76 | } finally {
77 | await server.stop()
78 | }
79 | })
80 |
--------------------------------------------------------------------------------
/packages/cli/bin/codemod:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | if (process.env.CODEMOD_RUN_WITH_ESBUILD) {
4 | require('esbuild-runner/register')
5 | }
6 |
7 | const run = (() => {
8 | try {
9 | return require('../src').default
10 | } catch {
11 | // ignore
12 | }
13 |
14 | try {
15 | return require('../').default
16 | } catch {
17 | process.stderr.write(
18 | 'codemod does not seem to be built and the development files could not be loaded'
19 | )
20 | process.exit(1)
21 | }
22 | })()
23 |
24 | run(process.argv)
25 | .then((status) => {
26 | process.exit(status)
27 | })
28 | .catch((err) => {
29 | console.error(err.stack)
30 | process.exit(-1)
31 | })
32 |
--------------------------------------------------------------------------------
/packages/cli/bin/codemod-dev:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
4 |
5 | export CODEMOD_RUN_WITH_ESBUILD=1
6 | exec "${SCRIPT_DIR}/codemod" $@
7 |
--------------------------------------------------------------------------------
/packages/cli/examples/convert-qunit-assert-expect-to-assert-async.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts deprecated `assert.expect(N)`-style tests to use `assert.async()`.
3 | *
4 | * @example
5 | *
6 | * test('my test', function (assert) {
7 | * assert.expect(1);
8 | * window.server.get('/some/api', () => {
9 | * asyncThing().then(() => {
10 | * assert.ok(true, 'API called!');
11 | * });
12 | * });
13 | * doStuff();
14 | * });
15 | *
16 | * // becomes
17 | *
18 | * test('my test', function (assert) {
19 | * const done = assert.async();
20 | * window.server.get('/some/api', () => {
21 | * asyncThing().then(() => {
22 | * assert.ok(true, 'API called!');
23 | * done();
24 | * });
25 | * });
26 | * doStuff();
27 | * });
28 | */
29 |
30 | import { defineCodemod, t } from '../src'
31 |
32 | export default defineCodemod(({ m, utils }) => {
33 | // capture `assert` parameter
34 | const assertBinding = m.capture(m.identifier())
35 |
36 | // capture `assert.expect();` inside the async test
37 | const assertExpect = m.capture(
38 | m.expressionStatement(
39 | m.callExpression(
40 | m.memberExpression(
41 | m.fromCapture(assertBinding),
42 | m.identifier('expect')
43 | ),
44 | [m.numericLiteral()]
45 | )
46 | )
47 | )
48 |
49 | // capture `assert.(…);` inside the callback
50 | const callbackAssertion = m.capture(
51 | m.expressionStatement(
52 | m.callExpression(
53 | m.memberExpression(m.fromCapture(assertBinding), m.identifier())
54 | )
55 | )
56 | )
57 |
58 | // callback function body
59 | const callbackFunctionBody = m.containerOf(
60 | m.blockStatement(
61 | m.anyList(m.zeroOrMore(), callbackAssertion, m.zeroOrMore())
62 | )
63 | )
64 |
65 | // async test function body
66 | const asyncTestFunctionBody = m.blockStatement(
67 | m.anyList(
68 | m.zeroOrMore(),
69 | assertExpect,
70 | m.zeroOrMore(),
71 | m.expressionStatement(
72 | m.callExpression(
73 | undefined,
74 | m.anyList(
75 | m.zeroOrMore(),
76 | m.or(
77 | m.functionExpression(undefined, undefined, callbackFunctionBody),
78 | m.arrowFunctionExpression(undefined, callbackFunctionBody)
79 | )
80 | )
81 | )
82 | ),
83 | m.zeroOrMore()
84 | )
85 | )
86 |
87 | // match the whole `test('description', function(assert) { … })`
88 | const asyncTestMatcher = m.callExpression(m.identifier('test'), [
89 | m.stringLiteral(),
90 | m.functionExpression(undefined, [assertBinding], asyncTestFunctionBody),
91 | ])
92 |
93 | const makeDone = utils.statement<{ assert: t.Identifier }>(
94 | 'const done = %%assert%%.async();'
95 | )
96 | const callDone = utils.statement('done();')
97 |
98 | return {
99 | visitor: {
100 | CallExpression(path) {
101 | m.matchPath(
102 | asyncTestMatcher,
103 | {
104 | assertExpect,
105 | callbackAssertion,
106 | assertBinding,
107 | },
108 | path,
109 | ({ assertExpect, callbackAssertion, assertBinding }) => {
110 | assertExpect.replaceWith(makeDone({ assert: assertBinding.node }))
111 | callbackAssertion.insertAfter(callDone())
112 | }
113 | )
114 | },
115 | },
116 | }
117 | })
118 |
--------------------------------------------------------------------------------
/packages/cli/examples/convert-static-class-to-named-exports.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts default-exported class with all static methods to named exports.
3 | *
4 | * @example
5 | *
6 | * class MobileAppUpsellHelper {
7 | * static getIosAppLink(specialTrackingLink) {
8 | * const trackingLink = specialTrackingLink || 'IOS_BRANCH_LINK';
9 | * return this.getBranchLink(trackingLink);
10 | * }
11 | *
12 | * static getAndroidAppLink(specialTrackingLink) {
13 | * const trackingLink = specialTrackingLink || 'ANDROID_BRANCH_LINK';
14 | * return this.getBranchLink(trackingLink);
15 | * }
16 | *
17 | * static getBranchLink(specialTrackingLink) {
18 | * if (specialTrackingLink && APP_DOWNLOAD_ASSETS[specialTrackingLink]) {
19 | * return APP_DOWNLOAD_ASSETS[specialTrackingLink];
20 | * }
21 | *
22 | * return APP_DOWNLOAD_ASSETS.DEFAULT_BRANCH_LINK;
23 | * }
24 | *
25 | * static getHideAppBanner() {
26 | * return CookieHelper.get('hide_app_banner');
27 | * }
28 | * }
29 | *
30 | * export default MobileAppUpsellHelper;
31 | *
32 | * // becomes
33 | *
34 | * export function getIosAppLink(specialTrackingLink) {
35 | * const trackingLink = specialTrackingLink || 'IOS_BRANCH_LINK';
36 | * return getBranchLink(trackingLink);
37 | * }
38 | *
39 | * export function getAndroidAppLink(specialTrackingLink) {
40 | * const trackingLink = specialTrackingLink || 'ANDROID_BRANCH_LINK';
41 | * return getBranchLink(trackingLink);
42 | * }
43 | *
44 | * export function getBranchLink(specialTrackingLink) {
45 | * if (specialTrackingLink && APP_DOWNLOAD_ASSETS[specialTrackingLink]) {
46 | * return APP_DOWNLOAD_ASSETS[specialTrackingLink];
47 | * }
48 | *
49 | * return APP_DOWNLOAD_ASSETS.DEFAULT_BRANCH_LINK;
50 | * }
51 | *
52 | * export function getHideAppBanner() {
53 | * return CookieHelper.get('hide_app_banner');
54 | * }
55 | */
56 |
57 | import { defineCodemod, t } from '../src'
58 |
59 | export default defineCodemod(({ t, m }) => {
60 | // capture the name of the exported class
61 | const classId = m.capture(m.identifier())
62 |
63 | // capture the class declaration
64 | const classDeclaration = m.capture(
65 | m.classDeclaration(
66 | classId,
67 | undefined,
68 | m.classBody(
69 | m.arrayOf(
70 | m.classMethod(
71 | 'method',
72 | m.identifier(),
73 | m.arrayOf(
74 | m.or(
75 | m.identifier(),
76 | m.assignmentPattern(),
77 | m.objectPattern(),
78 | m.arrayPattern(),
79 | m.restElement()
80 | )
81 | ),
82 | m.anything(),
83 | false,
84 | true
85 | )
86 | )
87 | )
88 | )
89 | )
90 |
91 | // capture the export, making sure to match the class name
92 | const exportDeclaration = m.capture(
93 | m.exportDefaultDeclaration(m.fromCapture(classId))
94 | )
95 |
96 | // match a program that contains a matching class and export declaration
97 | const matcher = m.program(
98 | m.anyList(
99 | m.zeroOrMore(),
100 | classDeclaration,
101 | m.zeroOrMore(),
102 | exportDeclaration,
103 | m.zeroOrMore()
104 | )
105 | )
106 |
107 | // match `this.*`, used internally
108 | const thisPropertyAccessMatcher = m.memberExpression(
109 | m.thisExpression(),
110 | m.identifier(),
111 | false
112 | )
113 |
114 | return {
115 | visitor: {
116 | Program(path) {
117 | m.matchPath(
118 | matcher,
119 | { exportDeclaration, classDeclaration },
120 | path,
121 | ({ exportDeclaration, classDeclaration }) => {
122 | const replacements: Array = []
123 | const classBody = classDeclaration.get('body')
124 |
125 | for (const property of classBody.get('body')) {
126 | if (!property.isClassMethod()) {
127 | throw new Error(
128 | `unexpected ${property.type} while looking for ClassMethod`
129 | )
130 | }
131 |
132 | if (!t.isIdentifier(property.node.key)) {
133 | throw new Error(
134 | `unexpected ${
135 | property.get('key').type
136 | } while looking for Identifier`
137 | )
138 | }
139 |
140 | if (
141 | property.node.params.some((p) => t.isTSParameterProperty(p))
142 | ) {
143 | continue
144 | }
145 |
146 | replacements.push(
147 | t.exportNamedDeclaration(
148 | t.functionDeclaration(
149 | property.node.key,
150 | property.node.params as Array<
151 | t.Identifier | t.Pattern | t.RestElement
152 | >,
153 | property.node.body,
154 | property.node.generator,
155 | property.node.async
156 | ),
157 | []
158 | )
159 | )
160 |
161 | property.get('body').traverse({
162 | enter(path) {
163 | if (path.isFunction()) {
164 | if (!path.isArrowFunctionExpression()) {
165 | path.skip()
166 | }
167 | } else if (
168 | path.isMemberExpression() &&
169 | thisPropertyAccessMatcher.match(path.node)
170 | ) {
171 | path.replaceWith(path.node.property)
172 | }
173 | },
174 | })
175 | }
176 |
177 | exportDeclaration.remove()
178 | classDeclaration.replaceWithMultiple(replacements)
179 | }
180 | )
181 | },
182 | },
183 | }
184 | })
185 |
--------------------------------------------------------------------------------
/packages/cli/jest.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | /** @type {import('@jest/types').Config.InitialOptions} */
4 | module.exports = {
5 | testEnvironment: 'node',
6 | testRegex: '/__tests__/(test|.*\\.test)\\.ts$',
7 | transform: {
8 | '\\.ts$': 'esbuild-runner/jest',
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/packages/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@codemod/cli",
3 | "version": "3.3.0",
4 | "description": "codemod rewrites JavaScript and TypeScript",
5 | "repository": "https://github.com/codemod-js/codemod.git",
6 | "license": "Apache-2.0",
7 | "author": "Brian Donovan",
8 | "main": "build/index.js",
9 | "types": "build/index.d.ts",
10 | "bin": {
11 | "codemod": "./bin/codemod"
12 | },
13 | "files": [
14 | "bin",
15 | "build"
16 | ],
17 | "scripts": {
18 | "build": "tsc --build tsconfig.build.json",
19 | "clean": "rm -rf build tsconfig.build.tsbuildinfo",
20 | "lint": "eslint .",
21 | "lint:fix": "eslint . --fix",
22 | "prepublishOnly": "pnpm clean && pnpm build",
23 | "test": "is-ci test:coverage test:watch",
24 | "test:coverage": "jest --coverage",
25 | "test:watch": "jest --watch"
26 | },
27 | "dependencies": {
28 | "@babel/core": "^7.20.12",
29 | "@babel/generator": "^7.20.14",
30 | "@babel/parser": "^7.20.15",
31 | "@babel/plugin-proposal-class-properties": "^7.18.6",
32 | "@babel/preset-env": "^7.20.2",
33 | "@babel/preset-typescript": "^7.18.6",
34 | "@babel/traverse": "^7.20.13",
35 | "@babel/types": "^7.20.7",
36 | "@codemod/core": "^2.2.0",
37 | "@codemod/matchers": "^1.6.0",
38 | "@codemod/parser": "^1.4.0",
39 | "@codemod/utils": "^1.1.0",
40 | "core-js": "^3.1.4",
41 | "cross-fetch": "^3.1.5",
42 | "esbuild": "^0.13.13",
43 | "esbuild-runner": "^2.2.1",
44 | "find-up": "^5.0.0",
45 | "get-stream": "^5.1.0",
46 | "globby": "^11.0.0",
47 | "ignore": "^5.1.9",
48 | "is-ci-cli": "^2.2.0",
49 | "pirates": "^4.0.0",
50 | "recast": "^0.19.0",
51 | "regenerator-runtime": "^0.13.3",
52 | "resolve": "^1.22.1",
53 | "source-map-support": "^0.5.6",
54 | "tmp": "^0.2.1"
55 | },
56 | "devDependencies": {
57 | "@types/babel__core": "^7.20.0",
58 | "@types/babel__generator": "^7.6.4",
59 | "@types/babel__traverse": "^7.18.3",
60 | "@types/glob": "^7.1.0",
61 | "@types/got": "^9.6.7",
62 | "@types/jest": "^27.0.2",
63 | "@types/node": "^18.14.0",
64 | "@types/resolve": "^1.17.1",
65 | "@types/rimraf": "3.0.0",
66 | "@types/semver": "^7.1.0",
67 | "@types/source-map-support": "^0.5.0",
68 | "@types/tmp": "^0.2.0",
69 | "get-port": "^5.0.0",
70 | "jest": "^27.3.1",
71 | "make-dir": "^3.1.0",
72 | "prettier": "^2.4.1",
73 | "rimraf": "3.0.2",
74 | "semver": "^7.3.5",
75 | "tempy": "^1",
76 | "typescript": "^4.9.5"
77 | },
78 | "engines": {
79 | "node": ">=12.0.0"
80 | },
81 | "publishConfig": {
82 | "access": "public"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/cli/src/CLIEngine.ts:
--------------------------------------------------------------------------------
1 | import { PluginItem } from '@babel/core'
2 | import { promises as fs } from 'fs'
3 | import { Config } from './Config'
4 | import { InlineTransformer } from './InlineTransformer'
5 | import { iterateSources } from './iterateSources'
6 | import {
7 | TransformRunner,
8 | Source,
9 | SourceTransformResult,
10 | SourceTransformResultKind,
11 | } from './TransformRunner'
12 | import getStream = require('get-stream')
13 |
14 | export class RunResult {
15 | constructor(readonly stats: RunStats) {}
16 | }
17 |
18 | export class RunStats {
19 | constructor(
20 | readonly modified: number = 0,
21 | readonly errors: number = 0,
22 | readonly total: number = 0
23 | ) {}
24 | }
25 |
26 | export class CLIEngine {
27 | constructor(
28 | readonly config: Config,
29 | readonly onTransform: (result: SourceTransformResult) => void = () => {
30 | // do nothing by default
31 | }
32 | ) {}
33 |
34 | private async loadPlugins(): Promise> {
35 | await this.config.loadBabelTranspile()
36 | this.config.loadRequires()
37 | return await this.config.getBabelPlugins()
38 | }
39 |
40 | async run(): Promise {
41 | const plugins = await this.loadPlugins()
42 | let modified = 0
43 | let errors = 0
44 | let total = 0
45 | const dryRun = this.config.dry
46 | let sources: AsyncGenerator
47 |
48 | if (this.config.stdio) {
49 | sources = (async function* getStdinSources(): AsyncGenerator {
50 | yield new Source('', await getStream(process.stdin))
51 | })()
52 | } else {
53 | sources = iterateSources(this.config.sourcePaths, {
54 | extensions: this.config.extensions,
55 | })
56 | }
57 |
58 | const runner = new TransformRunner(
59 | sources,
60 | new InlineTransformer(plugins, this.config.parserPlugins)
61 | )
62 |
63 | for await (const result of runner.run()) {
64 | this.onTransform(result)
65 |
66 | if (result.kind === SourceTransformResultKind.Transformed) {
67 | if (this.config.stdio) {
68 | process.stdout.write(result.output)
69 | } else {
70 | if (result.output !== result.source.content) {
71 | modified++
72 | if (!dryRun) {
73 | await fs.writeFile(result.source.path, result.output, 'utf8')
74 | }
75 | }
76 | }
77 | } else if (result.error) {
78 | errors++
79 | }
80 |
81 | total++
82 | }
83 |
84 | return new RunResult(new RunStats(modified, errors, total))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/packages/cli/src/InlineTransformer.ts:
--------------------------------------------------------------------------------
1 | import { PluginItem } from '@babel/core'
2 | import { ParserPlugin } from '@babel/parser'
3 | import { transform, TransformOptions } from '@codemod/core'
4 | import Transformer from './Transformer'
5 |
6 | export class InlineTransformer implements Transformer {
7 | constructor(
8 | private readonly plugins: Iterable,
9 | private readonly parserPlugins: Iterable = []
10 | ) {}
11 |
12 | async transform(filepath: string, content: string): Promise {
13 | const options: TransformOptions = {
14 | filename: filepath,
15 | babelrc: false,
16 | configFile: false,
17 | plugins: [...this.plugins],
18 | parserOpts: {
19 | plugins: [...this.parserPlugins],
20 | },
21 | }
22 |
23 | const result = transform(content, options)
24 |
25 | if (!result) {
26 | throw new Error(`[${filepath}] babel transform returned null`)
27 | }
28 |
29 | return result.code as string
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/cli/src/Options.ts:
--------------------------------------------------------------------------------
1 | import { isParserPluginName } from '@codemod/parser'
2 | import { existsSync, readFileSync } from 'fs'
3 | import { resolve } from 'path'
4 | import { sync as resolveSync } from 'resolve'
5 | import { Config, ConfigBuilder } from './Config'
6 | import { RequireableExtensions } from './extensions'
7 |
8 | export interface RunCommand {
9 | kind: 'run'
10 | config: Config
11 | }
12 |
13 | export interface HelpCommand {
14 | kind: 'help'
15 | }
16 |
17 | export interface VersionCommand {
18 | kind: 'version'
19 | }
20 |
21 | export type Command = RunCommand | HelpCommand | VersionCommand
22 |
23 | export class Options {
24 | constructor(readonly args: Array) {}
25 |
26 | parse(): RunCommand | HelpCommand | VersionCommand {
27 | const config = new ConfigBuilder()
28 | let lastPlugin: string | undefined
29 |
30 | for (let i = 0; i < this.args.length; i++) {
31 | const arg = this.args[i]
32 |
33 | switch (arg) {
34 | case '-p':
35 | case '--plugin':
36 | i++
37 | lastPlugin = this.args[i]
38 | config.addLocalPlugin(lastPlugin)
39 | break
40 |
41 | case '--remote-plugin':
42 | i++
43 | lastPlugin = this.args[i]
44 | config.addRemotePlugin(lastPlugin)
45 | break
46 |
47 | case '-o':
48 | case '--plugin-options': {
49 | i++
50 |
51 | const value = this.args[i]
52 | let name: string
53 | let optionsRaw: string
54 |
55 | if (value.startsWith('@')) {
56 | if (!lastPlugin) {
57 | throw new Error(
58 | `${arg} must follow --plugin or --remote-plugin if no name is given`
59 | )
60 | }
61 |
62 | optionsRaw = readFileSync(value.slice(1), 'utf8')
63 | name = lastPlugin
64 | } else if (/^\s*{/.test(value)) {
65 | if (!lastPlugin) {
66 | throw new Error(
67 | `${arg} must follow --plugin or --remote-plugin if no name is given`
68 | )
69 | }
70 |
71 | optionsRaw = value
72 | name = lastPlugin
73 | } else {
74 | const nameAndOptions = value.split('=')
75 | name = nameAndOptions[0]
76 | optionsRaw = nameAndOptions[1]
77 |
78 | if (optionsRaw.startsWith('@')) {
79 | optionsRaw = readFileSync(optionsRaw.slice(1), 'utf8')
80 | }
81 | }
82 |
83 | try {
84 | config.setOptionsForPlugin(JSON.parse(optionsRaw), name)
85 | } catch (err) {
86 | throw new Error(
87 | `unable to parse JSON config for ${name}: ${optionsRaw}`
88 | )
89 | }
90 | break
91 | }
92 |
93 | case '--parser-plugins': {
94 | i++
95 | const value = this.args[i]
96 | if (!value) {
97 | throw new Error(`${arg} must be followed by a comma-separated list`)
98 | }
99 | for (const plugin of value.split(',')) {
100 | if (isParserPluginName(plugin)) {
101 | config.addParserPlugin(plugin)
102 | } else {
103 | throw new Error(`unknown parser plugin: ${plugin}`)
104 | }
105 | }
106 | break
107 | }
108 |
109 | case '-r':
110 | case '--require':
111 | i++
112 | config.addRequire(getRequirableModulePath(this.args[i]))
113 | break
114 |
115 | case '--transpile-plugins':
116 | case '--no-transpile-plugins':
117 | config.transpilePlugins(arg === '--transpile-plugins')
118 | break
119 |
120 | case '--extensions':
121 | i++
122 | config.extensions(
123 | new Set(
124 | this.args[i]
125 | .split(',')
126 | .map((ext) => (ext[0] === '.' ? ext : `.${ext}`))
127 | )
128 | )
129 | break
130 |
131 | case '--add-extension':
132 | i++
133 | config.addExtension(this.args[i])
134 | break
135 |
136 | case '--source-type': {
137 | i++
138 | const sourceType = this.args[i]
139 | if (
140 | sourceType === 'module' ||
141 | sourceType === 'script' ||
142 | sourceType === 'unambiguous'
143 | ) {
144 | config.sourceType(sourceType)
145 | } else {
146 | throw new Error(
147 | `expected '--source-type' to be one of "module", "script", ` +
148 | `or "unambiguous" but got: "${sourceType}"`
149 | )
150 | }
151 | break
152 | }
153 |
154 | case '-s':
155 | case '--stdio':
156 | config.stdio(true)
157 | break
158 |
159 | case '-h':
160 | case '--help':
161 | return { kind: 'help' }
162 |
163 | case '--version':
164 | return { kind: 'version' }
165 |
166 | case '-d':
167 | case '--dry':
168 | config.dry(true)
169 | break
170 |
171 | default:
172 | if (arg[0] === '-') {
173 | throw new Error(`unexpected option: ${arg}`)
174 | } else {
175 | config.addSourcePath(arg)
176 | }
177 | break
178 | }
179 | }
180 |
181 | return {
182 | kind: 'run',
183 | config: config.build(),
184 | }
185 | }
186 | }
187 |
188 | /**
189 | * Gets a path that can be passed to `require` for a given module path.
190 | */
191 | function getRequirableModulePath(modulePath: string): string {
192 | if (existsSync(modulePath)) {
193 | return resolve(modulePath)
194 | }
195 |
196 | for (const ext of RequireableExtensions) {
197 | if (existsSync(modulePath + ext)) {
198 | return resolve(modulePath + ext)
199 | }
200 | }
201 |
202 | return resolveSync(modulePath, { basedir: process.cwd() })
203 | }
204 |
--------------------------------------------------------------------------------
/packages/cli/src/PluginLoader.ts:
--------------------------------------------------------------------------------
1 | import Resolver from './resolvers/Resolver'
2 |
3 | export class PluginLoader {
4 | constructor(private readonly resolvers: Array) {}
5 |
6 | async load(source: string): Promise