diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index d4a1947..634f08c 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -13,6 +13,8 @@ body: required: false - label: "`@eslint/config-array`" required: false + - label: "`@eslint/migrate-config`" + required: false - label: "`@eslint/object-schema`" required: false - type: textarea diff --git a/.github/ISSUE_TEMPLATE/change.yml b/.github/ISSUE_TEMPLATE/change.yml index da8f045..715f76c 100644 --- a/.github/ISSUE_TEMPLATE/change.yml +++ b/.github/ISSUE_TEMPLATE/change.yml @@ -12,6 +12,8 @@ body: required: false - label: "`@eslint/config-array`" required: false + - label: "`@eslint/migrate-config`" + required: false - label: "`@eslint/object-schema`" required: false - type: textarea diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index da8522f..a723dfc 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -49,6 +49,34 @@ jobs: # most internal dependencies are released last. #----------------------------------------------------------------------------- + #----------------------------------------------------------------------------- + # @eslint/migrate-config + #----------------------------------------------------------------------------- + + - name: Publish @eslint/migrate-config package to npm + run: npm publish -w packages/migrate-config + if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + # NOTE: No JSR package because JSR doesn't support CLIs + + - name: Tweet Release Announcement + run: npx @humanwhocodes/tweet "@eslint/migrate-config v${{ steps.release.outputs['packages/migrate-config--major'] }}.${{ steps.release.outputs['packages/migrate-config--minor'] }}.${{ steps.release.outputs['packages/migrate-config--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/v${{ steps.release.outputs['packages/migrate-config--tag_name'] }}" + if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }} + env: + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + + - name: Toot Release Announcement + run: npx @humanwhocodes/toot "@eslint/migrate-config v${{ steps.release.outputs['packages/migrate-config--major'] }}.${{ steps.release.outputs['packages/migrate-config--minor'] }}.${{ steps.release.outputs['packages/migrate-config--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/v${{ steps.release.outputs['packages/migrate-config--tag_name'] }}"' + if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }} + env: + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + MASTODON_HOST: ${{ secrets.MASTODON_HOST }} + #----------------------------------------------------------------------------- # @eslint/compat #----------------------------------------------------------------------------- diff --git a/.prettierignore b/.prettierignore index a3414d0..b7a8379 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ dist CHANGELOG.md +packages/*/tests/fixtures diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 99f41aa..5c4ad18 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,6 @@ { "packages/object-schema": "2.1.1", "packages/config-array": "0.14.1", - "packages/compat": "1.0.1" + "packages/compat": "1.0.1", + "packages/migrate-config": "0.0.0" } diff --git a/eslint.config.js b/eslint.config.js index b2f6a07..598353f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,9 @@ import js from "@eslint/js"; export default [ js.configs.recommended, + { + ignores: ["**/tests/fixtures/**/*.*"], + }, { files: ["**/*.test.js"], languageOptions: { diff --git a/package.json b/package.json index 02570e9..ee0fcef 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@eslint/js": "^9.0.0", "eslint": "^9.0.0", + "eslint-config-eslint": "^10.0.0", "lint-staged": "^15.2.0", "prettier": "^3.1.1", "yorkie": "^2.0.0" diff --git a/packages/migrate-config/CHANGELOG.md b/packages/migrate-config/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/migrate-config/LICENSE b/packages/migrate-config/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packages/migrate-config/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. diff --git a/packages/migrate-config/README.md b/packages/migrate-config/README.md new file mode 100644 index 0000000..400c05c --- /dev/null +++ b/packages/migrate-config/README.md @@ -0,0 +1,73 @@ +# ESLint Configuration Migrator + +## Overview + +This package aids in the migration of the legacy ESLint configuration file format (`.eslintrc.`) to the new ESLint configuration file format (`eslint.config.js`). + +**Note:** The generated configuration file isn't guaranteed to work in all cases, but it should get you a lot closer to a working configuration file than manually trying to migrate. + +## Limitations + +This tool currently works well for the following config file formats: + +- `.eslintrc` +- `.eslintrc.json` +- `.eslintrc.yml` + +If you are using a JavaScript configuration file (`.eslintrc.js`, `.eslintrc.cjs`, `.eslintrc.mjs`), this tool currently is only capable of migrating the _evaluated_ configuration. That means any logic you may have inside of the file will be lost. If your configuration file is mostly static, then you'll get a good result; if your configuration file is more complex (using functions, calculating paths, etc.) then this tool will not provide an equivalent configuration file. + +## Usage + +You can run this package on the command line without installing it first by using `npx` or a similar tool: + +```shell +npx @eslint/migrate-config .eslintrc.json +# or +bunx @eslint/migrate-config .eslintrc.json +``` + +The tool will automatically find your `.eslintignore` file in the same directory and migrate that into your new configuration file. + +### CommonJS Output + +By default, this tool generates an ESM file (`.mjs` extension). If you'd like to generate a CommonJS file instead, pass the `--commonjs` flag: + +```shell +npx @eslint/migrate-config .eslintrc.json --commonjs +# or +bunx @eslint/migrate-config .eslintrc.json --commonjs +``` + +## Followup Steps + +Once you have completed the migration, you may need to manually modify the resulting config file. + +### Double-Check Compatibility + +There are some plugins that might be used with compatibility features in the output that may no longer need them. You should double-check your plugins to see if they have fully ESLint v9-compatible versions of the packages. + +### `--ext` + +If you are using `--ext` on the command line, such as: + +```shell +npx eslint --ext .ts . +``` + +You'll need to remove the `--ext` from the command line and add an equivalent object into your configuration file. For example, `--ext .ts` requires an object like this in your configuration file: + +```js +export default [ + { + files: ["**/*.ts"], + }, + + // the rest of your config +]; +``` + +This tells ESLint to search for all files ending with `.ts` when a directory is passed on the command line. You can choose to add additional properties to this object if you'd like, but it's not required. + +## License + +Apache 2.0 diff --git a/packages/migrate-config/package.json b/packages/migrate-config/package.json new file mode 100644 index 0000000..e3cff31 --- /dev/null +++ b/packages/migrate-config/package.json @@ -0,0 +1,55 @@ +{ + "name": "@eslint/migrate-config", + "version": "0.0.0", + "description": "Configuration migration for ESLint", + "type": "module", + "bin": { + "eslint-migrate-config": "./src/migrate-config-cli.js" + }, + "files": [ + "src" + ], + "publishConfig": { + "access": "public" + }, + "directories": { + "test": "tests" + }, + "scripts": { + "test": "mocha tests/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/eslint/rewrite.git" + }, + "keywords": [ + "eslint", + "compatibility", + "configuration", + "config", + "eslintplugin", + "eslint-plugin" + ], + "author": "Nicholas C. Zakas", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/eslint/rewrite/issues" + }, + "homepage": "https://github.com/eslint/rewrite#readme", + "devDependencies": { + "@types/eslint": "^8.56.10", + "eslint": "^9.0.0", + "mocha": "^10.4.0", + "rollup": "^4.16.2", + "rollup-plugin-copy": "^3.5.0", + "typescript": "^5.4.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "dependencies": { + "@eslint/eslintrc": "^3.1.0", + "camelcase": "^8.0.0", + "recast": "^0.23.7" + } +} diff --git a/packages/migrate-config/src/compat-plugins.js b/packages/migrate-config/src/compat-plugins.js new file mode 100644 index 0000000..b188198 --- /dev/null +++ b/packages/migrate-config/src/compat-plugins.js @@ -0,0 +1,11 @@ +/** + * @fileoverview A list of plugins that need the compat utility. + * @author Nicholas C. Zakas + */ + +export default [ + "eslint-plugin-ava", + "eslint-plugin-react-hooks", + "eslint-plugin-react", + "eslint-plugin-import", +]; diff --git a/packages/migrate-config/src/migrate-config-cli.js b/packages/migrate-config/src/migrate-config-cli.js new file mode 100755 index 0000000..d68fce4 --- /dev/null +++ b/packages/migrate-config/src/migrate-config-cli.js @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * @fileoverview CLI to migrate an ESLint config. + * @author Nicholas C. Zakas + */ + +/* global process, console */ + +/* + * IMPORTANT! + * + * Because this file is executable, `npm install` changes its permission to + * include the executable bit. This is a problem because it causes the file to + * be marked as changed in Git, even though it hasn't. This, in turn, causes + * JSR to think the directory is dirty and fails the build. To prevent this, + * we ran: + * $ git update-index --chmod=+x packages/migrate-config/src/migrate-config-cli.js + * This tells Git to ignore changes to the executable bit on this file. + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import fsp from "node:fs/promises"; +import path from "node:path"; +import { migrateConfig } from "./migrate-config.js"; +import { Legacy } from "@eslint/eslintrc"; + +//----------------------------------------------------------------------------- +// Data +//----------------------------------------------------------------------------- + +const args = process.argv.slice(2); +const configFilePath = args[0]; +const commonjs = args.includes("--commonjs"); +const gitignore = args.includes("--gitignore"); +const { loadConfigFile } = Legacy; + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Loads an ignore file. + * @param {string} filePath The path to the ignore file. + * @returns {Promise} The list of patterns to ignore. + */ +async function loadIgnoreFile(filePath) { + try { + const lines = (await fsp.readFile(filePath, "utf8")).split(/\r?\n/); + return lines.filter( + line => line.trim() !== "" && !line.startsWith("#"), + ); + } catch { + return undefined; + } +} + +//----------------------------------------------------------------------------- +// Main +//----------------------------------------------------------------------------- + +if (!configFilePath) { + console.error("Usage: migrate-config "); + process.exit(1); +} + +const config = loadConfigFile(path.resolve(configFilePath)); +const ignorePatterns = await loadIgnoreFile( + path.resolve( + configFilePath, + "../", + gitignore ? ".gitignore" : ".eslintignore", + ), +); +const resultExtname = commonjs ? "cjs" : "mjs"; +const configFileExtname = path.extname(configFilePath); +const configFileBasename = path.basename(configFilePath, configFileExtname); +const resultFileBasename = configFileBasename.startsWith(".eslintrc") + ? "eslint.config" + : configFileBasename; +const resultFilePath = `${path.dirname(configFilePath)}/${resultFileBasename}.${resultExtname}`; + +console.log("\nMigrating", configFilePath); + +if (configFileExtname.endsWith("js")) { + console.error( + "\nWARNING: This tool does not yet work great for .eslintrc.(js|cjs|mjs) files.", + ); + console.error( + "It will convert the evaluated output of our config file, not the source code.", + ); + console.error( + "Please review the output carefully to ensure it is correct.\n", + ); +} + +if (ignorePatterns) { + console.log("Also importing your .eslintignore file"); + + if (!config.ignorePatterns) { + config.ignorePatterns = []; + } + + // put the .eslintignore patterns last so they can override config ignores + config.ignorePatterns = [...config.ignorePatterns, ...ignorePatterns]; +} + +const result = migrateConfig(config, { + sourceType: commonjs ? "commonjs" : "module", +}); +await fsp.writeFile(resultFilePath, result.code); + +console.log("\nWrote new config to", resultFilePath); + +if (result.imports.size) { + const addedImports = [...result.imports.entries()] + .filter(([key, imp]) => imp.added && !key.startsWith("node:")) + .map(([key]) => key); + + console.log( + "\nYou will need to install the following packages to use the new config:", + ); + console.log(addedImports.map(imp => `- ${imp}`).join("\n") + "\n"); + console.log("You can install them using the following command:\n"); + console.log(`npm install ${addedImports.join(" ")} -D\n`); +} + +if (result.messages.length) { + console.log("The following messages were generated during migration:"); + console.log(result.messages.map(msg => `- ${msg}`).join("\n") + "\n"); +} diff --git a/packages/migrate-config/src/migrate-config.js b/packages/migrate-config/src/migrate-config.js new file mode 100644 index 0000000..41c5abc --- /dev/null +++ b/packages/migrate-config/src/migrate-config.js @@ -0,0 +1,937 @@ +/** + * @filedescription Configuration Migration + * @author Nicholas C. Zakas + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import * as recast from "recast"; +import { Legacy } from "@eslint/eslintrc"; +import camelCase from "camelcase"; +import pluginsNeedingCompat from "./compat-plugins.js"; + +//----------------------------------------------------------------------------- +// Types +//----------------------------------------------------------------------------- + +/** @typedef {import("eslint").Linter.FlatConfig} FlatConfig */ +/** @typedef {import("eslint").Linter.Config} Config */ +/** @typedef {import("eslint").Linter.ConfigOverride} ConfigOverride */ +/** @typedef {import("recast").types.namedTypes.ObjectExpression} ObjectExpression */ +/** @typedef {import("recast").types.namedTypes.ArrayExpression} ArrayExpression */ +/** @typedef {import("recast").types.namedTypes.Property} Property */ +/** @typedef {import("recast").types.namedTypes.MemberExpression} MemberExpression */ +/** @typedef {import("recast").types.namedTypes.Program} Program */ +/** @typedef {import("recast").types.namedTypes.Statement} Statement */ +/** @typedef {import("recast").types.namedTypes.Literal} Literal */ +/** @typedef {import("recast").types.namedTypes.SpreadElement} SpreadElement */ +/** @typedef {import("./types.js").MigrationImport} MigrationImport */ + +//----------------------------------------------------------------------------- +// Data +//----------------------------------------------------------------------------- + +const keysToCopy = ["settings", "rules", "processor"]; +const linterOptionsKeysToCopy = [ + "noInlineConfig", + "reportUnusedDisableDirectives", +]; + +//----------------------------------------------------------------------------- +// Classes +//----------------------------------------------------------------------------- + +/** + * Represents a migration from one config to another. + */ +class Migration { + /** + * The config to migrate. + * @type {Config} + */ + config; + + /** + * Any imports required for the new config. + * @type {Map} + */ + imports = new Map(); + + /** + * Any messages to display to the user. + * @type {string[]} + */ + messages = []; + + /** + * Any initialization needed in the file. + * @type {Array} + */ + inits = []; + + /** + * Creates a new Migration object. + * @param {Config} config The config to migrate. + */ + constructor(config) { + this.config = config; + } +} + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const { builders: b } = recast.types; +const { naming } = Legacy; + +/** + * Determines if a string is a valid identifier. + * @param {string} name The name to check. + * @returns {boolean} `true` if the name is a valid identifier. + */ +function isValidIdentifier(name) { + return /^[a-z_$][0-9a-z_$]*$/iu.test(name); +} + +/** + * Gets the name of the variable to use for the parser. + * @param {string|undefined} parser The name of the parser. + * @returns {string|undefined} The variable name to use or undefined if none. + */ +function getParserVariableName(parser) { + if (!parser) { + return undefined; + } + + if (parser.includes("typescript-eslint")) { + return "tsParser"; + } + + if (parser.includes("babel")) { + return "babelParser"; + } + + if (parser === "espree") { + return "espree"; + } + + return "parser"; +} + +/** + * Converts an ESLint ignore pattern to a minimatch pattern. + * @param {string} pattern The .eslintignore pattern to convert. + * @returns {string} The converted pattern. + */ +function convertESLintIgnoreToMinimatch(pattern) { + const isNegated = pattern.startsWith("!"); + const negatedPrefix = isNegated ? "!" : ""; + const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd(); + + // special cases + if (["", "**", "/**", "**/"].includes(patternToTest)) { + return `${negatedPrefix}${patternToTest}`; + } + + const firstIndexOfSlash = patternToTest.indexOf("/"); + + const matchEverywherePrefix = + firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1 + ? "**/" + : ""; + + const patternWithoutLeadingSlash = + firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest; + + const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : ""; + + return `${negatedPrefix}${matchEverywherePrefix}${patternWithoutLeadingSlash}${matchInsideSuffix}`; +} + +/** + * Determines if a plugin needs the compat utility. + * @param {string} pluginName The name of the plugin. + * @returns {boolean} `true` if the plugin needs the compat utility. + */ +function pluginNeedsCompat(pluginName) { + const pluginNameToTest = pluginName.includes("/") + ? pluginName.slice(0, pluginName.indexOf("/")) + : pluginName; + + return pluginsNeedingCompat.includes( + naming.normalizePackageName(pluginNameToTest, "eslint-plugin"), + ); +} + +/** + * Gets the name of the variable to use for the plugin. If the plugin name + * contains slashes or an @ symbol, it will be normalized to a camelcase name. + * If the name is "import" or "export", it will be prefixed with an underscore. + * @param {string} pluginName The name of the plugin. + * @returns {string} The variable name to use. + */ +function getPluginVariableName(pluginName) { + let name = pluginName.replace(/^eslint-plugin-/, ""); + + if (name === "import" || name === "export") { + return `_${name}`; + } + + if (name.startsWith("@")) { + name = name.slice(1); + } + + return camelCase(name); +} + +/** + * Creates an initialization block for the FlatCompat utility. + * @param {"module"|"commonjs"} sourceType The module type to use. + * @returns {Array} The AST for the initialization block. + */ +function getFlatCompatInit(sourceType) { + let init = ` +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); +`; + + /* + * Need to calculate `__dirname` and `__filename` for ESM. Note that Recast + * doesn't support `import.meta.url`, so using an uppercase "I" to allow for + * parsing. We then need to replace it with the lowercase "i". + */ + if (sourceType === "module") { + init = ` +const __filename = fileURLToPath(Import.meta.url); +const __dirname = path.dirname(__filename); + +${init}`; + } + + const result = recast.parse(init).program.body; + + // Replace uppercase "I" with lowercase "i" in "Import.meta.url" + if (sourceType === "module") { + result[0].declarations[0].init.arguments[0].object.object.name = + "import"; + } + + return result; +} + +/** + * Converts a glob pattern to a format that can be used in a flat config. + * @param {string} pattern The glob pattern to convert. + * @returns {string} The converted glob pattern. + */ +function convertGlobPattern(pattern) { + const isNegated = pattern.startsWith("!"); + const patternToTest = isNegated ? pattern.slice(1) : pattern; + + // if the pattern is already in the correct format, return it + if (patternToTest === "**" || patternToTest.includes("/")) { + return pattern; + } + + return `${isNegated ? "!" : ""}**/${patternToTest}`; +} + +/** + * Creates the globals object from the config. + * @param {Config} config The config to create globals from. + * @returns {ObjectExpression|undefined} The globals object or undefined if none. + */ +function createGlobals(config) { + const { globals, env } = config; + + if (!globals && !env) { + return undefined; + } + + const properties = []; + + if (env) { + properties.push( + ...Object.keys(env) + .filter(env => !env.startsWith("es")) + .map(name => { + let envName = name; + const memberExpression = b.memberExpression( + b.identifier("globals"), + b.identifier(name), + ); + + // plugins environments in the form plugin/env + if (name.includes("/")) { + const [pluginName, pluginEnvName] = name.split("/"); + const pluginVariableName = + getPluginVariableName(pluginName); + + // looks like plugin.environments.envName.globals + memberExpression.object = b.memberExpression( + b.memberExpression( + b.identifier(pluginVariableName), + b.identifier("environments"), + ), + isValidIdentifier(pluginEnvName) + ? b.identifier(pluginEnvName) + : b.literal(pluginEnvName), + !isValidIdentifier(pluginEnvName), + ); + memberExpression.property = b.identifier("globals"); + envName = pluginEnvName; + } + + // if the name is not a valid identifier, use computed syntax + if (!isValidIdentifier(envName)) { + memberExpression.computed = true; + memberExpression.property = b.literal(envName); + } + + const envValue = env[name]; + + // environment is enabled + if (envValue) { + return b.spreadProperty(memberExpression); + } + + // environment is disabled + return b.spreadProperty( + b.callExpression( + b.memberExpression( + b.identifier("Object"), + b.identifier("fromEntries"), + ), + [ + b.callExpression( + b.memberExpression( + b.callExpression( + b.memberExpression( + b.identifier("Object"), + b.identifier("entries"), + ), + [memberExpression], + ), + b.identifier("map"), + ), + [ + b.arrowFunctionExpression( + [ + b.arrayPattern([ + b.identifier("key"), + ]), + ], + b.arrayExpression([ + b.identifier("key"), + b.literal("off"), + ]), + ), + ], + ), + ], + ), + ); + }), + ); + } + + if (globals) { + properties.push( + ...Object.keys(globals).map(name => { + return b.property( + "init", + b.identifier(name), + b.literal(globals[name]), + ); + }), + ); + } + + return b.objectExpression(properties); +} + +/** + * Creates the linter options object from the config. + * @param {Config} config The config to create linter options from. + * @returns {ObjectExpression|undefined} The linter options object or undefined if none. + */ +function createLinterOptions(config) { + if (!config.noInlineConfig && !config.reportUnusedDisableDirectives) { + return undefined; + } + + const properties = []; + + linterOptionsKeysToCopy.forEach(key => { + if (config[key]) { + properties.push( + b.property("init", b.identifier(key), b.literal(config[key])), + ); + } + }); + + return b.objectExpression(properties); +} + +/** + * Creates an array of function arguments from an array of extended configs. + * @param {string|string[]} extendedConfigs The extended configs to convert. + * @returns {Array} The AST for the array expression. + */ +function createExtendsArguments(extendedConfigs) { + // create an array of strings + if (typeof extendedConfigs === "string") { + return [b.literal(extendedConfigs)]; + } + + return extendedConfigs.map(config => b.literal(config)); +} + +/** + * Creates a an object expression that duplicates an existing object. + * @param {Object} value The object to create the AST for. + * @returns {ObjectExpression|ArrayExpression|Literal} The AST for the object. + */ +function createAST(value) { + if (Array.isArray(value)) { + return b.arrayExpression(value.map(item => createAST(item))); + } + + if (value && typeof value === "object") { + const properties = Object.keys(value).map(key => { + const propertyValue = value[key]; + const identifier = isValidIdentifier(key) + ? b.identifier(key) + : b.literal(key); + return b.property("init", identifier, createAST(propertyValue)); + }); + + return b.objectExpression(properties); + } + + return b.literal(value); +} + +/** + * Creates an array expression from an array of glob patterns. + * @param {string[]} patterns The glob patterns to convert. + * @returns {ArrayExpression} The AST for the array expression. + */ +function createFilesArray(patterns) { + return b.arrayExpression( + patterns.map(pattern => b.literal(convertGlobPattern(pattern))), + ); +} + +/** + * Creates an object expression for the language options. + * @param {Migration} migration The migration object. + * @param {Config} config The config to create language options from. + * @returns {ObjectExpression|undefined} The AST for the object expression or undefined if none. + */ +function createLanguageOptions(migration, config) { + const properties = []; + const { imports, messages } = migration; + + // Both `env` and `globals` end up as globals in flat config + const globals = createGlobals(config); + if (globals) { + properties.push(b.property("init", b.identifier("globals"), globals)); + } + + /* + * For `env`, we need the `globals` package if there are any environments + * that aren't ECMAScript environments and also aren't from plugins + * (the name contains a slash). + */ + const needsGlobals = + config.env && + Object.keys(config.env).some(envName => { + return !envName.startsWith("es") && !envName.includes("/"); + }); + + if (needsGlobals && !imports.has("globals")) { + imports.set("globals", { + name: "globals", + added: true, + }); + } + + // Copy over `parser` + const parserName = getParserVariableName(config.parser); + if (parserName) { + properties.push( + b.property( + "init", + b.identifier("parser"), + b.identifier(parserName), + ), + ); + imports.set(config.parser, { + name: parserName, + }); + } + + // Copy over `parserOptions` + if (config.parserOptions) { + const { + ecmaVersion = 5, + sourceType = "script", + ...otherParserOptions + } = config.parserOptions; + + // move `ecmaVersion` to `languageOptions` + properties.push( + b.property( + "init", + b.identifier("ecmaVersion"), + b.literal(ecmaVersion), + ), + ); + + // move `sourceType` to `languageOptions` -- be sure to check for Node.js environment + /** @type {"module"|"script"|"commonjs"} */ + let finalSourceType = sourceType; + if (config?.env?.node) { + if (sourceType === "module") { + messages.push( + "The 'node' environment is used, but the sourceType is 'module'. Using sourceType 'module'. If you want to use CommonJS modules, set the sourceType to 'commonjs'.", + ); + } else { + finalSourceType = "commonjs"; + messages.push( + "The 'node' environment is used, so switching sourceType to 'commonjs'.", + ); + } + } + + properties.push( + b.property( + "init", + b.identifier("sourceType"), + b.literal(finalSourceType), + ), + ); + + if (Object.keys(otherParserOptions).length > 0) { + properties.push( + b.property( + "init", + b.identifier("parserOptions"), + createAST(otherParserOptions), + ), + ); + } + } + + return properties.length ? b.objectExpression(properties) : undefined; +} + +/** + * Creates an object expression for the plugins array. Also adds the necessary imports + * to the migration imports map. + * @param {string[]} plugins The plugins to create the object expression for. + * @param {Migration} migration The migration object. + * @returns {ObjectExpression} The AST for the object expression. + */ +function createPlugins(plugins, migration) { + const { imports } = migration; + const properties = []; + + const compatNeeded = plugins.reduce((previous, pluginName) => { + const pluginVariableName = getPluginVariableName(pluginName); + const shortPluginName = naming.getShorthandName( + pluginName, + "eslint-plugin", + ); + const needsCompat = pluginNeedsCompat(pluginName); + + const pluginValue = needsCompat + ? b.callExpression(b.identifier("fixupPluginRules"), [ + b.identifier(pluginVariableName), + ]) + : b.identifier(pluginVariableName); + + const pluginsProperty = b.property( + "init", + isValidIdentifier(shortPluginName) + ? b.identifier(shortPluginName) + : b.literal(shortPluginName), + pluginValue, + ); + + if (pluginVariableName === shortPluginName && !needsCompat) { + pluginsProperty.shorthand = true; + } + + properties.push(pluginsProperty); + + imports.set(naming.normalizePackageName(pluginName, "eslint-plugin"), { + name: pluginVariableName, + }); + + return needsCompat || previous; + }, false); + + if (compatNeeded) { + if (!migration.imports.has("@eslint/compat")) { + migration.imports.set("@eslint/compat", { + bindings: ["fixupPluginRules"], + added: true, + }); + } else { + migration.imports + .get("@eslint/compat") + .bindings.push("fixupPluginRules"); + } + } + + return b.objectExpression(properties); +} + +/** + * Creates an object expression for the `ignorePatterns` property. + * @param {Config} config The config to create the object expression for. + * @returns {ObjectExpression} The AST for the object expression. + */ +function createGlobalIgnores(config) { + const ignorePatterns = Array.isArray(config.ignorePatterns) + ? config.ignorePatterns + : [config.ignorePatterns]; + const ignorePatternsArray = b.arrayExpression( + ignorePatterns.map(pattern => + b.literal(convertESLintIgnoreToMinimatch(pattern)), + ), + ); + return b.objectExpression([ + b.property("init", b.identifier("ignores"), ignorePatternsArray), + ]); +} + +/** + * Migrates a config object to the flat config format. + * @param {Migration} migration The migration object. + * @param {ConfigOverride} config The config object to migrate. + * @returns {Array} The AST for the object expression. + */ +function migrateConfigObject(migration, config) { + const configArrayElements = []; + const properties = []; + let files, ignores; + + // Copy over `files` -- should end up first by convention + if (config.files) { + files = createFilesArray( + Array.isArray(config.files) ? config.files : [config.files], + ); + properties.push(b.property("init", b.identifier("files"), files)); + } + + // Copy over `excludedFiles` -- should end up first if no `files` or second if `files` is present + if (config.excludedFiles) { + ignores = createFilesArray( + Array.isArray(config.excludedFiles) + ? config.excludedFiles + : [config.excludedFiles], + ); + properties.push(b.property("init", b.identifier("ignores"), ignores)); + } + + // Copy over `plugins` + if (config.plugins) { + properties.push( + b.property( + "init", + b.identifier("plugins"), + createPlugins(config.plugins, migration), + ), + ); + } + + // Handle `extends` + if (config.extends) { + let extendsCallExpression = b.callExpression( + b.memberExpression(b.identifier("compat"), b.identifier("extends")), + createExtendsArguments(config.extends), + ); + + const extendsArray = Array.isArray(config.extends) + ? config.extends + : [config.extends]; + + // Check if any of the extends are plugins that need the compat utility + const needsCompat = extendsArray.some(extend => { + if (!extend.startsWith("plugin:")) { + return false; + } + + return pluginNeedsCompat(extend.slice(7)); + }); + + if (needsCompat) { + if (!migration.imports.has("@eslint/compat")) { + migration.imports.set("@eslint/compat", { + bindings: ["fixupConfigRules"], + added: true, + }); + } else { + migration.imports + .get("@eslint/compat") + .bindings.push("fixupConfigRules"); + } + + extendsCallExpression = b.callExpression( + b.identifier("fixupConfigRules"), + [extendsCallExpression], + ); + } + + // if there are either files or ignores, map so the resulting object has files and ignores + if (files || ignores) { + extendsCallExpression = b.callExpression( + b.memberExpression(extendsCallExpression, b.identifier("map")), + [ + b.arrowFunctionExpression( + [b.identifier("config")], + b.objectExpression([ + b.spreadElement(b.identifier("config")), + ...(files + ? [ + b.property( + "init", + b.identifier("files"), + files, + ), + ] + : []), + ...(ignores + ? [ + b.property( + "init", + b.identifier("ignores"), + ignores, + ), + ] + : []), + ]), + ), + ], + ); + } + + configArrayElements.push(b.spreadElement(extendsCallExpression)); + } + + // Copy over `noInlineConfig` and `reportUnusedDisableDirectives` + const linterOptions = createLinterOptions(config); + if (linterOptions) { + properties.push( + b.property("init", b.identifier("linterOptions"), linterOptions), + ); + } + + // Create `languageOptions` from `env`, `globals`, `parser`, and `parserOptions` + const languageOptions = createLanguageOptions(migration, config); + if (languageOptions) { + properties.push( + b.property( + "init", + b.identifier("languageOptions"), + languageOptions, + ), + ); + } + + // Copy over everything that stays the same - `settings`, `rules`, `processor` + keysToCopy.forEach(key => { + if (config[key]) { + const propertyValue = + typeof config[key] === "object" + ? createAST(config[key]) + : b.literal(config[key]); + properties.push( + b.property("init", b.identifier(key), propertyValue), + ); + } + }); + + /* + * If there is an `extends` with a `files` and/or `ignores`, then it's possible this object + * will contain only `files` (and/or `ignores`), in which case we don't need it because there + * is already a config object with the same properties. + */ + const objectIsNeeded = + !config.extends || + properties.some(property => { + if (property.key.type === "Identifier") { + return ( + property.key.name !== "files" && + property.key.name !== "ignores" + ); + } + + return true; + }); + if (objectIsNeeded) { + configArrayElements.push(b.objectExpression(properties)); + } + + return configArrayElements; +} + +/** + * Migrates an eslintrc config to flat config format. + * @param {Config} config The eslintrc config to migrate. + * @param {Object} [options] Options for the migration. + * @param {"module"|"commonjs"} [options.sourceType] The module type to use. + * @returns {{code:string,messages:Array,imports:Map}} The migrated config and + * any messages to display to the user. + */ +export function migrateConfig(config, { sourceType = "module" } = {}) { + const migration = new Migration(config); + const body = []; + const configArrayElements = [ + ...migrateConfigObject( + migration, + /** @type {ConfigOverride} */ (config), + ), + ]; + + // if the base config has no properties, then remove the empty object + if ( + configArrayElements[0].type === "ObjectExpression" && + configArrayElements[0].properties.length === 0 + ) { + configArrayElements.shift(); + } + + // add any overrides + if (config.overrides) { + config.overrides.forEach(override => { + configArrayElements.push( + ...migrateConfigObject(migration, override), + ); + }); + } + + // if any config has extends then we need to add imports + if ( + config.extends || + config.overrides?.some(override => override.extends) + ) { + if (sourceType === "module") { + migration.imports.set("node:path", { + name: "path", + added: true, + }); + migration.imports.set("node:url", { + bindings: ["fileURLToPath"], + added: true, + }); + } + migration.imports.set("@eslint/js", { + name: "js", + added: true, + }); + migration.imports.set("@eslint/eslintrc", { + bindings: ["FlatCompat"], + added: true, + }); + migration.inits.push(...getFlatCompatInit(sourceType)); + } + + if (config.ignorePatterns) { + configArrayElements.unshift(createGlobalIgnores(config)); + } + + // add imports to the top of the file + if (sourceType === "commonjs") { + migration.imports.forEach(({ name, bindings }, path) => { + const bindingProperties = bindings?.map(binding => { + const bindingProperty = b.property( + "init", + b.identifier(binding), + b.identifier(binding), + ); + bindingProperty.shorthand = true; + return bindingProperty; + }); + + body.push( + name + ? b.variableDeclaration("const", [ + b.variableDeclarator( + b.identifier(name), + b.callExpression(b.identifier("require"), [ + b.literal(path), + ]), + ), + ]) + : b.variableDeclaration("const", [ + b.variableDeclarator( + b.objectPattern(bindingProperties), + b.callExpression(b.identifier("require"), [ + b.literal(path), + ]), + ), + ]), + ); + }); + } else { + migration.imports.forEach(({ name, bindings }, path) => { + body.push( + name + ? b.importDeclaration( + [b.importDefaultSpecifier(b.identifier(name))], + b.literal(path), + ) + : b.importDeclaration( + bindings.map(binding => + b.importSpecifier(b.identifier(binding)), + ), + b.literal(path), + ), + ); + }); + } + + // output any inits + body.push(...migration.inits); + + // output the actual config array to the program + if (sourceType === "commonjs") { + body.push( + b.expressionStatement( + b.assignmentExpression( + "=", + b.memberExpression( + b.identifier("module"), + b.identifier("exports"), + ), + b.arrayExpression(configArrayElements), + ), + ), + ); + } else { + body.push( + b.exportDefaultDeclaration(b.arrayExpression(configArrayElements)), + ); + } + + return { + // Recast doesn't export the `StatementKind` type so we need to cast the body to `Array` + code: recast.print(b.program(/** @type {Array}*/ (body)), { + tabWidth: 4, + trailingComma: true, + lineTerminator: "\n", + }).code, + messages: migration.messages, + imports: migration.imports, + }; +} diff --git a/packages/migrate-config/src/types.ts b/packages/migrate-config/src/types.ts new file mode 100644 index 0000000..9218752 --- /dev/null +++ b/packages/migrate-config/src/types.ts @@ -0,0 +1,20 @@ +/** + * @filedescription Types for migrate-config package. + */ + +export interface MigrationImport { + /** + * The name to use to import the entire module. + */ + name?: string; + + /** + * The names to import from the module. + */ + bindings?: string[]; + + /** + * Whether the import is added by the migration. + */ + added?: boolean; +} diff --git a/packages/migrate-config/tests/fixtures/.gitignore b/packages/migrate-config/tests/fixtures/.gitignore new file mode 100644 index 0000000..9d46bc0 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/.gitignore @@ -0,0 +1,4 @@ +# Generated files +eslint.config.* +basic-eslintrc.mjs +basic-eslintrc.cjs diff --git a/packages/migrate-config/tests/fixtures/basic-eslintrc/.eslintignore b/packages/migrate-config/tests/fixtures/basic-eslintrc/.eslintignore new file mode 100644 index 0000000..f505203 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/basic-eslintrc/.eslintignore @@ -0,0 +1,3 @@ +*/a.js +dir/** +dir/ diff --git a/packages/migrate-config/tests/fixtures/basic-eslintrc/basic-eslintrc.yml b/packages/migrate-config/tests/fixtures/basic-eslintrc/basic-eslintrc.yml new file mode 100644 index 0000000..ca5df10 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/basic-eslintrc/basic-eslintrc.yml @@ -0,0 +1,40 @@ +root: true +plugins: + - prettier + - import + - node + - promise + - standard + - "@typescript-eslint" +env: + shared-node-browser: true + es6: true + amd: false + node/base: true +extends: + - eslint:recommended + - plugin:import/errors +parserOptions: + ecmaVersion: 2018 +rules: + semi: + - error + quotes: + - error + no-console: + - warn +overrides: + - files: + - "*.ts" + excludedFiles: + - "*.d.ts" + parser: "@typescript-eslint/parser" + plugins: + - "@typescript-eslint" + extends: + - plugin:@typescript-eslint/recommended + rules: + "@typescript-eslint/no-explicit-any": + - error + "@typescript-eslint/no-unused-vars": + - error diff --git a/packages/migrate-config/tests/fixtures/basic-eslintrc/expected.cjs b/packages/migrate-config/tests/fixtures/basic-eslintrc/expected.cjs new file mode 100644 index 0000000..58a644a --- /dev/null +++ b/packages/migrate-config/tests/fixtures/basic-eslintrc/expected.cjs @@ -0,0 +1,75 @@ +const prettier = require("eslint-plugin-prettier"); +const _import = require("eslint-plugin-import"); +const node = require("eslint-plugin-node"); +const promise = require("eslint-plugin-promise"); +const standard = require("eslint-plugin-standard"); +const typescriptEslint = require("@typescript-eslint/eslint-plugin"); + +const { + fixupPluginRules, + fixupConfigRules, +} = require("@eslint/compat"); + +const globals = require("globals"); +const tsParser = require("@typescript-eslint/parser"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [{ + ignores: ["*/a.js", "dir/**/*", "**/dir/"], +}, ...fixupConfigRules(compat.extends("eslint:recommended", "plugin:import/errors")), { + plugins: { + prettier, + import: fixupPluginRules(_import), + node, + promise, + standard, + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals["shared-node-browser"], + ...Object.fromEntries(Object.entries(globals.amd).map(([key]) => [key, "off"])), + ...node.environments.base.globals, + }, + + ecmaVersion: 2018, + sourceType: "script", + }, + + rules: { + semi: ["error"], + quotes: ["error"], + "no-console": ["warn"], + }, +}, ...compat.extends("plugin:@typescript-eslint/recommended").map(config => ({ + ...config, + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], +})), { + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], + + plugins: { + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + parser: tsParser, + }, + + rules: { + "@typescript-eslint/no-explicit-any": ["error"], + "@typescript-eslint/no-unused-vars": ["error"], + }, +}]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/basic-eslintrc/expected.mjs b/packages/migrate-config/tests/fixtures/basic-eslintrc/expected.mjs new file mode 100644 index 0000000..f36e74d --- /dev/null +++ b/packages/migrate-config/tests/fixtures/basic-eslintrc/expected.mjs @@ -0,0 +1,72 @@ +import prettier from "eslint-plugin-prettier"; +import _import from "eslint-plugin-import"; +import node from "eslint-plugin-node"; +import promise from "eslint-plugin-promise"; +import standard from "eslint-plugin-standard"; +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import { fixupPluginRules, fixupConfigRules } from "@eslint/compat"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [{ + ignores: ["*/a.js", "dir/**/*", "**/dir/"], +}, ...fixupConfigRules(compat.extends("eslint:recommended", "plugin:import/errors")), { + plugins: { + prettier, + import: fixupPluginRules(_import), + node, + promise, + standard, + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals["shared-node-browser"], + ...Object.fromEntries(Object.entries(globals.amd).map(([key]) => [key, "off"])), + ...node.environments.base.globals, + }, + + ecmaVersion: 2018, + sourceType: "script", + }, + + rules: { + semi: ["error"], + quotes: ["error"], + "no-console": ["warn"], + }, +}, ...compat.extends("plugin:@typescript-eslint/recommended").map(config => ({ + ...config, + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], +})), { + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], + + plugins: { + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + parser: tsParser, + }, + + rules: { + "@typescript-eslint/no-explicit-any": ["error"], + "@typescript-eslint/no-unused-vars": ["error"], + }, +}]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/no-globals-for-env/.eslintrc.yml b/packages/migrate-config/tests/fixtures/no-globals-for-env/.eslintrc.yml new file mode 100644 index 0000000..d4519c1 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/no-globals-for-env/.eslintrc.yml @@ -0,0 +1,38 @@ +root: true +plugins: + - prettier + - import + - node + - promise + - standard + - "@typescript-eslint" +env: + es6: true + node/base: true +extends: + - eslint:recommended + - plugin:import/errors +parserOptions: + ecmaVersion: 2018 +rules: + semi: + - error + quotes: + - error + no-console: + - warn +overrides: + - files: + - "*.ts" + excludedFiles: + - "*.d.ts" + parser: "@typescript-eslint/parser" + plugins: + - "@typescript-eslint" + extends: + - plugin:@typescript-eslint/recommended + rules: + "@typescript-eslint/no-explicit-any": + - error + "@typescript-eslint/no-unused-vars": + - error diff --git a/packages/migrate-config/tests/fixtures/no-globals-for-env/expected.cjs b/packages/migrate-config/tests/fixtures/no-globals-for-env/expected.cjs new file mode 100644 index 0000000..b2a1e6c --- /dev/null +++ b/packages/migrate-config/tests/fixtures/no-globals-for-env/expected.cjs @@ -0,0 +1,75 @@ +const prettier = require("eslint-plugin-prettier"); +const _import = require("eslint-plugin-import"); +const node = require("eslint-plugin-node"); +const promise = require("eslint-plugin-promise"); +const standard = require("eslint-plugin-standard"); +const typescriptEslint = require("@typescript-eslint/eslint-plugin"); + +const { + fixupPluginRules, + fixupConfigRules, +} = require("@eslint/compat"); + +const tsParser = require("@typescript-eslint/parser"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [ + ...fixupConfigRules(compat.extends("eslint:recommended", "plugin:import/errors")), + { + plugins: { + prettier, + import: fixupPluginRules(_import), + node, + promise, + standard, + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + globals: { + ...node.environments.base.globals, + }, + + ecmaVersion: 2018, + sourceType: "script", + }, + + rules: { + semi: ["error"], + quotes: ["error"], + "no-console": ["warn"], + }, + }, + ...compat.extends("plugin:@typescript-eslint/recommended").map(config => ({ + ...config, + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], + })), + { + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], + + plugins: { + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + parser: tsParser, + }, + + rules: { + "@typescript-eslint/no-explicit-any": ["error"], + "@typescript-eslint/no-unused-vars": ["error"], + }, + }, +]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/no-globals-for-env/expected.mjs b/packages/migrate-config/tests/fixtures/no-globals-for-env/expected.mjs new file mode 100644 index 0000000..21f08df --- /dev/null +++ b/packages/migrate-config/tests/fixtures/no-globals-for-env/expected.mjs @@ -0,0 +1,72 @@ +import prettier from "eslint-plugin-prettier"; +import _import from "eslint-plugin-import"; +import node from "eslint-plugin-node"; +import promise from "eslint-plugin-promise"; +import standard from "eslint-plugin-standard"; +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import { fixupPluginRules, fixupConfigRules } from "@eslint/compat"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [ + ...fixupConfigRules(compat.extends("eslint:recommended", "plugin:import/errors")), + { + plugins: { + prettier, + import: fixupPluginRules(_import), + node, + promise, + standard, + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + globals: { + ...node.environments.base.globals, + }, + + ecmaVersion: 2018, + sourceType: "script", + }, + + rules: { + semi: ["error"], + quotes: ["error"], + "no-console": ["warn"], + }, + }, + ...compat.extends("plugin:@typescript-eslint/recommended").map(config => ({ + ...config, + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], + })), + { + files: ["**/*.ts"], + ignores: ["**/*.d.ts"], + + plugins: { + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + parser: tsParser, + }, + + rules: { + "@typescript-eslint/no-explicit-any": ["error"], + "@typescript-eslint/no-unused-vars": ["error"], + }, + }, +]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/overrides-extends/.eslintrc.json b/packages/migrate-config/tests/fixtures/overrides-extends/.eslintrc.json new file mode 100644 index 0000000..e87954d --- /dev/null +++ b/packages/migrate-config/tests/fixtures/overrides-extends/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "overrides": [ + { + "files": "src/app/**", + "excludedFiles": "*.test.js", + "extends": [ + "airbnb" + ] + }, + { + "files": "src/app/**/*.test.js", + "extends": [ + "airbnb-base" + ] + }, + { + "excludedFiles": "src/app/**/*.spec.js", + "extends": [ + "airbnb-base" + ] + } + ] +} diff --git a/packages/migrate-config/tests/fixtures/overrides-extends/expected.cjs b/packages/migrate-config/tests/fixtures/overrides-extends/expected.cjs new file mode 100644 index 0000000..77d7eac --- /dev/null +++ b/packages/migrate-config/tests/fixtures/overrides-extends/expected.cjs @@ -0,0 +1,23 @@ +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [...compat.extends("airbnb").map(config => ({ + ...config, + files: ["src/app/**"], + ignores: ["**/*.test.js"], +})), ...compat.extends("airbnb-base").map(config => ({ + ...config, + files: ["src/app/**/*.test.js"], +})), ...compat.extends("airbnb-base").map(config => ({ + ...config, + ignores: ["src/app/**/*.spec.js"], +}))]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/overrides-extends/expected.mjs b/packages/migrate-config/tests/fixtures/overrides-extends/expected.mjs new file mode 100644 index 0000000..552113c --- /dev/null +++ b/packages/migrate-config/tests/fixtures/overrides-extends/expected.mjs @@ -0,0 +1,25 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [...compat.extends("airbnb").map(config => ({ + ...config, + files: ["src/app/**"], + ignores: ["**/*.test.js"], +})), ...compat.extends("airbnb-base").map(config => ({ + ...config, + files: ["src/app/**/*.test.js"], +})), ...compat.extends("airbnb-base").map(config => ({ + ...config, + ignores: ["src/app/**/*.spec.js"], +}))]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/prisma/.eslintignore b/packages/migrate-config/tests/fixtures/prisma/.eslintignore new file mode 100644 index 0000000..cc36f4d --- /dev/null +++ b/packages/migrate-config/tests/fixtures/prisma/.eslintignore @@ -0,0 +1,26 @@ +# common +.github/renovate.json +dist/ +esm/ +build/ +fixtures/ +byline.ts +prism.ts +charm.ts +pnpm-lock.yaml +generated-dmmf.ts + +# client +packages/client/generator-build/ +packages/client/declaration/ +packages/client/runtime/ +packages/client/src/__tests__/types/ +packages/client/scripts/default-index.js + +# cli +packages/cli/prisma-client/ +packages/cli/install/ +packages/cli/preinstall/ +packages/cli/**/tmp-* + +sandbox/ diff --git a/packages/migrate-config/tests/fixtures/prisma/.eslintrc.cjs b/packages/migrate-config/tests/fixtures/prisma/.eslintrc.cjs new file mode 100644 index 0000000..b8f745f --- /dev/null +++ b/packages/migrate-config/tests/fixtures/prisma/.eslintrc.cjs @@ -0,0 +1,115 @@ +const path = require("path"); + +const project = "tsconfig.json"; + +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + plugins: [ + "@typescript-eslint", + "jest", + "simple-import-sort", + "import", + "local-rules", + ], + env: { + node: true, + es6: true, + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + project, + }, + overrides: [ + { + files: ["./packages/client/src/runtime/core/types/exported/*.ts"], + excludedFiles: ["index.ts"], + rules: { + "local-rules/all-types-are-exported": "error", + "local-rules/imports-from-same-directory": "error", + }, + }, + { + files: [ + "./packages/client/src/runtime/core/types/exported/index.ts", + ], + rules: { + "local-rules/valid-exported-types-index": "error", + }, + }, + ], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:prettier/recommended", + "plugin:jest/recommended", + ], + rules: { + "prettier/prettier": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "no-useless-escape": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-unsafe-return": "off", + // added at 2020/11/26 + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + // don't complain if we are omitting properties using spread operator, i.e. const { ignored, ...rest } = someObject + ignoreRestSiblings: true, + // for functions, allow to have unused arguments if they start with _. We need to do this from time to time to test type inference within the tests + argsIgnorePattern: "^_", + }, + ], + "eslint-comments/no-unlimited-disable": "off", + "eslint-comments/disable-enable-pair": "off", + "@typescript-eslint/no-misused-promises": "off", + "jest/expect-expect": "off", + "no-empty": "off", + "no-restricted-properties": [ + "error", + { + property: "substr", + message: "Deprecated: Use .slice() instead of .substr().", + }, + ], + "jest/valid-title": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + // low hanging fruits: + // to unblock eslint dep update in https://github.com/prisma/prisma/pull/21935 + "@typescript-eslint/no-unsafe-enum-comparison": "warn", + // to unblock eslint dep update in https://github.com/prisma/prisma/pull/9692 + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "jest/no-conditional-expect": "off", + "jest/no-export": "off", + "jest/no-standalone-expect": "off", + "@typescript-eslint/no-empty-interface": "off", + // https://github.com/lydell/eslint-plugin-simple-import-sort + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", + }, + settings: { + jest: { + version: 27, + globalAliases: { + describe: "describeIf", + }, + }, + }, +}; diff --git a/packages/migrate-config/tests/fixtures/prisma/LICENSE b/packages/migrate-config/tests/fixtures/prisma/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/prisma/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. diff --git a/packages/migrate-config/tests/fixtures/prisma/expected.cjs b/packages/migrate-config/tests/fixtures/prisma/expected.cjs new file mode 100644 index 0000000..17673ef --- /dev/null +++ b/packages/migrate-config/tests/fixtures/prisma/expected.cjs @@ -0,0 +1,150 @@ +const typescriptEslint = require("@typescript-eslint/eslint-plugin"); +const jest = require("eslint-plugin-jest"); +const simpleImportSort = require("eslint-plugin-simple-import-sort"); +const _import = require("eslint-plugin-import"); +const localRules = require("eslint-plugin-local-rules"); + +const { + fixupPluginRules, +} = require("@eslint/compat"); + +const globals = require("globals"); +const tsParser = require("@typescript-eslint/parser"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [{ + ignores: [ + ".github/renovate.json", + "**/dist/", + "**/esm/", + "**/build/", + "**/fixtures/", + "**/byline.ts", + "**/prism.ts", + "**/charm.ts", + "**/pnpm-lock.yaml", + "**/generated-dmmf.ts", + "packages/client/generator-build/", + "packages/client/declaration/", + "packages/client/runtime/", + "packages/client/src/__tests__/types/", + "packages/client/scripts/default-index.js", + "packages/cli/prisma-client/", + "packages/cli/install/", + "packages/cli/preinstall/", + "packages/cli/**/tmp-*", + "**/sandbox/", + ], +}, ...compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:prettier/recommended", + "plugin:jest/recommended", +), { + plugins: { + "@typescript-eslint": typescriptEslint, + jest, + "simple-import-sort": simpleImportSort, + import: fixupPluginRules(_import), + "local-rules": localRules, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 2020, + sourceType: "module", + + parserOptions: { + project: "tsconfig.json", + }, + }, + + settings: { + jest: { + version: 27, + + globalAliases: { + describe: "describeIf", + }, + }, + }, + + rules: { + "prettier/prettier": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "no-useless-escape": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + + "@typescript-eslint/no-unused-vars": ["error", { + ignoreRestSiblings: true, + argsIgnorePattern: "^_", + }], + + "eslint-comments/no-unlimited-disable": "off", + "eslint-comments/disable-enable-pair": "off", + "@typescript-eslint/no-misused-promises": "off", + "jest/expect-expect": "off", + "no-empty": "off", + + "no-restricted-properties": ["error", { + property: "substr", + message: "Deprecated: Use .slice() instead of .substr().", + }], + + "jest/valid-title": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/no-unsafe-enum-comparison": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "jest/no-conditional-expect": "off", + "jest/no-export": "off", + "jest/no-standalone-expect": "off", + "@typescript-eslint/no-empty-interface": "off", + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", + }, +}, { + files: ["./packages/client/src/runtime/core/types/exported/*.ts"], + ignores: ["**/index.ts"], + + rules: { + "local-rules/all-types-are-exported": "error", + "local-rules/imports-from-same-directory": "error", + }, +}, { + files: ["./packages/client/src/runtime/core/types/exported/index.ts"], + + rules: { + "local-rules/valid-exported-types-index": "error", + }, +}]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/prisma/expected.mjs b/packages/migrate-config/tests/fixtures/prisma/expected.mjs new file mode 100644 index 0000000..1151da9 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/prisma/expected.mjs @@ -0,0 +1,148 @@ +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import jest from "eslint-plugin-jest"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import _import from "eslint-plugin-import"; +import localRules from "eslint-plugin-local-rules"; +import { fixupPluginRules } from "@eslint/compat"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [{ + ignores: [ + ".github/renovate.json", + "**/dist/", + "**/esm/", + "**/build/", + "**/fixtures/", + "**/byline.ts", + "**/prism.ts", + "**/charm.ts", + "**/pnpm-lock.yaml", + "**/generated-dmmf.ts", + "packages/client/generator-build/", + "packages/client/declaration/", + "packages/client/runtime/", + "packages/client/src/__tests__/types/", + "packages/client/scripts/default-index.js", + "packages/cli/prisma-client/", + "packages/cli/install/", + "packages/cli/preinstall/", + "packages/cli/**/tmp-*", + "**/sandbox/", + ], +}, ...compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:prettier/recommended", + "plugin:jest/recommended", +), { + plugins: { + "@typescript-eslint": typescriptEslint, + jest, + "simple-import-sort": simpleImportSort, + import: fixupPluginRules(_import), + "local-rules": localRules, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 2020, + sourceType: "module", + + parserOptions: { + project: "tsconfig.json", + }, + }, + + settings: { + jest: { + version: 27, + + globalAliases: { + describe: "describeIf", + }, + }, + }, + + rules: { + "prettier/prettier": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "no-useless-escape": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + + "@typescript-eslint/no-unused-vars": ["error", { + ignoreRestSiblings: true, + argsIgnorePattern: "^_", + }], + + "eslint-comments/no-unlimited-disable": "off", + "eslint-comments/disable-enable-pair": "off", + "@typescript-eslint/no-misused-promises": "off", + "jest/expect-expect": "off", + "no-empty": "off", + + "no-restricted-properties": ["error", { + property: "substr", + message: "Deprecated: Use .slice() instead of .substr().", + }], + + "jest/valid-title": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/no-unsafe-enum-comparison": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "jest/no-conditional-expect": "off", + "jest/no-export": "off", + "jest/no-standalone-expect": "off", + "@typescript-eslint/no-empty-interface": "off", + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", + }, +}, { + files: ["./packages/client/src/runtime/core/types/exported/*.ts"], + ignores: ["**/index.ts"], + + rules: { + "local-rules/all-types-are-exported": "error", + "local-rules/imports-from-same-directory": "error", + }, +}, { + files: ["./packages/client/src/runtime/core/types/exported/index.ts"], + + rules: { + "local-rules/valid-exported-types-index": "error", + }, +}]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/release-it/.eslintrc.json b/packages/migrate-config/tests/fixtures/release-it/.eslintrc.json new file mode 100644 index 0000000..7d7bd47 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/release-it/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "env": { + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "extends": ["eslint:recommended", "plugin:ava/recommended", "prettier"], + "plugins": ["prettier", "import"], + "rules": { + "prettier/prettier": 2, + "ava/no-ignored-test-files": 0, + "ava/no-import-test-files": 0, + "import/no-unresolved": [ + 2, + { + "ignore": ["ava", "got"] + } + ], + "import/no-unused-modules": 2, + "import/order": [ + 2, + { + "newlines-between": "never" + } + ] + } +} diff --git a/packages/migrate-config/tests/fixtures/release-it/LICENSE b/packages/migrate-config/tests/fixtures/release-it/LICENSE new file mode 100644 index 0000000..d0579a7 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/release-it/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Lars Kappert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/migrate-config/tests/fixtures/release-it/expected.cjs b/packages/migrate-config/tests/fixtures/release-it/expected.cjs new file mode 100644 index 0000000..95062ec --- /dev/null +++ b/packages/migrate-config/tests/fixtures/release-it/expected.cjs @@ -0,0 +1,55 @@ +const prettier = require("eslint-plugin-prettier"); +const _import = require("eslint-plugin-import"); + +const { + fixupPluginRules, + fixupConfigRules, +} = require("@eslint/compat"); + +const globals = require("globals"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [ + ...fixupConfigRules(compat.extends("eslint:recommended", "plugin:ava/recommended", "prettier")), + { + plugins: { + prettier, + import: fixupPluginRules(_import), + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 2020, + sourceType: "module", + }, + + rules: { + "prettier/prettier": 2, + "ava/no-ignored-test-files": 0, + "ava/no-import-test-files": 0, + + "import/no-unresolved": [2, { + ignore: ["ava", "got"], + }], + + "import/no-unused-modules": 2, + + "import/order": [2, { + "newlines-between": "never", + }], + }, + }, +]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/release-it/expected.mjs b/packages/migrate-config/tests/fixtures/release-it/expected.mjs new file mode 100644 index 0000000..09485da --- /dev/null +++ b/packages/migrate-config/tests/fixtures/release-it/expected.mjs @@ -0,0 +1,52 @@ +import prettier from "eslint-plugin-prettier"; +import _import from "eslint-plugin-import"; +import { fixupPluginRules, fixupConfigRules } from "@eslint/compat"; +import globals from "globals"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [ + ...fixupConfigRules(compat.extends("eslint:recommended", "plugin:ava/recommended", "prettier")), + { + plugins: { + prettier, + import: fixupPluginRules(_import), + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 2020, + sourceType: "module", + }, + + rules: { + "prettier/prettier": 2, + "ava/no-ignored-test-files": 0, + "ava/no-import-test-files": 0, + + "import/no-unresolved": [2, { + ignore: ["ava", "got"], + }], + + "import/no-unused-modules": 2, + + "import/order": [2, { + "newlines-between": "never", + }], + }, + }, +]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/reveal-md/.eslintrc b/packages/migrate-config/tests/fixtures/reveal-md/.eslintrc new file mode 100644 index 0000000..66a2939 --- /dev/null +++ b/packages/migrate-config/tests/fixtures/reveal-md/.eslintrc @@ -0,0 +1,28 @@ +{ + "env": { + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "extends": [ + "eslint:recommended", + "prettier" + ], + "plugins": [ + "prettier", + "import" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "singleQuote": true, + "printWidth": 120 + } + ], + "import/no-unresolved": 2 + } +} diff --git a/packages/migrate-config/tests/fixtures/reveal-md/LICENSE b/packages/migrate-config/tests/fixtures/reveal-md/LICENSE new file mode 100644 index 0000000..cf6b7eb --- /dev/null +++ b/packages/migrate-config/tests/fixtures/reveal-md/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2023 Lars Kappert, https://webpro.nl + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/migrate-config/tests/fixtures/reveal-md/expected.cjs b/packages/migrate-config/tests/fixtures/reveal-md/expected.cjs new file mode 100644 index 0000000..3651eef --- /dev/null +++ b/packages/migrate-config/tests/fixtures/reveal-md/expected.cjs @@ -0,0 +1,44 @@ +const prettier = require("eslint-plugin-prettier"); +const _import = require("eslint-plugin-import"); + +const { + fixupPluginRules, +} = require("@eslint/compat"); + +const globals = require("globals"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [...compat.extends("eslint:recommended", "prettier"), { + plugins: { + prettier, + import: fixupPluginRules(_import), + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 2022, + sourceType: "module", + }, + + rules: { + "prettier/prettier": ["error", { + singleQuote: true, + printWidth: 120, + }], + + "import/no-unresolved": 2, + }, +}]; \ No newline at end of file diff --git a/packages/migrate-config/tests/fixtures/reveal-md/expected.mjs b/packages/migrate-config/tests/fixtures/reveal-md/expected.mjs new file mode 100644 index 0000000..99f37bf --- /dev/null +++ b/packages/migrate-config/tests/fixtures/reveal-md/expected.mjs @@ -0,0 +1,42 @@ +import prettier from "eslint-plugin-prettier"; +import _import from "eslint-plugin-import"; +import { fixupPluginRules } from "@eslint/compat"; +import globals from "globals"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [...compat.extends("eslint:recommended", "prettier"), { + plugins: { + prettier, + import: fixupPluginRules(_import), + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 2022, + sourceType: "module", + }, + + rules: { + "prettier/prettier": ["error", { + singleQuote: true, + printWidth: 120, + }], + + "import/no-unresolved": 2, + }, +}]; \ No newline at end of file diff --git a/packages/migrate-config/tests/migrate-config-cli.test.js b/packages/migrate-config/tests/migrate-config-cli.test.js new file mode 100644 index 0000000..7cdbf3c --- /dev/null +++ b/packages/migrate-config/tests/migrate-config-cli.test.js @@ -0,0 +1,87 @@ +/** + * @filedescription Fixup tests + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import assert from "node:assert"; +import fsp from "node:fs/promises"; +import path from "node:path"; +import { execSync } from "node:child_process"; + +//----------------------------------------------------------------------------- +// Data +//----------------------------------------------------------------------------- + +const filePaths = [ + "basic-eslintrc/basic-eslintrc.yml", + "prisma/.eslintrc.cjs", + "reveal-md/.eslintrc", + "release-it/.eslintrc.json", + "no-globals-for-env/.eslintrc.yml", +].map(file => `tests/fixtures/${file}`); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Normalizes line endings in a string. + * @param {string} text The text to normalize. + * @returns {string} The normalized text. + */ +function normalizeLineEndings(text) { + return text.replace(/\r\n/g, "\n"); +} + +/** + * Asserts that two files have the same contents. + * @param {string} resultPath The path to the actual file. + * @param {string} expectedPath The path to the expected file. + * @returns {Promise} + * @throws {AssertionError} If the files do not have the same contents. + */ +async function assertFilesEqual(resultPath, expectedPath) { + const expected = await fsp.readFile(expectedPath, "utf8"); + const actual = await fsp.readFile(resultPath, "utf8"); + assert.strictEqual( + normalizeLineEndings(actual), + normalizeLineEndings(expected), + `Files ${resultPath} and ${expectedPath} do not have the same contents`, + ); +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("@eslint/migrate-config", async () => { + for (const filePath of filePaths) { + const fileBaseName = path.basename(filePath); + const fileBaseNameWithoutExt = fileBaseName.replace(/\.\w+$/, ""); + const isESLintRC = fileBaseName.startsWith(".eslintrc"); + const fixturePath = path.dirname(filePath); + const expectedMjsPath = `${fixturePath}/expected.mjs`; + const expectedCjsPath = `${fixturePath}/expected.cjs`; + const resultMjsPath = `${fixturePath}/${isESLintRC ? "eslint.config.mjs" : fileBaseNameWithoutExt + ".mjs"}`; + const resultCjsPath = `${fixturePath}/${isESLintRC ? "eslint.config.cjs" : fileBaseNameWithoutExt + ".cjs"}`; + + it(`should migrate ${filePath}`, async () => { + // Note: Using execSync instead of exec due to race conditions + + // run the migration for mjs + execSync(`node src/migrate-config-cli.js ${filePath}`); + + // run the migration for cjs + execSync(`node src/migrate-config-cli.js ${filePath} --commonjs`); + + // check the mjs file + await assertFilesEqual(resultMjsPath, expectedMjsPath); + + // check the cjs file + await assertFilesEqual(resultCjsPath, expectedCjsPath); + }); + } +}); diff --git a/packages/migrate-config/tsconfig.json b/packages/migrate-config/tsconfig.json new file mode 100644 index 0000000..08bc479 --- /dev/null +++ b/packages/migrate-config/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": ["src/migrate-config-cli.js"], + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "allowJs": true, + "checkJs": true, + "outDir": "dist/esm", + "target": "ES2022", + "moduleResolution": "NodeNext", + "module": "NodeNext" + } +} diff --git a/release-please-config.json b/release-please-config.json index 8f4c3b0..000b9c7 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -32,6 +32,9 @@ "jsonpath": "$.version" } ] + }, + "packages/migrate-config": { + "release-type": "node" } } } diff --git a/scripts/build.js b/scripts/build.js index d21ae98..58f1f84 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -56,9 +56,7 @@ async function calculatePackageDependencies(packageDirs) { if (pkg.dependencies) { for (const dep of Object.keys(pkg.dependencies)) { - if (dep.startsWith("@eslint")) { - dependencies.add(dep); - } + dependencies.add(dep); } } @@ -84,9 +82,13 @@ function createBuildOrder(dependencies) { function visit(name) { if (!seen.has(name)) { seen.add(name); - const { dependencies: deps, dir } = dependencies.get(name); - deps.forEach(visit); - buildOrder.push(dir); + + // we only need to deal with dependencies in this monorepo + if (dependencies.has(name)) { + const { dependencies: deps, dir } = dependencies.get(name); + deps.forEach(visit); + buildOrder.push(dir); + } } }