aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/turbo-codemod
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /packages/turbo-codemod
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'packages/turbo-codemod')
-rw-r--r--packages/turbo-codemod/LICENSE373
-rw-r--r--packages/turbo-codemod/README.md55
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/has-package-manager/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/no-package-manager/package.json6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/add-package-manager/wrong-package-manager/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/package.json28
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/both-configs/turbo.json18
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-config/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-package-json-file/a-random-file.txt1
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/no-turbo-json-config/package.json24
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/create-turbo-config/turbo-json-config/turbo.json18
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-deps/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-package/README.md1
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/no-turbo/package.json6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces-dev-install/package.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/normal-workspaces/package.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/package.json8
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces-dev-install/pnpm-workspace.yaml3
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/package.json8
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/pnpm-workspaces/pnpm-workspace.yaml3
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package-dev-install/package.json8
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/get-turbo-upgrade-command/single-package/package.json8
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/env-dependencies/turbo.json21
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/migrated-env-dependencies/turbo.json25
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/no-turbo-json/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/package.json20
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/old-config/turbo.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/index.js6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/docs/turbo.json9
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/index.js6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/apps/web/turbo.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/package.json14
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/index.js6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/packages/ui/turbo.json9
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate-env-var-dependencies/workspace-configs/turbo.json21
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate/no-repo/README.md1
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/migrate/old-turbo/package.json26
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/invalid-outputs/turbo.json36
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-outputs/turbo.json14
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-pipeline/turbo.json5
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/no-turbo-json/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/package.json20
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-config/turbo.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/package.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/old-outputs/turbo.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/index.js6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/docs/turbo.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/index.js6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/apps/web/turbo.json10
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/package.json14
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/index.js6
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/package.json4
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/packages/ui/turbo.json7
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/set-default-outputs/workspace-configs/turbo.json12
-rw-r--r--packages/turbo-codemod/__tests__/__fixtures__/transform/basic/package.json8
-rw-r--r--packages/turbo-codemod/__tests__/add-package-manager.test.ts504
-rw-r--r--packages/turbo-codemod/__tests__/create-turbo-config.test.ts416
-rw-r--r--packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts576
-rw-r--r--packages/turbo-codemod/__tests__/migrate-env-var-dependencies.test.ts758
-rw-r--r--packages/turbo-codemod/__tests__/migrate.test.ts761
-rw-r--r--packages/turbo-codemod/__tests__/set-default-outputs.test.ts391
-rw-r--r--packages/turbo-codemod/__tests__/transform.test.ts172
-rw-r--r--packages/turbo-codemod/index.d.ts1
-rw-r--r--packages/turbo-codemod/jest.config.js18
-rw-r--r--packages/turbo-codemod/package.json67
-rw-r--r--packages/turbo-codemod/plopfile.js46
-rw-r--r--packages/turbo-codemod/src/cli.ts73
-rw-r--r--packages/turbo-codemod/src/commands/index.ts11
-rw-r--r--packages/turbo-codemod/src/commands/migrate/index.ts215
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts45
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts31
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts25
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts182
-rw-r--r--packages/turbo-codemod/src/commands/migrate/types.ts9
-rw-r--r--packages/turbo-codemod/src/commands/migrate/utils.ts16
-rw-r--r--packages/turbo-codemod/src/commands/transform/index.ts101
-rw-r--r--packages/turbo-codemod/src/commands/transform/types.ts7
-rw-r--r--packages/turbo-codemod/src/runner/FileTransform.ts94
-rw-r--r--packages/turbo-codemod/src/runner/Runner.ts132
-rw-r--r--packages/turbo-codemod/src/runner/index.ts3
-rw-r--r--packages/turbo-codemod/src/runner/types.ts40
-rw-r--r--packages/turbo-codemod/src/transforms/README.md36
-rw-r--r--packages/turbo-codemod/src/transforms/add-package-manager.ts75
-rw-r--r--packages/turbo-codemod/src/transforms/create-turbo-config.ts70
-rw-r--r--packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts181
-rw-r--r--packages/turbo-codemod/src/transforms/set-default-outputs.ts97
-rw-r--r--packages/turbo-codemod/src/types.ts24
-rw-r--r--packages/turbo-codemod/src/utils/checkGitStatus.ts40
-rw-r--r--packages/turbo-codemod/src/utils/directoryInfo.ts10
-rw-r--r--packages/turbo-codemod/src/utils/getPackageManager.ts42
-rw-r--r--packages/turbo-codemod/src/utils/getPackageManagerVersion.ts16
-rw-r--r--packages/turbo-codemod/src/utils/getTransformerHelpers.ts23
-rw-r--r--packages/turbo-codemod/src/utils/loadTransformers.ts27
-rw-r--r--packages/turbo-codemod/src/utils/logger.ts47
-rw-r--r--packages/turbo-codemod/src/utils/looksLikeRepo.ts12
-rw-r--r--packages/turbo-codemod/src/utils/notifyUpdate.ts35
-rw-r--r--packages/turbo-codemod/templates/transformer.hbs45
-rw-r--r--packages/turbo-codemod/templates/transformer.test.hbs25
-rw-r--r--packages/turbo-codemod/tsconfig.json6
-rw-r--r--packages/turbo-codemod/tsup.config.ts9
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,
+}));