diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /packages/turbo-codemod | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'packages/turbo-codemod')
109 files changed, 6567 insertions, 0 deletions
diff --git a/packages/turbo-codemod/LICENSE b/packages/turbo-codemod/LICENSE new file mode 100644 index 0000000..fa0086a --- /dev/null +++ b/packages/turbo-codemod/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0.
\ No newline at end of file diff --git a/packages/turbo-codemod/README.md b/packages/turbo-codemod/README.md new file mode 100644 index 0000000..5545561 --- /dev/null +++ b/packages/turbo-codemod/README.md @@ -0,0 +1,55 @@ +# Turborepo Codemods + +Turborepo provides Codemod transformations to help upgrade your Turborepo codebase. + +Codemods are transformations that run on your codebase programmatically. This allows for a large amount of changes to be applied without having to manually go through every file. + +## Commands + +### `migrate` + +Updates your Turborepo codebase to the specified version of Turborepo (defaults to the latest), running any required codemods, and installing the new version of Turborepo. + +``` +Usage: @turbo/codemod migrate|update [options] [path] + +Migrate a project to the latest version of Turborepo + +Arguments: + path Directory where the transforms should be applied + +Options: + --from <version> Specify the version to migrate from (default: current version) + --to <version> Specify the version to migrate to (default: latest) + --install Install new version of turbo after migration (default: true) + --force Bypass Git safety checks and forcibly run codemods (default: false) + --dry Dry run (no changes are made to files) (default: false) + --print Print transformed files to your terminal (default: false) + -h, --help display help for command +``` + +### `transform` (default) + +Runs a single codemod on your codebase. This is the default command, and can be omitted. + +``` +Usage: @turbo/codemod transform [options] [transform] [path] + @turbo/codemod [options] [transform] [path] + +Apply a single code transformation to a project + +Arguments: + transform The transformer to run + path Directory where the transforms should be applied + +Options: + --force Bypass Git safety checks and forcibly run codemods (default: false) + --list List all available transforms (default: false) + --dry Dry run (no changes are made to files) (default: false) + --print Print transformed files to your terminal (default: false) + -h, --help display help for command +``` + +## Developing + +To add a new transformer, run `pnpm add-transformer`, or [view the complete guide](./src/transforms/README.md). diff --git a/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/has-package-manager/package.json b/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/has-package-manager/package.json new file mode 100644 index 0000000..d6edac5 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/has-package-manager/package.json @@ -0,0 +1,7 @@ +{ + "name": "has-package-manager", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/no-package-manager/package.json b/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/no-package-manager/package.json new file mode 100644 index 0000000..2e28fe4 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/no-package-manager/package.json @@ -0,0 +1,6 @@ +{ + "name": "no-package-manager", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {} +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/wrong-package-manager/package.json b/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/wrong-package-manager/package.json new file mode 100644 index 0000000..f58aca2 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/wrong-package-manager/package.json @@ -0,0 +1,7 @@ +{ + "name": "has-package-manager", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "turbo@1.7.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/package.json new file mode 100644 index 0000000..c4606fa --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/package.json @@ -0,0 +1,28 @@ +{ + "name": "both-configs", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3", + "turbo": { + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "package-only": { + "cache": false, + "persistent": true + }, + "build": { + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + }, + "lint": { + "outputs": [] + }, + "dev": { + "cache": false + } + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/turbo.json new file mode 100644 index 0000000..e6eb652 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/turbo.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "turbo-only": { + "cache": false, + "persistent": true + }, + "build": { + "outputs": [".next/**", "!.next/cache/**"] + }, + "lint": { + "outputs": [] + }, + "dev": { + "cache": false + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-config/package.json b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-config/package.json new file mode 100644 index 0000000..b965b7d --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "no-turbo-json-config", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-file/a-random-file.txt b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-file/a-random-file.txt new file mode 100644 index 0000000..7488fec --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-file/a-random-file.txt @@ -0,0 +1 @@ +Nothing exists here diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-turbo-json-config/package.json b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-turbo-json-config/package.json new file mode 100644 index 0000000..7754c7d --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-turbo-json-config/package.json @@ -0,0 +1,24 @@ +{ + "name": "no-turbo-json-config", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3", + "turbo": { + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + }, + "lint": { + "outputs": [] + }, + "dev": { + "cache": false + } + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/package.json b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/package.json new file mode 100644 index 0000000..a48d0ec --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "both-configs", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/turbo.json new file mode 100644 index 0000000..e6eb652 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/turbo.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "turbo-only": { + "cache": false, + "persistent": true + }, + "build": { + "outputs": [".next/**", "!.next/cache/**"] + }, + "lint": { + "outputs": [] + }, + "dev": { + "cache": false + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-deps/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-deps/package.json new file mode 100644 index 0000000..b632eef --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-deps/package.json @@ -0,0 +1,4 @@ +{ + "name": "no-turbo", + "version": "0.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-package/README.md b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-package/README.md new file mode 100644 index 0000000..64355e7 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-package/README.md @@ -0,0 +1 @@ +Nothing here diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-turbo/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-turbo/package.json new file mode 100644 index 0000000..524df50 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-turbo/package.json @@ -0,0 +1,6 @@ +{ + "name": "no-turbo", + "version": "0.0.0", + "dependencies": {}, + "devDependencies": {} +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces-dev-install/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces-dev-install/package.json new file mode 100644 index 0000000..f5b2368 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces-dev-install/package.json @@ -0,0 +1,12 @@ +{ + "name": "normal-workspaces", + "version": "0.0.0", + "workspaces": [ + "apps/*", + "packages/*" + ], + "dependencies": {}, + "devDependencies": { + "turbo": "1.0.0" + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces/package.json new file mode 100644 index 0000000..6344a38 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces/package.json @@ -0,0 +1,12 @@ +{ + "name": "normal-workspaces", + "version": "0.0.0", + "workspaces": [ + "apps/*", + "packages/*" + ], + "dependencies": { + "turbo": "1.0.0" + }, + "devDependencies": {} +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/package.json new file mode 100644 index 0000000..5c12f28 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/package.json @@ -0,0 +1,8 @@ +{ + "name": "pnpm-workspaces", + "version": "0.0.0", + "dependencies": {}, + "devDependencies": { + "turbo": "1.0.0" + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/pnpm-workspace.yaml b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/package.json new file mode 100644 index 0000000..fedeb8d --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/package.json @@ -0,0 +1,8 @@ +{ + "name": "pnpm-workspaces", + "version": "0.0.0", + "dependencies": { + "turbo": "1.0.0" + }, + "devDependencies": {} +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/pnpm-workspace.yaml b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package-dev-install/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package-dev-install/package.json new file mode 100644 index 0000000..38bd995 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package-dev-install/package.json @@ -0,0 +1,8 @@ +{ + "name": "single-package-dev-install", + "version": "0.0.0", + "dependencies": {}, + "devDependencies": { + "turbo": "1.0.0" + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package/package.json b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package/package.json new file mode 100644 index 0000000..0fd3453 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package/package.json @@ -0,0 +1,8 @@ +{ + "name": "single-package", + "version": "0.0.0", + "dependencies": { + "turbo": "1.0.0" + }, + "devDependencies": {} +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/env-dependencies/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/env-dependencies/turbo.json new file mode 100644 index 0000000..bb3e248 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/env-dependencies/turbo.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["$NEXT_PUBLIC_API_KEY", "$STRIPE_API_KEY", ".env"], + "pipeline": { + "build": { + "outputs": [".next/**", "!.next/cache/**"], + "dependsOn": ["^build", "$PROD_API_KEY"] + }, + "lint": { + "outputs": [], + "dependsOn": ["$IS_CI"] + }, + "test": { + "outputs": [], + "dependsOn": ["$IS_CI", "test"] + }, + "dev": { + "cache": false + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/migrated-env-dependencies/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/migrated-env-dependencies/turbo.json new file mode 100644 index 0000000..9217af6 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/migrated-env-dependencies/turbo.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": [], + "globalEnv": ["NEXT_PUBLIC_API_KEY", "STRIPE_API_KEY"], + "pipeline": { + "build": { + "dependsOn": ["^build"], + "env": ["PROD_API_KEY"], + "outputs": [".next/**", "!.next/cache/**"] + }, + "dev": { + "cache": false + }, + "lint": { + "dependsOn": [], + "env": ["IS_CI"], + "outputs": [] + }, + "test": { + "dependsOn": ["test"], + "env": ["IS_CI"], + "outputs": [] + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/no-turbo-json/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/no-turbo-json/package.json new file mode 100644 index 0000000..83443be --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/no-turbo-json/package.json @@ -0,0 +1,7 @@ +{ + "name": "no-turbo-json", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/package.json new file mode 100644 index 0000000..6774d3c --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/package.json @@ -0,0 +1,20 @@ +{ + "name": "migrate-env-var-dependencies-old-config", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3", + "turbo": { + "pipeline": { + "build-one": { + "outputs": [ + "foo" + ] + }, + "build-two": { + "outputs": [] + }, + "build-three": {} + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/turbo.json new file mode 100644 index 0000000..b0f6150 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build-one": { + "outputs": ["foo"] + }, + "build-two": { + "outputs": [] + }, + "build-three": {} + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/index.js b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/index.js new file mode 100644 index 0000000..4de53f5 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/index.js @@ -0,0 +1,6 @@ +export default function docs() { + if (process.env.ENV_1 === undefined) { + return "does not exist"; + } + return "exists"; +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/package.json new file mode 100644 index 0000000..82f9a44 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/package.json @@ -0,0 +1,4 @@ +{ + "name": "docs", + "version": "1.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/turbo.json new file mode 100644 index 0000000..a3713ef --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + "env": ["ENV_3"] + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/index.js b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/index.js new file mode 100644 index 0000000..bfd3ab8 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/index.js @@ -0,0 +1,6 @@ +export default function web() { + if (!process.env.ENV_2) { + return "bar"; + } + return "foo"; +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/package.json new file mode 100644 index 0000000..d8a83ed --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/package.json @@ -0,0 +1,4 @@ +{ + "name": "web", + "version": "1.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/turbo.json new file mode 100644 index 0000000..dd69c31 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + // old + "dependsOn": ["build", "$ENV_2"], + // new + "env": ["ENV_1"] + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/package.json new file mode 100644 index 0000000..c6616a6 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "turbo run build" + }, + "devDependencies": { + "turbo": "latest" + }, + "packageManager": "yarn@1.22.19" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/index.js b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/index.js new file mode 100644 index 0000000..dee5e80 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/index.js @@ -0,0 +1,6 @@ +export default function foo() { + if (!process.env.IS_SERVER) { + return "bar"; + } + return "foo"; +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/package.json new file mode 100644 index 0000000..7cb7cf1 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/package.json @@ -0,0 +1,4 @@ +{ + "name": "ui", + "version": "1.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/turbo.json new file mode 100644 index 0000000..6ce7b30 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + "dependsOn": ["$IS_SERVER"] + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/turbo.json new file mode 100644 index 0000000..718e461 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/turbo.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["$NEXT_PUBLIC_API_KEY", "$STRIPE_API_KEY", ".env"], + "pipeline": { + "build": { + "outputs": [".next/**", "!.next/cache/**"], + "dependsOn": ["^build", "$PROD_API_KEY"] + }, + "lint": { + "outputs": [], + "dependsOn": ["$IS_TEST"] + }, + "test": { + "outputs": [], + "dependsOn": ["$IS_CI", "test"] + }, + "dev": { + "cache": false + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate/no-repo/README.md b/packages/turbo-codemod/__tests__/__fixtures__/migrate/no-repo/README.md new file mode 100644 index 0000000..64355e7 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate/no-repo/README.md @@ -0,0 +1 @@ +Nothing here diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate/old-turbo/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate/old-turbo/package.json new file mode 100644 index 0000000..62959b8 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate/old-turbo/package.json @@ -0,0 +1,26 @@ +{ + "name": "no-turbo-json", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": { + "turbo": "1.0.0" + }, + "turbo": { + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + }, + "lint": { + "outputs": [] + }, + "test": {}, + "dev": { + "cache": false + } + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/package.json new file mode 100644 index 0000000..6b50aac --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/package.json @@ -0,0 +1,7 @@ +{ + "name": "invalid-outputs", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/turbo.json new file mode 100644 index 0000000..33c2b93 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/turbo.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build-one": { + "outputs": ["foo"] + }, + "build-two": { + "outputs": [] + }, + "build-three": {}, + "garbage-in-numeric-0": { + "outputs": 0 + }, + "garbage-in-numeric": { + "outputs": 42 + }, + "garbage-in-string": { + "outputs": "string" + }, + "garbage-in-empty-string": { + "outputs": "" + }, + "garbage-in-null": { + "outputs": null + }, + "garbage-in-false": { + "outputs": false + }, + "garbage-in-true": { + "outputs": true + }, + "garbage-in-object": { + "outputs": {} + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/package.json new file mode 100644 index 0000000..4e17dc1 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/package.json @@ -0,0 +1,7 @@ +{ + "name": "no-outputs", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/turbo.json new file mode 100644 index 0000000..f5d57fc --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/turbo.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build-one": { + "dependsOn": ["build-two"] + }, + "build-two": { + "cache": false + }, + "build-three": { + "persistent": true + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/package.json new file mode 100644 index 0000000..6e20fc8 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/package.json @@ -0,0 +1,7 @@ +{ + "name": "no-pipeline", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/turbo.json new file mode 100644 index 0000000..0e2d6fd --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/turbo.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["$NEXT_PUBLIC_API_KEY", "$STRIPE_API_KEY", ".env"], + "pipeline": {} +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-turbo-json/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-turbo-json/package.json new file mode 100644 index 0000000..cd98334 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-turbo-json/package.json @@ -0,0 +1,7 @@ +{ + "name": "set-default-outputs-no-turbo-json", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/package.json new file mode 100644 index 0000000..4c816c2 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/package.json @@ -0,0 +1,20 @@ +{ + "name": "set-default-outputs-old-config", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3", + "turbo": { + "pipeline": { + "build-one": { + "outputs": [ + "foo" + ] + }, + "build-two": { + "outputs": [] + }, + "build-three": {} + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/turbo.json new file mode 100644 index 0000000..b0f6150 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build-one": { + "outputs": ["foo"] + }, + "build-two": { + "outputs": [] + }, + "build-three": {} + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/package.json new file mode 100644 index 0000000..e4220ba --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/package.json @@ -0,0 +1,7 @@ +{ + "name": "old-outputs", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": {}, + "packageManager": "npm@1.2.3" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/turbo.json new file mode 100644 index 0000000..b0f6150 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build-one": { + "outputs": ["foo"] + }, + "build-two": { + "outputs": [] + }, + "build-three": {} + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/index.js b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/index.js new file mode 100644 index 0000000..4de53f5 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/index.js @@ -0,0 +1,6 @@ +export default function docs() { + if (process.env.ENV_1 === undefined) { + return "does not exist"; + } + return "exists"; +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/package.json new file mode 100644 index 0000000..82f9a44 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/package.json @@ -0,0 +1,4 @@ +{ + "name": "docs", + "version": "1.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/turbo.json new file mode 100644 index 0000000..e60cdb7 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/turbo.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": {} + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/index.js b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/index.js new file mode 100644 index 0000000..bfd3ab8 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/index.js @@ -0,0 +1,6 @@ +export default function web() { + if (!process.env.ENV_2) { + return "bar"; + } + return "foo"; +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/package.json new file mode 100644 index 0000000..d8a83ed --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/package.json @@ -0,0 +1,4 @@ +{ + "name": "web", + "version": "1.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/turbo.json new file mode 100644 index 0000000..b239cbf --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/turbo.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + // old + "outputs": [] + } + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/package.json new file mode 100644 index 0000000..c6616a6 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "turbo run build" + }, + "devDependencies": { + "turbo": "latest" + }, + "packageManager": "yarn@1.22.19" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/index.js b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/index.js new file mode 100644 index 0000000..dee5e80 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/index.js @@ -0,0 +1,6 @@ +export default function foo() { + if (!process.env.IS_SERVER) { + return "bar"; + } + return "foo"; +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/package.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/package.json new file mode 100644 index 0000000..7cb7cf1 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/package.json @@ -0,0 +1,4 @@ +{ + "name": "ui", + "version": "1.0.0" +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/turbo.json new file mode 100644 index 0000000..fe51119 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/turbo.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build-three": {} + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/turbo.json new file mode 100644 index 0000000..b0f6150 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build-one": { + "outputs": ["foo"] + }, + "build-two": { + "outputs": [] + }, + "build-three": {} + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/transform/basic/package.json b/packages/turbo-codemod/__tests__/__fixtures__/transform/basic/package.json new file mode 100644 index 0000000..651edb6 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/transform/basic/package.json @@ -0,0 +1,8 @@ +{ + "name": "transform-basic", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": { + "turbo": "1.0.0" + } +} diff --git a/packages/turbo-codemod/__tests__/add-package-manager.test.ts b/packages/turbo-codemod/__tests__/add-package-manager.test.ts new file mode 100644 index 0000000..5bde7e0 --- /dev/null +++ b/packages/turbo-codemod/__tests__/add-package-manager.test.ts @@ -0,0 +1,504 @@ +import { transformer } from "../src/transforms/add-package-manager"; +import { setupTestFixtures } from "@turbo/test-utils"; +import fs from "fs-extra"; +import * as getPackageManager from "../src/utils/getPackageManager"; +import * as getPackageManagerVersion from "../src/utils/getPackageManagerVersion"; + +describe("add-package-manager", () => { + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "add-package-manager", + }); + test("no package manager - basic", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should now exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + `${packageManager}@${packageManagerVersion}` + ); + // result should be correct + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + mockGetPackageManager.mockRestore(); + }); + + test("no package manager - repeat run", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should now exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + `${packageManager}@${packageManagerVersion}` + ); + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + } + `); + + // run the transformer again to ensure nothing changes on a second run + const repeatResult = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + expect(repeatResult.fatalError).toBeUndefined(); + expect(repeatResult.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + mockGetPackageManager.mockRestore(); + }); + + test("no package manager - dry", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const packageManager = "npm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // result should be correct + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "skipped", + "additions": 1, + "deletions": 0, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + }); + + test("no package manager - print", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const packageManager = "yarn"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: true }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + // package manager should now exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + `${packageManager}@${packageManagerVersion}` + ); + // result should be correct + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + }); + + test("no package manager - dry & print", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const packageManager = "npm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: true }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // result should be correct + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "skipped", + "additions": 1, + "deletions": 0, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + mockGetPackageManager.mockRestore(); + }); + + test("package manager already exists", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "has-package-manager" }); + const packageManager = "npm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + `${packageManager}@${packageManagerVersion}` + ); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should still exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + `${packageManager}@${packageManagerVersion}` + ); + // result should be correct + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + mockGetPackageManager.mockRestore(); + }); + + test("package manager exists but is wrong", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "wrong-package-manager" }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + // package manager should exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + "turbo@1.7.0" + ); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should still exist + expect(JSON.parse(read("package.json") || "{}").packageManager).toBe( + `${packageManager}@${packageManagerVersion}` + ); + // result should be correct + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 1, + "deletions": 1, + }, + } + `); + + mockGetPackageManagerVersion.mockRestore(); + mockGetPackageManager.mockRestore(); + }); + + test("errors when unable to determine workspace manager", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(undefined); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledTimes(1); + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + + // result should be correct + // result should be correct + expect(result.fatalError?.message).toMatch( + /Unable to determine package manager for .*?/ + ); + + mockGetPackageManager.mockRestore(); + }); + + test("errors when unable to determine package manager", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockImplementation(() => { + throw new Error("package manager not supported"); + }); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManagerVersion).toHaveBeenCalledTimes(1); + + // result should be correct + expect(result.fatalError?.message).toMatch( + /Unable to determine package manager version for .*?/ + ); + + mockGetPackageManagerVersion.mockRestore(); + }); + + test("errors when unable to write json", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-manager" }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // mock out workspace and version detection so we're not dependent on our actual repo + const mockGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + const mockWriteJsonSync = jest + .spyOn(fs, "writeJsonSync") + .mockImplementation(() => { + throw new Error("could not write file"); + }); + + // package manager should not exist + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(mockGetPackageManager).toHaveBeenCalledWith({ directory: root }); + expect(mockGetPackageManagerVersion).toHaveBeenCalledWith( + packageManager, + root + ); + + // package manager should still not exist (we couldn't write it) + expect( + JSON.parse(read("package.json") || "{}").packageManager + ).toBeUndefined(); + + // result should be correct + expect(result.fatalError?.message).toMatch( + "Encountered an error while transforming files" + ); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "error", + "additions": 1, + "deletions": 0, + "error": [Error: could not write file], + }, + } + `); + + mockWriteJsonSync.mockRestore(); + mockGetPackageManagerVersion.mockRestore(); + mockGetPackageManager.mockRestore(); + }); +}); diff --git a/packages/turbo-codemod/__tests__/create-turbo-config.test.ts b/packages/turbo-codemod/__tests__/create-turbo-config.test.ts new file mode 100644 index 0000000..8938c78 --- /dev/null +++ b/packages/turbo-codemod/__tests__/create-turbo-config.test.ts @@ -0,0 +1,416 @@ +import { transformer } from "../src/transforms/create-turbo-config"; +import { setupTestFixtures } from "@turbo/test-utils"; +import fs from "fs-extra"; + +describe("create-turbo-config", () => { + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "create-turbo-config", + }); + + test("package.json config exists but no turbo.json config - basic", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-turbo-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // turbo.json should now exist (and match the package.json config) + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboConfig); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 0, + "deletions": 1, + }, + "turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + } + `); + }); + + test("package.json config exists but no turbo.json config - repeat run", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-turbo-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // turbo.json should now exist (and match the package.json config) + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboConfig); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 0, + "deletions": 1, + }, + "turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + } + `); + + // run the transformer + const repeatResult = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + // result should be correct + expect(repeatResult.fatalError).toBeUndefined(); + expect(repeatResult.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + test("package.json config exists but no turbo.json config - dry", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-turbo-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: false }, + }); + + // turbo.json still not exist (dry run) + expect(read("turbo.json")).toBeUndefined(); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "skipped", + "additions": 0, + "deletions": 1, + }, + "turbo.json": Object { + "action": "skipped", + "additions": 1, + "deletions": 0, + }, + } + `); + }); + + test("package.json config exists but no turbo.json config - print", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-turbo-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: true }, + }); + + // turbo.json should now exist (and match the package.json config) + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboConfig); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "modified", + "additions": 0, + "deletions": 1, + }, + "turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + } + `); + }); + + test("package.json config exists but no turbo.json config - dry & print", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-turbo-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: true }, + }); + + // turbo.json still not exist (dry run) + expect(read("turbo.json")).toBeUndefined(); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "skipped", + "additions": 0, + "deletions": 1, + }, + "turbo.json": Object { + "action": "skipped", + "additions": 1, + "deletions": 0, + }, + } + `); + }); + + test("no package.json config or turbo.json file exists", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const packageJsonConfig = JSON.parse(read("package.json") || "{}"); + const turboConfig = packageJsonConfig.turbo; + expect(turboConfig).toBeUndefined(); + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // turbo.json should still not exist + expect(read("turbo.json")).toBeUndefined(); + + // make sure we didn't change the package.json + expect(JSON.parse(read("package.json") || "{}")).toEqual(packageJsonConfig); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + test("no package.json file exists", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-package-json-file" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // turbo.json should still not exist + expect(read("turbo.json")).toBeUndefined(); + + // result should be correct + expect(result.fatalError?.message).toMatch( + /No package\.json found at .*?\. Is the path correct\?/ + ); + }); + + test("turbo.json file exists and no package.json config exists", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "turbo-json-config" }); + + // turbo.json should exist + expect(read("turbo.json")).toBeDefined(); + + // no config should exist in package.json + const packageJsonConfig = JSON.parse(read("package.json") || "{}"); + const turboConfig = packageJsonConfig.turbo; + expect(turboConfig).toBeUndefined(); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // turbo.json should still exist + expect(read("turbo.json")).toBeDefined(); + + // make sure we didn't change the package.json + expect(JSON.parse(read("package.json") || "{}")).toEqual(packageJsonConfig); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + test("turbo.json file exists and package.json config exists", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "both-configs" }); + + // turbo.json should exist + const turboJsonConfig = JSON.parse(read("turbo.json") || "{}"); + expect(turboJsonConfig.pipeline).toBeDefined(); + + // no config should exist in package.json + const packageJsonConfig = JSON.parse(read("package.json") || "{}"); + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // make sure we didn't change the package.json + expect(JSON.parse(read("package.json") || "{}")).toEqual(packageJsonConfig); + + // make sure we didn't change the turbo.json + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboJsonConfig); + + // result should be correct + expect(result.fatalError?.message).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + test("errors when unable to write json", () => { + // load the fixture for the test + const { root, read } = useFixture({ fixture: "no-turbo-json-config" }); + + // turbo.json should not exist + expect(read("turbo.json")).toBeUndefined(); + + // get config from package.json for comparison later + const turboConfig = JSON.parse(read("package.json") || "{}").turbo; + expect(turboConfig).toBeDefined(); + + const mockWriteJsonSync = jest + .spyOn(fs, "writeJsonSync") + .mockImplementation(() => { + throw new Error("could not write file"); + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // turbo.json should still not exist (error writing) + expect(read("turbo.json")).toBeUndefined(); + + // result should be correct + expect(result.fatalError).toBeDefined(); + expect(result.fatalError?.message).toMatch( + "Encountered an error while transforming files" + ); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "package.json": Object { + "action": "error", + "additions": 0, + "deletions": 1, + "error": [Error: could not write file], + }, + "turbo.json": Object { + "action": "error", + "additions": 1, + "deletions": 0, + "error": [Error: could not write file], + }, + } + `); + + mockWriteJsonSync.mockRestore(); + }); +}); diff --git a/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts b/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts new file mode 100644 index 0000000..1015589 --- /dev/null +++ b/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts @@ -0,0 +1,576 @@ +import { setupTestFixtures } from "@turbo/test-utils"; +import getTurboUpgradeCommand from "../src/commands/migrate/steps/getTurboUpgradeCommand"; +import * as utils from "../src/commands/migrate/utils"; +import * as getPackageManager from "../src/utils/getPackageManager"; +import * as getPackageManagerVersion from "../src/utils/getPackageManagerVersion"; + +const LOCAL_INSTALL_COMMANDS = [ + // npm - workspaces + [ + "latest", + "npm", + "7.0.0", + "normal-workspaces-dev-install", + "npm install turbo@latest --save-dev", + ], + [ + "1.6.3", + "npm", + "7.0.0", + "normal-workspaces-dev-install", + "npm install turbo@1.6.3 --save-dev", + ], + [ + "canary", + "npm", + "7.0.0", + "normal-workspaces-dev-install", + "npm install turbo@canary --save-dev", + ], + ["latest", "npm", "7.0.0", "normal-workspaces", "npm install turbo@latest"], + // npm - single package + [ + "latest", + "npm", + "7.0.0", + "single-package-dev-install", + "npm install turbo@latest --save-dev", + ], + ["latest", "npm", "7.0.0", "single-package", "npm install turbo@latest"], + // pnpm - workspaces + [ + "latest", + "pnpm", + "7.0.0", + "pnpm-workspaces-dev-install", + "pnpm install turbo@latest --save-dev -w", + ], + [ + "1.6.3", + "pnpm", + "7.0.0", + "pnpm-workspaces-dev-install", + "pnpm install turbo@1.6.3 --save-dev -w", + ], + [ + "canary", + "pnpm", + "7.0.0", + "pnpm-workspaces-dev-install", + "pnpm install turbo@canary --save-dev -w", + ], + [ + "latest", + "pnpm", + "7.0.0", + "pnpm-workspaces", + "pnpm install turbo@latest -w", + ], + // pnpm - single package + [ + "latest", + "pnpm", + "7.0.0", + "single-package-dev-install", + "pnpm install turbo@latest --save-dev", + ], + ["latest", "pnpm", "7.0.0", "single-package", "pnpm install turbo@latest"], + // yarn 1.x - workspaces + [ + "latest", + "yarn", + "1.22.19", + "normal-workspaces-dev-install", + "yarn add turbo@latest --dev -W", + ], + [ + "latest", + "yarn", + "1.22.19", + "normal-workspaces", + "yarn add turbo@latest -W", + ], + [ + "1.6.3", + "yarn", + "1.22.19", + "normal-workspaces-dev-install", + "yarn add turbo@1.6.3 --dev -W", + ], + [ + "canary", + "yarn", + "1.22.19", + "normal-workspaces-dev-install", + "yarn add turbo@canary --dev -W", + ], + // yarn 1.x - single package + [ + "latest", + "yarn", + "1.22.19", + "single-package-dev-install", + "yarn add turbo@latest --dev", + ], + ["latest", "yarn", "1.22.19", "single-package", "yarn add turbo@latest"], + // yarn 2.x - workspaces + [ + "latest", + "yarn", + "2.3.4", + "normal-workspaces-dev-install", + "yarn add turbo@latest --dev", + ], + ["latest", "yarn", "2.3.4", "normal-workspaces", "yarn add turbo@latest"], + [ + "1.6.3", + "yarn", + "2.3.4", + "normal-workspaces-dev-install", + "yarn add turbo@1.6.3 --dev", + ], + [ + "canary", + "yarn", + "2.3.4", + "normal-workspaces-dev-install", + "yarn add turbo@canary --dev", + ], + // yarn 2.x - single package + [ + "latest", + "yarn", + "2.3.4", + "single-package-dev-install", + "yarn add turbo@latest --dev", + ], + ["latest", "yarn", "2.3.4", "single-package", "yarn add turbo@latest"], + // yarn 3.x - workspaces + [ + "latest", + "yarn", + "3.3.4", + "normal-workspaces-dev-install", + "yarn add turbo@latest --dev", + ], + ["latest", "yarn", "3.3.4", "normal-workspaces", "yarn add turbo@latest"], + [ + "1.6.3", + "yarn", + "3.3.4", + "normal-workspaces-dev-install", + "yarn add turbo@1.6.3 --dev", + ], + [ + "canary", + "yarn", + "3.3.4", + "normal-workspaces-dev-install", + "yarn add turbo@canary --dev", + ], + // yarn 3.x - single package + [ + "latest", + "yarn", + "3.3.4", + "single-package-dev-install", + "yarn add turbo@latest --dev", + ], + ["latest", "yarn", "3.3.4", "single-package", "yarn add turbo@latest"], +]; + +const GLOBAL_INSTALL_COMMANDS = [ + // npm + [ + "latest", + "npm", + "7.0.0", + "normal-workspaces-dev-install", + "npm install turbo@latest --global", + ], + [ + "1.6.3", + "npm", + "7.0.0", + "normal-workspaces-dev-install", + "npm install turbo@1.6.3 --global", + ], + [ + "latest", + "npm", + "7.0.0", + "normal-workspaces", + "npm install turbo@latest --global", + ], + [ + "latest", + "npm", + "7.0.0", + "single-package", + "npm install turbo@latest --global", + ], + [ + "latest", + "npm", + "7.0.0", + "single-package-dev-install", + "npm install turbo@latest --global", + ], + // pnpm + [ + "latest", + "pnpm", + "7.0.0", + "pnpm-workspaces-dev-install", + "pnpm install turbo@latest --global", + ], + [ + "1.6.3", + "pnpm", + "7.0.0", + "pnpm-workspaces-dev-install", + "pnpm install turbo@1.6.3 --global", + ], + [ + "latest", + "pnpm", + "7.0.0", + "pnpm-workspaces", + "pnpm install turbo@latest --global", + ], + [ + "latest", + "pnpm", + "7.0.0", + "single-package", + "pnpm install turbo@latest --global", + ], + [ + "latest", + "pnpm", + "7.0.0", + "single-package-dev-install", + "pnpm install turbo@latest --global", + ], + // yarn 1.x + [ + "latest", + "yarn", + "1.22.19", + "normal-workspaces-dev-install", + "yarn global add turbo@latest", + ], + [ + "latest", + "yarn", + "1.22.19", + "normal-workspaces", + "yarn global add turbo@latest", + ], + [ + "1.6.3", + "yarn", + "1.22.19", + "normal-workspaces-dev-install", + "yarn global add turbo@1.6.3", + ], + [ + "latest", + "yarn", + "1.22.19", + "single-package", + "yarn global add turbo@latest", + ], + [ + "latest", + "yarn", + "1.22.19", + "single-package-dev-install", + "yarn global add turbo@latest", + ], + // yarn 2.x + [ + "latest", + "yarn", + "2.3.4", + "normal-workspaces-dev-install", + "yarn global add turbo@latest", + ], + [ + "latest", + "yarn", + "2.3.4", + "normal-workspaces", + "yarn global add turbo@latest", + ], + [ + "1.6.3", + "yarn", + "2.3.4", + "normal-workspaces-dev-install", + "yarn global add turbo@1.6.3", + ], + ["latest", "yarn", "2.3.4", "single-package", "yarn global add turbo@latest"], + [ + "latest", + "yarn", + "2.3.4", + "single-package-dev-install", + "yarn global add turbo@latest", + ], + // yarn 3.x + [ + "latest", + "yarn", + "3.3.3", + "normal-workspaces-dev-install", + "yarn global add turbo@latest", + ], + [ + "latest", + "yarn", + "3.3.3", + "normal-workspaces", + "yarn global add turbo@latest", + ], + [ + "1.6.3", + "yarn", + "3.3.3", + "normal-workspaces-dev-install", + "yarn global add turbo@1.6.3", + ], + ["latest", "yarn", "3.3.4", "single-package", "yarn global add turbo@latest"], + [ + "latest", + "yarn", + "3.3.4", + "single-package-dev-install", + "yarn global add turbo@latest", + ], +]; + +describe("get-turbo-upgrade-command", () => { + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "get-turbo-upgrade-command", + }); + + test.each(LOCAL_INSTALL_COMMANDS)( + "returns correct upgrade command for local install of turbo@%s using %s@%s (fixture: %s)", + ( + turboVersion, + packageManager, + packageManagerVersion, + fixture, + expectedUpgradeCommand + ) => { + const { root } = useFixture({ + fixture, + }); + + const mockedExec = jest + .spyOn(utils, "exec") + .mockImplementation((command: string) => { + // fail the check for the turbo, and package manager bins to force local + if (command.includes("bin")) { + return undefined; + } + }); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager as getPackageManager.PackageManager); + + // get the command + const upgradeCommand = getTurboUpgradeCommand({ + directory: root, + to: turboVersion === "latest" ? undefined : turboVersion, + }); + + expect(upgradeCommand).toEqual(expectedUpgradeCommand); + + mockedExec.mockRestore(); + mockedGetPackageManager.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + } + ); + + test.each(GLOBAL_INSTALL_COMMANDS)( + "returns correct upgrade command for global install of turbo@%s using %s@%s (fixture: %s)", + ( + turboVersion, + packageManager, + packageManagerVersion, + fixture, + expectedUpgradeCommand + ) => { + const { root } = useFixture({ + fixture, + }); + + const mockedExec = jest + .spyOn(utils, "exec") + .mockImplementation((command: string) => { + if (command === "turbo bin") { + return `/global/${packageManager}/bin/turbo`; + } + if (command.includes(packageManager)) { + return `/global/${packageManager}/bin`; + } + }); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager as getPackageManager.PackageManager); + + // get the command + const upgradeCommand = getTurboUpgradeCommand({ + directory: root, + to: turboVersion === "latest" ? undefined : turboVersion, + }); + + expect(upgradeCommand).toEqual(expectedUpgradeCommand); + + mockedExec.mockRestore(); + mockedGetPackageManager.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + } + ); + + test("fails gracefully if no package.json exists", () => { + const { root } = useFixture({ + fixture: "no-package", + }); + + const mockedExec = jest + .spyOn(utils, "exec") + .mockImplementation((command: string) => { + // fail the check for the turbo, and package manager bins to force local + if (command.includes("bin")) { + return undefined; + } + }); + + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue("8.0.0"); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue("pnpm" as getPackageManager.PackageManager); + + // get the command + const upgradeCommand = getTurboUpgradeCommand({ + directory: root, + }); + + expect(upgradeCommand).toEqual(undefined); + + mockedExec.mockRestore(); + mockedGetPackageManager.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + }); + + test("fails gracefully if turbo cannot be found in package.json", () => { + const { root } = useFixture({ + fixture: "no-turbo", + }); + + const mockedExec = jest + .spyOn(utils, "exec") + .mockImplementation((command: string) => { + // fail the check for the turbo, and package manager bins to force local + if (command.includes("bin")) { + return undefined; + } + }); + + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue("8.0.0"); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue("pnpm" as getPackageManager.PackageManager); + + // get the command + const upgradeCommand = getTurboUpgradeCommand({ + directory: root, + }); + + expect(upgradeCommand).toEqual(undefined); + + mockedExec.mockRestore(); + mockedGetPackageManager.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + }); + + test("fails gracefully if package.json has no deps or devDeps", () => { + const { root } = useFixture({ + fixture: "no-deps", + }); + + const mockedExec = jest + .spyOn(utils, "exec") + .mockImplementation((command: string) => { + // fail the check for the turbo, and package manager bins to force local + if (command.includes("bin")) { + return undefined; + } + }); + + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue("8.0.0"); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue("pnpm" as getPackageManager.PackageManager); + + // get the command + const upgradeCommand = getTurboUpgradeCommand({ + directory: root, + }); + + expect(upgradeCommand).toEqual(undefined); + + mockedExec.mockRestore(); + mockedGetPackageManager.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + }); + + test("fails gracefully if can't find packageManager", () => { + const { root } = useFixture({ + fixture: "no-deps", + }); + + const mockedExec = jest + .spyOn(utils, "exec") + .mockImplementation((command: string) => { + // fail the check for the turbo, and package manager bins to force local + if (command.includes("bin")) { + return undefined; + } + }); + + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue("8.0.0"); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue("pnpm" as getPackageManager.PackageManager); + + // get the command + const upgradeCommand = getTurboUpgradeCommand({ + directory: root, + }); + + expect(upgradeCommand).toEqual(undefined); + + mockedExec.mockRestore(); + mockedGetPackageManager.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + }); +}); diff --git a/packages/turbo-codemod/__tests__/migrate-env-var-dependencies.test.ts b/packages/turbo-codemod/__tests__/migrate-env-var-dependencies.test.ts new file mode 100644 index 0000000..fbc5d8d --- /dev/null +++ b/packages/turbo-codemod/__tests__/migrate-env-var-dependencies.test.ts @@ -0,0 +1,758 @@ +import merge from "deepmerge"; +import { + hasLegacyEnvVarDependencies, + migratePipeline, + migrateConfig, + transformer, +} from "../src/transforms/migrate-env-var-dependencies"; +import { setupTestFixtures } from "@turbo/test-utils"; +import type { Schema } from "@turbo/types"; + +const getTestTurboConfig = (override: Schema = { pipeline: {} }): Schema => { + const config = { + $schema: "./docs/public/schema.json", + globalDependencies: ["$GLOBAL_ENV_KEY"], + pipeline: { + test: { + outputs: ["coverage/**/*"], + dependsOn: ["^build"], + }, + lint: { + outputs: [], + }, + dev: { + cache: false, + }, + build: { + outputs: ["dist/**/*", ".next/**/*", "!.next/cache/**"], + dependsOn: ["^build", "$TASK_ENV_KEY", "$ANOTHER_ENV_KEY"], + }, + }, + }; + + return merge(config, override, { + arrayMerge: (_, sourceArray) => sourceArray, + }); +}; + +describe("migrate-env-var-dependencies", () => { + describe("hasLegacyEnvVarDependencies - utility", () => { + it("finds env keys in legacy turbo.json - has keys", async () => { + const config = getTestTurboConfig(); + const { hasKeys, envVars } = hasLegacyEnvVarDependencies(config); + expect(hasKeys).toEqual(true); + expect(envVars).toMatchInlineSnapshot(` + Array [ + "$GLOBAL_ENV_KEY", + "$TASK_ENV_KEY", + "$ANOTHER_ENV_KEY", + ] + `); + }); + + it("finds env keys in legacy turbo.json - multiple pipeline keys", async () => { + const config = getTestTurboConfig({ + pipeline: { test: { dependsOn: ["$MY_ENV"] } }, + }); + const { hasKeys, envVars } = hasLegacyEnvVarDependencies(config); + expect(hasKeys).toEqual(true); + expect(envVars).toMatchInlineSnapshot(` + Array [ + "$GLOBAL_ENV_KEY", + "$MY_ENV", + "$TASK_ENV_KEY", + "$ANOTHER_ENV_KEY", + ] + `); + }); + + it("finds env keys in legacy turbo.json - no keys", async () => { + // override to exclude keys + const config = getTestTurboConfig({ + globalDependencies: [], + pipeline: { build: { dependsOn: [] } }, + }); + const { hasKeys, envVars } = hasLegacyEnvVarDependencies(config); + expect(hasKeys).toEqual(false); + expect(envVars).toMatchInlineSnapshot(`Array []`); + }); + + it("finds env keys in turbo.json - no global", async () => { + const { hasKeys, envVars } = hasLegacyEnvVarDependencies({ + pipeline: { build: { dependsOn: ["$cool"] } }, + }); + expect(hasKeys).toEqual(true); + expect(envVars).toMatchInlineSnapshot(` + Array [ + "$cool", + ] + `); + }); + }); + + describe("migratePipeline - utility", () => { + it("migrates pipeline with env var dependencies", async () => { + const config = getTestTurboConfig(); + const { build } = config.pipeline; + const pipeline = migratePipeline(build); + expect(pipeline).toHaveProperty("env"); + expect(pipeline?.env).toMatchInlineSnapshot(` + Array [ + "TASK_ENV_KEY", + "ANOTHER_ENV_KEY", + ] + `); + expect(pipeline?.dependsOn).toMatchInlineSnapshot(` + Array [ + "^build", + ] + `); + }); + + it("migrates pipeline with no env var dependencies", async () => { + const config = getTestTurboConfig(); + const { test } = config.pipeline; + const pipeline = migratePipeline(test); + expect(pipeline.env).toBeUndefined(); + expect(pipeline?.dependsOn).toMatchInlineSnapshot(` + Array [ + "^build", + ] + `); + }); + + it("migrates pipeline with existing env key", async () => { + const config = getTestTurboConfig({ + pipeline: { test: { env: ["$MY_ENV"], dependsOn: ["^build"] } }, + }); + const { test } = config.pipeline; + const pipeline = migratePipeline(test); + expect(pipeline).toHaveProperty("env"); + expect(pipeline?.env).toMatchInlineSnapshot(` + Array [ + "$MY_ENV", + ] + `); + expect(pipeline?.dependsOn).toMatchInlineSnapshot(` + Array [ + "^build", + ] + `); + }); + + it("migrates pipeline with incomplete env key", async () => { + const config = getTestTurboConfig({ + pipeline: { + test: { env: ["$MY_ENV"], dependsOn: ["^build", "$SUPER_COOL"] }, + }, + }); + const { test } = config.pipeline; + const pipeline = migratePipeline(test); + expect(pipeline).toHaveProperty("env"); + expect(pipeline?.env).toMatchInlineSnapshot(` + Array [ + "$MY_ENV", + "SUPER_COOL", + ] + `); + expect(pipeline?.dependsOn).toMatchInlineSnapshot(` + Array [ + "^build", + ] + `); + }); + + it("migrates pipeline with duplicate env keys", async () => { + const config = getTestTurboConfig({ + pipeline: { + test: { env: ["$MY_ENV"], dependsOn: ["^build", "$MY_ENV"] }, + }, + }); + const { test } = config.pipeline; + const pipeline = migratePipeline(test); + expect(pipeline).toHaveProperty("env"); + expect(pipeline?.env).toMatchInlineSnapshot(` + Array [ + "$MY_ENV", + "MY_ENV", + ] + `); + expect(pipeline?.dependsOn).toMatchInlineSnapshot(` + Array [ + "^build", + ] + `); + }); + }); + + describe("migrateConfig - utility", () => { + it("migrates config with env var dependencies", async () => { + const config = getTestTurboConfig(); + const pipeline = migrateConfig(config); + expect(pipeline).toMatchInlineSnapshot(` + Object { + "$schema": "./docs/public/schema.json", + "globalEnv": Array [ + "GLOBAL_ENV_KEY", + ], + "pipeline": Object { + "build": Object { + "dependsOn": Array [ + "^build", + ], + "env": Array [ + "TASK_ENV_KEY", + "ANOTHER_ENV_KEY", + ], + "outputs": Array [ + "dist/**/*", + ".next/**/*", + "!.next/cache/**", + ], + }, + "dev": Object { + "cache": false, + }, + "lint": Object { + "outputs": Array [], + }, + "test": Object { + "dependsOn": Array [ + "^build", + ], + "outputs": Array [ + "coverage/**/*", + ], + }, + }, + } + `); + }); + + it("migrates config with no env var dependencies", async () => { + const config = getTestTurboConfig({ + globalDependencies: [], + pipeline: { + build: { dependsOn: ["^build"] }, + }, + }); + const pipeline = migrateConfig(config); + expect(pipeline).toMatchInlineSnapshot(` + Object { + "$schema": "./docs/public/schema.json", + "pipeline": Object { + "build": Object { + "dependsOn": Array [ + "^build", + ], + "outputs": Array [ + "dist/**/*", + ".next/**/*", + "!.next/cache/**", + ], + }, + "dev": Object { + "cache": false, + }, + "lint": Object { + "outputs": Array [], + }, + "test": Object { + "dependsOn": Array [ + "^build", + ], + "outputs": Array [ + "coverage/**/*", + ], + }, + }, + } + `); + }); + + it("migrates config with inconsistent config", async () => { + const config = getTestTurboConfig({ + pipeline: { + test: { env: ["$MY_ENV"], dependsOn: ["^build", "$SUPER_COOL"] }, + }, + }); + const pipeline = migrateConfig(config); + expect(pipeline).toMatchInlineSnapshot(` + Object { + "$schema": "./docs/public/schema.json", + "globalEnv": Array [ + "GLOBAL_ENV_KEY", + ], + "pipeline": Object { + "build": Object { + "dependsOn": Array [ + "^build", + ], + "env": Array [ + "TASK_ENV_KEY", + "ANOTHER_ENV_KEY", + ], + "outputs": Array [ + "dist/**/*", + ".next/**/*", + "!.next/cache/**", + ], + }, + "dev": Object { + "cache": false, + }, + "lint": Object { + "outputs": Array [], + }, + "test": Object { + "dependsOn": Array [ + "^build", + ], + "env": Array [ + "$MY_ENV", + "SUPER_COOL", + ], + "outputs": Array [ + "coverage/**/*", + ], + }, + }, + } + `); + }); + + it("migrates config with duplicate env keys", async () => { + const config = getTestTurboConfig({ + pipeline: { + test: { env: ["$MY_ENV"], dependsOn: ["^build", "$MY_ENV"] }, + }, + }); + const pipeline = migrateConfig(config); + expect(pipeline).toMatchInlineSnapshot(` + Object { + "$schema": "./docs/public/schema.json", + "globalEnv": Array [ + "GLOBAL_ENV_KEY", + ], + "pipeline": Object { + "build": Object { + "dependsOn": Array [ + "^build", + ], + "env": Array [ + "TASK_ENV_KEY", + "ANOTHER_ENV_KEY", + ], + "outputs": Array [ + "dist/**/*", + ".next/**/*", + "!.next/cache/**", + ], + }, + "dev": Object { + "cache": false, + }, + "lint": Object { + "outputs": Array [], + }, + "test": Object { + "dependsOn": Array [ + "^build", + ], + "env": Array [ + "$MY_ENV", + "MY_ENV", + ], + "outputs": Array [ + "coverage/**/*", + ], + }, + }, + } + `); + }); + }); + + describe("transform", () => { + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "migrate-env-var-dependencies", + }); + + it("migrates turbo.json env var dependencies - basic", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "env-dependencies", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + globalDependencies: [".env"], + globalEnv: ["NEXT_PUBLIC_API_KEY", "STRIPE_API_KEY"], + pipeline: { + build: { + dependsOn: ["^build"], + env: ["PROD_API_KEY"], + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: { + dependsOn: [], + env: ["IS_CI"], + outputs: [], + }, + test: { + dependsOn: ["test"], + env: ["IS_CI"], + outputs: [], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 4, + "deletions": 4, + }, + } + `); + }); + + it("migrates turbo.json env var dependencies - workspace configs", async () => { + // load the fixture for the test + const { root, readJson } = useFixture({ + fixture: "workspace-configs", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(readJson("turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + globalDependencies: [".env"], + globalEnv: ["NEXT_PUBLIC_API_KEY", "STRIPE_API_KEY"], + pipeline: { + build: { + dependsOn: ["^build"], + env: ["PROD_API_KEY"], + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: { + dependsOn: [], + env: ["IS_TEST"], + outputs: [], + }, + test: { + dependsOn: ["test"], + env: ["IS_CI"], + outputs: [], + }, + }, + }); + + expect(readJson("apps/web/turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + extends: ["//"], + pipeline: { + build: { + // old + dependsOn: ["build"], + // new + env: ["ENV_1", "ENV_2"], + }, + }, + }); + + expect(readJson("packages/ui/turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + extends: ["//"], + pipeline: { + build: { + dependsOn: [], + env: ["IS_SERVER"], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "apps/web/turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + "packages/ui/turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 1, + }, + "turbo.json": Object { + "action": "modified", + "additions": 4, + "deletions": 4, + }, + } + `); + }); + + it("migrates turbo.json env var dependencies - repeat run", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "env-dependencies", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + globalDependencies: [".env"], + globalEnv: ["NEXT_PUBLIC_API_KEY", "STRIPE_API_KEY"], + pipeline: { + build: { + dependsOn: ["^build"], + env: ["PROD_API_KEY"], + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: { + dependsOn: [], + env: ["IS_CI"], + outputs: [], + }, + test: { + dependsOn: ["test"], + env: ["IS_CI"], + outputs: [], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 4, + "deletions": 4, + }, + } + `); + + // run the transformer + const repeatResult = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(repeatResult.fatalError).toBeUndefined(); + expect(repeatResult.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + it("migrates turbo.json env var dependencies - dry", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "env-dependencies", + }); + + const turboJson = JSON.parse(read("turbo.json") || "{}"); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: false }, + }); + + // make sure it didn't change + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboJson); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "skipped", + "additions": 4, + "deletions": 4, + }, + } + `); + }); + + it("migrates turbo.json env var dependencies - print", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "env-dependencies", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: true }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + globalEnv: ["NEXT_PUBLIC_API_KEY", "STRIPE_API_KEY"], + globalDependencies: [".env"], + pipeline: { + build: { + dependsOn: ["^build"], + env: ["PROD_API_KEY"], + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: { + dependsOn: [], + env: ["IS_CI"], + outputs: [], + }, + test: { + dependsOn: ["test"], + env: ["IS_CI"], + outputs: [], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 4, + "deletions": 4, + }, + } + `); + }); + + it("migrates turbo.json env var dependencies - dry & print", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "env-dependencies", + }); + + const turboJson = JSON.parse(read("turbo.json") || "{}"); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: true }, + }); + + // make sure it didn't change + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboJson); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "skipped", + "additions": 4, + "deletions": 4, + }, + } + `); + }); + + it("does not change turbo.json if already migrated", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "migrated-env-dependencies", + }); + + const turboJson = JSON.parse(read("turbo.json") || "{}"); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboJson); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + it("errors if no turbo.json can be found", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "no-turbo-json", + }); + + expect(read("turbo.json")).toBeUndefined(); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(read("turbo.json")).toBeUndefined(); + expect(result.fatalError).toBeDefined(); + expect(result.fatalError?.message).toMatch( + /No turbo\.json found at .*?\. Is the path correct\?/ + ); + }); + + it("errors if package.json config exists and has not been migrated", async () => { + // load the fixture for the test + const { root } = useFixture({ + fixture: "old-config", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(result.fatalError).toBeDefined(); + expect(result.fatalError?.message).toMatch( + 'turbo" key detected in package.json. Run `npx @turbo/codemod transform create-turbo-config` first' + ); + }); + }); +}); diff --git a/packages/turbo-codemod/__tests__/migrate.test.ts b/packages/turbo-codemod/__tests__/migrate.test.ts new file mode 100644 index 0000000..652ea41 --- /dev/null +++ b/packages/turbo-codemod/__tests__/migrate.test.ts @@ -0,0 +1,761 @@ +import { MigrateCommandArgument } from "../src/commands"; +import migrate from "../src/commands/migrate"; +import { setupTestFixtures, spyExit } from "@turbo/test-utils"; +import childProcess from "child_process"; +import * as checkGitStatus from "../src/utils/checkGitStatus"; +import * as getCurrentVersion from "../src/commands/migrate/steps/getCurrentVersion"; +import * as getLatestVersion from "../src/commands/migrate/steps/getLatestVersion"; +import * as getTurboUpgradeCommand from "../src/commands/migrate/steps/getTurboUpgradeCommand"; +import * as workspaceImplementation from "../src/utils/getPackageManager"; +import * as getPackageManagerVersion from "../src/utils/getPackageManagerVersion"; + +describe("migrate", () => { + const mockExit = spyExit(); + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "migrate", + }); + + it("migrates from 1.0.0 to 1.7.0", async () => { + const { root, readJson } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.0.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue("pnpm install -g turbo@latest"); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "no-turbo-json", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + expect(readJson("turbo.json")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + build: { + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: {}, + test: { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + }); + + it("migrates from 1.0.0 to 1.2.0 (dry run)", async () => { + const { root, readJson } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.0.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.2.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue("pnpm install -g turbo@latest"); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + + const packageJson = readJson("package.json"); + const turboJson = readJson("turbo.json"); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: true, + print: false, + install: true, + }); + + // make sure nothing changed + expect(readJson("package.json")).toStrictEqual(packageJson); + expect(readJson("turbo.json")).toStrictEqual(turboJson); + + // verify mocks were called + expect(mockedCheckGitStatus).not.toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + }); + + it("next version can be passed as an option", async () => { + const { root, readJson } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.0.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue("pnpm install -g turbo@latest"); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + to: "1.7.0", + }); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "no-turbo-json", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + expect(readJson("turbo.json")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + build: { + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + test: { + outputs: ["dist/**", "build/**"], + }, + lint: {}, + }, + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + }); + + it("current version can be passed as an option", async () => { + const { root, readJson } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue("pnpm install -g turbo@latest"); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + from: "1.0.0", + }); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "no-turbo-json", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + expect(readJson("turbo.json")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + build: { + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: {}, + test: { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + }); + + it("exits if the current version is the same as the new version", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.7.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(0); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + }); + + it("continues when migration doesn't require codemods", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.3.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.3.1"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue("npm install turbo@1.3.1"); + const mockExecSync = jest + .spyOn(childProcess, "execSync") + .mockReturnValue("installed"); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: true, + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockExecSync).toHaveBeenCalledWith("npm install turbo@1.3.1", { + cwd: root, + }); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockExecSync.mockRestore(); + }); + + it("installs the correct turbo version", async () => { + const { root, readJson } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.0.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue("pnpm install -g turbo@1.7.0"); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + const mockExecSync = jest + .spyOn(childProcess, "execSync") + .mockReturnValue("installed"); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: true, + }); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "no-turbo-json", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + expect(readJson("turbo.json")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + build: { + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: {}, + test: { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + expect(mockExecSync).toHaveBeenCalled(); + expect(mockExecSync).toHaveBeenCalledWith("pnpm install -g turbo@1.7.0", { + cwd: root, + }); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + mockExecSync.mockRestore(); + }); + + it("fails gracefully when the correct upgrade command cannot be found", async () => { + const { root, readJson } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.0.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "default") + .mockReturnValue(undefined); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + const mockExecSync = jest + .spyOn(childProcess, "execSync") + .mockReturnValue("installed"); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: true, + }); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "no-turbo-json", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + expect(readJson("turbo.json")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + build: { + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: {}, + test: { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + expect(mockExecSync).not.toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + mockExecSync.mockRestore(); + }); + + it("exits if current version is not passed and cannot be inferred", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue(undefined); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + }); + + it("exits if latest version is not passed and cannot be inferred", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.5.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue(undefined); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + }); + + it("exits if latest version throws", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.5.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockRejectedValue(new Error("failed to fetch version")); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + }); + + it("exits if any transforms encounter an error", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "default") + .mockReturnValue("1.0.0"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "default") + .mockResolvedValue("1.7.0"); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetWorkspaceImplementation = jest + .spyOn(workspaceImplementation, "default") + .mockReturnValue(packageManager); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: true, + print: false, + install: true, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).not.toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetWorkspaceImplementation).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetWorkspaceImplementation.mockRestore(); + }); + + it("exits if invalid directory is passed", async () => { + const { root } = useFixture({ + fixture: "old-turbo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + + await migrate("~/path/that/does/not/exist" as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + }); + + it("exits if directory with no repo is passed", async () => { + const { root } = useFixture({ + fixture: "no-repo", + }); + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + + await migrate(root as MigrateCommandArgument, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + }); +}); diff --git a/packages/turbo-codemod/__tests__/set-default-outputs.test.ts b/packages/turbo-codemod/__tests__/set-default-outputs.test.ts new file mode 100644 index 0000000..4a71fa7 --- /dev/null +++ b/packages/turbo-codemod/__tests__/set-default-outputs.test.ts @@ -0,0 +1,391 @@ +import { transformer } from "../src/transforms/set-default-outputs"; +import { setupTestFixtures } from "@turbo/test-utils"; + +describe("set-default-outputs", () => { + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "set-default-outputs", + }); + it("migrates turbo.json outputs - basic", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "old-outputs", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + "build-one": { + outputs: ["foo"], + }, + "build-two": {}, + "build-three": { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 2, + "deletions": 1, + }, + } + `); + }); + + it("migrates turbo.json outputs - workspace configs", async () => { + // load the fixture for the test + const { root, readJson } = useFixture({ + fixture: "workspace-configs", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(readJson("turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + "build-one": { + outputs: ["foo"], + }, + "build-two": {}, + "build-three": { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(readJson("apps/docs/turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + extends: ["//"], + pipeline: { + build: { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(readJson("apps/web/turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + extends: ["//"], + pipeline: { + build: {}, + }, + }); + + expect(readJson("packages/ui/turbo.json") || "{}").toStrictEqual({ + $schema: "https://turbo.build/schema.json", + extends: ["//"], + pipeline: { + "build-three": { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "apps/docs/turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 1, + }, + "apps/web/turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 0, + }, + "packages/ui/turbo.json": Object { + "action": "modified", + "additions": 1, + "deletions": 1, + }, + "turbo.json": Object { + "action": "modified", + "additions": 2, + "deletions": 1, + }, + } + `); + }); + + it("migrates turbo.json outputs - dry", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "old-outputs", + }); + + const turboJson = JSON.parse(read("turbo.json") || "{}"); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: false }, + }); + + // make sure it didn't change + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboJson); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "skipped", + "additions": 2, + "deletions": 1, + }, + } + `); + }); + + it("migrates turbo.json outputs - print", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "old-outputs", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: true }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + "build-one": { + outputs: ["foo"], + }, + "build-two": {}, + "build-three": { + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 2, + "deletions": 1, + }, + } + `); + }); + + it("migrates turbo.json outputs - dry & print", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "old-outputs", + }); + + const turboJson = JSON.parse(read("turbo.json") || "{}"); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: true, print: false }, + }); + + // make sure it didn't change + expect(JSON.parse(read("turbo.json") || "{}")).toEqual(turboJson); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "skipped", + "additions": 2, + "deletions": 1, + }, + } + `); + }); + + it("migrates turbo.json outputs - invalid", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "invalid-outputs", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + "build-one": { + outputs: ["foo"], + }, + "build-two": {}, + "build-three": { + outputs: ["dist/**", "build/**"], + }, + "garbage-in-numeric-0": { + outputs: ["dist/**", "build/**"], + }, + "garbage-in-numeric": { + outputs: 42, + }, + "garbage-in-string": { + outputs: "string", + }, + "garbage-in-empty-string": { + outputs: ["dist/**", "build/**"], + }, + "garbage-in-null": { + outputs: ["dist/**", "build/**"], + }, + "garbage-in-false": { + outputs: ["dist/**", "build/**"], + }, + "garbage-in-true": { + outputs: true, + }, + "garbage-in-object": { + outputs: {}, + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 6, + "deletions": 5, + }, + } + `); + }); + + it("migrates turbo.json outputs - config with no pipeline", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "no-pipeline", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + globalDependencies: ["$NEXT_PUBLIC_API_KEY", "$STRIPE_API_KEY", ".env"], + pipeline: {}, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "unchanged", + "additions": 0, + "deletions": 0, + }, + } + `); + }); + + it("migrates turbo.json outputs - config with no outputs", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "no-outputs", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(JSON.parse(read("turbo.json") || "{}")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + pipeline: { + "build-one": { + dependsOn: ["build-two"], + outputs: ["dist/**", "build/**"], + }, + "build-two": { + cache: false, + }, + "build-three": { + persistent: true, + outputs: ["dist/**", "build/**"], + }, + }, + }); + + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(` + Object { + "turbo.json": Object { + "action": "modified", + "additions": 2, + "deletions": 0, + }, + } + `); + }); + + it("errors if no turbo.json can be found", async () => { + // load the fixture for the test + const { root, read } = useFixture({ + fixture: "no-turbo-json", + }); + + expect(read("turbo.json")).toBeUndefined(); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(read("turbo.json")).toBeUndefined(); + expect(result.fatalError).toBeDefined(); + expect(result.fatalError?.message).toMatch( + /No turbo\.json found at .*?\. Is the path correct\?/ + ); + }); + + it("errors if package.json config exists and has not been migrated", async () => { + // load the fixture for the test + const { root } = useFixture({ + fixture: "old-config", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + expect(result.fatalError).toBeDefined(); + expect(result.fatalError?.message).toMatch( + 'turbo" key detected in package.json. Run `npx @turbo/codemod transform create-turbo-config` first' + ); + }); +}); diff --git a/packages/turbo-codemod/__tests__/transform.test.ts b/packages/turbo-codemod/__tests__/transform.test.ts new file mode 100644 index 0000000..abd015d --- /dev/null +++ b/packages/turbo-codemod/__tests__/transform.test.ts @@ -0,0 +1,172 @@ +import transform from "../src/commands/transform"; +import { MigrateCommandArgument } from "../src/commands"; +import { setupTestFixtures, spyExit } from "@turbo/test-utils"; +import * as checkGitStatus from "../src/utils/checkGitStatus"; +import * as getPackageManager from "../src/utils/getPackageManager"; +import * as getPackageManagerVersion from "../src/utils/getPackageManagerVersion"; + +describe("transform", () => { + const mockExit = spyExit(); + const { useFixture } = setupTestFixtures({ + directory: __dirname, + test: "transform", + }); + + it("runs the selected transform", async () => { + const { root, readJson } = useFixture({ + fixture: "basic", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + await transform( + "add-package-manager" as MigrateCommandArgument, + root as MigrateCommandArgument, + { + list: false, + force: false, + dry: false, + print: false, + } + ); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "transform-basic", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetPackageManager).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetPackageManager.mockRestore(); + }); + + it("runs the selected transform - dry & print", async () => { + const { root, readJson } = useFixture({ + fixture: "basic", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "default") + .mockReturnValue(undefined); + const mockedGetPackageManagerVersion = jest + .spyOn(getPackageManagerVersion, "default") + .mockReturnValue(packageManagerVersion); + const mockedGetPackageManager = jest + .spyOn(getPackageManager, "default") + .mockReturnValue(packageManager); + + await transform( + "add-package-manager" as MigrateCommandArgument, + root as MigrateCommandArgument, + { + list: false, + force: false, + dry: true, + print: true, + } + ); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.0.0", + }, + name: "transform-basic", + version: "1.0.0", + }); + + // verify mocks were called + expect(mockedCheckGitStatus).not.toHaveBeenCalled(); + expect(mockedGetPackageManagerVersion).toHaveBeenCalled(); + expect(mockedGetPackageManager).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetPackageManagerVersion.mockRestore(); + mockedGetPackageManager.mockRestore(); + }); + + it("lists transforms", async () => { + const { root } = useFixture({ + fixture: "basic", + }); + + await transform( + "add-package-manager" as MigrateCommandArgument, + root as MigrateCommandArgument, + { + list: true, + force: false, + dry: false, + print: false, + } + ); + + expect(mockExit.exit).toHaveBeenCalledWith(0); + }); + + it("exits on invalid transform", async () => { + const { root } = useFixture({ + fixture: "basic", + }); + + await transform( + "not-a-real-option" as MigrateCommandArgument, + root as MigrateCommandArgument, + { + list: false, + force: false, + dry: false, + print: false, + } + ); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + }); + + it("exits on invalid directory", async () => { + const { root } = useFixture({ + fixture: "basic", + }); + + await transform( + "add-package-manager" as MigrateCommandArgument, + "~/path/that/does/not/exist" as MigrateCommandArgument, + { + list: false, + force: false, + dry: false, + print: false, + } + ); + + expect(mockExit.exit).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/turbo-codemod/index.d.ts b/packages/turbo-codemod/index.d.ts new file mode 100644 index 0000000..c3a4874 --- /dev/null +++ b/packages/turbo-codemod/index.d.ts @@ -0,0 +1 @@ +declare module "is-git-clean"; diff --git a/packages/turbo-codemod/jest.config.js b/packages/turbo-codemod/jest.config.js new file mode 100644 index 0000000..2c7542a --- /dev/null +++ b/packages/turbo-codemod/jest.config.js @@ -0,0 +1,18 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest/presets/js-with-ts", + testEnvironment: "node", + transformIgnorePatterns: ["/node_modules/(?!(ansi-regex)/)"], + modulePathIgnorePatterns: ["<rootDir>/node_modules", "<rootDir>/dist"], + testPathIgnorePatterns: ["/__fixtures__/"], + coveragePathIgnorePatterns: ["/__fixtures__/"], + collectCoverage: true, + coverageThreshold: { + global: { + branches: 80, + functions: 89, + lines: 89, + statements: 89, + }, + }, +}; diff --git a/packages/turbo-codemod/package.json b/packages/turbo-codemod/package.json new file mode 100644 index 0000000..d16bb05 --- /dev/null +++ b/packages/turbo-codemod/package.json @@ -0,0 +1,67 @@ +{ + "name": "@turbo/codemod", + "version": "1.9.4-canary.2", + "description": "Provides Codemod transformations to help upgrade your Turborepo codebase when a feature is deprecated.", + "homepage": "https://turbo.build/repo", + "license": "MPL-2.0", + "repository": { + "type": "git", + "url": "https://github.com/vercel/turbo", + "directory": "packages/turbo-codemod" + }, + "bugs": { + "url": "https://github.com/vercel/turbo/issues" + }, + "bin": "dist/cli.js", + "scripts": { + "build": "tsup", + "test": "jest", + "lint": "eslint src/**/*.ts", + "check-types": "tsc --noEmit", + "add-transformer": "plop" + }, + "dependencies": { + "axios": "0.27.2", + "chalk": "2.4.2", + "commander": "^9.5.0", + "diff": "^5.1.0", + "find-up": "4.1.0", + "fs-extra": "^10.0.0", + "gradient-string": "^2.0.0", + "inquirer": "^8.2.4", + "inquirer-file-tree-selection-prompt": "^1.0.19", + "is-git-clean": "^1.1.0", + "ora": "4.1.1", + "semver": "^7.3.7", + "update-check": "^1.5.4" + }, + "devDependencies": { + "@types/chalk-animation": "^1.6.0", + "@types/diff": "^5.0.2", + "@types/fs-extra": "^9.0.13", + "@types/gradient-string": "^1.1.2", + "@types/inquirer": "^8.2.0", + "@types/jest": "^27.4.0", + "@types/node": "^16.11.12", + "@types/semver": "^7.3.9", + "@types/uuid": "^9.0.0", + "deepmerge": "^4.2.2", + "eslint": "^7.23.0", + "jest": "^27.4.3", + "plop": "^3.1.1", + "semver": "^7.3.5", + "ts-jest": "^27.1.1", + "@turbo/tsconfig": "workspace:*", + "tsup": "^5.10.3", + "@turbo/test-utils": "workspace:*", + "@turbo/types": "workspace:*", + "@turbo/utils": "workspace:*", + "typescript": "^4.5.5" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/turbo-codemod/plopfile.js b/packages/turbo-codemod/plopfile.js new file mode 100644 index 0000000..9cc2dd7 --- /dev/null +++ b/packages/turbo-codemod/plopfile.js @@ -0,0 +1,46 @@ +const fs = require("fs-extra"); + +module.exports = function plopConfig(plop) { + // controller generator + plop.setGenerator("controller", { + description: "Add a new transformer", + prompts: [ + { + type: "input", + name: "name", + message: 'key for the transform (example: "create-turbo-config")', + }, + { + type: "input", + name: "description", + message: + 'description for the transform (example: "Create the `turbo.json` file from an existing "turbo" key in `package.json`")', + }, + { + type: "input", + name: "introducedIn", + message: + 'the semantic version of turbo where this change was introduced (example: "1.1.0")', + }, + ], + actions: [ + { + type: "add", + path: "src/transforms/{{name}}.ts", + templateFile: "templates/transformer.hbs", + }, + { + type: "add", + path: "__tests__/{{name}}.test.ts", + templateFile: "templates/transformer.test.hbs", + }, + function createFixturesDirectory(answers) { + process.chdir(plop.getPlopfilePath()); + const directory = `__tests__/__fixtures__/${answers.name}`; + fs.mkdirSync(`__tests__/__fixtures__/${answers.name}`); + + return `created empty ${directory} directory for fixtures`; + }, + ], + }); +}; diff --git a/packages/turbo-codemod/src/cli.ts b/packages/turbo-codemod/src/cli.ts new file mode 100644 index 0000000..451816f --- /dev/null +++ b/packages/turbo-codemod/src/cli.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +import chalk from "chalk"; +import { Command } from "commander"; + +import { transform, migrate } from "./commands"; +import notifyUpdate from "./utils/notifyUpdate"; +import cliPkg from "../package.json"; + +const codemodCli = new Command(); + +codemodCli + .name("@turbo/codemod") + .description( + "Codemod transformations to help upgrade your Turborepo codebase when a feature is deprecated." + ) + .version(cliPkg.version, "-v, --version", "output the current version"); + +// migrate +codemodCli + .command("migrate") + .aliases(["update", "upgrade"]) + .description("Migrate a project to the latest version of Turborepo") + .argument("[path]", "Directory where the transforms should be applied") + .option( + "--from <version>", + "Specify the version to migrate from (default: current version)" + ) + .option( + "--to <version>", + "Specify the version to migrate to (default: latest)" + ) + .option("--install", "Install new version of turbo after migration", true) + .option( + "--force", + "Bypass Git safety checks and forcibly run codemods", + false + ) + .option("--dry", "Dry run (no changes are made to files)", false) + .option("--print", "Print transformed files to your terminal", false) + .action(migrate); + +// transform +codemodCli + .command("transform", { isDefault: true }) + .description("Apply a single code transformation to a project") + .argument("[transform]", "The transformer to run") + .argument("[path]", "Directory where the transforms should be applied") + .option( + "--force", + "Bypass Git safety checks and forcibly run codemods", + false + ) + .option("--list", "List all available transforms", false) + .option("--dry", "Dry run (no changes are made to files)", false) + .option("--print", "Print transformed files to your terminal", false) + .action(transform); + +codemodCli + .parseAsync() + .then(notifyUpdate) + .catch(async (reason) => { + console.log(); + if (reason.command) { + console.log(` ${chalk.cyan(reason.command)} has failed.`); + } else { + console.log(chalk.red("Unexpected error. Please report it as a bug:")); + console.log(reason); + } + console.log(); + await notifyUpdate(); + process.exit(1); + }); diff --git a/packages/turbo-codemod/src/commands/index.ts b/packages/turbo-codemod/src/commands/index.ts new file mode 100644 index 0000000..a7aeee6 --- /dev/null +++ b/packages/turbo-codemod/src/commands/index.ts @@ -0,0 +1,11 @@ +export { default as migrate } from "./migrate"; +export { default as transform } from "./transform"; + +export type { + TransformCommandArgument, + TransformCommandOptions, +} from "./transform/types"; +export type { + MigrateCommandArgument, + MigrateCommandOptions, +} from "./migrate/types"; diff --git a/packages/turbo-codemod/src/commands/migrate/index.ts b/packages/turbo-codemod/src/commands/migrate/index.ts new file mode 100644 index 0000000..c4c6d02 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/index.ts @@ -0,0 +1,215 @@ +import chalk from "chalk"; +import os from "os"; +import inquirer from "inquirer"; +import { execSync } from "child_process"; + +import getCurrentVersion from "./steps/getCurrentVersion"; +import getLatestVersion from "./steps/getLatestVersion"; +import getCodemodsForMigration from "./steps/getTransformsForMigration"; +import checkGitStatus from "../../utils/checkGitStatus"; +import directoryInfo from "../../utils/directoryInfo"; +import getTurboUpgradeCommand from "./steps/getTurboUpgradeCommand"; +import Runner from "../../runner/Runner"; +import type { MigrateCommandArgument, MigrateCommandOptions } from "./types"; +import looksLikeRepo from "../../utils/looksLikeRepo"; + +function endMigration({ + message, + success, +}: { + message?: string; + success: boolean; +}) { + if (success) { + console.log(chalk.bold(chalk.green("Migration completed"))); + if (message) { + console.log(message); + } + return process.exit(0); + } + + console.log(chalk.bold(chalk.red("Migration failed"))); + if (message) { + console.log(message); + } + return process.exit(1); +} + +/** +Migration is done in 4 steps: + -- gather information + 1. find the version (x) of turbo to migrate from (if not specified) + 2. find the version (y) of turbo to migrate to (if not specified) + 3. determine which codemods need to be run to move from version x to version y + -- action + 4. execute the codemods (serially, and in order) + 5. update the turbo version (optionally) +**/ +export default async function migrate( + directory: MigrateCommandArgument, + options: MigrateCommandOptions +) { + // check git status + if (!options.dry) { + checkGitStatus({ directory, force: options.force }); + } + + const answers = await inquirer.prompt<{ + directoryInput?: string; + }>([ + { + type: "input", + name: "directoryInput", + message: "Where is the root of the repo to migrate?", + when: !directory, + default: ".", + validate: (directory: string) => { + const { exists, absolute } = directoryInfo({ directory }); + if (exists) { + return true; + } else { + return `Directory ${chalk.dim(`(${absolute})`)} does not exist`; + } + }, + filter: (directory: string) => directory.trim(), + }, + ]); + + const { directoryInput: selectedDirectory = directory as string } = answers; + const { exists, absolute: root } = directoryInfo({ + directory: selectedDirectory, + }); + if (!exists) { + return endMigration({ + success: false, + message: `Directory ${chalk.dim(`(${root})`)} does not exist`, + }); + } + + if (!looksLikeRepo({ directory: root })) { + return endMigration({ + success: false, + message: `Directory (${chalk.dim( + root + )}) does not appear to be a repository`, + }); + } + + // step 1 + const fromVersion = getCurrentVersion(selectedDirectory, options); + if (!fromVersion) { + return endMigration({ + success: false, + message: `Unable to infer the version of turbo being used by ${directory}`, + }); + } + + // step 2 + let toVersion = options.to; + try { + toVersion = await getLatestVersion(options); + } catch (err) { + let message = "UNKNOWN_ERROR"; + if (err instanceof Error) { + message = err.message; + } + return endMigration({ + success: false, + message, + }); + } + + if (!toVersion) { + return endMigration({ + success: false, + message: `Unable to fetch the latest version of turbo`, + }); + } + + if (fromVersion === toVersion) { + return endMigration({ + success: true, + message: `Nothing to do, current version (${chalk.bold( + fromVersion + )}) is the same as the requested version (${chalk.bold(toVersion)})`, + }); + } + + // step 3 + const codemods = getCodemodsForMigration({ fromVersion, toVersion }); + if (codemods.length === 0) { + console.log( + `No codemods required to migrate from ${fromVersion} to ${toVersion}`, + os.EOL + ); + } + + // step 4 + console.log( + `Upgrading turbo from ${chalk.bold(fromVersion)} to ${chalk.bold( + toVersion + )} (${ + codemods.length === 0 + ? "no codemods required" + : `${codemods.length} required codemod${ + codemods.length === 1 ? "" : "s" + }` + })`, + os.EOL + ); + const results = codemods.map((codemod, idx) => { + console.log( + `(${idx + 1}/${codemods.length}) ${chalk.bold( + `Running ${codemod.value}` + )}` + ); + + const result = codemod.transformer({ root: selectedDirectory, options }); + Runner.logResults(result); + return result; + }); + + const hasTransformError = results.some( + (result) => + result.fatalError || + Object.keys(result.changes).some((key) => result.changes[key].error) + ); + + if (hasTransformError) { + return endMigration({ + success: false, + message: `Could not complete migration due to codemod errors. Please fix the errors and try again.`, + }); + } + + // step 5 + const upgradeCommand = getTurboUpgradeCommand({ + directory: selectedDirectory, + to: options.to, + }); + + if (!upgradeCommand) { + return endMigration({ + success: false, + message: "Unable to determine upgrade command", + }); + } + + if (options.install) { + if (options.dry) { + console.log( + `Upgrading turbo with ${chalk.bold(upgradeCommand)} ${chalk.dim( + "(dry run)" + )}`, + os.EOL + ); + } else { + console.log(`Upgrading turbo with ${chalk.bold(upgradeCommand)}`, os.EOL); + execSync(upgradeCommand, { cwd: selectedDirectory }); + } + } else { + console.log(`Upgrade turbo with ${chalk.bold(upgradeCommand)}`, os.EOL); + } + + endMigration({ success: true }); +} diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts b/packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts new file mode 100644 index 0000000..3644f8b --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts @@ -0,0 +1,45 @@ +import path from "path"; +import { existsSync } from "fs-extra"; + +import getPackageManager from "../../../utils/getPackageManager"; +import { exec } from "../utils"; +import type { MigrateCommandOptions } from "../types"; + +function getCurrentVersion( + directory: string, + opts: MigrateCommandOptions +): string | undefined { + const { from } = opts; + if (from) { + return from; + } + + // try global first + const turboVersionFromGlobal = exec(`turbo --version`, { cwd: directory }); + + if (turboVersionFromGlobal) { + return turboVersionFromGlobal; + } + + // try to use the package manager to find the version + const packageManager = getPackageManager({ directory }); + if (packageManager) { + if (packageManager === "yarn") { + return exec(`yarn turbo --version`, { cwd: directory }); + } + if (packageManager === "pnpm") { + return exec(`pnpm turbo --version`, { cwd: directory }); + } else { + // this doesn't work for npm, so manually build the binary path + const turboBin = path.join(directory, "node_modules", ".bin", "turbo"); + if (existsSync(turboBin)) { + return exec(`${turboBin} --version`, { cwd: directory }); + } + } + } + + // unable to determine local version, + return undefined; +} + +export default getCurrentVersion; diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts b/packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts new file mode 100644 index 0000000..a6ab7e6 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts @@ -0,0 +1,31 @@ +import axios from "axios"; + +import type { MigrateCommandOptions } from "../types"; + +const REGISTRY = "https://registry.npmjs.org"; + +async function getPackageDetails({ packageName }: { packageName: string }) { + try { + const result = await axios.get(`${REGISTRY}/${packageName}`); + return result.data; + } catch (err) { + throw new Error(`Unable to fetch the latest version of ${packageName}`); + } +} + +export default async function getLatestVersion({ + to, +}: MigrateCommandOptions): Promise<string | undefined> { + const packageDetails = await getPackageDetails({ packageName: "turbo" }); + const { "dist-tags": tags, versions } = packageDetails; + + if (to) { + if (tags[to] || versions[to]) { + return to; + } else { + throw new Error(`turbo@${to} does not exist`); + } + } + + return tags.latest as string; +} diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts b/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts new file mode 100644 index 0000000..2224c06 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts @@ -0,0 +1,25 @@ +import { gt, lte } from "semver"; + +import loadTransformers from "../../../utils/loadTransformers"; +import type { Transformer } from "../../../types"; + +/** + Returns all transformers introduced after fromVersion, but before or equal to toVersion +**/ +function getTransformsForMigration({ + fromVersion, + toVersion, +}: { + fromVersion: string; + toVersion: string; +}): Array<Transformer> { + const transforms = loadTransformers(); + return transforms.filter((transformer) => { + return ( + gt(transformer.introducedIn, fromVersion) && + lte(transformer.introducedIn, toVersion) + ); + }); +} + +export default getTransformsForMigration; diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts b/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts new file mode 100644 index 0000000..8fd5972 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts @@ -0,0 +1,182 @@ +import os from "os"; +import path from "path"; +import fs from "fs-extra"; +import { gte } from "semver"; + +import { exec } from "../utils"; +import getPackageManager, { + PackageManager, +} from "../../../utils/getPackageManager"; +import getPackageManagerVersion from "../../../utils/getPackageManagerVersion"; + +type InstallType = "dependencies" | "devDependencies"; + +function getGlobalBinaryPaths(): Record<PackageManager, string | undefined> { + return { + // we run these from a tmpdir to avoid corepack interference + yarn: exec(`yarn global bin`, { cwd: os.tmpdir() }), + npm: exec(`npm bin --global`, { cwd: os.tmpdir() }), + pnpm: exec(`pnpm bin --global`, { cwd: os.tmpdir() }), + }; +} + +function getGlobalUpgradeCommand( + packageManager: PackageManager, + to: string = "latest" +) { + switch (packageManager) { + case "yarn": + return `yarn global add turbo@${to}`; + case "npm": + return `npm install turbo@${to} --global`; + case "pnpm": + return `pnpm install turbo@${to} --global`; + } +} + +function getLocalUpgradeCommand({ + packageManager, + packageManagerVersion, + installType, + isUsingWorkspaces, + to = "latest", +}: { + packageManager: PackageManager; + packageManagerVersion: string; + installType: InstallType; + isUsingWorkspaces?: boolean; + to?: string; +}) { + const renderCommand = ( + command: Array<string | boolean | undefined> + ): string => command.filter(Boolean).join(" "); + switch (packageManager) { + // yarn command differs depending on the version + case "yarn": + // yarn 2.x and 3.x (berry) + if (gte(packageManagerVersion, "2.0.0")) { + return renderCommand([ + "yarn", + "add", + `turbo@${to}`, + installType === "devDependencies" && "--dev", + ]); + // yarn 1.x + } else { + return renderCommand([ + "yarn", + "add", + `turbo@${to}`, + installType === "devDependencies" && "--dev", + isUsingWorkspaces && "-W", + ]); + } + case "npm": + return renderCommand([ + "npm", + "install", + `turbo@${to}`, + installType === "devDependencies" && "--save-dev", + ]); + case "pnpm": + return renderCommand([ + "pnpm", + "install", + `turbo@${to}`, + installType === "devDependencies" && "--save-dev", + isUsingWorkspaces && "-w", + ]); + } +} + +function getInstallType({ directory }: { directory: string }): { + installType?: InstallType; + isUsingWorkspaces?: boolean; +} { + // read package.json to make sure we have a reference to turbo + const packageJsonPath = path.join(directory, "package.json"); + const pnpmWorkspaceConfig = path.join(directory, "pnpm-workspace.yaml"); + const isPnpmWorkspaces = fs.existsSync(pnpmWorkspaceConfig); + + if (!fs.existsSync(packageJsonPath)) { + console.error(`Unable to find package.json at ${packageJsonPath}`); + return { installType: undefined, isUsingWorkspaces: undefined }; + } + + const packageJson = fs.readJsonSync(packageJsonPath); + const isDevDependency = + packageJson.devDependencies && "turbo" in packageJson.devDependencies; + const isDependency = + packageJson.dependencies && "turbo" in packageJson.dependencies; + let isUsingWorkspaces = "workspaces" in packageJson || isPnpmWorkspaces; + + if (isDependency || isDevDependency) { + return { + installType: isDependency ? "dependencies" : "devDependencies", + isUsingWorkspaces, + }; + } + + return { + installType: undefined, + isUsingWorkspaces, + }; +} + +/** + Finding the correct command to upgrade depends on two things: + 1. The package manager + 2. The install method (local or global) + + We try global first to let turbo handle the inference, then we try local. +**/ +export default function getTurboUpgradeCommand({ + directory, + to, +}: { + directory: string; + to?: string; +}) { + const turboBinaryPathFromGlobal = exec(`turbo bin`, { + cwd: directory, + stdio: "pipe", + }); + const packageManagerGlobalBinaryPaths = getGlobalBinaryPaths(); + + const globalPackageManager = Object.keys( + packageManagerGlobalBinaryPaths + ).find((packageManager) => { + const packageManagerBinPath = + packageManagerGlobalBinaryPaths[packageManager as PackageManager]; + if (packageManagerBinPath && turboBinaryPathFromGlobal) { + return turboBinaryPathFromGlobal.includes(packageManagerBinPath); + } + + return false; + }) as PackageManager; + + if (turboBinaryPathFromGlobal && globalPackageManager) { + // figure which package manager we need to upgrade + return getGlobalUpgradeCommand(globalPackageManager, to); + } else { + const packageManager = getPackageManager({ directory }); + // we didn't find a global install, so we'll try to find a local one + const { installType, isUsingWorkspaces } = getInstallType({ directory }); + if (packageManager && installType) { + const packageManagerVersion = getPackageManagerVersion( + packageManager, + directory + ); + + return getLocalUpgradeCommand({ + packageManager, + packageManagerVersion, + installType, + isUsingWorkspaces, + to, + }); + } + } + + return undefined; +} diff --git a/packages/turbo-codemod/src/commands/migrate/types.ts b/packages/turbo-codemod/src/commands/migrate/types.ts new file mode 100644 index 0000000..ae90965 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/types.ts @@ -0,0 +1,9 @@ +import { TransformerOptions } from "../../types"; + +export type MigrateCommandArgument = "string" | undefined; + +export interface MigrateCommandOptions extends TransformerOptions { + from?: string; + to?: string; + install: boolean; +} diff --git a/packages/turbo-codemod/src/commands/migrate/utils.ts b/packages/turbo-codemod/src/commands/migrate/utils.ts new file mode 100644 index 0000000..512d78b --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/utils.ts @@ -0,0 +1,16 @@ +import { execSync, ExecSyncOptions } from "child_process"; + +function exec( + command: string, + opts: ExecSyncOptions, + fallback?: string +): string | undefined { + try { + const rawResult = execSync(command, opts); + return rawResult.toString("utf8").trim(); + } catch (err) { + return fallback || undefined; + } +} + +export { exec }; diff --git a/packages/turbo-codemod/src/commands/transform/index.ts b/packages/turbo-codemod/src/commands/transform/index.ts new file mode 100644 index 0000000..e3b86aa --- /dev/null +++ b/packages/turbo-codemod/src/commands/transform/index.ts @@ -0,0 +1,101 @@ +import chalk from "chalk"; +import inquirer from "inquirer"; + +import loadTransformers from "../../utils/loadTransformers"; +import checkGitStatus from "../../utils/checkGitStatus"; +import directoryInfo from "../../utils/directoryInfo"; +import type { + TransformCommandOptions, + TransformCommandArgument, +} from "./types"; +import { Runner } from "../../runner"; + +export default async function transform( + transform: TransformCommandArgument, + directory: TransformCommandArgument, + options: TransformCommandOptions +) { + const transforms = loadTransformers(); + if (options.list) { + console.log( + transforms + .map((transform) => `- ${chalk.cyan(transform.value)}`) + .join("\n") + ); + return process.exit(0); + } + + // check git status + if (!options.dry) { + checkGitStatus({ directory, force: options.force }); + } + + const answers = await inquirer.prompt<{ + directoryInput?: string; + transformerInput?: string; + }>([ + { + type: "input", + name: "directoryInput", + message: "Where is the root of the repo where the transform should run?", + when: !directory, + default: ".", + validate: (directory: string) => { + const { exists, absolute } = directoryInfo({ directory }); + if (exists) { + return true; + } else { + return `Directory ${chalk.dim(`(${absolute})`)} does not exist`; + } + }, + filter: (directory: string) => directory.trim(), + }, + { + type: "list", + name: "transformerInput", + message: "Which transform would you like to apply?", + when: !transform, + pageSize: transforms.length, + choices: transforms, + }, + ]); + + const { + directoryInput: selectedDirectory = directory as string, + transformerInput: selectedTransformer = transform as string, + } = answers; + const { exists, absolute: root } = directoryInfo({ + directory: selectedDirectory, + }); + if (!exists) { + console.error(`Directory ${chalk.dim(`(${root})`)} does not exist`); + return process.exit(1); + } + + const transformKeys = transforms.map((transform) => transform.value); + const transformData = transforms.find( + (transform) => transform.value === selectedTransformer + ); + + // validate transforms + if (!transformData) { + console.error( + `Invalid transform choice ${chalk.dim(`(${transform})`)}, pick one of:` + ); + console.error(transformKeys.map((key) => `- ${key}`).join("\n")); + return process.exit(1); + } + + // run the transform + const result = transformData.transformer({ + root, + options, + }); + + if (result.fatalError) { + // Runner already logs this, so we can just exit + return process.exit(1); + } + + Runner.logResults(result); +} diff --git a/packages/turbo-codemod/src/commands/transform/types.ts b/packages/turbo-codemod/src/commands/transform/types.ts new file mode 100644 index 0000000..9ac2db0 --- /dev/null +++ b/packages/turbo-codemod/src/commands/transform/types.ts @@ -0,0 +1,7 @@ +import { TransformerOptions } from "../../types"; + +export type TransformCommandArgument = "string" | undefined; + +export interface TransformCommandOptions extends TransformerOptions { + list: boolean; +} diff --git a/packages/turbo-codemod/src/runner/FileTransform.ts b/packages/turbo-codemod/src/runner/FileTransform.ts new file mode 100644 index 0000000..3b23f73 --- /dev/null +++ b/packages/turbo-codemod/src/runner/FileTransform.ts @@ -0,0 +1,94 @@ +import chalk from "chalk"; +import { diffLines, Change, diffJson } from "diff"; +import fs from "fs-extra"; +import os from "os"; +import path from "path"; + +import type { FileTransformArgs, LogFileArgs } from "./types"; + +export default class FileTransform { + filePath: string; + rootPath: string; + before: string | object; + after?: string | object; + error?: Error; + changes: Array<Change> = []; + + constructor(args: FileTransformArgs) { + this.filePath = args.filePath; + this.rootPath = args.rootPath; + this.after = args.after; + this.error = args.error; + + // load original file for comparison + if (args.before === undefined) { + try { + if (path.extname(args.filePath) === ".json") { + this.before = fs.readJsonSync(args.filePath); + } else { + this.before = fs.readFileSync(args.filePath); + } + } catch (err) { + this.before = ""; + } + } else if (args.before === null) { + this.before = ""; + } else { + this.before = args.before; + } + + // determine diff + if (args.after) { + if (typeof this.before === "object" || typeof args.after === "object") { + this.changes = diffJson(this.before, args.after); + } else { + this.changes = diffLines(this.before, args.after); + } + } else { + this.changes = []; + } + } + + fileName(): string { + return path.relative(this.rootPath, this.filePath); + } + + write(): void { + if (this.after) { + if (typeof this.after === "object") { + fs.writeJsonSync(this.filePath, this.after, { spaces: 2 }); + } else { + fs.writeFileSync(this.filePath, this.after); + } + } + } + + additions(): number { + return this.changes.filter((c) => c.added).length; + } + + deletions(): number { + return this.changes.filter((c) => c.removed).length; + } + + hasChanges(): boolean { + return this.additions() > 0 || this.deletions() > 0; + } + + log(args: LogFileArgs): void { + if (args.diff) { + this.changes.forEach((part) => { + if (part.added) { + process.stdout.write(chalk.green(part.value)); + } else if (part.removed) { + process.stdout.write(chalk.red(part.value)); + } else { + process.stdout.write(chalk.dim(part.value)); + } + }); + console.log(os.EOL); + } else { + console.log(this.after); + } + } +} diff --git a/packages/turbo-codemod/src/runner/Runner.ts b/packages/turbo-codemod/src/runner/Runner.ts new file mode 100644 index 0000000..8f8803d --- /dev/null +++ b/packages/turbo-codemod/src/runner/Runner.ts @@ -0,0 +1,132 @@ +import chalk from "chalk"; + +import FileTransform from "./FileTransform"; +import Logger from "../utils/logger"; +import type { UtilityArgs } from "../types"; +import type { + FileResult, + ModifyFileArgs, + AbortTransformArgs, + TransformerResults, +} from "./types"; + +class Runner { + transform: string; + rootPath: string; + dry: boolean; + print: boolean; + modifications: Record<string, FileTransform> = {}; + logger: Logger; + + constructor(options: UtilityArgs) { + this.transform = options.transformer; + this.rootPath = options.rootPath; + this.dry = options.dry; + this.print = options.print; + this.logger = new Logger(options); + } + + abortTransform(args: AbortTransformArgs): TransformerResults { + this.logger.error(args.reason); + return { + fatalError: new Error(args.reason), + changes: args.changes || {}, + }; + } + + // add a file to be transformed + modifyFile(args: ModifyFileArgs): void { + this.modifications[args.filePath] = new FileTransform({ + rootPath: this.rootPath, + ...args, + }); + } + + // execute all transforms and track results for reporting + finish(): TransformerResults { + const results: TransformerResults = { changes: {} }; + // perform all actions and track results + Object.keys(this.modifications).forEach((filePath) => { + const mod = this.modifications[filePath]; + const result: FileResult = { + action: "unchanged", + additions: mod.additions(), + deletions: mod.deletions(), + }; + + if (mod.hasChanges()) { + if (this.dry) { + result.action = "skipped"; + this.logger.skipped(chalk.dim(mod.fileName())); + } else { + try { + mod.write(); + result.action = "modified"; + this.logger.modified(chalk.bold(mod.fileName())); + } catch (err) { + let message = "Unknown error"; + if (err instanceof Error) { + message = err.message; + } + result.error = new Error(message); + result.action = "error"; + this.logger.error(mod.fileName(), message); + } + } + + if (this.print) { + mod.log({ diff: true }); + } + } else { + this.logger.unchanged(chalk.dim(mod.fileName())); + } + + results.changes[mod.fileName()] = result; + }); + + const encounteredError = Object.keys(results.changes).some((fileName) => { + return results.changes[fileName].action === "error"; + }); + + if (encounteredError) { + return this.abortTransform({ + reason: "Encountered an error while transforming files", + changes: results.changes, + }); + } + + return results; + } + + static logResults(results: TransformerResults): void { + const changedFiles = Object.keys(results.changes); + console.log(); + if (changedFiles.length > 0) { + console.log(chalk.bold(`Results:`)); + const table: Record< + string, + { + action: FileResult["action"]; + additions: FileResult["additions"]; + deletions: FileResult["deletions"]; + error?: string; + } + > = {}; + + changedFiles.forEach((fileName) => { + const fileChanges = results.changes[fileName]; + table[fileName] = { + action: fileChanges.action, + additions: fileChanges.additions, + deletions: fileChanges.deletions, + error: fileChanges.error?.message || "None", + }; + }); + + console.table(table); + console.log(); + } + } +} + +export default Runner; diff --git a/packages/turbo-codemod/src/runner/index.ts b/packages/turbo-codemod/src/runner/index.ts new file mode 100644 index 0000000..2aa323d --- /dev/null +++ b/packages/turbo-codemod/src/runner/index.ts @@ -0,0 +1,3 @@ +export { default as Runner } from "./Runner"; + +export type { TransformerResults, FileDiffer, FileWriter } from "./types"; diff --git a/packages/turbo-codemod/src/runner/types.ts b/packages/turbo-codemod/src/runner/types.ts new file mode 100644 index 0000000..e7c37d4 --- /dev/null +++ b/packages/turbo-codemod/src/runner/types.ts @@ -0,0 +1,40 @@ +import { Change } from "diff"; + +export interface FileResult { + action: "skipped" | "modified" | "unchanged" | "error"; + error?: Error; + additions: number; + deletions: number; +} + +export interface FileTransformArgs extends ModifyFileArgs { + rootPath: string; +} + +export interface ModifyFileArgs { + filePath: string; + before?: string | object; + after?: string | object; + error?: Error; +} + +export interface AbortTransformArgs { + reason: string; + changes?: Record<string, FileResult>; +} + +export interface LogFileArgs { + diff?: boolean; +} + +export type FileWriter = (filePath: string, contents: string | object) => void; + +export type FileDiffer = ( + before: string | object, + after: string | object +) => Array<Change>; + +export interface TransformerResults { + fatalError?: Error; + changes: Record<string, FileResult>; +} diff --git a/packages/turbo-codemod/src/transforms/README.md b/packages/turbo-codemod/src/transforms/README.md new file mode 100644 index 0000000..8e4430f --- /dev/null +++ b/packages/turbo-codemod/src/transforms/README.md @@ -0,0 +1,36 @@ +# `@turbo/codemod` Transformers + +## Adding new transformers + +Add new transformers using the [plopjs](https://github.com/plopjs/plop) template by running: + +```bash +pnpm add-transformer +``` + +New Transformers will be automatically surfaced to the `transform` CLI command and used by the `migrate` CLI command when appropriate. + +## How it works + +Transformers are loaded automatically from the `src/transforms/` directory via the [`loadTransforms`](../utils/loadTransformers.ts) function. + +All new transformers must contain a default export that matches the [`Transformer`](../types.ts) type: + +```ts +export type Transformer = { + name: string; + value: string; + introducedIn: string; + transformer: (args: TransformerArgs) => TransformerResults; +}; +``` + +## Writing a Transform + +Transforms are ran using the [TransformRunner](../runner/Runner.ts). This class is designed to make writing transforms as simple as possible by abstracting away all of the boilerplate that determines what should be logged, saved, or output as a result. + +To use the TransformRunner: + +1. Transform each file in memory (do not write it back to disk `TransformRunner` takes care of this depending on the options passed in by the user), and pass to `TransformRunner.modifyFile` method. +2. If the transform encounters an unrecoverable error, pass it to the `TransformRunner.abortTransform` method. +3. When all files have been modified and passed to `TransformRunner.modifyFile`, call `TransformRunner.finish` method to write the files to disk (when not running in `dry` mode) and log the results. diff --git a/packages/turbo-codemod/src/transforms/add-package-manager.ts b/packages/turbo-codemod/src/transforms/add-package-manager.ts new file mode 100644 index 0000000..bd6581f --- /dev/null +++ b/packages/turbo-codemod/src/transforms/add-package-manager.ts @@ -0,0 +1,75 @@ +import path from "path"; +import fs from "fs-extra"; + +import getPackageManager from "../utils/getPackageManager"; +import getPackageManagerVersion from "../utils/getPackageManagerVersion"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import { TransformerResults } from "../runner"; +import type { TransformerArgs } from "../types"; + +// transformer details +const TRANSFORMER = "add-package-manager"; +const DESCRIPTION = "Set the `packageManager` key in root `package.json` file"; +const INTRODUCED_IN = "1.1.0"; + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info(`Set "packageManager" key in root "package.json" file...`); + const packageManager = getPackageManager({ directory: root }); + if (!packageManager) { + return runner.abortTransform({ + reason: `Unable to determine package manager for ${root}`, + }); + } + + // handle workspaces... + let version = null; + try { + version = getPackageManagerVersion(packageManager, root); + } catch (err) { + return runner.abortTransform({ + reason: `Unable to determine package manager version for ${root}`, + }); + } + const pkgManagerString = `${packageManager}@${version}`; + const rootPackageJsonPath = path.join(root, "package.json"); + const rootPackageJson = fs.readJsonSync(rootPackageJsonPath); + const allWorkspaces = [ + { + name: "package.json", + path: root, + packageJson: { + ...rootPackageJson, + packageJsonPath: rootPackageJsonPath, + }, + }, + ]; + + for (const workspace of allWorkspaces) { + const { packageJsonPath, ...pkgJson } = workspace.packageJson; + const newJson = { ...pkgJson, packageManager: pkgManagerString }; + runner.modifyFile({ + filePath: packageJsonPath, + after: newJson, + }); + } + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/transforms/create-turbo-config.ts b/packages/turbo-codemod/src/transforms/create-turbo-config.ts new file mode 100644 index 0000000..0e8549a --- /dev/null +++ b/packages/turbo-codemod/src/transforms/create-turbo-config.ts @@ -0,0 +1,70 @@ +import fs from "fs-extra"; +import path from "path"; + +import { TransformerResults } from "../runner"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import type { TransformerArgs } from "../types"; + +// transformer details +const TRANSFORMER = "create-turbo-config"; +const DESCRIPTION = + 'Create the `turbo.json` file from an existing "turbo" key in `package.json`'; +const INTRODUCED_IN = "1.1.0"; + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info(`Migrating "package.json" "turbo" key to "turbo.json" file...`); + const turboConfigPath = path.join(root, "turbo.json"); + const rootPackageJsonPath = path.join(root, "package.json"); + if (!fs.existsSync(rootPackageJsonPath)) { + return runner.abortTransform({ + reason: `No package.json found at ${root}. Is the path correct?`, + }); + } + + // read files + const rootPackageJson = fs.readJsonSync(rootPackageJsonPath); + let rootTurboJson = null; + try { + rootTurboJson = fs.readJSONSync(turboConfigPath); + } catch (err) { + rootTurboJson = null; + } + + // modify files + let transformedPackageJson = rootPackageJson; + let transformedTurboConfig = rootTurboJson; + if (!rootTurboJson && rootPackageJson["turbo"]) { + const { turbo: turboConfig, ...remainingPkgJson } = rootPackageJson; + transformedTurboConfig = turboConfig; + transformedPackageJson = remainingPkgJson; + } + + runner.modifyFile({ + filePath: turboConfigPath, + after: transformedTurboConfig, + }); + runner.modifyFile({ + filePath: rootPackageJsonPath, + after: transformedPackageJson, + }); + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts b/packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts new file mode 100644 index 0000000..ef3a34c --- /dev/null +++ b/packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts @@ -0,0 +1,181 @@ +import fs from "fs-extra"; +import path from "path"; +import { getTurboConfigs } from "@turbo/utils"; +import type { Schema, Pipeline } from "@turbo/types"; + +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import { TransformerResults } from "../runner"; +import type { TransformerArgs } from "../types"; + +// transformer details +const TRANSFORMER = "migrate-env-var-dependencies"; +const DESCRIPTION = + 'Migrate environment variable dependencies from "dependsOn" to "env" in `turbo.json`'; +const INTRODUCED_IN = "1.5.0"; + +export function hasLegacyEnvVarDependencies(config: Schema) { + const dependsOn = [ + "extends" in config ? [] : config.globalDependencies, + Object.values(config.pipeline).flatMap( + (pipeline) => pipeline.dependsOn ?? [] + ), + ].flat(); + const envVars = dependsOn.filter((dep) => dep?.startsWith("$")); + return { hasKeys: !!envVars.length, envVars }; +} + +export function migrateDependencies({ + env, + deps, +}: { + env?: string[]; + deps?: string[]; +}) { + const envDeps: Set<string> = new Set(env); + const otherDeps: string[] = []; + deps?.forEach((dep) => { + if (dep.startsWith("$")) { + envDeps.add(dep.slice(1)); + } else { + otherDeps.push(dep); + } + }); + if (envDeps.size) { + return { + deps: otherDeps, + env: Array.from(envDeps), + }; + } else { + return { env, deps }; + } +} + +export function migratePipeline(pipeline: Pipeline) { + const { deps: dependsOn, env } = migrateDependencies({ + env: pipeline.env, + deps: pipeline.dependsOn, + }); + const migratedPipeline = { ...pipeline }; + if (dependsOn) { + migratedPipeline.dependsOn = dependsOn; + } else { + delete migratedPipeline.dependsOn; + } + if (env && env.length) { + migratedPipeline.env = env; + } else { + delete migratedPipeline.env; + } + + return migratedPipeline; +} + +export function migrateGlobal(config: Schema) { + if ("extends" in config) { + return config; + } + + const { deps: globalDependencies, env } = migrateDependencies({ + env: config.globalEnv, + deps: config.globalDependencies, + }); + const migratedConfig = { ...config }; + if (globalDependencies && globalDependencies.length) { + migratedConfig.globalDependencies = globalDependencies; + } else { + delete migratedConfig.globalDependencies; + } + if (env && env.length) { + migratedConfig.globalEnv = env; + } else { + delete migratedConfig.globalEnv; + } + return migratedConfig; +} + +export function migrateConfig(config: Schema) { + let migratedConfig = migrateGlobal(config); + Object.keys(config.pipeline).forEach((pipelineKey) => { + config.pipeline; + if (migratedConfig.pipeline && config.pipeline[pipelineKey]) { + const pipeline = migratedConfig.pipeline[pipelineKey]; + migratedConfig.pipeline[pipelineKey] = { + ...pipeline, + ...migratePipeline(pipeline), + }; + } + }); + return migratedConfig; +} + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info( + `Migrating environment variable dependencies from "globalDependencies" and "dependsOn" to "env" in "turbo.json"...` + ); + + // validate we don't have a package.json config + const packageJsonPath = path.join(root, "package.json"); + let packageJSON = {}; + try { + packageJSON = fs.readJSONSync(packageJsonPath); + } catch (e) { + // readJSONSync probably failed because the file doesn't exist + } + + if ("turbo" in packageJSON) { + return runner.abortTransform({ + reason: + '"turbo" key detected in package.json. Run `npx @turbo/codemod transform create-turbo-config` first', + }); + } + + // validate we have a root config + const turboConfigPath = path.join(root, "turbo.json"); + if (!fs.existsSync(turboConfigPath)) { + return runner.abortTransform({ + reason: `No turbo.json found at ${root}. Is the path correct?`, + }); + } + + let turboJson: Schema = fs.readJsonSync(turboConfigPath); + if (hasLegacyEnvVarDependencies(turboJson).hasKeys) { + turboJson = migrateConfig(turboJson); + } + + runner.modifyFile({ + filePath: turboConfigPath, + after: turboJson, + }); + + // find and migrate any workspace configs + const workspaceConfigs = getTurboConfigs(root); + workspaceConfigs.forEach((workspaceConfig) => { + const { config, turboConfigPath } = workspaceConfig; + if (hasLegacyEnvVarDependencies(config).hasKeys) { + runner.modifyFile({ + filePath: turboConfigPath, + after: migrateConfig(config), + }); + } + }); + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/transforms/set-default-outputs.ts b/packages/turbo-codemod/src/transforms/set-default-outputs.ts new file mode 100644 index 0000000..44f7fd1 --- /dev/null +++ b/packages/turbo-codemod/src/transforms/set-default-outputs.ts @@ -0,0 +1,97 @@ +import path from "path"; +import fs from "fs-extra"; +import { getTurboConfigs } from "@turbo/utils"; +import type { Schema as TurboJsonSchema } from "@turbo/types"; + +import type { TransformerArgs } from "../types"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import { TransformerResults } from "../runner"; + +const DEFAULT_OUTPUTS = ["dist/**", "build/**"]; + +// transformer details +const TRANSFORMER = "set-default-outputs"; +const DESCRIPTION = + 'Add the "outputs" key with defaults where it is missing in `turbo.json`'; +const INTRODUCED_IN = "1.7.0"; + +function migrateConfig(config: TurboJsonSchema) { + for (const [_, taskDef] of Object.entries(config.pipeline)) { + if (taskDef.cache !== false) { + if (!taskDef.outputs) { + taskDef.outputs = DEFAULT_OUTPUTS; + } else if ( + Array.isArray(taskDef.outputs) && + taskDef.outputs.length === 0 + ) { + delete taskDef.outputs; + } + } + } + + return config; +} + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + // If `turbo` key is detected in package.json, require user to run the other codemod first. + const packageJsonPath = path.join(root, "package.json"); + // package.json should always exist, but if it doesn't, it would be a silly place to blow up this codemod + let packageJSON = {}; + + try { + packageJSON = fs.readJSONSync(packageJsonPath); + } catch (e) { + // readJSONSync probably failed because the file doesn't exist + } + + if ("turbo" in packageJSON) { + return runner.abortTransform({ + reason: + '"turbo" key detected in package.json. Run `npx @turbo/codemod transform create-turbo-config` first', + }); + } + + log.info(`Adding default \`outputs\` key into tasks if it doesn't exist`); + const turboConfigPath = path.join(root, "turbo.json"); + if (!fs.existsSync(turboConfigPath)) { + return runner.abortTransform({ + reason: `No turbo.json found at ${root}. Is the path correct?`, + }); + } + + const turboJson: TurboJsonSchema = fs.readJsonSync(turboConfigPath); + runner.modifyFile({ + filePath: turboConfigPath, + after: migrateConfig(turboJson), + }); + + // find and migrate any workspace configs + const workspaceConfigs = getTurboConfigs(root); + workspaceConfigs.forEach((workspaceConfig) => { + const { config, turboConfigPath } = workspaceConfig; + runner.modifyFile({ + filePath: turboConfigPath, + after: migrateConfig(config), + }); + }); + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/types.ts b/packages/turbo-codemod/src/types.ts new file mode 100644 index 0000000..d5c13c3 --- /dev/null +++ b/packages/turbo-codemod/src/types.ts @@ -0,0 +1,24 @@ +import { TransformerResults } from "./runner"; + +export type Transformer = { + name: string; + value: string; + introducedIn: string; + transformer: (args: TransformerArgs) => TransformerResults; +}; + +export type TransformerOptions = { + force: boolean; + dry: boolean; + print: boolean; +}; + +export type TransformerArgs = { + root: string; + options: TransformerOptions; +}; + +export interface UtilityArgs extends TransformerOptions { + transformer: string; + rootPath: string; +} diff --git a/packages/turbo-codemod/src/utils/checkGitStatus.ts b/packages/turbo-codemod/src/utils/checkGitStatus.ts new file mode 100644 index 0000000..68d39ae --- /dev/null +++ b/packages/turbo-codemod/src/utils/checkGitStatus.ts @@ -0,0 +1,40 @@ +import chalk from "chalk"; +import isGitClean from "is-git-clean"; + +export default function checkGitStatus({ + directory, + force, +}: { + directory?: string; + force: boolean; +}) { + let clean = false; + let errorMessage = "Unable to determine if git directory is clean"; + try { + clean = isGitClean.sync(directory || process.cwd()); + errorMessage = "Git directory is not clean"; + } catch (err: any) { + if (err && err.stderr && err.stderr.indexOf("not a git repository") >= 0) { + clean = true; + } + } + + if (!clean) { + if (force) { + console.log( + `${chalk.yellow("WARNING")}: ${errorMessage}. Forcibly continuing...` + ); + } else { + console.log("Thank you for using @turbo/codemod!"); + console.log( + chalk.yellow( + "\nBut before we continue, please stash or commit your git changes." + ) + ); + console.log( + "\nYou may use the --force flag to override this safety check." + ); + process.exit(1); + } + } +} diff --git a/packages/turbo-codemod/src/utils/directoryInfo.ts b/packages/turbo-codemod/src/utils/directoryInfo.ts new file mode 100644 index 0000000..7cb3594 --- /dev/null +++ b/packages/turbo-codemod/src/utils/directoryInfo.ts @@ -0,0 +1,10 @@ +import path from "path"; +import fs from "fs"; + +export default function directoryInfo({ directory }: { directory: string }) { + const dir = path.isAbsolute(directory) + ? directory + : path.join(process.cwd(), directory); + + return { exists: fs.existsSync(dir), absolute: dir }; +} diff --git a/packages/turbo-codemod/src/utils/getPackageManager.ts b/packages/turbo-codemod/src/utils/getPackageManager.ts new file mode 100644 index 0000000..1df0acc --- /dev/null +++ b/packages/turbo-codemod/src/utils/getPackageManager.ts @@ -0,0 +1,42 @@ +import findUp from "find-up"; +import path from "path"; + +export type PackageManager = "yarn" | "pnpm" | "npm"; + +const cache: { [cwd: string]: PackageManager } = {}; + +export default function getPackageManager({ + directory, +}: { directory?: string } = {}): PackageManager | undefined { + const cwd = directory || process.cwd(); + if (cache[cwd]) { + return cache[cwd]; + } + + const lockFile = findUp.sync( + ["yarn.lock", "pnpm-lock.yaml", "package-lock.json"], + { + cwd, + } + ); + + if (!lockFile) { + return; + } + + switch (path.basename(lockFile)) { + case "yarn.lock": + cache[cwd] = "yarn"; + break; + + case "pnpm-lock.yaml": + cache[cwd] = "pnpm"; + break; + + case "package-lock.json": + cache[cwd] = "npm"; + break; + } + + return cache[cwd]; +} diff --git a/packages/turbo-codemod/src/utils/getPackageManagerVersion.ts b/packages/turbo-codemod/src/utils/getPackageManagerVersion.ts new file mode 100644 index 0000000..54a572a --- /dev/null +++ b/packages/turbo-codemod/src/utils/getPackageManagerVersion.ts @@ -0,0 +1,16 @@ +import { execSync } from "child_process"; +import type { PackageManager } from "./getPackageManager"; + +export default function getPackageManagerVersion( + packageManager: PackageManager, + root: string +): string { + switch (packageManager) { + case "yarn": + return execSync("yarn --version", { cwd: root }).toString().trim(); + case "pnpm": + return execSync("pnpm --version", { cwd: root }).toString().trim(); + case "npm": + return execSync("npm --version", { cwd: root }).toString().trim(); + } +} diff --git a/packages/turbo-codemod/src/utils/getTransformerHelpers.ts b/packages/turbo-codemod/src/utils/getTransformerHelpers.ts new file mode 100644 index 0000000..e37da6e --- /dev/null +++ b/packages/turbo-codemod/src/utils/getTransformerHelpers.ts @@ -0,0 +1,23 @@ +import { TransformerOptions } from "../types"; +import { Runner } from "../runner"; +import Logger from "./logger"; + +export default function getTransformerHelpers({ + transformer, + rootPath, + options, +}: { + transformer: string; + rootPath: string; + options: TransformerOptions; +}) { + const utilArgs = { + transformer, + rootPath, + ...options, + }; + const log = new Logger(utilArgs); + const runner = new Runner(utilArgs); + + return { log, runner }; +} diff --git a/packages/turbo-codemod/src/utils/loadTransformers.ts b/packages/turbo-codemod/src/utils/loadTransformers.ts new file mode 100644 index 0000000..9ba5ca1 --- /dev/null +++ b/packages/turbo-codemod/src/utils/loadTransformers.ts @@ -0,0 +1,27 @@ +import path from "path"; +import fs from "fs-extra"; +import type { Transformer } from "../types"; + +// transforms/ is a sibling when built in in dist/ +export const transformerDirectory = + process.env.NODE_ENV === "test" + ? path.join(__dirname, "../transforms") + : path.join(__dirname, "./transforms"); + +export default function loadTransformers(): Array<Transformer> { + const transformerFiles = fs.readdirSync(transformerDirectory); + return transformerFiles + .map((transformerFilename) => { + const transformerPath = path.join( + transformerDirectory, + transformerFilename + ); + try { + return require(transformerPath).default; + } catch (e) { + // we ignore this error because it's likely that the file is not a transformer (README, etc) + return undefined; + } + }) + .filter(Boolean); +} diff --git a/packages/turbo-codemod/src/utils/logger.ts b/packages/turbo-codemod/src/utils/logger.ts new file mode 100644 index 0000000..123a836 --- /dev/null +++ b/packages/turbo-codemod/src/utils/logger.ts @@ -0,0 +1,47 @@ +import chalk from "chalk"; +import { UtilityArgs } from "../types"; + +export default class Logger { + transform: string; + dry: boolean; + + constructor(args: UtilityArgs) { + this.transform = args.transformer; + this.dry = args.dry; + } + modified(...args: any[]) { + console.log( + chalk.green(` MODIFIED `), + ...args, + this.dry ? chalk.dim(`(dry run)`) : "" + ); + } + unchanged(...args: any[]) { + console.log( + chalk.gray(` UNCHANGED `), + ...args, + this.dry ? chalk.dim(`(dry run)`) : "" + ); + } + skipped(...args: any[]) { + console.log( + chalk.yellow(` SKIPPED `), + ...args, + this.dry ? chalk.dim(`(dry run)`) : "" + ); + } + error(...args: any[]) { + console.log( + chalk.red(` ERROR `), + ...args, + this.dry ? chalk.dim(`(dry run)`) : "" + ); + } + info(...args: any[]) { + console.log( + chalk.bold(` INFO `), + ...args, + this.dry ? chalk.dim(`(dry run)`) : "" + ); + } +} diff --git a/packages/turbo-codemod/src/utils/looksLikeRepo.ts b/packages/turbo-codemod/src/utils/looksLikeRepo.ts new file mode 100644 index 0000000..77f0e5c --- /dev/null +++ b/packages/turbo-codemod/src/utils/looksLikeRepo.ts @@ -0,0 +1,12 @@ +import path from "path"; +import { existsSync } from "fs-extra"; + +const HINTS = ["package.json", "turbo.json", ".git"]; + +export default function looksLikeRepo({ + directory, +}: { + directory: string; +}): boolean { + return HINTS.some((hint) => existsSync(path.join(directory, hint))); +} diff --git a/packages/turbo-codemod/src/utils/notifyUpdate.ts b/packages/turbo-codemod/src/utils/notifyUpdate.ts new file mode 100644 index 0000000..634ffd8 --- /dev/null +++ b/packages/turbo-codemod/src/utils/notifyUpdate.ts @@ -0,0 +1,35 @@ +import chalk from "chalk"; +import checkForUpdate from "update-check"; + +import cliPkgJson from "../../package.json"; +import getWorkspaceImplementation from "./getPackageManager"; + +const update = checkForUpdate(cliPkgJson).catch(() => null); + +export default async function notifyUpdate(): Promise<void> { + try { + const res = await update; + if (res?.latest) { + const ws = getWorkspaceImplementation(); + + console.log(); + console.log( + chalk.yellow.bold("A new version of `@turbo/codemod` is available!") + ); + console.log( + "You can update by running: " + + chalk.cyan( + ws === "yarn" + ? "yarn global add @turbo/codemod" + : ws === "pnpm" + ? "pnpm i -g @turbo/codemod" + : "npm i -g @turbo/codemod" + ) + ); + console.log(); + } + process.exit(); + } catch (_e: any) { + // ignore error + } +} diff --git a/packages/turbo-codemod/templates/transformer.hbs b/packages/turbo-codemod/templates/transformer.hbs new file mode 100644 index 0000000..593490a --- /dev/null +++ b/packages/turbo-codemod/templates/transformer.hbs @@ -0,0 +1,45 @@ +import { TransformerArgs } from "../types"; +import { TransformerResults } from "../runner"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; + +// transformer details +const TRANSFORMER = "{{ name }}"; +const DESCRIPTION = "{{ description }}"; +const INTRODUCED_IN = "{{ introducedIn }}"; + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info("Short description about {{ name }}") + + /* + Make changes to required files, and track each modified file with: + + runner.modifyFile({ + filePath: packageJsonPath, // absolute path to file + after: transformedFile, // file after modifications have been made + }); + + This automatically handles all cases of print / dry etc. + */ + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; + + diff --git a/packages/turbo-codemod/templates/transformer.test.hbs b/packages/turbo-codemod/templates/transformer.test.hbs new file mode 100644 index 0000000..c63a9df --- /dev/null +++ b/packages/turbo-codemod/templates/transformer.test.hbs @@ -0,0 +1,25 @@ +import { transformer } from "../src/transforms/{{ name }}"; +import { setupTestFixtures } from "./test-utils"; + +describe("{{ name }}", () => { + + const { useFixture } = setupTestFixtures({ test: "{{ name }}" }); + + test("basic", () => { + // load the fixture for the test + const { root, read, readJson } = useFixture({ + fixture: "specific-fixture", + }); + + // run the transformer + const result = transformer({ + root, + options: { force: false, dry: false, print: false }, + }); + + // result should be correct + expect(result.fatalError).toBeUndefined(); + expect(result.changes).toMatchInlineSnapshot(); + }); +}); + diff --git a/packages/turbo-codemod/tsconfig.json b/packages/turbo-codemod/tsconfig.json new file mode 100644 index 0000000..0620a3c --- /dev/null +++ b/packages/turbo-codemod/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@turbo/tsconfig/library.json", + "compilerOptions": { + "rootDir": "." + } +} diff --git a/packages/turbo-codemod/tsup.config.ts b/packages/turbo-codemod/tsup.config.ts new file mode 100644 index 0000000..8e92107 --- /dev/null +++ b/packages/turbo-codemod/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, Options } from "tsup"; + +export default defineConfig((options: Options) => ({ + entry: ["src/cli.ts", "src/transforms/*.ts"], + format: ["cjs"], + clean: true, + minify: true, + ...options, +})); |
