diff --git a/packages/turbo-codemod/plopfile.js b/packages/turbo-codemod/generators/config.js similarity index 79% rename from packages/turbo-codemod/plopfile.js rename to packages/turbo-codemod/generators/config.js index 9cc2dd71beb611..c2d5c4fa344150 100644 --- a/packages/turbo-codemod/plopfile.js +++ b/packages/turbo-codemod/generators/config.js @@ -1,8 +1,8 @@ const fs = require("fs-extra"); +const path = require("path"); -module.exports = function plopConfig(plop) { - // controller generator - plop.setGenerator("controller", { +module.exports = function (plop, config) { + plop.setGenerator("transformer", { description: "Add a new transformer", prompts: [ { @@ -36,8 +36,13 @@ module.exports = function plopConfig(plop) { }, function createFixturesDirectory(answers) { process.chdir(plop.getPlopfilePath()); - const directory = `__tests__/__fixtures__/${answers.name}`; - fs.mkdirSync(`__tests__/__fixtures__/${answers.name}`); + const directory = path.join( + config.destBasePath, + "__tests__", + "__fixtures__", + answers.name + ); + fs.mkdirSync(directory); return `created empty ${directory} directory for fixtures`; }, diff --git a/packages/turbo-codemod/templates/transformer.hbs b/packages/turbo-codemod/generators/templates/transformer.hbs similarity index 100% rename from packages/turbo-codemod/templates/transformer.hbs rename to packages/turbo-codemod/generators/templates/transformer.hbs diff --git a/packages/turbo-codemod/templates/transformer.test.hbs b/packages/turbo-codemod/generators/templates/transformer.test.hbs similarity index 100% rename from packages/turbo-codemod/templates/transformer.test.hbs rename to packages/turbo-codemod/generators/templates/transformer.test.hbs diff --git a/packages/turbo-gen/LICENSE b/packages/turbo-gen/LICENSE new file mode 100644 index 00000000000000..fa0086a9522369 --- /dev/null +++ b/packages/turbo-gen/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-gen/README.md b/packages/turbo-gen/README.md new file mode 100644 index 00000000000000..17edd840e90aea --- /dev/null +++ b/packages/turbo-gen/README.md @@ -0,0 +1,66 @@ +# `@turbo/gen` + +> This package is currently in **beta**. Please report any issues you encounter, and give us feedback about your experience using it! + +Easily extend your Turborepo with new apps, and packages. Create new empty workspaces, copy existing workspaces, add workspaces from remote sources (just like `create-turbo`!).or run custom generators defined using [Plop](https://plopjs.com/) configurations. + +## Usage + +```bash +Usage: @turbo/gen [options] [command] + +Extend your Turborepo + +Options: + -v, --version Output the current version + -h, --help Display help for command + +Commands: + add|a [options] Add a new package or app to your project + generate|g [options] [generator-name] Run custom generators + help [command] display help for command +``` + +## Add + +Extend your Turborepo with new apps or packages. Create new empty workspaces, copy existing workspaces, or add workspaces from remote sources (just like `create-turbo`!). + +### Usage + +#### Blank Workspace + +```bash +@turbo/gen add +``` + +#### Copy a Local Workspace + +```bash +@turbo/gen add --copy +``` + +#### Copy a Remote Workspace + +```bash +@turbo/gen add -e +``` + +## Generate + +Extend your Turborepo with custom generators defined using [Plop](https://plopjs.com/) configurations. + +### Usage + +```bash +@turbo/gen generate [generator-name] +``` + +### Writing Generators + +`@turbo/gen` will search the root of your monorepo, and every workspace for generators defined at: + +```bash +generators/config.js +``` + +**NOTE**: By default, generators are run from the _root_ of the _workspace_ where they are defined. diff --git a/packages/turbo-gen/__tests__/test-utils.ts b/packages/turbo-gen/__tests__/test-utils.ts new file mode 100644 index 00000000000000..fa6c20420e3172 --- /dev/null +++ b/packages/turbo-gen/__tests__/test-utils.ts @@ -0,0 +1,34 @@ +import path from "path"; +import { PackageManager } from "@turbo/workspaces"; + +export function getWorkspaceDetailsMockReturnValue({ + root, + packageManager = "npm", +}: { + root: string; + packageManager: PackageManager; +}) { + return { + name: "mock-project", + packageManager, + paths: { + root, + packageJson: path.join(root, "package.json"), + lockfile: path.join(root, "yarn.lock"), + nodeModules: path.join(root, "node_modules"), + }, + workspaceData: { + globs: ["packages/*"], + workspaces: [ + { + name: "packages/mock-package", + paths: { + root: path.join(root, "packages/mock-package"), + packageJson: path.join(root, "packages/mock-package/package.json"), + nodeModules: path.join(root, "packages/mock-package/node_modules"), + }, + }, + ], + }, + }; +} diff --git a/packages/turbo-gen/jest.config.js b/packages/turbo-gen/jest.config.js new file mode 100644 index 00000000000000..b738f4b2bd92b0 --- /dev/null +++ b/packages/turbo-gen/jest.config.js @@ -0,0 +1,11 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest/presets/js-with-ts", + testEnvironment: "node", + testPathIgnorePatterns: ["/__fixtures__/", "/__tests__/test-utils.ts"], + coveragePathIgnorePatterns: ["/__fixtures__/", "/__tests__/test-utils.ts"], + transformIgnorePatterns: ["/node_modules/(?!(ansi-regex)/)"], + modulePathIgnorePatterns: ["/node_modules", "/dist"], + collectCoverage: true, + verbose: true, +}; diff --git a/packages/turbo-gen/package.json b/packages/turbo-gen/package.json new file mode 100644 index 00000000000000..d786ee7bd19cea --- /dev/null +++ b/packages/turbo-gen/package.json @@ -0,0 +1,56 @@ +{ + "name": "@turbo/gen", + "version": "0.0.1-alpha.3", + "description": "Extend a Turborepo", + "homepage": "https://turbo.build/repo", + "license": "MPL-2.0", + "repository": { + "type": "git", + "url": "https://github.com/vercel/turbo", + "directory": "packages/turbo-gen" + }, + "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" + }, + "dependencies": { + "chalk": "2.4.2", + "commander": "^10.0.0", + "fs-extra": "^10.1.0", + "inquirer": "^8.2.4", + "minimatch": "^9.0.0", + "node-plop": "^0.26.3", + "semver": "^7.3.8", + "update-check": "^1.5.4", + "validate-npm-package-name": "^5.0.0" + }, + "devDependencies": { + "@turbo/test-utils": "workspace:*", + "@turbo/tsconfig": "workspace:*", + "@turbo/utils": "workspace:*", + "@turbo/workspaces": "workspace:*", + "@types/fs-extra": "^9.0.13", + "@types/inquirer": "^8.2.5", + "@types/jest": "^27.4.0", + "@types/node": "^16.11.12", + "@types/semver": "^7.3.9", + "@types/validate-npm-package-name": "^4.0.0", + "eslint": "^7.23.0", + "jest": "^27.4.3", + "ts-jest": "^27.1.1", + "tsup": "^6.7.0", + "typescript": "^4.5.5" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/turbo-gen/src/cli.ts b/packages/turbo-gen/src/cli.ts new file mode 100644 index 00000000000000..96b595606831b5 --- /dev/null +++ b/packages/turbo-gen/src/cli.ts @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +import chalk from "chalk"; +import { Argument, Command, Option } from "commander"; +import notifyUpdate from "./utils/notifyUpdate"; +import { logger } from "@turbo/utils"; + +import { add, generate, raw } from "./commands"; +import cliPkg from "../package.json"; + +const turboGenCli = new Command(); + +turboGenCli + .name(chalk.bold(logger.turboGradient("@turbo/gen"))) + .description("Extend your Turborepo") + .version(cliPkg.version, "-v, --version", "Output the current version") + .helpOption("-h, --help", "Display help for command") + .showHelpAfterError(false); + +turboGenCli + .command("raw", { hidden: true }) + .argument("", "The type of generator to run") + .addOption(new Option("--json ", "Arguments as raw JSON")) + .action(raw); + +turboGenCli + .command("add") + .aliases(["a"]) + .description("Add a new package or app to your project") + .addOption( + new Option("-n, --name ", "Name for the new workspace") + ) + .addOption( + new Option("-b, --empty", "Generate an empty workspace") + .conflicts("copy") + .default(true) + ) + .addOption( + new Option( + "-c, --copy", + "Generate a workspace using an existing workspace as a template" + ).conflicts("empty") + ) + .addOption( + new Option( + "-d, --destination ", + "Where the new workspace should be created" + ) + ) + .addOption( + new Option("-w, --what ", "The type of workspace to create").choices([ + "app", + "package", + ]) + ) + .addOption( + new Option( + "-r, --root ", + "The root of your repository (default: directory with root turbo.json)" + ) + ) + .addOption( + new Option( + "-e, --example [github-url]", + `An example package to add. You can use a GitHub URL with any branch and/or subdirectory.` + ).implies({ copy: true }) + ) + .addOption( + new Option( + "-p, --example-path ", + `In a rare case, your GitHub URL might contain a branch name with +a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). +In this case, you must specify the path to the example separately: +--example-path foo/bar +` + ).implies({ copy: true }) + ) + .addOption( + new Option( + "--show-all-dependencies", + "Do not filter available dependencies by the workspace type" + ).default(false) + ) + .action(add); + +turboGenCli + .command("generate") + .aliases(["g", "gen"]) + .description("Run custom generators") + .addArgument( + new Argument("[generator-name]", "The name of the generator to run") + ) + .addOption( + new Option( + "-c, --config ", + "Generator configuration file (default: generators/config.js" + ) + ) + .addOption( + new Option( + "-r, --root ", + "The root of your repository (default: directory with root turbo.json)" + ) + ) + .addOption( + new Option( + "-a, --args ", + "Arguments passed directly to generator" + ).default([]) + ) + .action(generate); + +turboGenCli + .parseAsync() + .then(notifyUpdate) + .catch(async (reason) => { + console.log(); + if (reason.command) { + logger.error(`${chalk.bold(reason.command)} has failed.`); + } else { + logger.error("Unexpected error. Please report it as a bug:"); + console.log(reason); + } + console.log(); + await notifyUpdate(); + process.exit(1); + }); diff --git a/packages/turbo-gen/src/commands/add/index.ts b/packages/turbo-gen/src/commands/add/index.ts new file mode 100644 index 00000000000000..1dc7af25095b55 --- /dev/null +++ b/packages/turbo-gen/src/commands/add/index.ts @@ -0,0 +1,38 @@ +import { logger } from "@turbo/utils"; +import { getProject } from "../../utils/getProject"; +import { copy, empty } from "../../generators"; +import { WorkspaceType } from "../../generators/types"; + +export interface TurboGeneratorOptions { + name?: string; + // default to true + empty: boolean; + copy?: boolean; + destination?: string; + what?: WorkspaceType; + root?: string; + example?: string; + examplePath?: string; + // defaults to false + showAllDependencies: boolean; +} + +export async function add(opts: TurboGeneratorOptions) { + const project = await getProject(opts); + + console.log(); + const args = { project, opts }; + if (opts.copy) { + if (opts.example) { + logger.info(`Copy a remote workspace from ${opts.example}`); + } else { + logger.info(`Copy an existing workspace from ${project.name}`); + } + console.log(); + await copy(args); + } else { + logger.info(`Add an empty workspace to ${project.name}`); + console.log(); + await empty(args); + } +} diff --git a/packages/turbo-gen/src/commands/add/prompts.ts b/packages/turbo-gen/src/commands/add/prompts.ts new file mode 100644 index 00000000000000..d8a8893c693ad8 --- /dev/null +++ b/packages/turbo-gen/src/commands/add/prompts.ts @@ -0,0 +1,266 @@ +import fs from "fs-extra"; +import path from "path"; +import inquirer from "inquirer"; +import { minimatch } from "minimatch"; +import validName from "validate-npm-package-name"; +import type { Project, Workspace } from "@turbo/workspaces"; +import { validateDirectory } from "@turbo/utils"; +import { getWorkspaceStructure } from "../../utils/getWorkspaceStructure"; +import type { WorkspaceType } from "../../generators/types"; +import { getWorkspaceList } from "../../utils/getWorkspaceList"; +import type { DependencyGroups, PackageJson } from "../../types"; + +export async function name({ + override, + suggestion, + what, +}: { + override?: string; + suggestion?: string; + what: WorkspaceType; +}): Promise<{ answer: string }> { + const { validForNewPackages } = validName(override || ""); + if (override && validForNewPackages) { + return { answer: override }; + } + return inquirer.prompt<{ answer: string }>({ + type: "input", + name: "answer", + default: suggestion, + validate: (input: string) => { + const { validForNewPackages } = validName(input); + return validForNewPackages || `Invalid ${what} name`; + }, + message: `What is the name of the ${what}?`, + }); +} + +export async function what({ + override, +}: { + override?: WorkspaceType; +}): Promise<{ answer: WorkspaceType }> { + if (override) { + return { answer: override }; + } + + return inquirer.prompt<{ answer: WorkspaceType }>({ + type: "list", + name: "answer", + message: `What type of workspace should be added?`, + choices: [ + { + name: "app", + value: "app", + }, + { + name: "package", + value: "package", + }, + ], + }); +} + +export async function location({ + what, + name, + destination, + project, +}: { + what: "app" | "package"; + name: string; + destination?: string; + project: Project; +}): Promise<{ absolute: string; relative: string }> { + // handle names with scopes + const nameAsPath = name.includes("/") ? name.split("/")[1] : name; + + // handle destination option (NOTE: this intentionally allows adding packages to non workspace directories) + if (destination) { + const { valid, root } = validateDirectory(destination); + if (valid) { + return { + absolute: root, + relative: path.relative(project.paths.root, root), + }; + } + } + + // build default name based on what is being added + let newWorkspaceLocation: string | undefined = undefined; + const workspaceStructure = getWorkspaceStructure({ project }); + + if (what === "app" && workspaceStructure.hasRootApps) { + newWorkspaceLocation = `${project.paths.root}/apps/${nameAsPath}`; + } else if (what === "package" && workspaceStructure.hasRootPackages) { + newWorkspaceLocation = `${project.paths.root}/packages/${nameAsPath}`; + } + + const { answer } = await inquirer.prompt<{ + answer: string; + }>({ + type: "input", + name: "answer", + message: `Where should "${name}" be added?`, + default: newWorkspaceLocation + ? path.relative(project.paths.root, newWorkspaceLocation) + : undefined, + validate: (input: string) => { + const base = path.join(project.paths.root, input); + const { valid, error } = validateDirectory(base); + const isWorkspace = project.workspaceData.globs.some((glob) => + minimatch(input, glob) + ); + + if (valid && isWorkspace) { + return true; + } + + if (!isWorkspace) { + return `${input} is not a valid workspace location`; + } + + return error; + }, + }); + + return { + absolute: path.join(project.paths.root, answer), + relative: answer, + }; +} + +export async function source({ + workspaces, + name, +}: { + workspaces: Array; + name: string; +}) { + const sourceAnswer = await inquirer.prompt<{ + answer: Workspace; + }>({ + type: "list", + name: "answer", + loop: false, + pageSize: 25, + message: `Which workspace should "${name}" start from?`, + choices: workspaces.map((choice) => { + if (choice instanceof inquirer.Separator) { + return choice; + } + return { + name: ` ${choice.name}`, + value: choice, + }; + }), + }); + + return sourceAnswer; +} + +export async function dependencies({ + name, + project, + source, + showAllDependencies, +}: { + name: string; + project: Project; + source?: Workspace; + showAllDependencies?: boolean; +}) { + const selectedDependencies: DependencyGroups = { + dependencies: {}, + devDependencies: {}, + peerDependencies: {}, + optionalDependencies: {}, + }; + const { answer: addDependencies } = await confirm({ + message: `Add workspace dependencies to "${name}"?`, + }); + if (!addDependencies) { + return selectedDependencies; + } + + const { answer: dependencyGroups } = await inquirer.prompt<{ + answer: Array; + }>({ + type: "checkbox", + name: "answer", + message: `Select all dependencies types to modify for "${name}"`, + loop: false, + choices: [ + { name: "dependencies", value: "dependencies" }, + { name: "devDependencies", value: "devDependencies" }, + { name: "peerDependencies", value: "peerDependencies" }, + { name: "optionalDependencies", value: "optionalDependencies" }, + ], + }); + + // supported workspace dependencies (apps can never be dependencies) + let depChoices = getWorkspaceList({ + project, + what: "package", + showAllDependencies, + }); + + const sourcePackageJson = source + ? (fs.readJsonSync(source.paths.packageJson) as PackageJson) + : undefined; + + for (let group of dependencyGroups) { + const { answer: selected } = await inquirer.prompt<{ + answer: Array; + }>({ + type: "checkbox", + name: "answer", + default: + sourcePackageJson && Object.keys(sourcePackageJson?.[group] || {}), + pageSize: 15, + message: `Which packages should be added as ${group} to "${name}?`, + loop: false, + choices: depChoices.map((choice) => { + if (choice instanceof inquirer.Separator) { + return choice; + } + return { + name: ` ${choice.name}`, + value: choice.name, + }; + }), + }); + + const newDependencyGroup = sourcePackageJson?.[group] || {}; + if (Object.keys(newDependencyGroup).length) { + const existingDependencyKeys = new Set(Object.keys(newDependencyGroup)); + + selected.forEach((dep) => { + if (!existingDependencyKeys.has(dep)) { + newDependencyGroup[dep] = + project.packageManager === "pnpm" ? "workspace:*" : "*"; + } + }); + + selectedDependencies[group] = newDependencyGroup; + } else { + selectedDependencies[group] = selected.reduce( + (acc, dep) => ({ + ...acc, + [dep]: project.packageManager === "pnpm" ? "workspace:*" : "*", + }), + {} + ); + } + } + + return selectedDependencies; +} + +export async function confirm({ message }: { message: string }) { + return await inquirer.prompt<{ answer: boolean }>({ + type: "confirm", + name: "answer", + message, + }); +} diff --git a/packages/turbo-gen/src/commands/generate/index.ts b/packages/turbo-gen/src/commands/generate/index.ts new file mode 100644 index 00000000000000..d61ca72c4d97a0 --- /dev/null +++ b/packages/turbo-gen/src/commands/generate/index.ts @@ -0,0 +1,22 @@ +import { logger } from "@turbo/utils"; +import { getProject } from "../../utils/getProject"; +import { custom } from "../../generators"; + +export interface CustomGeneratorOptions { + config?: string; + root?: string; + args?: Array; +} + +export async function generate( + generator: string | undefined, + opts: CustomGeneratorOptions +) { + const project = await getProject(opts); + + console.log(); + logger.info(`Modify ${project.name} using custom generators`); + console.log(); + + await custom({ generator, project, opts }); +} diff --git a/packages/turbo-gen/src/commands/generate/prompts.ts b/packages/turbo-gen/src/commands/generate/prompts.ts new file mode 100644 index 00000000000000..5008ec1caeaf95 --- /dev/null +++ b/packages/turbo-gen/src/commands/generate/prompts.ts @@ -0,0 +1,44 @@ +import inquirer from "inquirer"; +import type { Generator } from "../../utils/plop"; + +export async function customGenerators({ + generators, + generator, +}: { + generators: Array; + generator?: string; +}) { + if ( + generator && + generators.find( + (g) => !(g instanceof inquirer.Separator) && g.name === generator + ) + ) { + return { + selectedGenerator: generator, + }; + } + + const generatorAnswer = await inquirer.prompt<{ + selectedGenerator: string; + }>({ + type: "list", + name: "selectedGenerator", + default: generator, + when: !generator, + message: `Select generator to run`, + choices: generators.map((gen) => { + if (gen instanceof inquirer.Separator) { + return gen; + } + return { + name: gen.description + ? ` ${gen.name}: ${gen.description}` + : ` ${gen.name}`, + value: gen.name, + }; + }), + }); + + return generatorAnswer; +} diff --git a/packages/turbo-gen/src/commands/index.ts b/packages/turbo-gen/src/commands/index.ts new file mode 100644 index 00000000000000..68ce13b2a0043e --- /dev/null +++ b/packages/turbo-gen/src/commands/index.ts @@ -0,0 +1,3 @@ +export { add } from "./add"; +export { generate } from "./generate"; +export { raw } from "./raw"; diff --git a/packages/turbo-gen/src/commands/raw/index.ts b/packages/turbo-gen/src/commands/raw/index.ts new file mode 100644 index 00000000000000..9341c401d78ba8 --- /dev/null +++ b/packages/turbo-gen/src/commands/raw/index.ts @@ -0,0 +1,32 @@ +import { add, type TurboGeneratorOptions } from "../add"; +import { generate, type CustomGeneratorOptions } from "../generate"; + +interface MinimalOptions { + generator?: string; + [arg: string]: any; +} + +export async function raw(command: string, options: { json: string }) { + let incomingOptions: MinimalOptions = {}; + try { + incomingOptions = JSON.parse(options.json); + } catch (err) { + console.error("Error parsing arguments", err); + process.exit(1); + } + + switch (command) { + case "add": + await add(incomingOptions as TurboGeneratorOptions); + break; + case "generate": + const { generator } = incomingOptions; + await generate(generator, incomingOptions as CustomGeneratorOptions); + break; + default: + console.error( + `Received unknown command - "${command}" (must be one of "add" | "generate")` + ); + process.exit(1); + } +} diff --git a/packages/turbo-gen/src/generators/copy.ts b/packages/turbo-gen/src/generators/copy.ts new file mode 100644 index 00000000000000..ecb6a2b9f07cb9 --- /dev/null +++ b/packages/turbo-gen/src/generators/copy.ts @@ -0,0 +1,99 @@ +import path from "path"; +import fs from "fs-extra"; +import chalk from "chalk"; +import { CopyFilterAsync } from "fs-extra"; +import { createProject, logger } from "@turbo/utils"; +import { gatherAddRequirements } from "../utils/gatherAddRequirements"; +import type { TurboGeneratorArguments } from "./types"; +import { DependencyGroups, PackageJson } from "../types"; + +export async function generate({ project, opts }: TurboGeneratorArguments) { + const { name, what, location, source, dependencies } = + await gatherAddRequirements({ + project, + opts, + }); + + const newPackageJsonPath = path.join(location.absolute, "package.json"); + + // copying from a remote example + if (opts.example) { + console.log(); + logger.warn("Some manual modifications may be required."); + logger.dimmed( + `This ${what} may require local dependencies or a different package manager than what is available in this repo` + ); + await createProject({ + appPath: location.absolute, + example: opts.example, + examplePath: opts.examplePath, + }); + + try { + if (fs.existsSync(newPackageJsonPath)) { + const packageJson = (await fs.readJSON( + newPackageJsonPath + )) as PackageJson; + if (packageJson.workspaces) { + throw new Error( + "New workspace root detected - unexpected 'workspaces' field in package.json" + ); + } + } else { + throw new Error("New workspace is missing a package.json file"); + } + + if (fs.existsSync(path.join(location.absolute, "pnpm-workspace.yaml"))) { + throw new Error( + "New workspace root detected - unexpected pnpm-workspace.yaml" + ); + } + } catch (err) { + let message = "UNKNOWN_ERROR"; + if (err instanceof Error) { + message = err.message; + } + logger.error(message); + + // rollback changes + await fs.rm(location.absolute, { recursive: true, force: true }); + return; + } + } else if (source) { + const filterFunc: CopyFilterAsync = async (src, dest) => { + if (src.includes("node_modules")) { + return false; + } + return true; + }; + + const loader = logger.turboLoader( + `Creating "${name}" from "${source.name}"...` + ); + loader.start(); + await fs.copy(source.paths.root, location.absolute, { + filter: filterFunc, + }); + loader.stop(); + } + + // update package.json with new name + const packageJson = await fs.readJSON(newPackageJsonPath); + packageJson.name = name; + + // update dependencies + Object.keys(dependencies).forEach((group) => { + const deps = dependencies[group as keyof DependencyGroups]; + if (deps && Object.keys(deps).length > 0) { + packageJson[group as keyof DependencyGroups] = deps; + } + }); + await fs.writeJSON(newPackageJsonPath, packageJson, { spaces: 2 }); + + console.log(); + console.log( + `${chalk.bold(logger.turboGradient(">>> Success!"))} Created ${name} at "${ + location.relative + }"` + ); +} diff --git a/packages/turbo-gen/src/generators/custom.ts b/packages/turbo-gen/src/generators/custom.ts new file mode 100644 index 00000000000000..a112fe773c92b2 --- /dev/null +++ b/packages/turbo-gen/src/generators/custom.ts @@ -0,0 +1,34 @@ +import chalk from "chalk"; +import { logger } from "@turbo/utils"; +import { getCustomGenerators, runCustomGenerator } from "../utils/plop"; +import * as prompts from "../commands/generate/prompts"; +import type { CustomGeneratorArguments } from "./types"; + +export async function generate({ + generator, + project, + opts, +}: CustomGeneratorArguments) { + const generators = getCustomGenerators({ project, configPath: opts.config }); + if (!generators.length) { + logger.error(`No custom generators found.`); + console.log(); + logger.dimmed( + `Visit https://turbo.build/repo/docs/generators to get started.` + ); + return; + } + const { selectedGenerator } = await prompts.customGenerators({ + generators, + generator, + }); + + await runCustomGenerator({ + project, + generator: selectedGenerator, + bypassArgs: opts.args, + configPath: opts.config, + }); + console.log(); + console.log(chalk.bold(logger.turboGradient(">>> Success!"))); +} diff --git a/packages/turbo-gen/src/generators/empty.ts b/packages/turbo-gen/src/generators/empty.ts new file mode 100644 index 00000000000000..f14fcf5d3252f5 --- /dev/null +++ b/packages/turbo-gen/src/generators/empty.ts @@ -0,0 +1,50 @@ +import path from "path"; +import fs from "fs-extra"; +import chalk from "chalk"; +import { logger } from "@turbo/utils"; +import { gatherAddRequirements } from "../utils/gatherAddRequirements"; +import type { TurboGeneratorArguments } from "./types"; +import type { PackageJson, DependencyGroups } from "../types"; + +export async function generate({ project, opts }: TurboGeneratorArguments) { + const { name, location, dependencies } = await gatherAddRequirements({ + project, + opts, + }); + + const packageJson: PackageJson = { + name, + version: "0.0.0", + private: true, + scripts: { + build: "turbo build", + }, + }; + + // update dependencies + Object.keys(dependencies).forEach((group) => { + const deps = dependencies[group as keyof DependencyGroups]; + if (deps && Object.keys(deps).length > 0) { + packageJson[group as keyof DependencyGroups] = deps; + } + }); + + // write the directory + fs.mkdirSync(location.absolute, { recursive: true }); + + // create package.json + fs.writeFileSync( + path.join(location.absolute, "package.json"), + JSON.stringify(packageJson, null, 2) + ); + + // create README + fs.writeFileSync(path.join(location.absolute, "README.md"), `# \`${name}\``); + + console.log(); + console.log( + `${chalk.bold(logger.turboGradient(">>> Success!"))} Created ${name} at "${ + location.relative + }"` + ); +} diff --git a/packages/turbo-gen/src/generators/index.ts b/packages/turbo-gen/src/generators/index.ts new file mode 100644 index 00000000000000..7dcbaa801ef72d --- /dev/null +++ b/packages/turbo-gen/src/generators/index.ts @@ -0,0 +1,3 @@ +export { generate as custom } from "./custom"; +export { generate as empty } from "./empty"; +export { generate as copy } from "./copy"; diff --git a/packages/turbo-gen/src/generators/types.ts b/packages/turbo-gen/src/generators/types.ts new file mode 100644 index 00000000000000..8bf87a46f522d2 --- /dev/null +++ b/packages/turbo-gen/src/generators/types.ts @@ -0,0 +1,16 @@ +import type { Project } from "@turbo/workspaces"; +import type { TurboGeneratorOptions } from "../commands/add"; +import type { CustomGeneratorOptions } from "../commands/generate"; + +export type WorkspaceType = "app" | "package"; + +export interface TurboGeneratorArguments { + project: Project; + opts: TurboGeneratorOptions; +} + +export interface CustomGeneratorArguments { + generator: string | undefined; + project: Project; + opts: CustomGeneratorOptions; +} diff --git a/packages/turbo-gen/src/types.ts b/packages/turbo-gen/src/types.ts new file mode 100644 index 00000000000000..c3db3d7e8b781f --- /dev/null +++ b/packages/turbo-gen/src/types.ts @@ -0,0 +1,18 @@ +export interface DependencyGroups { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; +} + +export interface PackageJson extends DependencyGroups { + name: string; + version: string; + private?: boolean; + description?: string; + workspaces?: Array | Record>; + main?: string; + module?: string; + exports?: object; + scripts?: Record; +} diff --git a/packages/turbo-gen/src/utils/gatherAddRequirements.ts b/packages/turbo-gen/src/utils/gatherAddRequirements.ts new file mode 100644 index 00000000000000..71871db0507b04 --- /dev/null +++ b/packages/turbo-gen/src/utils/gatherAddRequirements.ts @@ -0,0 +1,51 @@ +import { Workspace } from "@turbo/workspaces"; +import type { TurboGeneratorArguments } from "../generators/types"; +import * as prompts from "../commands/add/prompts"; +import { getWorkspaceList } from "./getWorkspaceList"; + +export async function gatherAddRequirements({ + project, + opts, +}: TurboGeneratorArguments) { + let source: Workspace | undefined = undefined; + const { answer: what } = await prompts.what({ override: opts.what }); + + // suggestion for the name based on the (optional) example path + const suggestion = + opts.examplePath?.split("/").pop() || + opts.example?.split("/").pop() || + undefined; + const { answer: name } = await prompts.name({ + override: opts.name, + what, + suggestion, + }); + if (opts.copy && !opts.example) { + const { answer } = await prompts.source({ + workspaces: getWorkspaceList({ project, what }), + name, + }); + source = answer; + } + const location = await prompts.location({ + what, + name, + project, + destination: opts.destination, + }); + + const dependencies = await prompts.dependencies({ + name, + project, + source, + showAllDependencies: opts.showAllDependencies, + }); + + return { + what, + name, + location, + source, + dependencies, + }; +} diff --git a/packages/turbo-gen/src/utils/getProject.ts b/packages/turbo-gen/src/utils/getProject.ts new file mode 100644 index 00000000000000..e4aabe3b8a46f9 --- /dev/null +++ b/packages/turbo-gen/src/utils/getProject.ts @@ -0,0 +1,28 @@ +import { getTurboRoot } from "@turbo/utils"; +import { type Project, getWorkspaceDetails } from "@turbo/workspaces"; +import { logger } from "@turbo/utils"; + +interface GetProjectArguments { + root?: string; +} + +export async function getProject({ + root, +}: GetProjectArguments): Promise { + const directory = root || process.cwd(); + const repoRoot = getTurboRoot(directory); + + if (!repoRoot) { + logger.error("Unable to infer repository root - override with --root"); + } else { + try { + return getWorkspaceDetails({ root: repoRoot }); + } catch (err) { + logger.error( + `Unable to determine workspace details. Make sure "${root}" is the root, or add "packageManager" to "package.json" or ensure a lockfile is present.` + ); + } + } + + process.exit(1); +} diff --git a/packages/turbo-gen/src/utils/getWorkspaceList.ts b/packages/turbo-gen/src/utils/getWorkspaceList.ts new file mode 100644 index 00000000000000..a33fa6e9a73368 --- /dev/null +++ b/packages/turbo-gen/src/utils/getWorkspaceList.ts @@ -0,0 +1,42 @@ +import type { Project, Workspace } from "@turbo/workspaces"; +import inquirer from "inquirer"; +import { + getWorkspaceStructure, + getGroupFromWorkspace, +} from "./getWorkspaceStructure"; +import { WorkspaceType } from "../generators/types"; + +export function getWorkspaceList({ + project, + what, + showAllDependencies, +}: { + project: Project; + what: WorkspaceType; + showAllDependencies?: boolean; +}): Array { + const structure = getWorkspaceStructure({ project }); + const workspaceChoices: Array = []; + + let workspacesForDisplay: Array = project.workspaceData.workspaces; + if (!showAllDependencies) { + if (what === "app" && structure.hasRootApps) { + workspacesForDisplay = structure.workspacesByGroup.apps; + } else if (what === "package" && structure.nonAppWorkspaces.length > 0) { + workspacesForDisplay = structure.nonAppWorkspaces; + } + } + + // build final list with separators between groups + let lastGroup: string | undefined; + workspacesForDisplay.forEach((workspace) => { + const group = getGroupFromWorkspace({ project, workspace }); + if (group !== lastGroup) { + workspaceChoices.push(new inquirer.Separator(group)); + } + lastGroup = group; + workspaceChoices.push(workspace); + }); + + return workspaceChoices; +} diff --git a/packages/turbo-gen/src/utils/getWorkspaceStructure.ts b/packages/turbo-gen/src/utils/getWorkspaceStructure.ts new file mode 100644 index 00000000000000..22ce6d5df96317 --- /dev/null +++ b/packages/turbo-gen/src/utils/getWorkspaceStructure.ts @@ -0,0 +1,56 @@ +import path from "path"; +import type { Project, Workspace } from "@turbo/workspaces"; +import { getWorkspaceRoots } from "./workspaceRoots"; + +interface WorkspaceStructure { + hasRootApps: boolean; + hasRootPackages: boolean; + workspacesByGroup: Record>; + nonAppWorkspaces: Array; +} + +export function getGroupFromWorkspace({ + project, + workspace, +}: { + project: Project; + workspace: Workspace; +}) { + return path + .relative(project.paths.root, workspace.paths.root) + .split(path.sep)[0]; +} + +export function getWorkspaceStructure({ + project, +}: { + project: Project; +}): WorkspaceStructure { + // get the workspace roots first, any assumptions we make + // should at least be based around configured workspaces + const roots = getWorkspaceRoots({ project }); + const hasRootApps = roots.includes("apps"); + const hasRootPackages = roots.includes("packages"); + + const workspacesByGroup: WorkspaceStructure["workspacesByGroup"] = {}; + const nonAppWorkspaces: WorkspaceStructure["nonAppWorkspaces"] = []; + project.workspaceData.workspaces.forEach((w) => { + const group = getGroupFromWorkspace({ project, workspace: w }); + if (group !== "apps") { + nonAppWorkspaces.push(w); + } + + // add to group + if (!workspacesByGroup[group]) { + workspacesByGroup[group] = []; + } + workspacesByGroup[group].push(w); + }); + + return { + hasRootApps, + hasRootPackages, + workspacesByGroup, + nonAppWorkspaces, + }; +} diff --git a/packages/turbo-gen/src/utils/notifyUpdate.ts b/packages/turbo-gen/src/utils/notifyUpdate.ts new file mode 100644 index 00000000000000..cbb4e034d475ed --- /dev/null +++ b/packages/turbo-gen/src/utils/notifyUpdate.ts @@ -0,0 +1,24 @@ +import chalk from "chalk"; +import checkForUpdate from "update-check"; + +import cliPkgJson from "../../package.json"; + +const update = checkForUpdate(cliPkgJson).catch(() => null); + +export default async function notifyUpdate(): Promise { + try { + const res = await update; + if (res?.latest) { + console.log(); + console.log( + chalk.yellow.bold( + `A new version of \`${cliPkgJson.name}\` is available!` + ) + ); + console.log(); + } + process.exit(); + } catch (_e: any) { + // ignore error + } +} diff --git a/packages/turbo-gen/src/utils/plop.ts b/packages/turbo-gen/src/utils/plop.ts new file mode 100644 index 00000000000000..da876eefeddf2e --- /dev/null +++ b/packages/turbo-gen/src/utils/plop.ts @@ -0,0 +1,264 @@ +import fs from "fs-extra"; +import { Project } from "@turbo/workspaces"; +import nodePlop, { NodePlopAPI, PlopCfg, PlopGenerator } from "node-plop"; +import path from "path"; +import inquirer from "inquirer"; +import { searchUp, getTurboConfigs, logger } from "@turbo/utils"; + +// TODO: Support a TS config file +const TURBO_GENERATOR_CONFIG = path.join("generators", "config.js"); + +// support root plopfile so that users with existing configurations can use them immediately +const DEFAULT_CONFIG_LOCATIONS = [ + TURBO_GENERATOR_CONFIG, + "plopfile.js", + "plopfile.cjs", + "plopfile.mjs", +]; + +export type Generator = PlopGenerator & { + basePath: string; + name: string; +}; + +export function getPlop({ + project, + configPath, +}: { + project: Project; + configPath?: string; +}): NodePlopAPI | undefined { + // fetch all the workspace generator configs + const workspaceConfigs = getWorkspaceGeneratorConfigs({ project }); + let plop: NodePlopAPI | undefined = undefined; + + if (configPath) { + try { + plop = nodePlop(configPath, { + destBasePath: configPath, + force: false, + }); + } catch (e) { + // skip + } + } else { + // look for a root config + for (const defaultConfigPath of DEFAULT_CONFIG_LOCATIONS) { + const plopFile = path.join(project.paths.root, defaultConfigPath); + try { + plop = nodePlop(plopFile, { + destBasePath: project.paths.root, + force: false, + }); + break; + } catch (e) { + // skip + } + } + + if (!plop && workspaceConfigs.length > 0) { + // if no root config, use the first workspace config as the entrypoint + plop = nodePlop(workspaceConfigs[0].config, { + destBasePath: workspaceConfigs[0].root, + force: false, + }); + workspaceConfigs.shift(); + } + } + + if (plop) { + // add in all the workspace configs + workspaceConfigs.forEach((c) => { + plop?.load(c.config, { + destBasePath: c.root, + force: false, + }); + }); + } + + return plop; +} + +export function getCustomGenerators({ + project, + configPath, +}: { + project: Project; + configPath?: string; +}): Array { + const plop = getPlop({ project, configPath }); + + if (!plop) { + return []; + } + + const gens = plop.getGeneratorList(); + const gensWithDetails = gens.map((g) => plop.getGenerator(g.name)); + + // group by workspace + const gensByWorkspace: Record> = {}; + gensWithDetails.forEach((g) => { + const generatorDetails = g as Generator; + const gensWorkspace = project.workspaceData.workspaces.find((w) => { + if (generatorDetails.basePath === project.paths.root) { + return false; + } + // we can strip the directory to get the workspace root + const parts = generatorDetails.basePath.split(path.sep); + parts.pop(); + const workspaceRoot = path.join("/", ...parts); + return workspaceRoot == w.paths.root; + }); + + if (gensWorkspace) { + if (!gensByWorkspace[gensWorkspace.name]) { + gensByWorkspace[gensWorkspace.name] = []; + } + gensByWorkspace[gensWorkspace.name].push(generatorDetails); + } else { + if (!gensByWorkspace["root"]) { + gensByWorkspace["root"] = []; + } + gensByWorkspace["root"].push(generatorDetails); + } + }); + + // add in separators to group by workspace + const gensWithSeparators: Array = []; + const lastGroup = undefined; + Object.keys(gensByWorkspace).forEach((group) => { + if (group !== lastGroup) { + gensWithSeparators.push(new inquirer.Separator(group)); + } + gensWithSeparators.push(...gensByWorkspace[group]); + }); + + if (!gensWithSeparators.length) { + throw new Error("No generators found"); + } + + return gensWithSeparators; +} + +export function getCustomGenerator({ + project, + generator, + configPath, +}: { + project: Project; + generator: string; + configPath?: string; +}): string | undefined { + const plop = getPlop({ project, configPath }); + if (!plop) { + return undefined; + } + + try { + const gen = plop.getGenerator(generator); + if (gen) { + return generator; + } + return undefined; + } catch (e) { + return undefined; + } +} + +function injectTurborepoData({ + project, + generator, +}: { + project: Project; + generator: PlopGenerator & { basePath?: string }; +}) { + const paths = { + root: project.paths.root, + workspace: generator.basePath + ? searchUp({ cwd: generator.basePath, target: "package.json" }) + : undefined, + }; + let turboConfigs = {}; + try { + turboConfigs = getTurboConfigs(generator.basePath); + } catch (e) { + // ignore + } + + return { + turbo: { + paths, + configs: turboConfigs, + }, + }; +} + +function getWorkspaceGeneratorConfigs({ project }: { project: Project }) { + const workspaceGeneratorConfigs: Array<{ + config: string; + root: string; + }> = []; + project.workspaceData.workspaces.forEach((w) => { + if (fs.existsSync(path.join(w.paths.root, TURBO_GENERATOR_CONFIG))) { + workspaceGeneratorConfigs.push({ + config: path.join(w.paths.root, TURBO_GENERATOR_CONFIG), + root: w.paths.root, + }); + } + }); + return workspaceGeneratorConfigs; +} + +export async function runCustomGenerator({ + project, + generator, + bypassArgs, + configPath, +}: { + project: Project; + generator: string; + bypassArgs?: Array; + configPath?: string; +}): Promise { + const plop = getPlop({ project, configPath }); + if (!plop) { + throw new Error("Unable to load generators"); + } + const gen: PlopGenerator & { basePath?: string } = + plop.getGenerator(generator); + + if (!gen) { + throw new Error(`Generator ${generator} not found`); + } + + const answers = await gen.runPrompts(bypassArgs); + const results = await gen.runActions( + { ...answers, ...injectTurborepoData({ project, generator: gen }) }, + { + onComment: (comment: string) => { + console.info(comment); + }, + } + ); + + if (results.failures && results.failures.length > 0) { + // log all errors: + results.failures.forEach((f) => { + if (f instanceof Error) { + logger.error(`Error - ${f.message}`); + } else { + logger.error(`Error - ${f.error}. Unable to ${f.type} to "${f.path}"`); + } + }); + throw new Error(`Failed to run "${generator}" generator`); + } + + if (results.changes && results.changes.length > 0) { + logger.info("Changes made:"); + results.changes.forEach((c) => { + if (c.path) { + logger.item(`${c.path} (${c.type})`); + } + }); + } +} diff --git a/packages/turbo-gen/src/utils/workspaceRoots.ts b/packages/turbo-gen/src/utils/workspaceRoots.ts new file mode 100644 index 00000000000000..0b19ee78b9ac56 --- /dev/null +++ b/packages/turbo-gen/src/utils/workspaceRoots.ts @@ -0,0 +1,32 @@ +import path from "path"; +import type { Project } from "@turbo/workspaces"; + +// This function is not perfect and could be improved to be more accurate. +// Given a list of workspace globs, it aims to return a selectable list of paths that are valid workspace locations. +// This current naive approach does not work with globs that contain nested wildcards, for example: `packages/*/utils` will not work. +export function getWorkspaceRoots({ + project, +}: { + project: Project; +}): Array { + const allWorkspaces = project.workspaceData.workspaces; + const allWorkspacePaths = allWorkspaces.map((workspace) => + path.relative(project.paths.root, workspace.paths.root) + ); + + // find valid workspace locations + const workspaceRoots = new Set(); + project.workspaceData.globs.forEach((glob) => { + if (allWorkspacePaths.includes(glob)) { + return; + } else if (glob.startsWith("!")) { + return; + } else { + const globParts = glob.split("/"); + const globRoot = globParts[0]; + workspaceRoots.add(globRoot); + } + }); + + return Array.from(workspaceRoots); +} diff --git a/packages/turbo-gen/tsconfig.json b/packages/turbo-gen/tsconfig.json new file mode 100644 index 00000000000000..0620a3c25ef39c --- /dev/null +++ b/packages/turbo-gen/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@turbo/tsconfig/library.json", + "compilerOptions": { + "rootDir": "." + } +} diff --git a/packages/turbo-gen/tsup.config.ts b/packages/turbo-gen/tsup.config.ts new file mode 100644 index 00000000000000..18b0666b270a3e --- /dev/null +++ b/packages/turbo-gen/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, Options } from "tsup"; + +export default defineConfig((options: Options) => ({ + entry: ["src/cli.ts"], + format: ["cjs"], + clean: true, + minify: true, + ...options, +})); diff --git a/packages/turbo-gen/turbo.json b/packages/turbo-gen/turbo.json new file mode 100644 index 00000000000000..6466b2d9e7ea9b --- /dev/null +++ b/packages/turbo-gen/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../docs/public/schema.json", + "extends": ["//"], + "pipeline": { + "test": { + "dependsOn": ["build"] + }, + "build": { + "dependsOn": ["^build"] + } + } +} diff --git a/packages/turbo-utils/src/logger.ts b/packages/turbo-utils/src/logger.ts index ee6d5845b27a14..fc8e7a357fb17e 100644 --- a/packages/turbo-utils/src/logger.ts +++ b/packages/turbo-utils/src/logger.ts @@ -30,3 +30,11 @@ export const error = (...args: any[]) => { export const warn = (...args: any[]) => { console.error(yellow.bold(">>>"), ...args); }; + +export const dimmed = (...args: any[]) => { + console.log(chalk.dim(...args)); +}; + +export const item = (...args: any[]) => { + console.log(turboBlue.bold(" •"), ...args); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d6aac2253c232..7ec818834c56ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,6 +384,59 @@ importers: tsup: 5.12.9_typescript@4.7.4 typescript: 4.7.4 + packages/turbo-gen: + specifiers: + '@turbo/test-utils': workspace:* + '@turbo/tsconfig': workspace:* + '@turbo/utils': workspace:* + '@turbo/workspaces': workspace:* + '@types/fs-extra': ^9.0.13 + '@types/inquirer': ^8.2.5 + '@types/jest': ^27.4.0 + '@types/node': ^16.11.12 + '@types/semver': ^7.3.9 + '@types/validate-npm-package-name': ^4.0.0 + chalk: 2.4.2 + commander: ^10.0.0 + eslint: ^7.23.0 + fs-extra: ^10.1.0 + inquirer: ^8.2.4 + jest: ^27.4.3 + minimatch: ^9.0.0 + node-plop: ^0.26.3 + semver: ^7.3.8 + ts-jest: ^27.1.1 + tsup: ^6.7.0 + typescript: ^4.5.5 + update-check: ^1.5.4 + validate-npm-package-name: ^5.0.0 + dependencies: + chalk: 2.4.2 + commander: 10.0.0 + fs-extra: 10.1.0 + inquirer: 8.2.4 + minimatch: 9.0.0 + node-plop: 0.26.3 + semver: 7.5.0 + update-check: 1.5.4 + validate-npm-package-name: 5.0.0 + devDependencies: + '@turbo/test-utils': link:../turbo-test-utils + '@turbo/tsconfig': link:../tsconfig + '@turbo/utils': link:../turbo-utils + '@turbo/workspaces': link:../turbo-workspaces + '@types/fs-extra': 9.0.13 + '@types/inquirer': 8.2.5 + '@types/jest': 27.5.2 + '@types/node': 16.11.56 + '@types/semver': 7.3.12 + '@types/validate-npm-package-name': 4.0.0 + eslint: 7.32.0 + jest: 27.5.1 + ts-jest: 27.1.5_fu5qd3dwfwo63mklk7zcmwwv6q + tsup: 6.7.0_typescript@4.9.4 + typescript: 4.9.4 + packages/turbo-ignore: specifiers: '@turbo/test-utils': workspace:^0.0.0 @@ -974,13 +1027,12 @@ packages: dependencies: core-js-pure: 3.26.1 regenerator-runtime: 0.13.11 - dev: true /@babel/runtime/7.18.9: resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} engines: {node: '>=6.9.0'} dependencies: - regenerator-runtime: 0.13.9 + regenerator-runtime: 0.13.11 /@babel/runtime/7.20.6: resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} @@ -1441,7 +1493,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 chalk: 4.1.2 jest-message-util: 27.5.1 jest-util: 27.5.1 @@ -1499,7 +1551,7 @@ packages: dependencies: '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 jest-mock: 27.5.1 dev: true @@ -1509,7 +1561,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@sinonjs/fake-timers': 8.1.0 - '@types/node': 16.11.56 + '@types/node': 18.13.0 jest-message-util: 27.5.1 jest-mock: 27.5.1 jest-util: 27.5.1 @@ -1538,7 +1590,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -1622,7 +1674,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -2570,7 +2622,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 16.11.56 + '@types/node': 18.13.0 '@types/responselike': 1.0.0 dev: true @@ -2622,7 +2674,7 @@ packages: /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 dev: true /@types/glob/7.2.0: @@ -2630,12 +2682,11 @@ packages: dependencies: '@types/minimatch': 5.1.1 '@types/node': 16.11.56 - dev: true /@types/graceful-fs/4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 dev: true /@types/gradient-string/1.1.2: @@ -2654,6 +2705,13 @@ packages: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: true + /@types/inquirer/6.5.0: + resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} + dependencies: + '@types/through': 0.0.30 + rxjs: 6.6.7 + dev: false + /@types/inquirer/7.3.3: resolution: {integrity: sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==} dependencies: @@ -2712,7 +2770,7 @@ packages: /@types/keyv/3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 dev: true /@types/liftoff/4.0.0: @@ -2740,7 +2798,6 @@ packages: /@types/minimatch/5.1.1: resolution: {integrity: sha512-v55NF6Dz0wrj14Rn8iEABTWrhYRmgkJYuokduunSiq++t3hZ9VZ6dvcDt+850Pm5sGJZk8RaHzkFCXPxVINZ+g==} - dev: true /@types/ms/0.7.31: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} @@ -2756,7 +2813,6 @@ packages: /@types/node/16.11.56: resolution: {integrity: sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==} - dev: true /@types/node/18.11.11: resolution: {integrity: sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g==} @@ -2782,7 +2838,7 @@ packages: /@types/responselike/1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 dev: true /@types/retry/0.12.2: @@ -2793,7 +2849,7 @@ packages: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} dependencies: '@types/glob': 7.2.0 - '@types/node': 16.11.56 + '@types/node': 18.13.0 dev: true /@types/scheduler/0.16.2: @@ -2810,7 +2866,7 @@ packages: /@types/tar/6.1.4: resolution: {integrity: sha512-Cp4oxpfIzWt7mr2pbhHT2OTXGMAL0szYCzuf8lRWyIMCgsx6/Hfc3ubztuhvzXHXgraTQxyOCmmg7TDGIMIJJQ==} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 minipass: 4.0.0 dev: true @@ -2818,7 +2874,6 @@ packages: resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==} dependencies: '@types/node': 18.13.0 - dev: true /@types/tinycolor2/1.4.3: resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==} @@ -2831,6 +2886,10 @@ packages: resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==} dev: true + /@types/validate-npm-package-name/4.0.0: + resolution: {integrity: sha512-RpO62vB2lkjEkyLbwTheA2+uwYmtVMWTr/kWRI++UAgVdZqNqdAuIQl/SxBCGeMKfdjWaXPbyhZbiCc4PAj+KA==} + dev: true + /@types/webidl-conversions/7.0.0: resolution: {integrity: sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==} dev: false @@ -3218,7 +3277,6 @@ packages: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - dev: true /ajv-keywords/3.5.2_ajv@6.12.6: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} @@ -3730,6 +3788,12 @@ packages: ieee754: 1.2.1 dev: true + /builtins/5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + dependencies: + semver: 7.5.0 + dev: false + /bundle-require/3.1.0_esbuild@0.14.49: resolution: {integrity: sha512-IIXtAO7fKcwPHNPt9kY/WNVJqy7NDy6YqJvv6ENH0TOZoJ+yjpEsn1w40WKZbR2ibfu5g1rfgJTvmFHpm5aOMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3816,6 +3880,13 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + /camel-case/3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + /camel-case/4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: @@ -3896,6 +3967,29 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: true + /change-case/3.1.0: + resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} + dependencies: + camel-case: 3.0.0 + constant-case: 2.0.0 + dot-case: 2.1.1 + header-case: 1.0.1 + is-lower-case: 1.1.3 + is-upper-case: 1.1.2 + lower-case: 1.1.4 + lower-case-first: 1.0.2 + no-case: 2.3.2 + param-case: 2.1.1 + pascal-case: 2.0.1 + path-case: 2.1.1 + sentence-case: 2.1.1 + snake-case: 2.1.0 + swap-case: 1.1.2 + title-case: 2.1.1 + upper-case: 1.1.3 + upper-case-first: 1.1.2 + dev: false + /change-case/4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} dependencies: @@ -3991,7 +4085,6 @@ packages: /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - dev: true /cli-cursor/3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} @@ -4165,6 +4258,13 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /constant-case/2.0.0: + resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} + dependencies: + snake-case: 2.1.0 + upper-case: 1.1.3 + dev: false + /constant-case/3.0.4: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} dependencies: @@ -4211,7 +4311,6 @@ packages: /core-js-pure/3.26.1: resolution: {integrity: sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==} requiresBuild: true - dev: true /core-util-is/1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -4438,6 +4537,20 @@ packages: resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==} dev: true + /del/5.1.0: + resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} + engines: {node: '>=8'} + dependencies: + globby: 10.0.2 + graceful-fs: 4.2.10 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 3.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + dev: false + /del/6.1.1: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} @@ -4539,6 +4652,12 @@ packages: webidl-conversions: 5.0.0 dev: true + /dot-case/2.1.1: + resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} + dependencies: + no-case: 2.3.2 + dev: false + /dot-case/3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: @@ -6441,7 +6560,6 @@ packages: /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true /function.prototype.name/1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} @@ -6635,6 +6753,20 @@ packages: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} dev: true + /globby/10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + glob: 7.2.3 + ignore: 5.2.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: false + /globby/11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -6717,7 +6849,6 @@ packages: wordwrap: 1.0.0 optionalDependencies: uglify-js: 3.17.4 - dev: true /has-bigints/1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -6790,7 +6921,6 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - dev: true /hash-obj/4.0.0: resolution: {integrity: sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==} @@ -6902,6 +7032,13 @@ packages: space-separated-tokens: 2.0.1 dev: false + /header-case/1.0.1: + resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + /header-case/2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} dependencies: @@ -7037,7 +7174,6 @@ packages: /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - dev: true /inflight/1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -7065,6 +7201,25 @@ packages: rxjs: 7.6.0 dev: false + /inquirer/7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + dev: false + /inquirer/8.2.4: resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} engines: {node: '>=12.0.0'} @@ -7192,7 +7347,6 @@ packages: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: has: 1.0.3 - dev: true /is-data-descriptor/0.1.4: resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} @@ -7300,6 +7454,12 @@ packages: engines: {node: '>=12'} dev: true + /is-lower-case/1.1.3: + resolution: {integrity: sha512-+5A1e/WJpLLXZEDlgz4G//WYSHyQBD32qa4Jd3Lw06qQlv3fJHnp3YIHjTQSGzHMgzmVKz2ZP3rBxTHkPw/lxA==} + dependencies: + lower-case: 1.1.4 + dev: false + /is-negative-zero/2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -7336,12 +7496,10 @@ packages: /is-path-cwd/2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} - dev: true /is-path-inside/3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - dev: true /is-plain-obj/3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} @@ -7455,6 +7613,12 @@ packages: engines: {node: '>=12'} dev: true + /is-upper-case/1.1.2: + resolution: {integrity: sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw==} + dependencies: + upper-case: 1.1.3 + dev: false + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -7480,7 +7644,6 @@ packages: /isbinaryfile/4.0.10: resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} engines: {node: '>= 8.0.0'} - dev: true /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -7559,7 +7722,7 @@ packages: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -7684,7 +7847,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 jest-mock: 27.5.1 jest-util: 27.5.1 jsdom: 16.7.0 @@ -7702,7 +7865,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 jest-mock: 27.5.1 jest-util: 27.5.1 dev: true @@ -7718,7 +7881,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@types/graceful-fs': 4.1.6 - '@types/node': 16.11.56 + '@types/node': 18.13.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 @@ -7740,7 +7903,7 @@ packages: '@jest/source-map': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 chalk: 4.1.2 co: 4.6.0 expect: 27.5.1 @@ -7795,7 +7958,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 dev: true /jest-pnp-resolver/1.2.3_jest-resolve@27.5.1: @@ -7851,7 +8014,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 chalk: 4.1.2 emittery: 0.8.1 graceful-fs: 4.2.10 @@ -7908,7 +8071,7 @@ packages: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 graceful-fs: 4.2.10 dev: true @@ -7937,7 +8100,7 @@ packages: jest-util: 27.5.1 natural-compare: 1.4.0 pretty-format: 27.5.1 - semver: 7.3.8 + semver: 7.5.0 transitivePeerDependencies: - supports-color dev: true @@ -7972,7 +8135,7 @@ packages: dependencies: '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.56 + '@types/node': 18.13.0 ansi-escapes: 4.3.2 chalk: 4.1.2 jest-util: 27.5.1 @@ -7983,7 +8146,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 16.11.56 + '@types/node': 18.13.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -8340,7 +8503,6 @@ packages: /lodash.get/4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true /lodash.memoize/4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -8401,6 +8563,16 @@ packages: dependencies: js-tokens: 4.0.0 + /lower-case-first/1.0.2: + resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} + dependencies: + lower-case: 1.1.4 + dev: false + + /lower-case/1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + dev: false + /lower-case/2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -9125,6 +9297,13 @@ packages: brace-expansion: 2.0.1 dev: false + /minimatch/9.0.0: + resolution: {integrity: sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + /minimist/1.2.6: resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} dev: false @@ -9309,7 +9488,6 @@ packages: /neo-async/2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true /next-seo/5.15.0_vwuoxxmoeydgew5hhkavebkk4e: resolution: {integrity: sha512-LGbcY91yDKGMb7YI+28n3g+RuChUkt6pXNpa8FkfKkEmNiJkeRDEXTnnjVtwT9FmMhG6NH8qwHTelGrlYm9rgg==} @@ -9516,6 +9694,12 @@ packages: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: true + /no-case/2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + dependencies: + lower-case: 1.1.4 + dev: false + /no-case/3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -9550,6 +9734,23 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true + /node-plop/0.26.3: + resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} + engines: {node: '>=8.9.4'} + dependencies: + '@babel/runtime-corejs3': 7.20.6 + '@types/inquirer': 6.5.0 + change-case: 3.1.0 + del: 5.1.0 + globby: 10.0.2 + handlebars: 4.7.7 + inquirer: 7.3.3 + isbinaryfile: 4.0.10 + lodash.get: 4.4.2 + mkdirp: 0.5.6 + resolve: 1.22.1 + dev: false + /node-plop/0.31.0: resolution: {integrity: sha512-aKLPxiBoFTNUovvtK8j/Whc4PZREkYx6htw2HJPiU8wYquXmN8pkd9B3xlFo6AJ4ZlzFsQSf/NXR5xET8EqRYw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9913,6 +10114,13 @@ packages: dependencies: p-limit: 3.1.0 + /p-map/3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + dependencies: + aggregate-error: 3.1.0 + dev: false + /p-map/4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -9924,6 +10132,12 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /param-case/2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + dependencies: + no-case: 2.3.2 + dev: false + /param-case/3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -10008,6 +10222,13 @@ packages: entities: 4.5.0 dev: false + /pascal-case/2.0.1: + resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} + dependencies: + camel-case: 3.0.0 + upper-case-first: 1.1.2 + dev: false + /pascal-case/3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: @@ -10020,6 +10241,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /path-case/2.1.1: + resolution: {integrity: sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==} + dependencies: + no-case: 2.3.2 + dev: false + /path-case/3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: @@ -10055,7 +10282,6 @@ packages: /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-root-regex/0.1.2: resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} @@ -10483,9 +10709,6 @@ packages: /regenerator-runtime/0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - /regenerator-runtime/0.13.9: - resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - /regex-not/1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} engines: {node: '>=0.10.0'} @@ -10685,7 +10908,6 @@ packages: is-core-module: 2.11.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /resolve/2.0.0-next.4: resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} @@ -10785,7 +11007,6 @@ packages: engines: {npm: '>=2.0.0'} dependencies: tslib: 1.14.1 - dev: true /rxjs/7.5.6: resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==} @@ -10912,6 +11133,13 @@ packages: dependencies: lru-cache: 6.0.0 + /sentence-case/2.1.1: + resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} + dependencies: + no-case: 2.3.2 + upper-case-first: 1.1.2 + dev: false + /sentence-case/3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} dependencies: @@ -11070,6 +11298,12 @@ packages: engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} dev: false + /snake-case/2.1.0: + resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} + dependencies: + no-case: 2.3.2 + dev: false + /snake-case/3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: @@ -11159,7 +11393,6 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map/0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -11503,7 +11736,13 @@ packages: /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true + + /swap-case/1.1.2: + resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} + dependencies: + lower-case: 1.1.4 + upper-case: 1.1.3 + dev: false /swr/1.3.0_react@18.2.0: resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} @@ -11709,6 +11948,13 @@ packages: '@types/tinycolor2': 1.4.3 tinycolor2: 1.4.2 + /title-case/2.1.1: + resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + /title-case/3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: @@ -11861,7 +12107,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.5.0 typescript: 4.7.4 yargs-parser: 20.2.9 dev: true @@ -11902,6 +12148,42 @@ packages: yargs-parser: 20.2.9 dev: true + /ts-jest/27.1.5_fu5qd3dwfwo63mklk7zcmwwv6q: + resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@types/jest': ^27.0.0 + babel-jest: '>=27.0.0 <28' + esbuild: '*' + jest: ^27.0.0 + typescript: '>=3.8 <5.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@types/jest': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.20.12 + '@types/jest': 27.5.2 + bs-logger: 0.2.6 + esbuild: 0.17.18 + fast-json-stable-stringify: 2.1.0 + jest: 27.5.1 + jest-util: 27.5.1 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.0 + typescript: 4.9.4 + yargs-parser: 20.2.9 + dev: true + /ts-jest/27.1.5_ndeyfkjscoamnbcpt4q6qsiybu: resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11932,7 +12214,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.5.0 typescript: 4.9.4 yargs-parser: 20.2.9 dev: true @@ -11968,7 +12250,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.5.0 typescript: 4.7.4 yargs-parser: 20.2.9 dev: true @@ -12004,7 +12286,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.5.0 typescript: 4.9.4 yargs-parser: 20.2.9 dev: true @@ -12184,6 +12466,42 @@ packages: - ts-node dev: true + /tsup/6.7.0_typescript@4.9.4: + resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.1_esbuild@0.17.18 + cac: 6.7.12 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.17.18 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 3.1.4 + resolve-from: 5.0.0 + rollup: 3.21.5 + source-map: 0.8.0-beta.0 + sucrase: 3.24.0 + tree-kill: 1.2.2 + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tsutils/3.21.0_typescript@4.8.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -12279,7 +12597,6 @@ packages: engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true - dev: true optional: true /unbox-primitive/1.0.2: @@ -12448,12 +12765,22 @@ packages: registry-url: 3.1.0 dev: false + /upper-case-first/1.1.2: + resolution: {integrity: sha512-wINKYvI3Db8dtjikdAqoBbZoP6Q+PZUyfMR7pmwHzjC2quzSkUq5DmPrTtPEqHaz8AGtmsB4TqwapMTM1QAQOQ==} + dependencies: + upper-case: 1.1.3 + dev: false + /upper-case-first/2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: tslib: 2.4.1 dev: true + /upper-case/1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + dev: false + /upper-case/2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} dependencies: @@ -12531,6 +12858,13 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validate-npm-package-name/5.0.0: + resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + builtins: 5.0.1 + dev: false + /vfile-location/4.0.1: resolution: {integrity: sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==} dependencies: @@ -12737,7 +13071,6 @@ packages: /wordwrap/1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true /wrap-ansi/6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}