├── .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 { 7 | for (const resolver of this.resolvers) { 8 | if (await resolver.canResolve(source)) { 9 | const resolvedPath = await resolver.resolve(source) 10 | return require(resolvedPath) 11 | } 12 | } 13 | 14 | throw new Error(`unable to resolve a plugin from source: ${source}`) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/src/TransformRunner.ts: -------------------------------------------------------------------------------- 1 | import Transformer from './Transformer' 2 | 3 | export class Source { 4 | constructor(readonly path: string, readonly content: string) {} 5 | } 6 | 7 | export enum SourceTransformResultKind { 8 | Transformed = 'Transformed', 9 | Error = 'Error', 10 | } 11 | 12 | export type SourceTransformResult = 13 | | { 14 | kind: SourceTransformResultKind.Transformed 15 | source: Source 16 | output: string 17 | } 18 | | { kind: SourceTransformResultKind.Error; source: Source; error: Error } 19 | 20 | export class TransformRunner { 21 | constructor( 22 | readonly sources: AsyncGenerator, 23 | readonly transformer: Transformer 24 | ) {} 25 | 26 | async *run(): AsyncIterableIterator { 27 | for await (const source of this.sources) { 28 | let result: SourceTransformResult 29 | 30 | try { 31 | const output = await this.transformer.transform( 32 | source.path, 33 | source.content 34 | ) 35 | result = { 36 | kind: SourceTransformResultKind.Transformed, 37 | source, 38 | output, 39 | } 40 | } catch (error) { 41 | result = { kind: SourceTransformResultKind.Error, source, error } 42 | } 43 | 44 | yield result 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/cli/src/Transformer.ts: -------------------------------------------------------------------------------- 1 | export default interface Transformer { 2 | transform(filepath: string, content: string): Promise 3 | } 4 | -------------------------------------------------------------------------------- /packages/cli/src/defineCodemod.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '@codemod/utils' 2 | import * as matchers from '@codemod/matchers' 3 | 4 | /** 5 | * Defines a codemod function that can be used with `@codemod/cli`, 6 | * `@codemod/core`, or `@babel/core`. Provides easy access to the 7 | * `@codemod/matchers` and `@codemod/utils` APIs. 8 | */ 9 | export function defineCodemod( 10 | fn: ( 11 | helpers: { 12 | utils: typeof utils 13 | matchers: typeof matchers 14 | types: typeof utils.types 15 | m: typeof matchers 16 | t: typeof utils.types 17 | }, 18 | options?: T 19 | ) => utils.Babel.PluginItem 20 | ) { 21 | return function (_?: unknown, options?: T) { 22 | return fn( 23 | { utils, matchers, types: utils.types, m: matchers, t: utils.types }, 24 | options 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/extensions.ts: -------------------------------------------------------------------------------- 1 | function union(...sets: Array>): Set { 2 | return new Set(sets.reduce((result, set) => [...result, ...set], [])) 3 | } 4 | 5 | export const TypeScriptExtensions = new Set([ 6 | '.ts', 7 | '.tsx', 8 | '.mts', 9 | '.mtsx', 10 | '.cts', 11 | '.ctsx', 12 | ]) 13 | export const JavaScriptExtensions = new Set([ 14 | '.js', 15 | '.jsx', 16 | '.mjs', 17 | '.mjsx', 18 | '.cjs', 19 | '.cjsx', 20 | '.es', 21 | '.es6', 22 | ]) 23 | export const PluginExtensions = union( 24 | TypeScriptExtensions, 25 | JavaScriptExtensions 26 | ) 27 | export const RequireableExtensions = union( 28 | TypeScriptExtensions, 29 | JavaScriptExtensions 30 | ) 31 | export const TransformableExtensions = union( 32 | TypeScriptExtensions, 33 | JavaScriptExtensions 34 | ) 35 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { basename, relative } from 'path' 2 | import { CLIEngine } from './CLIEngine' 3 | import { Config } from './Config' 4 | import { Options, Command } from './Options' 5 | import { 6 | SourceTransformResult, 7 | SourceTransformResultKind, 8 | } from './TransformRunner' 9 | import * as matchers from '@codemod/matchers' 10 | 11 | export * from './defineCodemod' 12 | export { t, types } from '@codemod/utils' 13 | export { matchers, matchers as m } 14 | 15 | function optionAnnotation( 16 | value: boolean | Array | Map | string 17 | ): string { 18 | if (Array.isArray(value) || value instanceof Map) { 19 | return ' (allows multiple)' 20 | } else if (typeof value === 'boolean') { 21 | return ` (default: ${value ? 'on' : 'off'})` 22 | } else if (typeof value === 'string') { 23 | return ` (default: ${value})` 24 | } else { 25 | return '' 26 | } 27 | } 28 | 29 | function printHelp(argv: Array, out: NodeJS.WritableStream): void { 30 | const $0 = basename(argv[1]) 31 | const defaults = new Config() 32 | 33 | out.write( 34 | ` 35 | ${$0} [OPTIONS] [PATH … | --stdio] 36 | 37 | OPTIONS 38 | -p, --plugin PLUGIN Transform sources with PLUGIN${optionAnnotation( 39 | defaults.localPlugins 40 | )}. 41 | --remote-plugin URL Fetch a plugin from URL${optionAnnotation( 42 | defaults.remotePlugins 43 | )}. 44 | -o, --plugin-options OPTS JSON-encoded OPTS for the last plugin provided${optionAnnotation( 45 | defaults.pluginOptions 46 | )}. 47 | --parser-plugins PLUGINS Comma-separated PLUGINS to use with @babel/parser. 48 | -r, --require PATH Require PATH before transform${optionAnnotation( 49 | defaults.requires 50 | )}. 51 | --add-extension EXT Add an extension to the list of supported extensions. 52 | --extensions EXTS Comma-separated extensions to process (default: "${Array.from( 53 | defaults.extensions 54 | ).join(',')}"). 55 | --source-type Parse as "module", "script", or "unambiguous" (meaning babel 56 | will try to guess, default: "${ 57 | defaults.sourceType 58 | }"). 59 | --[no-]transpile-plugins Transpile plugins to enable future syntax${optionAnnotation( 60 | defaults.transpilePlugins 61 | )}. 62 | -s, --stdio Read source from stdin and print to stdout${optionAnnotation( 63 | defaults.stdio 64 | )}. 65 | -d, --dry Run plugins without modifying files on disk${optionAnnotation( 66 | defaults.dry 67 | )}. 68 | --version Print the version of ${$0}. 69 | -h, --help Show this help message. 70 | 71 | NOTE: \`--remote-plugin\` should only be used as a convenience to load code that you or someone 72 | you trust wrote. It will run with your full user privileges, so please exercise caution! 73 | 74 | EXAMPLES 75 | # Run with a relative plugin on all files in \`src/\`. 76 | $ ${$0} -p ./typecheck.js src/ 77 | 78 | # Run with a remote plugin from astexplorer.net on all files in \`src/\`. 79 | $ ${$0} --remote-plugin 'https://astexplorer.net/#/gist/688274…' src/ 80 | 81 | # Run with multiple plugins. 82 | $ ${$0} -p ./a.js -p ./b.js some-file.js 83 | 84 | # Transform TypeScript sources. 85 | # ${$0} -p ./a.js my-typescript-file.ts a-component.tsx 86 | 87 | # Run with a plugin in \`node_modules\` on stdin. 88 | $ ${$0} -s -p babel-plugin-typecheck <, out: NodeJS.WritableStream): void { 108 | // eslint-disable-next-line @typescript-eslint/no-var-requires 109 | out.write(require('../package.json').version) 110 | out.write('\n') 111 | } 112 | 113 | export async function run(argv: Array): Promise { 114 | let command: Command 115 | 116 | try { 117 | command = new Options(argv.slice(2)).parse() 118 | } catch (error) { 119 | process.stderr.write(`ERROR: ${error.message}\n`) 120 | printHelp(argv, process.stderr) 121 | return 1 122 | } 123 | 124 | if (command.kind === 'help') { 125 | printHelp(argv, process.stdout) 126 | return 0 127 | } 128 | 129 | if (command.kind === 'version') { 130 | printVersion(argv, process.stdout) 131 | return 0 132 | } 133 | 134 | const config = command.config 135 | const dim = process.stdout.isTTY ? '\x1b[2m' : '' 136 | const reset = process.stdout.isTTY ? '\x1b[0m' : '' 137 | 138 | function onTransform(result: SourceTransformResult): void { 139 | const relativePath = relative(process.cwd(), result.source.path) 140 | if (result.kind === SourceTransformResultKind.Transformed) { 141 | if (!config.stdio) { 142 | if (result.output === result.source.content) { 143 | process.stdout.write(`${dim}${relativePath}${reset}\n`) 144 | } else { 145 | process.stdout.write(`${relativePath}\n`) 146 | } 147 | } 148 | } else if (result.error) { 149 | if (!config.stdio) { 150 | process.stderr.write( 151 | `Encountered an error while processing ${relativePath}:\n` 152 | ) 153 | } 154 | 155 | process.stderr.write(`${result.error.stack}\n`) 156 | } 157 | } 158 | 159 | const { stats } = await new CLIEngine(config, onTransform).run() 160 | 161 | if (!config.stdio) { 162 | if (config.dry) { 163 | process.stdout.write('DRY RUN: no files affected\n') 164 | } 165 | 166 | process.stdout.write( 167 | `${stats.total} file(s), ${stats.modified} modified, ${stats.errors} errors\n` 168 | ) 169 | } 170 | 171 | // exit status is number of errors up to byte max value 172 | return Math.min(stats.errors, 255) 173 | } 174 | 175 | export default run 176 | -------------------------------------------------------------------------------- /packages/cli/src/iterateSources.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert' 2 | import { promises as fs } from 'fs' 3 | import ignore, { Ignore } from 'ignore' 4 | import { basename, dirname, extname, isAbsolute, join, relative } from 'path' 5 | import { Source } from './TransformRunner' 6 | import globby = require('globby') 7 | import findUp = require('find-up') 8 | 9 | export interface Options { 10 | extensions?: Set 11 | cwd?: string 12 | } 13 | 14 | const DOTGIT = '.git' 15 | const GITIGNORE = '.gitignore' 16 | 17 | function pathWithin(container: string, contained: string): string | undefined { 18 | const pathInContainer = relative(container, contained) 19 | return pathInContainer.startsWith('../') ? undefined : pathInContainer 20 | } 21 | 22 | function pathContains(container: string, contained: string): boolean { 23 | return typeof pathWithin(container, contained) === 'string' 24 | } 25 | 26 | async function readIgnoreFile(path: string): Promise { 27 | const ig = ignore() 28 | ig.add(await fs.readFile(path, { encoding: 'utf-8' })) 29 | return ig 30 | } 31 | 32 | async function findGitroot(from: string): Promise { 33 | const dotgit = await findUp(DOTGIT, { cwd: from, type: 'directory' }) 34 | return dotgit && dirname(dotgit) 35 | } 36 | 37 | class FileFilter { 38 | private readonly cwd: string 39 | private readonly extensions?: Set 40 | private readonly ignoresByGitignoreDirectory = new Map() 41 | 42 | constructor({ cwd, extensions }: { cwd: string; extensions?: Set }) { 43 | this.cwd = cwd 44 | this.extensions = extensions 45 | } 46 | 47 | static async build({ 48 | cwd, 49 | extensions, 50 | }: { 51 | cwd: string 52 | extensions?: Set 53 | }): Promise { 54 | return new FileFilter({ 55 | cwd, 56 | extensions, 57 | }).addGitignoreFilesTraversingUpToGitRoot() 58 | } 59 | 60 | async addGitignoreFilesTraversingUpToGitRoot( 61 | start = this.cwd 62 | ): Promise { 63 | const gitroot = await findGitroot(start) 64 | 65 | if (gitroot) { 66 | let search = start 67 | 68 | while (pathContains(gitroot, search)) { 69 | const gitignorePath = await findUp(GITIGNORE, { 70 | cwd: search, 71 | type: 'file', 72 | }) 73 | 74 | if (!gitignorePath) { 75 | break 76 | } 77 | 78 | await this.addGitignoreFile(gitignorePath) 79 | search = dirname(dirname(gitignorePath)) 80 | } 81 | } 82 | 83 | return this 84 | } 85 | 86 | async addGitignoreFile(gitignorePath: string): Promise { 87 | if (!this.ignoresByGitignoreDirectory.has(dirname(gitignorePath))) { 88 | this.ignoresByGitignoreDirectory.set( 89 | dirname(gitignorePath), 90 | await readIgnoreFile(gitignorePath) 91 | ) 92 | } 93 | } 94 | 95 | test(path: string, { isFile }: { isFile: boolean }): boolean { 96 | const base = basename(path) 97 | 98 | if (base === DOTGIT || base === GITIGNORE) { 99 | return true 100 | } 101 | 102 | if (isFile && this.extensions && !this.extensions.has(extname(path))) { 103 | return true 104 | } 105 | 106 | for (const [directory, ig] of this.ignoresByGitignoreDirectory) { 107 | const pathInDirectory = pathWithin(directory, path) 108 | if (!pathInDirectory) { 109 | continue 110 | } 111 | 112 | if (ig.ignores(pathInDirectory)) { 113 | return true 114 | } 115 | } 116 | 117 | return false 118 | } 119 | } 120 | 121 | async function* iterateDirectory( 122 | root: string, 123 | { extensions }: Options = {} 124 | ): AsyncGenerator { 125 | assert(isAbsolute(root), `expected absolute path: ${root}`) 126 | 127 | const filter = await FileFilter.build({ cwd: root, extensions }) 128 | const queue: Array = [root] 129 | 130 | while (queue.length > 0) { 131 | const directory = queue.shift() as string 132 | const entries = await fs.readdir(directory, { withFileTypes: true }) 133 | 134 | for (const entry of entries) { 135 | if (entry.isFile() && entry.name === GITIGNORE) { 136 | const path = join(directory, entry.name) 137 | await filter.addGitignoreFile(path) 138 | } 139 | } 140 | 141 | for (const entry of entries) { 142 | const path = join(directory, entry.name) 143 | 144 | if (filter.test(path, { isFile: entry.isFile() })) { 145 | continue 146 | } 147 | 148 | if (entry.isFile()) { 149 | const content = await fs.readFile(path, { encoding: 'utf-8' }) 150 | yield { path, content } 151 | } else if (entry.isDirectory()) { 152 | queue.push(path) 153 | } 154 | } 155 | } 156 | } 157 | 158 | async function* iterateFiles( 159 | paths: Array, 160 | { extensions, cwd }: { extensions?: Set; cwd: string } 161 | ): AsyncGenerator { 162 | const filter = await FileFilter.build({ cwd, extensions }) 163 | 164 | for (const path of paths) { 165 | await filter.addGitignoreFilesTraversingUpToGitRoot(dirname(path)) 166 | if (!filter.test(path, { isFile: true })) { 167 | const content = await fs.readFile(path, { encoding: 'utf-8' }) 168 | yield { path, content } 169 | } 170 | } 171 | } 172 | 173 | /** 174 | * Builds an iterator that loops through all the files in the given paths, 175 | * matching an allowlist of extensions. Ignores files excluded by git. 176 | */ 177 | export async function* iterateSources( 178 | roots: Array, 179 | { 180 | extensions, 181 | cwd = process.cwd(), 182 | }: { extensions?: Set; cwd?: string } = {} 183 | ): AsyncGenerator { 184 | assert(isAbsolute(cwd), `expected absolute path: ${cwd}`) 185 | 186 | for (const root of roots) { 187 | if (globby.hasMagic(root)) { 188 | const matches = await globby(isAbsolute(root) ? root : join(cwd, root), { 189 | cwd, 190 | }) 191 | yield* iterateFiles(matches, { cwd, extensions }) 192 | } else if ((await fs.lstat(root)).isDirectory()) { 193 | yield* iterateDirectory(isAbsolute(root) ? root : join(cwd, root), { 194 | cwd, 195 | extensions, 196 | }) 197 | } else { 198 | yield* iterateFiles([root], { cwd }) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /packages/cli/src/resolvers/AstExplorerResolver.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { URL } from 'url' 3 | import { NetworkResolver } from './NetworkResolver' 4 | 5 | const EDITOR_HASH_PATTERN = /^#\/gist\/(\w+)(?:\/(\w+))?$/ 6 | 7 | /** 8 | * Resolves plugins from AST Explorer transforms. 9 | * 10 | * astexplorer.net uses GitHub gists to save and facilitate sharing. This 11 | * resolver accepts either the editor URL or the gist API URL. 12 | */ 13 | export class AstExplorerResolver extends NetworkResolver { 14 | constructor( 15 | private readonly baseURL: URL = new URL('https://astexplorer.net/') 16 | ) { 17 | super() 18 | } 19 | 20 | async canResolve(source: string): Promise { 21 | if (await super.canResolve(source)) { 22 | const url = new URL(await this.normalize(source)) 23 | const canResolve = 24 | this.matchesHost(url) && 25 | /^\/api\/v1\/gist\/[a-f0-9]+(\/(?:[a-f0-9]+|latest))?$/.test( 26 | url.pathname 27 | ) 28 | return canResolve 29 | } 30 | 31 | return false 32 | } 33 | 34 | async resolve(source: string): Promise { 35 | const filename = await super.resolve(await this.normalize(source)) 36 | const text = await fs.readFile(filename, { encoding: 'utf8' }) 37 | let data 38 | 39 | try { 40 | data = JSON.parse(text) 41 | } catch { 42 | throw new Error( 43 | `data loaded from ${source} is not JSON: ${text.slice(0, 100)}` 44 | ) 45 | } 46 | 47 | if ( 48 | !data || 49 | !data.files || 50 | !data.files['transform.js'] || 51 | !data.files['transform.js'].content 52 | ) { 53 | throw new Error( 54 | "'transform.js' could not be found, perhaps transform is disabled" 55 | ) 56 | } 57 | 58 | await fs.writeFile(filename, data.files['transform.js'].content, { 59 | encoding: 'utf8', 60 | }) 61 | 62 | return filename 63 | } 64 | 65 | async normalize(source: string): Promise { 66 | const url = new URL(source) 67 | 68 | if (!this.matchesHost(url)) { 69 | return source 70 | } 71 | 72 | const match = url.hash.match(EDITOR_HASH_PATTERN) 73 | 74 | if (!match) { 75 | return source 76 | } 77 | 78 | let path = `/api/v1/gist/${match[1]}` 79 | 80 | if (match[2]) { 81 | path += `/${match[2]}` 82 | } 83 | 84 | return new URL(path, this.baseURL).toString() 85 | } 86 | 87 | private matchesHost(url: URL): boolean { 88 | if (url.host !== this.baseURL.host) { 89 | return false 90 | } 91 | 92 | // use SSL even if the URL doesn't use it 93 | return url.protocol === this.baseURL.protocol || url.protocol === 'http:' 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/cli/src/resolvers/FileSystemResolver.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { resolve } from 'path' 3 | import { PluginExtensions } from '../extensions' 4 | import Resolver from './Resolver' 5 | 6 | async function isFile(path: string): Promise { 7 | try { 8 | return (await fs.stat(path)).isFile() 9 | } catch { 10 | return false 11 | } 12 | } 13 | 14 | /** 15 | * Resolves file system paths to plugins. 16 | */ 17 | export class FileSystemResolver implements Resolver { 18 | constructor( 19 | private readonly optionalExtensions: Set = PluginExtensions 20 | ) {} 21 | 22 | private *enumerateCandidateSources(source: string): IterableIterator { 23 | yield resolve(source) 24 | 25 | for (const ext of this.optionalExtensions) { 26 | if (ext[0] !== '.') { 27 | yield resolve(`${source}.${ext}`) 28 | } else { 29 | yield resolve(source + ext) 30 | } 31 | } 32 | } 33 | 34 | async canResolve(source: string): Promise { 35 | for (const candidate of this.enumerateCandidateSources(source)) { 36 | if (await isFile(candidate)) { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | 44 | async resolve(source: string): Promise { 45 | for (const candidate of this.enumerateCandidateSources(source)) { 46 | if (await isFile(candidate)) { 47 | return candidate 48 | } 49 | } 50 | 51 | throw new Error(`unable to resolve file from source: ${source}`) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/cli/src/resolvers/NetworkResolver.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { fetch } from 'cross-fetch' 3 | import { tmpNameSync as tmp } from 'tmp' 4 | import { URL } from 'url' 5 | import Resolver from './Resolver' 6 | 7 | export class NetworkLoadError extends Error { 8 | constructor(readonly response: Response) { 9 | super(`failed to load plugin from '${response.url}'`) 10 | } 11 | } 12 | 13 | /** 14 | * Resolves plugins over the network to a local file. 15 | * 16 | * This plugin accepts only absolute HTTP URLs. 17 | */ 18 | export class NetworkResolver implements Resolver { 19 | async canResolve(source: string): Promise { 20 | try { 21 | const url = new URL(source) 22 | return url.protocol === 'http:' || url.protocol === 'https:' 23 | } catch { 24 | return false 25 | } 26 | } 27 | 28 | async resolve(source: string): Promise { 29 | const response = await fetch(source, { redirect: 'follow' }) 30 | 31 | if (response.status !== 200) { 32 | throw new NetworkLoadError(response) 33 | } 34 | 35 | const filename = tmp({ postfix: '.js' }) 36 | await fs.writeFile(filename, await response.text()) 37 | return filename 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/cli/src/resolvers/PackageResolver.ts: -------------------------------------------------------------------------------- 1 | import { sync as resolveSync } from 'resolve' 2 | import Resolver from './Resolver' 3 | 4 | /** 5 | * Resolves node modules by name relative to the working directory. 6 | * 7 | * For example, if used in a project that includes `myplugin` in its 8 | * `node_modules`, this class will resolve to the main file of `myplugin`. 9 | */ 10 | export class PackageResolver implements Resolver { 11 | async canResolve(source: string): Promise { 12 | try { 13 | await this.resolve(source) 14 | return true 15 | } catch { 16 | return false 17 | } 18 | } 19 | 20 | async resolve(source: string): Promise { 21 | return resolveSync(source, { basedir: process.cwd() }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/cli/src/resolvers/Resolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resolvers locate plugin paths given a source string. 3 | */ 4 | export default interface Resolver { 5 | /** 6 | * Determines whether the source can be resolved by this resolver. This should 7 | * not necessarily determine whether the source actually does resolve to 8 | * anything, but rather whether it is of the right form to be resolved. 9 | * 10 | * For example, if a resolver were written to load a plugin from a `data:` URI 11 | * then this method might simply check that the URI is a valid `data:` URI 12 | * rather than actually decoding and handling the contents of said URI. 13 | */ 14 | canResolve(source: string): Promise 15 | 16 | /** 17 | * Determines a file path that, when loaded as JavaScript, exports a babel 18 | * plugin suitable for use in the transform pipeline. If `source` does not 19 | * actually resolve to a file already on disk, consider writing the contents 20 | * to disk in a temporary location. 21 | */ 22 | resolve(source: string): Promise 23 | } 24 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/__tests__"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "rootDir": "src", 10 | "outDir": "build" 11 | }, 12 | "references": [ 13 | { "path": "../core/tsconfig.build.json" }, 14 | { "path": "../matchers/tsconfig.build.json" }, 15 | { "path": "../parser/tsconfig.build.json" }, 16 | { "path": "../utils/tsconfig.build.json" } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "lib": ["es2015", "es2016"], 6 | "composite": true, 7 | "noEmit": true, 8 | "noImplicitAny": false, 9 | "strictNullChecks": true 10 | }, 11 | "exclude": ["__tests__/fixtures/**/*.ts", "tmp", "build"], 12 | "references": [ 13 | { "path": "../core/tsconfig.build.json" }, 14 | { "path": "../matchers/tsconfig.build.json" }, 15 | { "path": "../parser/tsconfig.build.json" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | -------------------------------------------------------------------------------- /packages/core/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 | # [2.2.0](https://github.com/codemod-js/codemod/compare/@codemod/core@2.1.0...@codemod/core@2.2.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 | # [2.1.0](https://github.com/codemod-js/codemod/compare/@codemod/core@2.0.0...@codemod/core@2.1.0) (2023-02-17) 18 | 19 | ### Bug Fixes 20 | 21 | - update intra-monorepo dependencies ([3c95444](https://github.com/codemod-js/codemod/commit/3c95444e1f57b982634e635931ca08ecf5805e21)) 22 | 23 | ### Features 24 | 25 | - **cli,core:** use `esbuild-runner` ([ab527b2](https://github.com/codemod-js/codemod/commit/ab527b2cea23211732ab1a45512dc1f968c707c6)) 26 | - **cli:** add `--parser-plugins` option ([3593893](https://github.com/codemod-js/codemod/commit/3593893791c7e4e0e0c8cea31ea642b229c0bb8a)) 27 | 28 | ## [2.0.0](https://github.com/codemod-js/codemod/compare/@codemod/core@1.1.1...@codemod/core@2.0.0) (2021-11-12) 29 | 30 | - remove underused options: `--printer`, `--babelrc`, `--find-babel-config` ([50a864d](https://github.com/codemod-js/codemod/commit/50a864df7344767a5c0e9e3ab990a0f4d05d634d)) 31 | - update babel to v7.16.x ([1218bf9](https://github.com/codemod-js/codemod/commit/1218bf98145feaa8a692611152559aa6b46b9ba0)) 32 | 33 | ## [1.0.7](https://github.com/codemod-js/codemod/compare/@codemod/core@1.0.4...@codemod/core@1.0.7) (2020-04-05) 34 | 35 | ### Bug Fixes 36 | 37 | - **deps:** upgrade recast ([b6220f3](https://github.com/codemod-js/codemod/commit/b6220f3f26a41f4e58bdca7815bc8f6e9a820866)) 38 | - update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d)) 39 | - upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a)) 40 | - upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102)) 41 | 42 | ## [1.0.6](https://github.com/codemod-js/codemod/compare/@codemod/core@1.0.4...@codemod/core@1.0.6) (2020-03-25) 43 | 44 | ### Bug Fixes 45 | 46 | - update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d)) 47 | - upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a)) 48 | - upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102)) 49 | 50 | ## [1.0.2](https://github.com/codemod-js/codemod/compare/@codemod/core@1.0.1...@codemod/core@1.0.2) (2019-08-09) 51 | 52 | ### Bug Fixes 53 | 54 | - **license:** update outdated license files ([58e4b11](https://github.com/codemod-js/codemod/commit/58e4b11)) 55 | 56 | ## [1.0.1](https://github.com/codemod-js/codemod/compare/@codemod/core@1.0.0...@codemod/core@1.0.1) (2019-08-09) 57 | 58 | ### Bug Fixes 59 | 60 | - use `ParserOptions` from `[@codemod](https://github.com/codemod)/parser` ([675aa5c](https://github.com/codemod-js/codemod/commit/675aa5c)) 61 | - **package:** add "types" to package.json ([094d504](https://github.com/codemod-js/codemod/commit/094d504)) 62 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @codemod/core 2 | 3 | Runs babel plugins for codemods, i.e. by preserving formatting using [Recast](https://github.com/benjamn/recast). 4 | 5 | ## Install 6 | 7 | Install from [npm](https://npmjs.com/): 8 | 9 | ```sh 10 | $ npm install @codemod/core 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { transform } from '@codemod/core' 17 | 18 | const result = transform('a ?? b', { 19 | plugins: ['@babel/plugin-proposal-nullish-coalescing-operator'], 20 | }) 21 | 22 | console.log(result.code) 23 | /* 24 | var _a; 25 | (_a = a) !== null && _a !== void 0 ? _a : b 26 | */ 27 | ``` 28 | 29 | ## Contributing 30 | 31 | See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information on setting up the project for development and on contributing to the project. 32 | 33 | ## License 34 | 35 | Copyright 2019 Brian Donovan 36 | 37 | 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 38 | 39 | http://www.apache.org/licenses/LICENSE-2.0 40 | 41 | 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. 42 | -------------------------------------------------------------------------------- /packages/core/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/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codemod/core", 3 | "version": "2.2.0", 4 | "description": "Runs babel plugins for codemods, i.e. by preserving formatting using Recast.", 5 | "repository": "https://github.com/codemod-js/codemod", 6 | "license": "Apache-2.0", 7 | "author": "Brian Donovan", 8 | "main": "build/index.js", 9 | "types": "build/index.d.ts", 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc --build tsconfig.build.json", 15 | "clean": "rm -rf build tsconfig.build.tsbuildinfo", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "prepublishOnly": "pnpm clean && pnpm build", 19 | "test": "is-ci test:coverage test:watch", 20 | "test:coverage": "jest --coverage", 21 | "test:watch": "jest --watch" 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.20.12", 25 | "@babel/generator": "^7.20.14", 26 | "@codemod/parser": "^1.4.0", 27 | "is-ci-cli": "^2.2.0", 28 | "recast": "^0.19.0", 29 | "resolve": "^1.22.1" 30 | }, 31 | "devDependencies": { 32 | "@babel/types": "^7.20.7", 33 | "@types/babel__core": "^7.1.16", 34 | "@types/jest": "^25.1.0", 35 | "@types/node": "^18.14.0", 36 | "@types/prettier": "^2.0.0", 37 | "@types/resolve": "^1.14.0", 38 | "jest": "^27.3.1", 39 | "typescript": "^4.9.5" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/AllSyntaxPlugin.ts: -------------------------------------------------------------------------------- 1 | import { buildOptions, ParserOptions } from '@codemod/parser' 2 | import { TransformOptions } from '.' 3 | import { BabelPlugin, PluginObj } from './BabelPluginTypes' 4 | 5 | export function buildPlugin( 6 | sourceType: ParserOptions['sourceType'] 7 | ): BabelPlugin { 8 | return function (): PluginObj { 9 | return { 10 | manipulateOptions( 11 | opts: TransformOptions, 12 | parserOpts: ParserOptions 13 | ): void { 14 | const options = buildOptions({ 15 | ...parserOpts, 16 | sourceType, 17 | plugins: parserOpts.plugins, 18 | }) 19 | 20 | for (const key of Object.keys(options)) { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | ;(parserOpts as any)[key] = (options as any)[key] 23 | } 24 | }, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/BabelPluginTypes.ts: -------------------------------------------------------------------------------- 1 | import * as Babel from '@babel/core' 2 | import { File } from '@babel/types' 3 | import { ParserOptions } from '@codemod/parser' 4 | 5 | /** 6 | * Fixes the `PluginObj` type from `@babel/core` by making all fields optional 7 | * and adding parser and generator override methods. 8 | */ 9 | export interface PluginObj extends Partial> { 10 | parserOverride?( 11 | code: string, 12 | options: ParserOptions, 13 | parse: (code: string, options: ParserOptions) => File 14 | ): File 15 | 16 | generatorOverride?( 17 | ast: File, 18 | options: Babel.GeneratorOptions, 19 | code: string, 20 | generate: (ast: File, options: Babel.GeneratorOptions) => string 21 | ): { code: string; map?: object } 22 | } 23 | 24 | export type RawBabelPlugin = (babel: typeof Babel) => PluginObj 25 | export type RawBabelPluginWithOptions = [RawBabelPlugin, object] 26 | export type BabelPlugin = RawBabelPlugin | RawBabelPluginWithOptions 27 | -------------------------------------------------------------------------------- /packages/core/src/RecastPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ParserOptions } from '@codemod/parser' 2 | import { File } from '@babel/types' 3 | import * as recast from 'recast' 4 | import { PluginObj } from './BabelPluginTypes' 5 | 6 | export function parse( 7 | code: string, 8 | options: ParserOptions, 9 | parse: (code: string, options: ParserOptions) => File 10 | ): File { 11 | return recast.parse(code, { 12 | parser: { 13 | parse(code: string) { 14 | return parse(code, { ...options, tokens: true }) 15 | }, 16 | }, 17 | }) 18 | } 19 | 20 | export function generate(ast: File): { code: string; map?: object } { 21 | return recast.print(ast, { sourceMapName: 'map.json' }) 22 | } 23 | 24 | export default function (): PluginObj { 25 | return { 26 | parserOverride: parse, 27 | generatorOverride: generate, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/test.ts: -------------------------------------------------------------------------------- 1 | import { PluginItem } from '@babel/core' 2 | import { transform } from '../transform' 3 | 4 | const incrementNumbersPlugin: PluginItem = { 5 | visitor: { 6 | NumericLiteral(path) { 7 | path.node.value += 1 8 | }, 9 | }, 10 | } 11 | 12 | test('preserves formatting', () => { 13 | expect(transform('var a=1;').code).toBe('var a=1;') 14 | }) 15 | 16 | test('transforms using a custom babel plugin', () => { 17 | expect( 18 | transform('var a=1', { 19 | plugins: [incrementNumbersPlugin], 20 | }).code 21 | ).toBe('var a=2') 22 | }) 23 | 24 | test('parses with as many parser plugins as possible', () => { 25 | expect(() => transform('a ?? b').code).not.toThrow() 26 | }) 27 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transform' 2 | -------------------------------------------------------------------------------- /packages/core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BabelFileResult, 3 | TransformOptions as BabelTransformOptions, 4 | transformSync, 5 | } from '@babel/core' 6 | import { strict as assert } from 'assert' 7 | import { buildPlugin } from './AllSyntaxPlugin' 8 | import RecastPlugin from './RecastPlugin' 9 | 10 | export type TransformOptions = BabelTransformOptions 11 | 12 | /** 13 | * Transform `code` using `@babel/core` parsing using Recast. Additionally, 14 | * `@codemod/parser` is used to enable as many parser plugins as possible. 15 | */ 16 | export function transform( 17 | code: string, 18 | options: TransformOptions = {} 19 | ): BabelFileResult { 20 | const result = transformSync(code, { 21 | ...options, 22 | plugins: [ 23 | ...(options.plugins || []), 24 | buildPlugin(options.sourceType || 'unambiguous'), 25 | RecastPlugin, 26 | ], 27 | }) 28 | assert(result, 'transformSync must return a result') 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/__tests__"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "outDir": "build" 11 | }, 12 | "references": [{ "path": "../parser/tsconfig.build.json" }] 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "lib": ["es2015", "es2016"], 6 | "composite": true, 7 | "noEmit": true, 8 | "noImplicitAny": false, 9 | "strict": true 10 | }, 11 | "exclude": ["tmp", "build"], 12 | "references": [{ "path": "../parser/tsconfig.build.json" }] 13 | } 14 | -------------------------------------------------------------------------------- /packages/matchers/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | src/matchers.ts 4 | -------------------------------------------------------------------------------- /packages/matchers/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 | # [1.6.0](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.4.0...@codemod/matchers@1.6.0) (2023-02-18) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **rebuild-matchers:** update path to generated matchers ([e2173a4](https://github.com/codemod-js/codemod/commit/e2173a4e69f19cbf644ec22d36c748d81bdcd631)) 12 | 13 | 14 | ### Features 15 | 16 | * **cli:** add `defineCodemod` to ease creation ([86a62a1](https://github.com/codemod-js/codemod/commit/86a62a11d9f25f2e2e581ff6287ce885ce18f93a)) 17 | * **matchers:** rebuild `matchers.ts` ([30e4978](https://github.com/codemod-js/codemod/commit/30e4978ad1b0253f6ff6f5bf04e926b87b16fc00)) 18 | 19 | 20 | 21 | 22 | 23 | # [1.3.0](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.2.0...@codemod/matchers@1.3.0) (2023-02-17) 24 | 25 | ### Bug Fixes 26 | 27 | - update intra-monorepo dependencies ([3c95444](https://github.com/codemod-js/codemod/commit/3c95444e1f57b982634e635931ca08ecf5805e21)) 28 | 29 | ### Features 30 | 31 | - **cli,core:** use `esbuild-runner` ([ab527b2](https://github.com/codemod-js/codemod/commit/ab527b2cea23211732ab1a45512dc1f968c707c6)) 32 | 33 | ## [1.2.0](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.1.1...@codemod/matchers@1.2.0) (2021-11-12) 34 | 35 | - update babel to v7.16.x ([1218bf9](https://github.com/codemod-js/codemod/commit/1218bf98145feaa8a692611152559aa6b46b9ba0)) 36 | 37 | ## [1.0.11](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.8...@codemod/matchers@1.0.11) (2020-04-05) 38 | 39 | ### Bug Fixes 40 | 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 | ## [1.0.10](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.8...@codemod/matchers@1.0.10) (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 | ## [1.0.6](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.5...@codemod/matchers@1.0.6) (2019-10-16) 54 | 55 | ### Bug Fixes 56 | 57 | - **package:** ensure updated babel deps are used ([06864f2](https://github.com/codemod-js/codemod/commit/06864f2)) 58 | 59 | ## [1.0.5](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.4...@codemod/matchers@1.0.5) (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 | ## [1.0.4](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.3...@codemod/matchers@1.0.4) (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 | ## [1.0.3](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.2...@codemod/matchers@1.0.3) (2019-08-02) 72 | 73 | ### Refactors 74 | 75 | - extract `@codemod/parser` and `@codemod/core` ([c43ecd52](https://github.com/codemod-js/codemod/commit/c43ecd52)) 76 | 77 | ## [1.0.2](https://github.com/codemod-js/codemod/compare/@codemod/matchers@1.0.1...@codemod/matchers@1.0.2) (2019-07-31) 78 | 79 | ### Bug Fixes 80 | 81 | - update babel dependencies ([6984c7c](https://github.com/codemod-js/codemod/commit/6984c7c)) 82 | -------------------------------------------------------------------------------- /packages/matchers/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /** @type {import('@jest/types').Config.InitialOptions} */ 4 | module.exports = { 5 | clearMocks: true, 6 | moduleFileExtensions: ['ts', 'tsx', 'js'], 7 | testEnvironment: 'node', 8 | testRegex: '/__tests__/(test|.*\\.test)\\.ts$', 9 | transform: { 10 | '\\.ts$': 'esbuild-runner/jest', 11 | }, 12 | collectCoverageFrom: [ 13 | 'src/**/*.ts', 14 | '!**/__tests__/**/*.ts', 15 | '!src/matchers.ts', 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /packages/matchers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codemod/matchers", 3 | "version": "1.7.1", 4 | "description": "Matchers for JavaScript & TypeScript codemods.", 5 | "repository": "https://github.com/codemod-js/codemod", 6 | "license": "Apache-2.0", 7 | "author": "Brian Donovan", 8 | "main": "build/index.js", 9 | "types": "build/index.d.ts", 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc --build tsconfig.build.json", 15 | "clean": "rm -rf build tsconfig.build.tsbuildinfo", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "prepublishOnly": "pnpm clean && pnpm build", 19 | "test": "is-ci test:coverage test:watch", 20 | "test:coverage": "jest --coverage", 21 | "test:watch": "jest --watch" 22 | }, 23 | "dependencies": { 24 | "@babel/types": "^7.20.7", 25 | "@codemod/utils": "^1.1.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.20.12", 29 | "@babel/generator": "^7.20.14", 30 | "@babel/traverse": "^7.20.13", 31 | "@codemod/core": "^2.2.0", 32 | "@codemod/parser": "^1.4.0", 33 | "@types/babel__core": "^7.20.0", 34 | "@types/babel__generator": "^7.6.4", 35 | "@types/babel__template": "^7.4.1", 36 | "@types/babel__traverse": "^7.18.3", 37 | "@types/dedent": "^0.7.0", 38 | "@types/jest": "^25.1.0", 39 | "@types/node": "^18.14.0", 40 | "@types/prettier": "^2.0.0", 41 | "dedent": "^0.7.0", 42 | "is-ci-cli": "^2.2.0", 43 | "jest": "^27.3.1", 44 | "typescript": "^4.9.5" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/matchers/src/__tests__/distributeAcrossSlices.test.ts: -------------------------------------------------------------------------------- 1 | import { oneOrMore, slice, spacer, zeroOrMore } from '../matchers/slice' 2 | import { distributeAcrossSlices } from '../utils/distributeAcrossSlices' 3 | 4 | test('allocates nothing given an empty list of slices', () => { 5 | expect(Array.from(distributeAcrossSlices([], 1))).toEqual([[]]) 6 | }) 7 | 8 | test('allocates available to a single slice within its bounds', () => { 9 | expect( 10 | Array.from(distributeAcrossSlices([slice({ min: 0, max: 3 })], 2)) 11 | ).toEqual([[2]]) 12 | expect( 13 | Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 3)) 14 | ).toEqual([[3]]) 15 | }) 16 | 17 | test('allocates nothing if available is outside single slice bounds', () => { 18 | expect( 19 | Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 1)) 20 | ).toEqual([]) 21 | expect( 22 | Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 5)) 23 | ).toEqual([]) 24 | }) 25 | 26 | test('allocates a single space across multiple slices', () => { 27 | expect( 28 | Array.from( 29 | distributeAcrossSlices( 30 | [slice({ min: 0, max: 1 }), slice({ min: 0, max: 1 })], 31 | 1 32 | ) 33 | ) 34 | ).toEqual([ 35 | [1, 0], 36 | [0, 1], 37 | ]) 38 | }) 39 | 40 | test('allocates multiple spaces across multiple slices', () => { 41 | expect( 42 | Array.from( 43 | distributeAcrossSlices( 44 | [ 45 | slice({ min: 1, max: 2 }), 46 | slice({ min: 0, max: 1 }), 47 | slice({ min: 2, max: 3 }), 48 | ], 49 | 5 50 | ) 51 | ) 52 | ).toEqual([ 53 | [2, 1, 2], 54 | [2, 0, 3], 55 | [1, 1, 3], 56 | ]) 57 | }) 58 | 59 | test('never allocates to empty slices', () => { 60 | expect( 61 | Array.from( 62 | distributeAcrossSlices( 63 | [slice(0), slice({ min: 0, max: 1 }), slice({ min: 0, max: 1 })], 64 | 1 65 | ) 66 | ) 67 | ).toEqual([ 68 | [0, 1, 0], 69 | [0, 0, 1], 70 | ]) 71 | }) 72 | 73 | test('allocates correctly when slices have no upper bound', () => { 74 | expect(Array.from(distributeAcrossSlices([zeroOrMore()], 2))).toEqual([[2]]) 75 | }) 76 | 77 | test('allocates correctly with a trailing unbounded slice', () => { 78 | expect( 79 | Array.from(distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 1)) 80 | ).toEqual([]) 81 | expect( 82 | Array.from(distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 2)) 83 | ).toEqual([[0, 1, 1]]) 84 | expect( 85 | Array.from(distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 3)) 86 | ).toEqual([ 87 | [1, 1, 1], 88 | [0, 1, 2], 89 | ]) 90 | }) 91 | 92 | test('deprecated spacer() is an alias for slice(1)', () => { 93 | expect(spacer()).toEqual(slice(1)) 94 | }) 95 | -------------------------------------------------------------------------------- /packages/matchers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './matchers' 2 | export { match } from './utils/match' 3 | export { matchPath } from './utils/matchPath' 4 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/Matcher.ts: -------------------------------------------------------------------------------- 1 | export class Matcher { 2 | match(value: unknown, keys: ReadonlyArray = []): value is T { 3 | return this.matchValue(value, keys) 4 | } 5 | 6 | matchValue( 7 | /* eslint-disable @typescript-eslint/no-unused-vars */ 8 | value: unknown, 9 | keys: ReadonlyArray 10 | /* eslint-enable @typescript-eslint/no-unused-vars */ 11 | ): value is T { 12 | throw new Error(`${this.constructor.name}#matchValue is not implemented`) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anyExpression.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { Matcher } from './Matcher' 3 | 4 | export class AnyExpressionMatcher extends Matcher { 5 | matchValue(value: unknown): value is t.Expression { 6 | return t.isNode(value) && t.isExpression(value) 7 | } 8 | } 9 | 10 | export function anyExpression(): Matcher { 11 | return new AnyExpressionMatcher() 12 | } 13 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anyList.ts: -------------------------------------------------------------------------------- 1 | import { distributeAcrossSlices } from '../utils/distributeAcrossSlices' 2 | import { Matcher } from './Matcher' 3 | import { SliceMatcher } from './slice' 4 | 5 | export class AnyListMatcher extends Matcher> { 6 | private readonly sliceMatchers: Array> = [] 7 | 8 | constructor(private readonly matchers: Array>) { 9 | super() 10 | 11 | for (const matcher of matchers) { 12 | if (matcher instanceof SliceMatcher) { 13 | this.sliceMatchers.push(matcher) 14 | } 15 | } 16 | } 17 | 18 | matchValue( 19 | array: unknown, 20 | keys: ReadonlyArray 21 | ): array is Array { 22 | if (!Array.isArray(array)) { 23 | return false 24 | } 25 | 26 | if (this.matchers.length === 0 && array.length === 0) { 27 | return true 28 | } 29 | 30 | const spacerAllocations = distributeAcrossSlices( 31 | this.sliceMatchers, 32 | array.length - this.matchers.length + this.sliceMatchers.length 33 | ) 34 | 35 | for (const allocations of spacerAllocations) { 36 | const valuesToMatch: Array = array.slice() 37 | let matchedAll = true 38 | let key = 0 39 | 40 | for (const matcher of this.matchers) { 41 | if (matcher instanceof SliceMatcher) { 42 | let sliceValueCount = allocations.shift() || 0 43 | 44 | while (sliceValueCount > 0) { 45 | const valueToMatch = valuesToMatch.shift() 46 | if (!matcher.matchValue(valueToMatch, [...keys, key])) { 47 | matchedAll = false 48 | break 49 | } 50 | sliceValueCount-- 51 | key++ 52 | } 53 | } else if (!matcher.matchValue(valuesToMatch.shift(), [...keys, key])) { 54 | matchedAll = false 55 | break 56 | } else { 57 | key++ 58 | } 59 | } 60 | 61 | if (matchedAll) { 62 | if (valuesToMatch.length > 0) { 63 | throw new Error( 64 | `expected to consume all elements to match but ${valuesToMatch.length} remain!` 65 | ) 66 | } 67 | 68 | return true 69 | } 70 | } 71 | 72 | return false 73 | } 74 | } 75 | 76 | export function anyList(...elements: Array>): Matcher> { 77 | return new AnyListMatcher(elements) 78 | } 79 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anyNode.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { Matcher } from './Matcher' 3 | 4 | export class AnyNodeMatcher extends Matcher { 5 | matchValue(value: unknown): value is t.Node { 6 | return t.isNode(value) 7 | } 8 | } 9 | 10 | export function anyNode(): Matcher { 11 | return new AnyNodeMatcher() 12 | } 13 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anyNumber.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class NumberMatcher extends Matcher { 4 | matchValue(value: unknown): value is number { 5 | return typeof value === 'number' || value instanceof Number 6 | } 7 | } 8 | 9 | export function anyNumber(): Matcher { 10 | return new NumberMatcher() 11 | } 12 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anyStatement.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { Matcher } from './Matcher' 3 | 4 | export class AnyStatementMatcher extends Matcher { 5 | matchValue(value: unknown): value is t.Statement { 6 | return t.isNode(value) && t.isStatement(value) 7 | } 8 | } 9 | 10 | export function anyStatement(): Matcher { 11 | return new AnyStatementMatcher() 12 | } 13 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anyString.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class StringMatcher extends Matcher { 4 | matchValue(value: unknown): value is string { 5 | return typeof value === 'string' || value instanceof String 6 | } 7 | } 8 | 9 | export function anyString(): Matcher { 10 | return new StringMatcher() 11 | } 12 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/anything.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class AnythingMatcher extends Matcher { 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | matchValue(value: unknown): value is T { 6 | return true 7 | } 8 | } 9 | 10 | export function anything(): Matcher { 11 | return new AnythingMatcher() 12 | } 13 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/arrayOf.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class ArrayOfMatcher extends Matcher> { 4 | constructor(private readonly elementMatcher: Matcher) { 5 | super() 6 | } 7 | 8 | matchValue( 9 | value: unknown, 10 | keys: ReadonlyArray 11 | ): value is Array { 12 | if (!Array.isArray(value)) { 13 | return false 14 | } 15 | 16 | for (const [i, element] of value.entries()) { 17 | if (!this.elementMatcher.matchValue(element, [...keys, i])) { 18 | return false 19 | } 20 | } 21 | 22 | return true 23 | } 24 | } 25 | 26 | export function arrayOf(elementMatcher: Matcher): Matcher> { 27 | return new ArrayOfMatcher(elementMatcher) 28 | } 29 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/capture.ts: -------------------------------------------------------------------------------- 1 | import { anything } from './anything' 2 | import { Matcher } from './Matcher' 3 | 4 | export interface CaptureBase { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | [key: string]: any 7 | } 8 | 9 | export class CapturedMatcher extends Matcher { 10 | private _current?: C 11 | private _currentKeys?: ReadonlyArray 12 | 13 | constructor(private readonly matcher: Matcher = anything()) { 14 | super() 15 | } 16 | 17 | get current(): C | undefined { 18 | return this._current 19 | } 20 | 21 | get currentKeys(): ReadonlyArray | undefined { 22 | return this._currentKeys 23 | } 24 | 25 | matchValue(value: unknown, keys: ReadonlyArray): value is M { 26 | if (this.matcher.matchValue(value, keys)) { 27 | this.capture(value as unknown as C, keys) 28 | return true 29 | } else { 30 | return false 31 | } 32 | } 33 | 34 | protected capture(value: C, keys: ReadonlyArray): void { 35 | this._current = value 36 | this._currentKeys = keys 37 | } 38 | } 39 | 40 | export function capture(matcher?: Matcher): CapturedMatcher { 41 | return new CapturedMatcher(matcher) 42 | } 43 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/containerOf.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { Matcher } from './Matcher' 3 | import { CapturedMatcher } from './capture' 4 | 5 | /** 6 | * Matches and captures using another matcher by recursively checking all 7 | * descendants of a given node. The matched descendant is captured as the 8 | * current value of this capturing matcher. 9 | */ 10 | export class ContainerOfMatcher< 11 | C extends t.Node, 12 | M extends t.Node = C 13 | > extends CapturedMatcher { 14 | constructor(private readonly containedMatcher: Matcher) { 15 | super() 16 | } 17 | 18 | matchValue(value: unknown, keys: ReadonlyArray): value is M { 19 | if (!t.isNode(value)) { 20 | return false 21 | } 22 | 23 | if (this.containedMatcher.matchValue(value, keys)) { 24 | this.capture(value, keys) 25 | return true 26 | } 27 | 28 | for (const key in value) { 29 | const valueAtKey = value[key as keyof typeof value] 30 | if (Array.isArray(valueAtKey)) { 31 | for (const [i, element] of valueAtKey.entries()) { 32 | if (this.matchValue(element, [...keys, key, i])) { 33 | return true 34 | } 35 | } 36 | } else if (this.matchValue(valueAtKey, [...keys, key])) { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | } 44 | 45 | export function containerOf( 46 | containedMatcher: Matcher 47 | ): ContainerOfMatcher { 48 | return new ContainerOfMatcher(containedMatcher) 49 | } 50 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/fromCapture.ts: -------------------------------------------------------------------------------- 1 | import { nodesEquivalent, t } from '@codemod/utils' 2 | import { CapturedMatcher } from './capture' 3 | import { Matcher } from './Matcher' 4 | 5 | export class FromCaptureMatcher extends Matcher { 6 | constructor(private readonly capturedMatcher: CapturedMatcher) { 7 | super() 8 | } 9 | 10 | matchValue(value: unknown): value is T { 11 | if (t.isNode(this.capturedMatcher.current) && t.isNode(value)) { 12 | return nodesEquivalent(this.capturedMatcher.current, value) 13 | } 14 | return this.capturedMatcher.current === value 15 | } 16 | } 17 | 18 | export function fromCapture( 19 | capturedMatcher: CapturedMatcher 20 | ): Matcher { 21 | return new FromCaptureMatcher(capturedMatcher) 22 | } 23 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/function.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { Matcher } from './Matcher' 3 | import { tupleOf } from './tupleOf' 4 | 5 | export class FunctionMatcher extends Matcher { 6 | constructor( 7 | private readonly params?: Matcher> | Array>, 8 | private readonly body?: Matcher 9 | ) { 10 | super() 11 | } 12 | 13 | matchValue( 14 | value: unknown, 15 | keys: ReadonlyArray 16 | ): value is t.Function { 17 | if (!t.isNode(value) || !t.isFunction(value)) { 18 | return false 19 | } 20 | 21 | if (this.params) { 22 | if (Array.isArray(this.params)) { 23 | if ( 24 | !tupleOf(...this.params).matchValue(value.params, [...keys, 'params']) 25 | ) { 26 | return false 27 | } 28 | } else if (!this.params.matchValue(value.params, [...keys, 'params'])) { 29 | return false 30 | } 31 | } 32 | 33 | if (this.body && !this.body.matchValue(value.body, [...keys, 'body'])) { 34 | return false 35 | } 36 | 37 | return true 38 | } 39 | } 40 | 41 | export function Function( 42 | params?: Matcher> | Array>, 43 | body?: Matcher 44 | ): Matcher { 45 | return new FunctionMatcher(params, body) 46 | } 47 | 48 | export { Function as function } 49 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/index.ts: -------------------------------------------------------------------------------- 1 | export { anyExpression } from './anyExpression' 2 | export { anyList } from './anyList' 3 | export { anyNode } from './anyNode' 4 | export { anyNumber } from './anyNumber' 5 | export { anyStatement } from './anyStatement' 6 | export { anyString } from './anyString' 7 | export { anything } from './anything' 8 | export { arrayOf } from './arrayOf' 9 | export { capture, CaptureBase, CapturedMatcher } from './capture' 10 | export { containerOf } from './containerOf' 11 | export { fromCapture } from './fromCapture' 12 | export { Function, function } from './function' 13 | export * from './generated' 14 | export { Matcher } from './Matcher' 15 | export { oneOf } from './oneOf' 16 | export { or } from './or' 17 | export { predicate as matcher } from './predicate' 18 | export * from './slice' 19 | export { tupleOf } from './tupleOf' 20 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/oneOf.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class OneOfMatcher extends Matcher<[T]> { 4 | constructor(private readonly matcher: Matcher) { 5 | super() 6 | } 7 | 8 | matchValue(value: unknown, keys: ReadonlyArray): value is [T] { 9 | if (!Array.isArray(value)) { 10 | return false 11 | } 12 | 13 | if (value.length !== 1) { 14 | return false 15 | } 16 | 17 | return this.matcher.matchValue(value[0], [...keys, 0]) 18 | } 19 | } 20 | 21 | export function oneOf(matcher: Matcher): Matcher<[T]> { 22 | return new OneOfMatcher(matcher) 23 | } 24 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/or.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class OrMatcher | T>> extends Matcher { 4 | private readonly matchersOrValues: A 5 | 6 | constructor(...matchersOrValues: A) { 7 | super() 8 | this.matchersOrValues = matchersOrValues 9 | } 10 | 11 | matchValue(value: unknown, keys: ReadonlyArray): value is T { 12 | for (const matcherOrValue of this.matchersOrValues) { 13 | if (matcherOrValue instanceof Matcher) { 14 | if (matcherOrValue.matchValue(value, keys)) { 15 | return true 16 | } 17 | } else if (matcherOrValue === value) { 18 | return true 19 | } 20 | } 21 | return false 22 | } 23 | } 24 | 25 | export function or(): Matcher 26 | export function or(first: Matcher | T): Matcher 27 | export function or( 28 | first: Matcher | T, 29 | second: Matcher | U 30 | ): Matcher 31 | export function or( 32 | first: Matcher | T, 33 | second: Matcher | U, 34 | third: Matcher | V 35 | ): Matcher 36 | export function or( 37 | first: Matcher | T, 38 | second: Matcher | U, 39 | third: Matcher | V, 40 | fourth: Matcher | W 41 | ): Matcher 42 | export function or( 43 | first: Matcher | T, 44 | second: Matcher | U, 45 | third: Matcher | V, 46 | fourth: Matcher | W, 47 | fifth: Matcher | X 48 | ): Matcher 49 | export function or | T>>( 50 | ...matchersOrValues: A 51 | ): Matcher { 52 | return new OrMatcher(...matchersOrValues) 53 | } 54 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/predicate.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export type Predicate = (value: unknown) => boolean 4 | 5 | export class PredicateMatcher extends Matcher { 6 | constructor(private readonly predicate: Predicate) { 7 | super() 8 | } 9 | 10 | matchValue(value: unknown): value is T { 11 | return this.predicate(value) 12 | } 13 | } 14 | 15 | export function predicate(predicate: Predicate): Matcher { 16 | return new PredicateMatcher(predicate) 17 | } 18 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/slice.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | import { anything } from './anything' 3 | 4 | export class SliceMatcher extends Matcher { 5 | constructor( 6 | readonly min: number, 7 | readonly max: number, 8 | readonly matcher: Matcher 9 | ) { 10 | super() 11 | } 12 | 13 | matchValue(value: unknown, keys: ReadonlyArray): value is T { 14 | return this.matcher.matchValue(value, keys) 15 | } 16 | } 17 | 18 | /** 19 | * Match zero or more elements. For use with `anyList`. 20 | * 21 | * @example 22 | * 23 | * ```ts 24 | * // matches `['foo', 1]` and `['foo', 'bar', 2]` and `['foo', 'bar', 'baz', 3]` but not `['foo']` or `['foo', 'bar']` 25 | * m.anyList([m.anyString(), m.zeroOrMore(), m.anyNumber()]) 26 | * ``` 27 | */ 28 | export function zeroOrMore( 29 | matcher: Matcher = anything() 30 | ): SliceMatcher { 31 | return new SliceMatcher(0, Infinity, matcher) 32 | } 33 | 34 | /** 35 | * Match one or more elements. For use with `anyList`. 36 | * 37 | * @example 38 | * 39 | * ```ts 40 | * // matches `['foo']` and `['foo', 'bar']` but not `[]` 41 | * m.anyList([m.oneOrMore()]) 42 | * ``` 43 | */ 44 | export function oneOrMore( 45 | matcher: Matcher = anything() 46 | ): SliceMatcher { 47 | return new SliceMatcher(1, Infinity, matcher) 48 | } 49 | 50 | /** 51 | * Options for {@link slice}. 52 | */ 53 | export interface SliceOptions { 54 | min?: number 55 | max?: number 56 | matcher?: Matcher 57 | } 58 | 59 | /** 60 | * Match a slice of an array. For use with `anyList`. 61 | * 62 | * @example 63 | * 64 | * ```ts 65 | * // matches `['foo', 'bar', 'baz']` but not `['foo']` or `['foo', 'bar', 'baz', 'qux']` 66 | * m.anyList([m.anyString(), m.slice({ min: 1, max: 2 })]) 67 | * ``` 68 | */ 69 | export function slice({ 70 | min = 0, 71 | max = min, 72 | matcher = anything(), 73 | }: SliceOptions): SliceMatcher 74 | 75 | /** 76 | * Match a slice of an array of the given length. For use with `anyList`. 77 | * 78 | * @example 79 | * 80 | * ```ts 81 | * // matches `['foo', 'bar', 'baz']` but not `['foo']` or `['foo', 'bar', 'baz', 'qux']` 82 | * m.anyList([m.anyString(), m.slice(2)]) 83 | * ``` 84 | */ 85 | export function slice(length: number, matcher?: Matcher): SliceMatcher 86 | 87 | /** 88 | * Match a slice of an array. For use with `anyList`. 89 | * 90 | * @example 91 | * 92 | * ```ts 93 | * // matches `['foo', 'bar', 'baz']` but not `['foo']` or `['foo', 'bar', 'baz', 'qux']` 94 | * m.anyList([m.anyString(), m.slice({ min: 1, max: 2 })]) 95 | * ``` 96 | */ 97 | export function slice( 98 | optionsOrLength: SliceOptions | number, 99 | matcherOrUndefined?: Matcher 100 | ): SliceMatcher { 101 | let min: number 102 | let max: number 103 | let matcher: Matcher 104 | 105 | if (typeof optionsOrLength === 'number') { 106 | min = optionsOrLength 107 | max = optionsOrLength 108 | matcher = matcherOrUndefined ?? anything() 109 | } else if ( 110 | typeof optionsOrLength === 'object' && 111 | typeof matcherOrUndefined === 'undefined' 112 | ) { 113 | min = optionsOrLength.min ?? 0 114 | max = optionsOrLength.max ?? Infinity 115 | matcher = optionsOrLength.matcher ?? anything() 116 | } else { 117 | throw new Error('Invalid arguments') 118 | } 119 | 120 | return new SliceMatcher(min, max, matcher) 121 | } 122 | 123 | /** 124 | * @deprecated Use `slice` instead. 125 | */ 126 | export function spacer(min = 1, max = min): SliceMatcher { 127 | return new SliceMatcher(min, max, anything()) 128 | } 129 | -------------------------------------------------------------------------------- /packages/matchers/src/matchers/tupleOf.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from './Matcher' 2 | 3 | export class TupleOfMatcher< 4 | T, 5 | A extends Array = Array 6 | > extends Matcher { 7 | private readonly matchers: Array> 8 | 9 | constructor(...matchers: Array>) { 10 | super() 11 | this.matchers = matchers 12 | } 13 | 14 | matchValue(value: unknown, keys: ReadonlyArray): value is A { 15 | if (!Array.isArray(value)) { 16 | return false 17 | } 18 | 19 | if (value.length !== this.matchers.length) { 20 | return false 21 | } 22 | 23 | for (let i = 0; i < this.matchers.length; i++) { 24 | const matcher = this.matchers[i] 25 | const element = value[i] 26 | 27 | if (!matcher.matchValue(element, [...keys, i])) { 28 | return false 29 | } 30 | } 31 | 32 | return true 33 | } 34 | } 35 | 36 | export function tupleOf = Array>( 37 | ...matchers: Array> 38 | ): Matcher { 39 | return new TupleOfMatcher(...matchers) 40 | } 41 | -------------------------------------------------------------------------------- /packages/matchers/src/utils/distributeAcrossSlices.ts: -------------------------------------------------------------------------------- 1 | import { SliceMatcher } from '../matchers/slice' 2 | 3 | /** 4 | * Iterates through the possible allocations of `available` across `slices`. 5 | */ 6 | export function* distributeAcrossSlices( 7 | slices: Array>, 8 | available: number 9 | ): IterableIterator> { 10 | if (slices.length === 0) { 11 | yield [] 12 | } else if (slices.length === 1) { 13 | const spacer = slices[0] 14 | 15 | if (spacer.min <= available && available <= spacer.max) { 16 | yield [available] 17 | } 18 | } else { 19 | const last = slices[slices.length - 1] 20 | 21 | for ( 22 | let allocateToLast = last.min; 23 | allocateToLast <= last.max && allocateToLast <= available; 24 | allocateToLast++ 25 | ) { 26 | const allButLast = slices.slice(0, -1) 27 | 28 | for (const allButLastAllocations of distributeAcrossSlices( 29 | allButLast, 30 | available - allocateToLast 31 | )) { 32 | yield [...allButLastAllocations, allocateToLast] 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/matchers/src/utils/match.ts: -------------------------------------------------------------------------------- 1 | import * as m from '../matchers' 2 | 3 | /** 4 | * This helper makes it easier to use a matcher together with captured values, 5 | * especially from TypeScript. Essentially, this helper "unwraps" the captured 6 | * values and passes them to the callback if the matcher matches the value. This 7 | * prevents users of capturing matchers from needing to check the current 8 | * captured value before using it. 9 | * 10 | * Here is an example codemod that turns e.g. `a + a` into `a * 2`. This 11 | * is not actually something you'd want to do as `+` is used on more than 12 | * just numbers, but it is suitable for purposes of illustration. 13 | * 14 | * @example 15 | * 16 | * import * as m from '@codemod/matchers'; 17 | * 18 | * let id: m.CapturedMatcher; 19 | * const idPlusIdMatcher = m.binaryExpression( 20 | * '+', 21 | * (id = m.capture(m.identifier())), 22 | * m.fromCapture(id) 23 | * ); 24 | * 25 | * export default function() { 26 | * return { 27 | * BinaryExpression(path: NodePath): void { 28 | * m.match(idPlusIdMatcher, { id }, path.node, ({ id }) => { 29 | * path.replaceWith(t.binaryExpression('*', id, t.numericLiteral(2))); 30 | * }); 31 | * } 32 | * }; 33 | * } 34 | */ 35 | export function match( 36 | matcher: m.Matcher, 37 | captures: { [K in keyof C]: m.CapturedMatcher }, 38 | value: T, 39 | callback: (captures: C) => void 40 | ): void { 41 | if (matcher.match(value)) { 42 | const capturedValues = {} as C 43 | 44 | for (const key in captures) { 45 | if (Object.prototype.hasOwnProperty.call(captures, key)) { 46 | const capturedValue = captures[key as keyof C].current 47 | if (capturedValue !== undefined) { 48 | capturedValues[key as keyof C] = capturedValue 49 | } 50 | } 51 | } 52 | 53 | callback(capturedValues) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/matchers/src/utils/matchPath.ts: -------------------------------------------------------------------------------- 1 | import { NodePath } from '@babel/core' 2 | import * as t from '@babel/types' 3 | import * as m from '../matchers' 4 | 5 | export type CapturedNodePaths = { 6 | [K in keyof C]: C[K] extends t.Node ? NodePath : C[K] 7 | } 8 | export type CapturedMatchers = { [K in keyof C]: m.CapturedMatcher } 9 | 10 | /** 11 | * This helper makes it easier to use a matcher that captures `NodePath` values. 12 | * Here is an example codemod that removes a redundant `-1` argument on `slice` 13 | * calls: 14 | * 15 | * @example 16 | * 17 | * import * as m from '@codemod/matchers'; 18 | * import { PluginObj } from '@babel/core'; 19 | * 20 | * const negativeOneArgument = m.capture(m.numericLiteral(-1)); 21 | * const sliceCallMatcher = m.callExpression( 22 | * m.memberExpression( 23 | * m.anyExpression(), 24 | * m.identifier('slice'), 25 | * false 26 | * ), 27 | * [m.anything(), negativeOneArgument] 28 | * ); 29 | * 30 | * export default function(): PluginObj { 31 | * return { 32 | * CallExpression(path: NodePath): void { 33 | * m.matchPath(sliceCallMatcher, { negativeOneArgument }, path ({ negativeOneArgument }) => { 34 | * negativeOneArgument.remove(); 35 | * }); 36 | * } 37 | * }; 38 | * } 39 | */ 40 | export function matchPath( 41 | matcher: m.Matcher, 42 | captures: CapturedMatchers, 43 | value: NodePath, 44 | callback: (paths: CapturedNodePaths) => void 45 | ): void 46 | export function matchPath( 47 | matcher: m.Matcher>, 48 | captures: CapturedMatchers, 49 | value: Array>, 50 | callback: (paths: CapturedNodePaths) => void 51 | ): void 52 | export function matchPath( 53 | matcher: m.Matcher>, 54 | captures: CapturedMatchers, 55 | value: NodePath | Array>, 56 | callback: (paths: CapturedNodePaths) => void 57 | ): void { 58 | const toMatch = Array.isArray(value) 59 | ? value.map((element) => element.node) 60 | : value.node 61 | if (matcher.match(toMatch)) { 62 | const capturedPaths = {} as CapturedNodePaths 63 | 64 | for (const key in captures) { 65 | if (Object.prototype.hasOwnProperty.call(captures, key)) { 66 | const { current, currentKeys } = captures[key as keyof C] 67 | if (current !== undefined && currentKeys !== undefined) { 68 | capturedPaths[key as keyof C] = extractCapturedPath( 69 | value, 70 | currentKeys 71 | ) 72 | } 73 | } 74 | } 75 | 76 | callback(capturedPaths) 77 | } 78 | } 79 | 80 | function extractCapturedPath( 81 | value: NodePath | Array>, 82 | keys: ReadonlyArray 83 | ): C[keyof C] extends t.Node ? NodePath : C[keyof C] { 84 | let capturedPath: NodePath | Array> = value 85 | 86 | for (const [i, key] of keys.entries()) { 87 | if (typeof key === 'string') { 88 | if (Array.isArray(capturedPath)) { 89 | throw new Error( 90 | `failed to get '${keys.join('.')}'; at '${keys 91 | .slice(0, i + 1) 92 | .join('.')}' expected a NodePath but got an array` 93 | ) 94 | } 95 | 96 | capturedPath = capturedPath.get(key as string) 97 | } else if (typeof key === 'number') { 98 | if (!Array.isArray(capturedPath)) { 99 | throw new Error( 100 | `failed to get '${keys.join('.')}'; at '${keys 101 | .slice(0, i + 1) 102 | .join('.')}' expected an array but got a NodePath` 103 | ) 104 | } 105 | 106 | capturedPath = capturedPath[key] 107 | } else { 108 | throw new Error( 109 | `failed to get '${keys.join('.')}'; key '${String( 110 | key 111 | )}' is neither a string nor a number, not ${typeof key}` 112 | ) 113 | } 114 | } 115 | 116 | if (!Array.isArray(capturedPath) && typeof capturedPath.node !== 'object') { 117 | return capturedPath.node as C[keyof C] extends t.Node 118 | ? NodePath 119 | : C[keyof C] 120 | } else { 121 | return capturedPath as C[keyof C] extends t.Node 122 | ? NodePath 123 | : C[keyof C] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/matchers/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/__tests__"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "rootDir": "src", 8 | "outDir": "build", 9 | "declaration": true, 10 | "sourceMap": true 11 | }, 12 | "references": [ 13 | { "path": "../core/tsconfig.build.json" }, 14 | { "path": "../parser/tsconfig.build.json" }, 15 | { "path": "../utils/tsconfig.build.json" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/matchers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "composite": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": ["build"], 11 | "references": [ 12 | { "path": "../core/tsconfig.build.json" }, 13 | { "path": "../parser/tsconfig.build.json" }, 14 | { "path": "../utils/tsconfig.build.json" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/parser/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | -------------------------------------------------------------------------------- /packages/parser/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 | # [1.4.0](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.3.0...@codemod/parser@1.4.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 | # [1.3.0](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.2.0...@codemod/parser@1.3.0) (2023-02-17) 18 | 19 | ### Features 20 | 21 | - **cli,core:** use `esbuild-runner` ([ab527b2](https://github.com/codemod-js/codemod/commit/ab527b2cea23211732ab1a45512dc1f968c707c6)) 22 | - **cli:** add `--parser-plugins` option ([3593893](https://github.com/codemod-js/codemod/commit/3593893791c7e4e0e0c8cea31ea642b229c0bb8a)) 23 | 24 | ## [1.2.0](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.1.1...@codemod/parser@1.2.0) (2021-11-12) 25 | 26 | - update babel to v7.16.x ([1218bf9](https://github.com/codemod-js/codemod/commit/1218bf98145feaa8a692611152559aa6b46b9ba0)) 27 | 28 | ## [1.0.7](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.0.5...@codemod/parser@1.0.7) (2020-03-25) 29 | 30 | ### Bug Fixes 31 | 32 | - ensure `topLevelAwait` plugin is enabled ([fc07ce5](https://github.com/codemod-js/codemod/commit/fc07ce5edfe465938a24e59a5e4e9b851ca7e645)) 33 | - update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d)) 34 | - upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a)) 35 | - upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102)) 36 | 37 | ## [1.0.3](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.0.2...@codemod/parser@1.0.3) (2019-08-09) 38 | 39 | ### Bug Fixes 40 | 41 | - **license:** update outdated license files ([58e4b11](https://github.com/codemod-js/codemod/commit/58e4b11)) 42 | 43 | ## [1.0.2](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.0.1...@codemod/parser@1.0.2) (2019-08-09) 44 | 45 | ### Bug Fixes 46 | 47 | - **package:** add "types" to package.json ([094d504](https://github.com/codemod-js/codemod/commit/094d504)) 48 | 49 | ## [1.0.1](https://github.com/codemod-js/codemod/compare/@codemod/parser@1.0.0...@codemod/parser@1.0.1) (2019-08-07) 50 | 51 | ### Bug Fixes 52 | 53 | - fix: add more parsing plugins ([420c7db](https://github.com/codemod-js/codemod/commit/420c7db)) 54 | 55 | ## [1.0.0](https://github.com/codemod-js/codemod/commit/26fe442) (2019-07-31) 56 | 57 | ### Features 58 | 59 | - initial commit of [@codemod/parser](https://github.com/codemod/parser) ([84de839](https://github.com/codemod-js/codemod/commit/26fe442)) 60 | -------------------------------------------------------------------------------- /packages/parser/README.md: -------------------------------------------------------------------------------- 1 | # @codemod/parser 2 | 3 | Wrapper around `@babel/parser` that allows parsing everything. 4 | 5 | ## Install 6 | 7 | Install from [npm](https://npmjs.com/): 8 | 9 | ```sh 10 | $ npm install @codemod/parser 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { parse } from '@codemod/parser' 17 | 18 | console.log(parse('a ?? b').program.body[0].expression.operator) // '??' 19 | ``` 20 | 21 | ## Contributing 22 | 23 | See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information on setting up the project for development and on contributing to the project. 24 | 25 | ## License 26 | 27 | Copyright 2019 Brian Donovan 28 | 29 | 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 30 | 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | 33 | 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. 34 | -------------------------------------------------------------------------------- /packages/parser/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/parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codemod/parser", 3 | "version": "1.4.1", 4 | "description": "Wrapper around @babel/parser that allows parsing everything.", 5 | "repository": "https://github.com/codemod-js/codemod", 6 | "license": "Apache-2.0", 7 | "author": "Brian Donovan", 8 | "main": "build/index.js", 9 | "types": "build/index.d.ts", 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc --build tsconfig.build.json", 15 | "clean": "rm -rf build tsconfig.build.tsbuildinfo", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "prepublishOnly": "pnpm clean && pnpm build", 19 | "test": "is-ci test:coverage test:watch", 20 | "test:coverage": "jest --coverage", 21 | "test:watch": "jest --watch" 22 | }, 23 | "dependencies": { 24 | "@babel/parser": "^7.20.15" 25 | }, 26 | "devDependencies": { 27 | "@babel/types": "^7.20.7", 28 | "@types/jest": "^25.1.0", 29 | "@types/node": "^18.14.0", 30 | "is-ci-cli": "^2.2.0", 31 | "jest": "^27.3.1", 32 | "typescript": "^4.9.5" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/parser/src/__tests__/test.ts: -------------------------------------------------------------------------------- 1 | import { parse, buildOptions } from '..' 2 | import * as t from '@babel/types' 3 | 4 | test('defaults `sourceType` to "unambiguous"', () => { 5 | expect(buildOptions().sourceType).toBe('unambiguous') 6 | }) 7 | 8 | test('defaults `allowAwaitOutsideFunction` to true', () => { 9 | expect(buildOptions().allowAwaitOutsideFunction).toBe(true) 10 | }) 11 | 12 | test('defaults `allowImportExportEverywhere` to true', () => { 13 | expect(buildOptions().allowImportExportEverywhere).toBe(true) 14 | }) 15 | 16 | test('defaults `allowReturnOutsideFunction` to true', () => { 17 | expect(buildOptions().allowReturnOutsideFunction).toBe(true) 18 | }) 19 | 20 | test('defaults `allowSuperOutsideMethod` to true', () => { 21 | expect(buildOptions().allowSuperOutsideMethod).toBe(true) 22 | }) 23 | 24 | test('defaults `allowUndeclaredExports` to true', () => { 25 | expect(buildOptions().allowUndeclaredExports).toBe(true) 26 | }) 27 | 28 | test('includes various plugins by default', () => { 29 | expect(buildOptions().plugins).toBeInstanceOf(Array) 30 | }) 31 | 32 | test('includes "typescript" plugin when `sourceFilename` is not present', () => { 33 | expect(buildOptions().plugins).toContain('typescript') 34 | }) 35 | 36 | test('includes "flow" plugin when `sourceFilename` is not TypeScript', () => { 37 | expect(buildOptions({ sourceFilename: 'index.js' }).plugins).toContain('flow') 38 | expect(buildOptions({ sourceFilename: 'index.jsx' }).plugins).toContain( 39 | 'flow' 40 | ) 41 | }) 42 | 43 | test('includes "typescript" plugin when `sourceFilename` is TypeScript', () => { 44 | expect(buildOptions({ sourceFilename: 'index.ts' }).plugins).toContain( 45 | 'typescript' 46 | ) 47 | expect(buildOptions({ sourceFilename: 'index.tsx' }).plugins).toContain( 48 | 'typescript' 49 | ) 50 | }) 51 | 52 | test('does not include "typescript" plugin when "flow" is already enabled', () => { 53 | expect( 54 | buildOptions({ plugins: [['flow', { all: true }]] }).plugins 55 | ).not.toContain('typescript') 56 | }) 57 | 58 | test('does not mix conflicting "recordAndTuple" and "pipelineOperator" plugins', () => { 59 | // adding recordAndTuple to existing plugins 60 | expect( 61 | buildOptions({ plugins: [['pipelineOperator', { proposal: 'smart' }]] }) 62 | .plugins 63 | ).not.toContainEqual(['recordAndTuple', expect.anything()]) 64 | expect( 65 | buildOptions({ 66 | plugins: [['pipelineOperator', { proposal: 'hack', topicToken: '#' }]], 67 | }).plugins 68 | ).not.toContainEqual(['recordAndTuple', expect.anything()]) 69 | expect( 70 | buildOptions({ 71 | plugins: [['pipelineOperator', { proposal: 'hack', topicToken: '%' }]], 72 | }).plugins 73 | ).toContainEqual(['recordAndTuple', { syntaxType: 'hash' }]) 74 | 75 | // adding pipelineOperator to existing plugins 76 | expect( 77 | buildOptions({ plugins: [['recordAndTuple', { syntaxType: 'hash' }]] }) 78 | .plugins 79 | ).toContainEqual(['pipelineOperator', { proposal: 'minimal' }]) 80 | }) 81 | 82 | test('does not mutate `plugins` array', () => { 83 | const plugins = [] 84 | buildOptions({ plugins }) 85 | expect(plugins).toHaveLength(0) 86 | }) 87 | 88 | test('does not mutate options', () => { 89 | const options = {} 90 | buildOptions(options) 91 | expect(options).toEqual({}) 92 | }) 93 | 94 | test('includes "decorators" plugin with options by default', () => { 95 | expect(buildOptions().plugins).toContainEqual([ 96 | 'decorators', 97 | { decoratorsBeforeExport: true }, 98 | ]) 99 | }) 100 | 101 | test('does not include "decorators" plugin if "decorators-legacy" is already enabled', () => { 102 | expect(buildOptions({ plugins: ['decorators-legacy'] })).not.toContain( 103 | 'decorators' 104 | ) 105 | }) 106 | 107 | test('enables `topLevelAwait` even if `allowAwaitOutsideFunction` is disabled', () => { 108 | const options = buildOptions({ allowAwaitOutsideFunction: false }) 109 | expect(options.plugins).toContain('topLevelAwait') 110 | expect( 111 | (parse('await 0', options).program.body[0] as t.ExpressionStatement) 112 | .expression.type 113 | ).toEqual('AwaitExpression') 114 | }) 115 | 116 | test('parses with a very broad set of options', () => { 117 | expect( 118 | parse(` 119 | // demonstrate 'allowReturnOutsideFunction' option and 'throwExpressions' plugin 120 | return true || throw new Error(a ?? b); 121 | // demonstrate 'allowUndeclaredExports' option 122 | export { a }; 123 | // demonstrate 'typescript' plugin 124 | type Foo = Extract; 125 | // demonstrate 'logicalAssignment' plugin 126 | a ||= b 127 | // demonstrate 'partialApplication' plugin 128 | a(?, b) 129 | // demonstrate 'recordAndTuple' plugin 130 | #[1, 2, #{a: 3}] 131 | // demonstrate 'pipelineOperator' plugin with proposal=minimal 132 | x |> y 133 | `).program.body.map((node) => 134 | t.isExpressionStatement(node) ? node.expression.type : node.type 135 | ) 136 | ).toEqual([ 137 | 'ReturnStatement', 138 | 'ExportNamedDeclaration', 139 | 'TSTypeAliasDeclaration', 140 | 'AssignmentExpression', 141 | 'CallExpression', 142 | 'TupleExpression', 143 | 'BinaryExpression', 144 | ]) 145 | }) 146 | 147 | test('does not parse placeholders by default as they conflict with TypeScript', () => { 148 | const placeholderCode = ` 149 | // demonstrate 'placeholders' plugin 150 | %%statement%% 151 | ` 152 | 153 | expect(() => parse(placeholderCode)).toThrowError() 154 | const node = parse(placeholderCode, { plugins: ['placeholders'] }).program 155 | .body[0] 156 | expect(node.type).toBe('Placeholder') 157 | }) 158 | 159 | test('allows parsing records and tuples with "bar" syntax', () => { 160 | const tuple = ( 161 | parse(`[|1, 2, {|a: 1|}|]`, { 162 | plugins: [['recordAndTuple', { syntaxType: 'bar' }]], 163 | }).program.body[0] as t.ExpressionStatement 164 | ).expression 165 | expect(t.isTupleExpression(tuple)).toBe(true) 166 | expect(t.isRecordExpression((tuple as t.TupleExpression).elements[2])).toBe( 167 | true 168 | ) 169 | }) 170 | 171 | test('allows parsing of abstract classes with abstract methods', () => { 172 | expect( 173 | parse(` 174 | abstract class Foo { 175 | abstract bar(): void; 176 | } 177 | `).program.body[0].type 178 | ).toBe('ClassDeclaration') 179 | }) 180 | -------------------------------------------------------------------------------- /packages/parser/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parse as babelParse, 3 | ParserOptions as BabelParserOptions, 4 | } from '@babel/parser' 5 | import { File } from '@babel/types' 6 | import { 7 | buildOptions, 8 | isParserPluginName, 9 | ParserOptions, 10 | ParserPluginName, 11 | } from './options' 12 | 13 | export { buildOptions, isParserPluginName, ParserOptions, ParserPluginName } 14 | 15 | /** 16 | * Wraps `parse` from `@babel/parser`, but sets default options such that as few 17 | * restrictions as possible are placed on the `input` code. 18 | */ 19 | export function parse(input: string, options?: ParserOptions): File { 20 | return babelParse(input, buildOptions(options) as BabelParserOptions) 21 | } 22 | -------------------------------------------------------------------------------- /packages/parser/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/__tests__"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "rootDir": "src", 10 | "outDir": "build" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "lib": ["es2015", "es2016"], 6 | "composite": true, 7 | "noEmit": true, 8 | "noImplicitAny": false, 9 | "strict": true 10 | }, 11 | "exclude": ["tmp", "build"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | src/matchers.ts 4 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/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 | # 0.1.0 (2023-02-18) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **rebuild-matchers:** update path to generated matchers ([e2173a4](https://github.com/codemod-js/codemod/commit/e2173a4e69f19cbf644ec22d36c748d81bdcd631)) 12 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/bin/rebuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | /* eslint-env node */ 5 | 6 | require('esbuild-runner/register') 7 | 8 | require('../src/rebuild') 9 | .main() 10 | .catch((error) => { 11 | console.error(error.stack) 12 | process.exit(1) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /** @type {import('@jest/types').Config.InitialOptions} */ 4 | module.exports = { 5 | clearMocks: true, 6 | moduleFileExtensions: ['ts', 'tsx', 'js'], 7 | testEnvironment: 'node', 8 | testRegex: '/__tests__/(test|.*\\.test)\\.ts$', 9 | transform: { 10 | '\\.ts$': 'esbuild-runner/jest', 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codemod/rebuild-matchers", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "Rebuilds the bulk of `@codemod/matchers` based on the Babel AST.", 6 | "repository": "https://github.com/codemod-js/codemod", 7 | "license": "Apache-2.0", 8 | "author": "Brian Donovan", 9 | "main": "build/index.js", 10 | "types": "build/index.d.ts", 11 | "files": [ 12 | "build" 13 | ], 14 | "scripts": { 15 | "build": "tsc --build tsconfig.build.json", 16 | "clean": "rm -rf build tsconfig.build.tsbuildinfo", 17 | "lint": "eslint .", 18 | "lint:fix": "eslint . --fix", 19 | "prepublishOnly": "pnpm clean && pnpm build", 20 | "test": "is-ci test:coverage test:watch", 21 | "test:coverage": "jest --coverage", 22 | "test:watch": "jest --watch" 23 | }, 24 | "dependencies": { 25 | "@babel/types": "^7.20.7", 26 | "@codemod/utils": "^1.1.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.20.12", 30 | "@babel/generator": "^7.20.14", 31 | "@babel/traverse": "^7.20.13", 32 | "@codemod/core": "^2.2.0", 33 | "@codemod/parser": "^1.4.0", 34 | "@types/babel__core": "^7.20.0", 35 | "@types/babel__generator": "^7.6.4", 36 | "@types/babel__template": "^7.4.1", 37 | "@types/babel__traverse": "^7.18.3", 38 | "@types/dedent": "^0.7.0", 39 | "@types/jest": "^25.1.0", 40 | "@types/node": "^18.14.0", 41 | "@types/prettier": "^2.0.0", 42 | "dedent": "^0.7.0", 43 | "is-ci-cli": "^2.2.0", 44 | "jest": "^27.3.1", 45 | "typescript": "^4.9.5" 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/src/__tests__/generated.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { MATCHERS_FILE_PATH, rebuild } from '../utils/rebuild' 3 | 4 | test('generated file is up to date', async () => { 5 | const existingContent = await fs.readFile(MATCHERS_FILE_PATH, 'utf8') 6 | const newContent = await rebuild() 7 | expect(newContent).toEqual(existingContent) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/src/rebuild.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { rebuild, MATCHERS_FILE_PATH } from './utils/rebuild' 3 | 4 | export async function main(): Promise { 5 | await fs.writeFile(MATCHERS_FILE_PATH, await rebuild(), 'utf8') 6 | 7 | return 0 8 | } 9 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/src/utils/ast.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import generate from '@babel/generator' 3 | 4 | export interface ArrayValidator { 5 | each: Validator 6 | } 7 | 8 | export interface ChainOfValidator { 9 | chainOf: Array 10 | } 11 | 12 | export interface OneOfValidator { 13 | oneOf: Array 14 | } 15 | 16 | export interface OneOfNodeTypesValidator { 17 | oneOfNodeTypes: Array 18 | } 19 | 20 | export interface OneOfNodeOrValueTypesValidator { 21 | oneOfNodeOrValueTypes: Array 22 | } 23 | 24 | export interface Type { 25 | type: string 26 | } 27 | 28 | export type Validator = 29 | | ArrayValidator 30 | | ChainOfValidator 31 | | OneOfValidator 32 | | OneOfNodeTypesValidator 33 | | OneOfNodeOrValueTypesValidator 34 | | Type 35 | 36 | export function isValidatorOfType( 37 | type: string, 38 | validator: Validator | undefined 39 | ): boolean { 40 | if (!validator) { 41 | return false 42 | } 43 | 44 | if ('chainOf' in validator) { 45 | return validator.chainOf.some((child) => isValidatorOfType(type, child)) 46 | } 47 | 48 | if ('oneOf' in validator) { 49 | return validator.oneOf.some((child) => typeof child === type) 50 | } 51 | 52 | return 'type' in validator && validator.type === type 53 | } 54 | 55 | export function typeFromName(name: string): t.TSType { 56 | switch (name) { 57 | case 'string': 58 | return t.tsStringKeyword() 59 | case 'number': 60 | return t.tsNumberKeyword() 61 | case 'boolean': 62 | return t.tsBooleanKeyword() 63 | case 'null': 64 | return t.tsNullKeyword() 65 | case 'undefined': 66 | return t.tsUndefinedKeyword() 67 | case 'any': 68 | return t.tsAnyKeyword() 69 | default: 70 | return t.tsTypeReference(t.identifier(name)) 71 | } 72 | } 73 | 74 | export function typeForValidator(validator?: Validator): t.TSType { 75 | if (!validator) { 76 | return t.tsAnyKeyword() 77 | } 78 | 79 | if ('each' in validator) { 80 | return t.tsArrayType(typeForValidator(validator.each)) 81 | } 82 | 83 | if ('chainOf' in validator) { 84 | return typeForValidator(validator.chainOf[1]) 85 | } 86 | 87 | if ('oneOf' in validator) { 88 | return t.tsUnionType( 89 | validator.oneOf.map((type) => { 90 | if (typeof type === 'string') { 91 | return t.tsLiteralType(t.stringLiteral(type)) 92 | } else if (typeof type === 'boolean') { 93 | return t.tsLiteralType(t.booleanLiteral(type)) 94 | } else if (typeof type === 'number') { 95 | return t.tsLiteralType(t.numericLiteral(type)) 96 | } else { 97 | throw new Error(`unexpected 'oneOf' value: ${type}`) 98 | } 99 | }) 100 | ) 101 | } 102 | 103 | if ('oneOfNodeTypes' in validator) { 104 | return t.tsUnionType( 105 | validator.oneOfNodeTypes.map((type) => 106 | t.tsTypeReference(t.identifier(type)) 107 | ) 108 | ) 109 | } 110 | 111 | if ('oneOfNodeOrValueTypes' in validator) { 112 | return t.tsUnionType(validator.oneOfNodeOrValueTypes.map(typeFromName)) 113 | } 114 | 115 | if (validator.type) { 116 | return typeFromName(validator.type) 117 | } 118 | 119 | return t.tsAnyKeyword() 120 | } 121 | 122 | function stringifyQualifiedName(type: t.TSQualifiedName): string { 123 | if (t.isIdentifier(type.left)) { 124 | return `${type.left.name}.${type.right.name}` 125 | } else { 126 | return `${stringifyQualifiedName(type.left)}.${type.right.name}` 127 | } 128 | } 129 | 130 | export function stringifyType( 131 | type: t.TSType, 132 | replacer?: (type: t.TSType, value: string) => string | undefined 133 | ): string { 134 | function withReplacer(value: string): string { 135 | return (replacer && replacer(type, value)) || value 136 | } 137 | 138 | if (t.isTSUnionType(type)) { 139 | return withReplacer( 140 | type.types.map((child) => stringifyType(child, replacer)).join(' | ') 141 | ) 142 | } else if (t.isTSAnyKeyword(type)) { 143 | return withReplacer('any') 144 | } else if (t.isTSNumberKeyword(type)) { 145 | return withReplacer('number') 146 | } else if (t.isTSStringKeyword(type)) { 147 | return withReplacer('string') 148 | } else if (t.isTSUndefinedKeyword(type)) { 149 | return withReplacer('undefined') 150 | } else if (t.isTSTypeReference(type)) { 151 | if (t.isIdentifier(type.typeName)) { 152 | return withReplacer(type.typeName.name) 153 | } else { 154 | return withReplacer(stringifyQualifiedName(type.typeName)) 155 | } 156 | } else if (t.isTSArrayType(type)) { 157 | return withReplacer(`Array<${stringifyType(type.elementType, replacer)}>`) 158 | } else { 159 | return withReplacer(generate(type).code) 160 | } 161 | } 162 | 163 | export function stringifyValidator( 164 | validator: Validator | undefined, 165 | nodePrefix: string, 166 | nodeSuffix: string 167 | ): string { 168 | return stringifyType(typeForValidator(validator), (type, value) => { 169 | if (t.isTSTypeReference(type)) { 170 | return nodePrefix + value + nodeSuffix 171 | } else { 172 | return value 173 | } 174 | }) 175 | } 176 | 177 | export function toFunctionName(typeName: string): string { 178 | const _ = typeName.replace(/^TS/, 'ts').replace(/^JSX/, 'jsx') 179 | return _.slice(0, 1).toLowerCase() + _.slice(1) 180 | } 181 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import { format as prettier, resolveConfig } from 'prettier' 2 | 3 | export default async function format( 4 | code: string, 5 | path: string 6 | ): Promise { 7 | const config = await resolveConfig(path) 8 | 9 | return prettier(code, { ...config, filepath: path }) 10 | } 11 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/src/utils/git.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { dirname, join } from 'path' 3 | 4 | export function getRepoRoot(): string { 5 | let current = __dirname 6 | 7 | for (;;) { 8 | const next = dirname(current) 9 | 10 | if (current === next) { 11 | throw new Error('Could not find .git directory') 12 | } 13 | 14 | if (existsSync(join(next, '.git'))) { 15 | return next 16 | } 17 | 18 | current = next 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/__tests__"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "rootDir": "src", 8 | "outDir": "build", 9 | "declaration": true, 10 | "sourceMap": true 11 | }, 12 | "references": [ 13 | { "path": "../core/tsconfig.build.json" }, 14 | { "path": "../parser/tsconfig.build.json" }, 15 | { "path": "../utils/tsconfig.build.json" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/rebuild-matchers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "composite": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": ["build"], 11 | "references": [ 12 | { "path": "../core/tsconfig.build.json" }, 13 | { "path": "../parser/tsconfig.build.json" }, 14 | { "path": "../utils/tsconfig.build.json" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/utils/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | src/matchers.ts 4 | -------------------------------------------------------------------------------- /packages/utils/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 | # 1.1.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 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # @codemod/utils 2 | 3 | This package contains utilities for writing codemods. Mostly it exists to enable 4 | codemods to avoid depending on `@babel/types`, `@babel/core`, and others as well 5 | as `@codemod/cli`. It's easy to depend on incompatible versions of these 6 | packages, so this package wraps them and exports a single version of each. 7 | 8 | ## Install 9 | 10 | Install from [npm](https://npmjs.com/): 11 | 12 | ```sh 13 | $ npm install @codemod/core 14 | ``` 15 | 16 | ## Usage 17 | 18 | You can use this library directly, but typically it's provided by `@codemod/cli` 19 | when defining a codemod with `defineCodemod`. Here's an example of using it 20 | directly: 21 | 22 | ```ts 23 | // `m` is `@codemod/matchers`, a library of useful matchers 24 | // `t` is `@babel/types`, babel AST type predicates and builders 25 | import { t, m } from '@codemod/utils' 26 | ``` 27 | 28 | ## Contributing 29 | 30 | See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information on setting up the project for development and on contributing to the project. 31 | 32 | ## License 33 | 34 | Copyright 2023 Brian Donovan 35 | 36 | 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 37 | 38 | http://www.apache.org/licenses/LICENSE-2.0 39 | 40 | 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. 41 | -------------------------------------------------------------------------------- /packages/utils/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /** @type {import('@jest/types').Config.InitialOptions} */ 4 | module.exports = { 5 | clearMocks: true, 6 | moduleFileExtensions: ['ts', 'tsx', 'js'], 7 | testEnvironment: 'node', 8 | testRegex: '/__tests__/(test|.*\\.test)\\.ts$', 9 | transform: { 10 | '\\.ts$': 'esbuild-runner/jest', 11 | }, 12 | collectCoverageFrom: [ 13 | 'src/**/*.ts', 14 | '!**/__tests__/**/*.ts', 15 | '!src/matchers.ts', 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codemod/utils", 3 | "version": "1.1.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 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc --build tsconfig.build.json", 15 | "clean": "rm -rf build tsconfig.build.tsbuildinfo", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "prepublishOnly": "pnpm clean && pnpm build", 19 | "test": "is-ci test:coverage test:watch", 20 | "test:coverage": "jest --coverage", 21 | "test:watch": "jest --watch" 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.20.12", 25 | "@babel/traverse": "^7.20.13", 26 | "@babel/types": "^7.20.7", 27 | "@codemod/parser": "^1.4.0" 28 | }, 29 | "devDependencies": { 30 | "@types/babel__core": "^7.20.0", 31 | "@types/babel__generator": "^7.6.4", 32 | "@types/babel__traverse": "^7.18.3", 33 | "@types/jest": "^29.4.0", 34 | "@types/node": "^18.14.0", 35 | "is-ci-cli": "^2.2.0", 36 | "jest": "^27.3.1" 37 | }, 38 | "publishConfig": { 39 | "access": "public" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/utils/src/NodeTypes.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | 3 | export interface ArrayValidator { 4 | each: Validator 5 | } 6 | 7 | export interface ChainOfValidator { 8 | chainOf: Array 9 | } 10 | 11 | export interface OneOfValidator { 12 | oneOf: Array 13 | } 14 | 15 | export interface OneOfNodeTypesValidator { 16 | oneOfNodeTypes: Array 17 | } 18 | 19 | export interface OneOfNodeOrValueTypesValidator { 20 | oneOfNodeOrValueTypes: Array 21 | } 22 | 23 | export interface Type { 24 | type: string 25 | } 26 | 27 | export type Validator = 28 | | ArrayValidator 29 | | ChainOfValidator 30 | | OneOfValidator 31 | | OneOfNodeTypesValidator 32 | | OneOfNodeOrValueTypesValidator 33 | | Type 34 | 35 | export interface BuilderKeysByType { 36 | [key: string]: Array 37 | } 38 | 39 | export interface NodeFieldsByType { 40 | [key: string]: NodeFields 41 | } 42 | 43 | export interface NodeFields { 44 | [key: string]: NodeField 45 | } 46 | 47 | export interface NodeField { 48 | default: T | null 49 | optional?: boolean 50 | validate: Validator 51 | } 52 | 53 | export const { BUILDER_KEYS, NODE_FIELDS } = t as unknown as { 54 | BUILDER_KEYS: BuilderKeysByType 55 | NODE_FIELDS: NodeFieldsByType 56 | } 57 | -------------------------------------------------------------------------------- /packages/utils/src/__tests__/nodesEquivalent.test.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { nodesEquivalent } from '../nodesEquivalent' 3 | 4 | test('a node is equivalent to itself', () => { 5 | const node = t.identifier('a') 6 | expect(nodesEquivalent(node, node)).toBe(true) 7 | }) 8 | 9 | test('two nodes with different types are not equivalent', () => { 10 | const a = t.identifier('a') 11 | const two = t.numericLiteral(2) 12 | expect(nodesEquivalent(a, two)).toBe(false) 13 | }) 14 | 15 | test('two nodes with the same type and the same properties are equivalent', () => { 16 | const a = t.identifier('a') 17 | const b = t.identifier('a') 18 | expect(nodesEquivalent(a, b)).toBe(true) 19 | }) 20 | 21 | test('two nodes with the same type and different properties are not equivalent', () => { 22 | const a = t.identifier('a') 23 | const b = t.identifier('b') 24 | expect(nodesEquivalent(a, b)).toBe(false) 25 | }) 26 | 27 | test('two nodes with differing optional properties are not equivalent', () => { 28 | const a = t.identifier('a') 29 | const b = t.identifier('a') 30 | a.typeAnnotation = t.tsTypeAnnotation(t.tsAnyKeyword()) 31 | expect(nodesEquivalent(a, b)).toBe(false) 32 | }) 33 | 34 | test('two nodes with different children are not equivalent', () => { 35 | const a = t.arrayExpression([t.identifier('a')]) 36 | const b = t.arrayExpression([t.identifier('b')]) 37 | expect(nodesEquivalent(a, b)).toBe(false) 38 | }) 39 | 40 | test('two nodes with differing number of children are not equivalent', () => { 41 | const a = t.arrayExpression([t.identifier('a')]) 42 | const b = t.arrayExpression([t.identifier('a'), t.identifier('b')]) 43 | expect(nodesEquivalent(a, b)).toBe(false) 44 | }) 45 | 46 | test('two nodes with the same children are equivalent', () => { 47 | const a = t.arrayExpression([t.identifier('a')]) 48 | const b = t.arrayExpression([t.identifier('a')]) 49 | expect(nodesEquivalent(a, b)).toBe(true) 50 | }) 51 | 52 | test('two nodes with equivalent fixed children are equivalent', () => { 53 | const a = t.identifier('a') 54 | const b = t.identifier('a') 55 | a.typeAnnotation = t.tsTypeAnnotation(t.tsAnyKeyword()) 56 | b.typeAnnotation = t.tsTypeAnnotation(t.tsAnyKeyword()) 57 | expect(nodesEquivalent(a, b)).toBe(true) 58 | }) 59 | 60 | test('two nodes with non-equivalent fixed children are not equivalent', () => { 61 | const a = t.identifier('a') 62 | const b = t.identifier('a') 63 | a.typeAnnotation = t.tsTypeAnnotation(t.tsUnknownKeyword()) 64 | b.typeAnnotation = t.tsTypeAnnotation(t.tsAnyKeyword()) 65 | expect(nodesEquivalent(a, b)).toBe(false) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/utils/src/builders.ts: -------------------------------------------------------------------------------- 1 | import traverse, { NodePath } from '@babel/traverse' 2 | import * as t from '@babel/types' 3 | import { parse } from '@codemod/parser' 4 | 5 | export type Replacement = 6 | | t.Statement 7 | | t.Expression 8 | | Array 9 | | Array 10 | 11 | export interface ReplacementsBase { 12 | [key: string]: Replacement 13 | } 14 | 15 | export function program( 16 | template: string 17 | ): (replacements?: R) => t.File { 18 | const ast = parse(template, { 19 | plugins: ['placeholders'], 20 | }) 21 | 22 | return (replacements = {} as R) => { 23 | const unusedReplacements = new Set(Object.keys(replacements)) 24 | 25 | traverse(ast, { 26 | Placeholder(path: NodePath): void { 27 | const name = path.node.name.name 28 | const replacement = replacements[name] 29 | 30 | if (!replacement) { 31 | throw new Error( 32 | `no replacement found for placeholder with name: ${name}` 33 | ) 34 | } 35 | 36 | if (Array.isArray(replacement)) { 37 | path.replaceWithMultiple(replacement) 38 | } else { 39 | path.replaceWith(replacement) 40 | } 41 | 42 | unusedReplacements.delete(name) 43 | }, 44 | }) 45 | 46 | if (unusedReplacements.size > 0) { 47 | const names = Array.from(unusedReplacements).join(', ') 48 | 49 | throw new Error(`template replacements were not used: ${names}`) 50 | } 51 | 52 | return ast 53 | } 54 | } 55 | 56 | export function statement( 57 | template: string 58 | ): (replacements?: R) => t.Statement { 59 | const builder = program(template) 60 | return (replacements) => 61 | getSingleStatement(builder(replacements).program.body) 62 | } 63 | 64 | export function expression( 65 | template: string 66 | ): (replacements?: R) => t.Expression { 67 | const builder = program(template) 68 | return (replacements) => 69 | getSingleExpression(builder(replacements).program.body) 70 | } 71 | 72 | function getSingleStatement(statements: Array): t.Statement { 73 | if (statements.length !== 1) { 74 | throw new TypeError( 75 | `expected a single statement but ${statements.length} statements` 76 | ) 77 | } 78 | 79 | return statements[0] 80 | } 81 | 82 | function getSingleExpression(statements: Array): t.Expression { 83 | if (statements.length !== 1) { 84 | throw new TypeError( 85 | `expected a single expression but ${statements.length} statements` 86 | ) 87 | } 88 | 89 | const statement = statements[0] 90 | 91 | if (!t.isExpressionStatement(statement)) { 92 | throw new TypeError( 93 | `expected a single expression but got a single ${statement.type}` 94 | ) 95 | } 96 | 97 | return statement.expression 98 | } 99 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Babel from '@babel/core' 2 | import * as t from '@babel/types' 3 | 4 | export * from './NodeTypes' 5 | export * from './builders' 6 | export * from './js' 7 | export * from './nodesEquivalent' 8 | 9 | export { Babel, t, t as types } 10 | -------------------------------------------------------------------------------- /packages/utils/src/js.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { parse } from '@codemod/parser' 3 | import { NODE_FIELDS } from './NodeTypes' 4 | 5 | function fieldsForNodeType(nodeType: string): Set { 6 | return new Set(['type', ...Object.keys(NODE_FIELDS[nodeType])]) 7 | } 8 | 9 | export function js(code: string): t.File { 10 | return stripExtras(parse(code)) 11 | } 12 | 13 | function stripExtras(node: N): N { 14 | const fieldsToKeep = fieldsForNodeType(node.type) 15 | const allFields = Object.keys(node) 16 | 17 | for (const field of allFields) { 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | const nodeObj = node as any 20 | 21 | if (!fieldsToKeep.has(field)) { 22 | delete nodeObj[field] 23 | } else { 24 | const children = Array.isArray(nodeObj[field]) 25 | ? nodeObj[field] 26 | : [nodeObj[field]] 27 | 28 | for (const child of children) { 29 | if ( 30 | child && 31 | typeof child === 'object' && 32 | typeof child.type === 'string' 33 | ) { 34 | stripExtras(child) 35 | } 36 | } 37 | } 38 | } 39 | 40 | return node 41 | } 42 | -------------------------------------------------------------------------------- /packages/utils/src/nodesEquivalent.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { NODE_FIELDS } from './NodeTypes' 3 | 4 | /** 5 | * Determines whether two `@babel/types` nodes are equivalent. 6 | */ 7 | export function nodesEquivalent(a: t.Node, b: t.Node): boolean { 8 | if (a === b) { 9 | return true 10 | } 11 | 12 | if (a.type !== b.type) { 13 | return false 14 | } 15 | 16 | const fields = NODE_FIELDS[a.type] 17 | const aProps = a as unknown as { [key: string]: unknown } 18 | const bProps = b as unknown as { [key: string]: unknown } 19 | 20 | for (const [k, field] of Object.entries(fields)) { 21 | const key = k as keyof typeof fields 22 | 23 | if (field.optional && aProps[key] == null && bProps[key] == null) { 24 | continue 25 | } 26 | 27 | const aVal = aProps[key] 28 | const bVal = bProps[key] 29 | 30 | if (aVal === bVal) { 31 | continue 32 | } 33 | 34 | if (aVal == null || bVal == null) { 35 | return false 36 | } 37 | 38 | if (Array.isArray(aVal) && Array.isArray(bVal)) { 39 | if (aVal.length !== bVal.length) { 40 | return false 41 | } 42 | 43 | for (let i = 0; i < aVal.length; i++) { 44 | if (!nodesEquivalent(aVal[i], bVal[i])) { 45 | return false 46 | } 47 | } 48 | 49 | continue 50 | } 51 | 52 | if (t.isNode(aVal) && t.isNode(bVal)) { 53 | if (!nodesEquivalent(aVal, bVal)) { 54 | return false 55 | } 56 | 57 | continue 58 | } 59 | 60 | return false 61 | } 62 | 63 | return true 64 | } 65 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/__tests__"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "rootDir": "src", 8 | "outDir": "build", 9 | "declaration": true, 10 | "sourceMap": true 11 | }, 12 | "references": [ 13 | { "path": "../parser/tsconfig.build.json" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "composite": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": ["build"], 11 | "references": [ 12 | { "path": "../parser/tsconfig.build.json" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './packages/*' -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "lib": ["es2015", "es2016"], 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "strict": true, 9 | "declaration": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------