From 551eb51e1204cf15f5c4f0c7dfd68b28102b41aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sat, 2 Mar 2024 11:46:10 -0500 Subject: [PATCH] Initial commit --- .all-contributorsrc | 449 + .eslintrc.cjs | 184 + .github/CODE_OF_CONDUCT.md | 132 + .github/CONTRIBUTING.md | 97 + .github/DEVELOPMENT.md | 276 + .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE.md | 9 + .github/ISSUE_TEMPLATE/01-bug.yml | 33 + .github/ISSUE_TEMPLATE/02-documentation.yml | 25 + .github/ISSUE_TEMPLATE/03-feature.yml | 25 + .github/ISSUE_TEMPLATE/04-tooling.yml | 27 + .github/PULL_REQUEST_TEMPLATE.md | 13 + .github/SECURITY.md | 9 + .github/actions/prepare/action.yml | 14 + .github/codecov.yml | 5 + .github/renovate.json | 8 + .../workflows/accessibility-alt-text-bot.yml | 26 + .github/workflows/build.yml | 16 + .github/workflows/compliance.yml | 28 + .github/workflows/contributors.yml | 18 + .github/workflows/lint-knip.yml | 15 + .github/workflows/lint-markdown.yml | 15 + .github/workflows/lint-packages.yml | 15 + .github/workflows/lint-spelling.yml | 15 + .github/workflows/lint.yml | 16 + .github/workflows/post-release.yml | 27 + .github/workflows/pr-review-requested.yml | 21 + .github/workflows/prettier.yml | 15 + .github/workflows/release.yml | 28 + .github/workflows/test-create.yml | 23 + .github/workflows/test-initialize.yml | 27 + .github/workflows/test-migrate.yml | 27 + .github/workflows/test.yml | 19 + .github/workflows/tsc.yml | 15 + .gitignore | 3 + .husky/.gitignore | 1 + .husky/pre-commit | 3 + .markdownlint.json | 5 + .markdownlintignore | 4 + .nvmrc | 1 + .prettierignore | 4 + .prettierrc.json | 6 + .release-it.json | 18 + .vscode/extensions.json | 8 + .vscode/launch.json | 24 + .vscode/settings.json | 18 + .vscode/tasks.json | 11 + CHANGELOG.md | 542 ++ LICENSE.md | 20 + README.md | 136 + bin/index.js | 4 + cspell.json | 33 + docs/Creation.md | 40 + docs/FAQs.md | 220 + docs/Initialization.md | 46 + docs/Migration.md | 59 + docs/Options.md | 161 + docs/Tooling.md | 311 + docs/create-typescript-app.png | Bin 0 -> 3108 bytes knip.json | 7 + package.json | 110 + pnpm-lock.yaml | 7937 +++++++++++++++++ script/__snapshots__/migrate-test-e2e.js.snap | 159 + script/create-test-e2e.js | 44 + script/initialize-test-e2e.js | 59 + script/migrate-test-e2e.js | 160 + script/vitest.config.ts | 3 + src/bin/help.test.ts | 344 + src/bin/help.ts | 148 + src/bin/index.test.ts | 205 + src/bin/index.ts | 108 + src/bin/packageJson.test.ts | 31 + src/bin/packageJson.ts | 16 + src/bin/promptForMode.test.ts | 129 + src/bin/promptForMode.ts | 126 + src/create/createAndEnterGitDirectory.test.ts | 65 + src/create/createAndEnterGitDirectory.ts | 19 + src/create/createRerunSuggestion.test.ts | 160 + src/create/createRerunSuggestion.ts | 79 + src/create/createWithOptions.test.ts | 200 + src/create/createWithOptions.ts | 66 + src/create/index.test.ts | 72 + src/create/index.ts | 68 + src/greet.test.ts | 44 + src/greet.ts | 13 + src/index.ts | 2 + src/initialize/index.test.ts | 49 + src/initialize/index.ts | 44 + src/initialize/initializeWithOptions.ts | 65 + src/migrate/index.test.ts | 49 + src/migrate/index.ts | 44 + src/migrate/migrateWithOptions.ts | 70 + .../generateNextSteps.test.ts.snap | 183 + src/shared/cli/lines.ts | 14 + src/shared/cli/lowerFirst.ts | 3 + src/shared/cli/outro.test.ts | 61 + src/shared/cli/outro.ts | 28 + src/shared/cli/spinners.ts | 72 + src/shared/cli/startLineWithDots.ts | 38 + src/shared/codes.ts | 7 + src/shared/createCleanupCommands.test.ts | 28 + src/shared/createCleanupCommands.ts | 15 + src/shared/doesRepositoryExist.test.ts | 47 + src/shared/doesRepositoryExist.ts | 27 + src/shared/ensureGitRepository.test.ts | 29 + src/shared/ensureGitRepository.ts | 9 + src/shared/generateNextSteps.test.ts | 50 + src/shared/generateNextSteps.ts | 69 + .../getGitHubUserAsAllContributor.test.ts | 98 + src/shared/getGitHubUserAsAllContributor.ts | 48 + src/shared/options/args.ts | 316 + .../augmentOptionsWithExcludes.test.ts | 189 + .../options/augmentOptionsWithExcludes.ts | 107 + .../createOptionDefaults/index.test.ts | 161 + .../options/createOptionDefaults/index.ts | 65 + .../parsePackageAuthor.test.ts | 32 + .../parsePackageAuthor.ts | 22 + .../readDefaultsFromDevelopment.test.ts | 61 + .../readDefaultsFromDevelopment.ts | 20 + .../readDefaultsFromReadme.test.ts | 83 + .../readDefaultsFromReadme.ts | 23 + .../options/createRepositoryWithApi.test.ts | 71 + src/shared/options/createRepositoryWithApi.ts | 35 + .../options/detectEmailRedundancy.test.ts | 38 + src/shared/options/detectEmailRedundancy.ts | 23 + .../options/ensureRepositoryExists.test.ts | 169 + src/shared/options/ensureRepositoryExists.ts | 90 + src/shared/options/exclusionKeys.ts | 148 + src/shared/options/getBase.test.ts | 59 + src/shared/options/getBase.ts | 34 + src/shared/options/getGitHub.test.ts | 44 + src/shared/options/getGitHub.ts | 22 + .../getPrefillOrPromptedOption.test.ts | 106 + .../options/getPrefillOrPromptedOption.ts | 48 + src/shared/options/logInferredOptions.test.ts | 118 + src/shared/options/logInferredOptions.ts | 31 + src/shared/options/optionsSchema.ts | 65 + src/shared/options/readOptions.test.ts | 778 ++ src/shared/options/readOptions.ts | 297 + src/shared/packages.test.ts | 66 + src/shared/packages.ts | 24 + src/shared/prompts.ts | 5 + src/shared/readFileAsJson.test.ts | 36 + src/shared/readFileAsJson.ts | 12 + src/shared/readFileSafe.test.ts | 25 + src/shared/readFileSafe.ts | 9 + src/shared/readFileSafeAsJson.ts | 5 + src/shared/runOrRestore.test.ts | 95 + src/shared/runOrRestore.ts | 42 + src/shared/tryCatchAsync.ts | 7 + src/shared/tryCatchLazyValueAsync.test.ts | 30 + src/shared/tryCatchLazyValueAsync.ts | 7 + src/shared/types.ts | 119 + src/steps/addOwnerAsAllContributor.test.ts | 104 + src/steps/addOwnerAsAllContributor.ts | 55 + src/steps/addToolAllContributors.test.ts | 39 + src/steps/addToolAllContributors.ts | 14 + src/steps/clearChangelog.ts | 9 + src/steps/clearUnnecessaryFiles.ts | 37 + .../createJoshuaKGoldbergReplacement.test.ts | 26 + src/steps/createJoshuaKGoldbergReplacement.ts | 21 + src/steps/detectExistingContributors.test.ts | 55 + src/steps/detectExistingContributors.ts | 22 + src/steps/finalizeDependencies.test.ts | 103 + src/steps/finalizeDependencies.ts | 77 + ...initializeBranchProtectionSettings.test.ts | 166 + src/steps/initializeGitHubRepository/index.ts | 17 + .../initializeBranchProtectionSettings.ts | 54 + .../getExistingEquivalentLabels.test.ts | 76 + .../labels/getExistingEquivalentLabels.ts | 26 + .../labels/initializeRepositoryLabels.test.ts | 363 + .../labels/initializeRepositoryLabels.ts | 49 + .../labels/outcomeLabels.ts | 89 + src/steps/initializeGitRemote.test.ts | 68 + src/steps/initializeGitRemote.ts | 17 + src/steps/initializeRepositorySettings.ts | 35 + src/steps/populateCSpellDictionary.test.ts | 121 + src/steps/populateCSpellDictionary.ts | 42 + src/steps/removeSetupScripts.ts | 21 + src/steps/resetGitTags.ts | 13 + src/steps/runCleanup.test.ts | 71 + src/steps/runCleanup.ts | 43 + src/steps/uninstallPackages.test.ts | 50 + src/steps/uninstallPackages.ts | 49 + src/steps/updateAllContributorsTable.ts | 24 + src/steps/updateLocalFiles.test.ts | 550 ++ src/steps/updateLocalFiles.ts | 94 + src/steps/updateReadme.test.ts | 96 + src/steps/updateReadme.ts | 30 + .../writeReadme/findExistingBadges.test.ts | 111 + src/steps/writeReadme/findExistingBadges.ts | 30 + .../writeReadme/findIntroSectionClose.test.ts | 48 + .../writeReadme/findIntroSectionClose.ts | 22 + .../writeReadme/generateTopContent.test.ts | 147 + src/steps/writeReadme/generateTopContent.ts | 84 + src/steps/writeReadme/index.test.ts | 319 + src/steps/writeReadme/index.ts | 73 + .../creation/createDotGitignore.test.ts | 26 + .../writing/creation/createDotGitignore.ts | 10 + .../writing/creation/createESLintRC.test.ts | 276 + src/steps/writing/creation/createESLintRC.ts | 245 + .../writing/creation/createTsupConfig.test.ts | 43 + .../writing/creation/createTsupConfig.ts | 17 + .../creation/dotGitHub/actions.test.ts | 29 + .../writing/creation/dotGitHub/actions.ts | 28 + .../dotGitHub/createDevelopment/index.test.ts | 393 + .../dotGitHub/createDevelopment/index.ts | 157 + .../splitIntoSections.test.ts | 40 + .../createDevelopment/splitIntoSections.ts | 27 + .../dotGitHub/createDotGitHubFiles.ts | 290 + .../dotGitHub/createWorkflowFile.test.ts | 31 + .../creation/dotGitHub/createWorkflowFile.ts | 107 + .../dotGitHub/createWorkflows.test.ts | 470 + .../creation/dotGitHub/createWorkflows.ts | 228 + src/steps/writing/creation/dotGitHub/index.ts | 14 + .../creation/dotGitHub/issueTemplate.ts | 206 + src/steps/writing/creation/dotHusky.ts | 12 + src/steps/writing/creation/dotVSCode.test.ts | 301 + src/steps/writing/creation/dotVSCode.ts | 84 + .../creation/formatters/formatIgnoreFile.ts | 3 + .../creation/formatters/formatJson.test.ts | 14 + .../writing/creation/formatters/formatJson.ts | 15 + .../creation/formatters/formatTypeScript.ts | 5 + .../writing/creation/formatters/formatYaml.ts | 26 + src/steps/writing/creation/index.ts | 17 + src/steps/writing/creation/rootFiles.ts | 155 + src/steps/writing/creation/src.ts | 88 + .../creation/writeAllContributorsRC.ts | 26 + .../writing/creation/writePackageJson.test.ts | 185 + .../writing/creation/writePackageJson.ts | 124 + src/steps/writing/types.ts | 3 + src/steps/writing/writeStructure.test.ts | 46 + src/steps/writing/writeStructure.ts | 16 + .../writing/writeStructureWorker.test.ts | 55 + src/steps/writing/writeStructureWorker.ts | 55 + src/types.ts | 5 + tsconfig.eslint.json | 1 + tsconfig.json | 17 + tsup.config.ts | 11 + vitest.config.ts | 15 + 240 files changed, 26085 insertions(+) create mode 100644 .all-contributorsrc create mode 100644 .eslintrc.cjs create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/DEVELOPMENT.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/01-bug.yml create mode 100644 .github/ISSUE_TEMPLATE/02-documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/03-feature.yml create mode 100644 .github/ISSUE_TEMPLATE/04-tooling.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/actions/prepare/action.yml create mode 100644 .github/codecov.yml create mode 100644 .github/renovate.json create mode 100644 .github/workflows/accessibility-alt-text-bot.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/compliance.yml create mode 100644 .github/workflows/contributors.yml create mode 100644 .github/workflows/lint-knip.yml create mode 100644 .github/workflows/lint-markdown.yml create mode 100644 .github/workflows/lint-packages.yml create mode 100644 .github/workflows/lint-spelling.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/post-release.yml create mode 100644 .github/workflows/pr-review-requested.yml create mode 100644 .github/workflows/prettier.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test-create.yml create mode 100644 .github/workflows/test-initialize.yml create mode 100644 .github/workflows/test-migrate.yml create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/tsc.yml create mode 100644 .gitignore create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit create mode 100644 .markdownlint.json create mode 100644 .markdownlintignore create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .release-it.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 bin/index.js create mode 100644 cspell.json create mode 100644 docs/Creation.md create mode 100644 docs/FAQs.md create mode 100644 docs/Initialization.md create mode 100644 docs/Migration.md create mode 100644 docs/Options.md create mode 100644 docs/Tooling.md create mode 100644 docs/create-typescript-app.png create mode 100644 knip.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 script/__snapshots__/migrate-test-e2e.js.snap create mode 100644 script/create-test-e2e.js create mode 100644 script/initialize-test-e2e.js create mode 100644 script/migrate-test-e2e.js create mode 100644 script/vitest.config.ts create mode 100644 src/bin/help.test.ts create mode 100644 src/bin/help.ts create mode 100644 src/bin/index.test.ts create mode 100644 src/bin/index.ts create mode 100644 src/bin/packageJson.test.ts create mode 100644 src/bin/packageJson.ts create mode 100644 src/bin/promptForMode.test.ts create mode 100644 src/bin/promptForMode.ts create mode 100644 src/create/createAndEnterGitDirectory.test.ts create mode 100644 src/create/createAndEnterGitDirectory.ts create mode 100644 src/create/createRerunSuggestion.test.ts create mode 100644 src/create/createRerunSuggestion.ts create mode 100644 src/create/createWithOptions.test.ts create mode 100644 src/create/createWithOptions.ts create mode 100644 src/create/index.test.ts create mode 100644 src/create/index.ts create mode 100644 src/greet.test.ts create mode 100644 src/greet.ts create mode 100644 src/index.ts create mode 100644 src/initialize/index.test.ts create mode 100644 src/initialize/index.ts create mode 100644 src/initialize/initializeWithOptions.ts create mode 100644 src/migrate/index.test.ts create mode 100644 src/migrate/index.ts create mode 100644 src/migrate/migrateWithOptions.ts create mode 100644 src/shared/__snapshots__/generateNextSteps.test.ts.snap create mode 100644 src/shared/cli/lines.ts create mode 100644 src/shared/cli/lowerFirst.ts create mode 100644 src/shared/cli/outro.test.ts create mode 100644 src/shared/cli/outro.ts create mode 100644 src/shared/cli/spinners.ts create mode 100644 src/shared/cli/startLineWithDots.ts create mode 100644 src/shared/codes.ts create mode 100644 src/shared/createCleanupCommands.test.ts create mode 100644 src/shared/createCleanupCommands.ts create mode 100644 src/shared/doesRepositoryExist.test.ts create mode 100644 src/shared/doesRepositoryExist.ts create mode 100644 src/shared/ensureGitRepository.test.ts create mode 100644 src/shared/ensureGitRepository.ts create mode 100644 src/shared/generateNextSteps.test.ts create mode 100644 src/shared/generateNextSteps.ts create mode 100644 src/shared/getGitHubUserAsAllContributor.test.ts create mode 100644 src/shared/getGitHubUserAsAllContributor.ts create mode 100644 src/shared/options/args.ts create mode 100644 src/shared/options/augmentOptionsWithExcludes.test.ts create mode 100644 src/shared/options/augmentOptionsWithExcludes.ts create mode 100644 src/shared/options/createOptionDefaults/index.test.ts create mode 100644 src/shared/options/createOptionDefaults/index.ts create mode 100644 src/shared/options/createOptionDefaults/parsePackageAuthor.test.ts create mode 100644 src/shared/options/createOptionDefaults/parsePackageAuthor.ts create mode 100644 src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts create mode 100644 src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts create mode 100644 src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts create mode 100644 src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts create mode 100644 src/shared/options/createRepositoryWithApi.test.ts create mode 100644 src/shared/options/createRepositoryWithApi.ts create mode 100644 src/shared/options/detectEmailRedundancy.test.ts create mode 100644 src/shared/options/detectEmailRedundancy.ts create mode 100644 src/shared/options/ensureRepositoryExists.test.ts create mode 100644 src/shared/options/ensureRepositoryExists.ts create mode 100644 src/shared/options/exclusionKeys.ts create mode 100644 src/shared/options/getBase.test.ts create mode 100644 src/shared/options/getBase.ts create mode 100644 src/shared/options/getGitHub.test.ts create mode 100644 src/shared/options/getGitHub.ts create mode 100644 src/shared/options/getPrefillOrPromptedOption.test.ts create mode 100644 src/shared/options/getPrefillOrPromptedOption.ts create mode 100644 src/shared/options/logInferredOptions.test.ts create mode 100644 src/shared/options/logInferredOptions.ts create mode 100644 src/shared/options/optionsSchema.ts create mode 100644 src/shared/options/readOptions.test.ts create mode 100644 src/shared/options/readOptions.ts create mode 100644 src/shared/packages.test.ts create mode 100644 src/shared/packages.ts create mode 100644 src/shared/prompts.ts create mode 100644 src/shared/readFileAsJson.test.ts create mode 100644 src/shared/readFileAsJson.ts create mode 100644 src/shared/readFileSafe.test.ts create mode 100644 src/shared/readFileSafe.ts create mode 100644 src/shared/readFileSafeAsJson.ts create mode 100644 src/shared/runOrRestore.test.ts create mode 100644 src/shared/runOrRestore.ts create mode 100644 src/shared/tryCatchAsync.ts create mode 100644 src/shared/tryCatchLazyValueAsync.test.ts create mode 100644 src/shared/tryCatchLazyValueAsync.ts create mode 100644 src/shared/types.ts create mode 100644 src/steps/addOwnerAsAllContributor.test.ts create mode 100644 src/steps/addOwnerAsAllContributor.ts create mode 100644 src/steps/addToolAllContributors.test.ts create mode 100644 src/steps/addToolAllContributors.ts create mode 100644 src/steps/clearChangelog.ts create mode 100644 src/steps/clearUnnecessaryFiles.ts create mode 100644 src/steps/createJoshuaKGoldbergReplacement.test.ts create mode 100644 src/steps/createJoshuaKGoldbergReplacement.ts create mode 100644 src/steps/detectExistingContributors.test.ts create mode 100644 src/steps/detectExistingContributors.ts create mode 100644 src/steps/finalizeDependencies.test.ts create mode 100644 src/steps/finalizeDependencies.ts create mode 100644 src/steps/initializeBranchProtectionSettings.test.ts create mode 100644 src/steps/initializeGitHubRepository/index.ts create mode 100644 src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts create mode 100644 src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.test.ts create mode 100644 src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.ts create mode 100644 src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.test.ts create mode 100644 src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.ts create mode 100644 src/steps/initializeGitHubRepository/labels/outcomeLabels.ts create mode 100644 src/steps/initializeGitRemote.test.ts create mode 100644 src/steps/initializeGitRemote.ts create mode 100644 src/steps/initializeRepositorySettings.ts create mode 100644 src/steps/populateCSpellDictionary.test.ts create mode 100644 src/steps/populateCSpellDictionary.ts create mode 100644 src/steps/removeSetupScripts.ts create mode 100644 src/steps/resetGitTags.ts create mode 100644 src/steps/runCleanup.test.ts create mode 100644 src/steps/runCleanup.ts create mode 100644 src/steps/uninstallPackages.test.ts create mode 100644 src/steps/uninstallPackages.ts create mode 100644 src/steps/updateAllContributorsTable.ts create mode 100644 src/steps/updateLocalFiles.test.ts create mode 100644 src/steps/updateLocalFiles.ts create mode 100644 src/steps/updateReadme.test.ts create mode 100644 src/steps/updateReadme.ts create mode 100644 src/steps/writeReadme/findExistingBadges.test.ts create mode 100644 src/steps/writeReadme/findExistingBadges.ts create mode 100644 src/steps/writeReadme/findIntroSectionClose.test.ts create mode 100644 src/steps/writeReadme/findIntroSectionClose.ts create mode 100644 src/steps/writeReadme/generateTopContent.test.ts create mode 100644 src/steps/writeReadme/generateTopContent.ts create mode 100644 src/steps/writeReadme/index.test.ts create mode 100644 src/steps/writeReadme/index.ts create mode 100644 src/steps/writing/creation/createDotGitignore.test.ts create mode 100644 src/steps/writing/creation/createDotGitignore.ts create mode 100644 src/steps/writing/creation/createESLintRC.test.ts create mode 100644 src/steps/writing/creation/createESLintRC.ts create mode 100644 src/steps/writing/creation/createTsupConfig.test.ts create mode 100644 src/steps/writing/creation/createTsupConfig.ts create mode 100644 src/steps/writing/creation/dotGitHub/actions.test.ts create mode 100644 src/steps/writing/creation/dotGitHub/actions.ts create mode 100644 src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts create mode 100644 src/steps/writing/creation/dotGitHub/createDevelopment/index.ts create mode 100644 src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts create mode 100644 src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts create mode 100644 src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts create mode 100644 src/steps/writing/creation/dotGitHub/createWorkflowFile.test.ts create mode 100644 src/steps/writing/creation/dotGitHub/createWorkflowFile.ts create mode 100644 src/steps/writing/creation/dotGitHub/createWorkflows.test.ts create mode 100644 src/steps/writing/creation/dotGitHub/createWorkflows.ts create mode 100644 src/steps/writing/creation/dotGitHub/index.ts create mode 100644 src/steps/writing/creation/dotGitHub/issueTemplate.ts create mode 100644 src/steps/writing/creation/dotHusky.ts create mode 100644 src/steps/writing/creation/dotVSCode.test.ts create mode 100644 src/steps/writing/creation/dotVSCode.ts create mode 100644 src/steps/writing/creation/formatters/formatIgnoreFile.ts create mode 100644 src/steps/writing/creation/formatters/formatJson.test.ts create mode 100644 src/steps/writing/creation/formatters/formatJson.ts create mode 100644 src/steps/writing/creation/formatters/formatTypeScript.ts create mode 100644 src/steps/writing/creation/formatters/formatYaml.ts create mode 100644 src/steps/writing/creation/index.ts create mode 100644 src/steps/writing/creation/rootFiles.ts create mode 100644 src/steps/writing/creation/src.ts create mode 100644 src/steps/writing/creation/writeAllContributorsRC.ts create mode 100644 src/steps/writing/creation/writePackageJson.test.ts create mode 100644 src/steps/writing/creation/writePackageJson.ts create mode 100644 src/steps/writing/types.ts create mode 100644 src/steps/writing/writeStructure.test.ts create mode 100644 src/steps/writing/writeStructure.ts create mode 100644 src/steps/writing/writeStructureWorker.test.ts create mode 100644 src/steps/writing/writeStructureWorker.ts create mode 100644 src/types.ts create mode 100644 tsconfig.eslint.json create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 vitest.config.ts diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..dd52966 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,449 @@ +{ + "badgeTemplate": "\t\"👪\" src=\"https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-<%= contributors.length %>-21bb42.svg\" />", + "commit": false, + "commitConvention": "angular", + "commitType": "docs", + "contributors": [ + { + "avatar_url": "https://avatars.githubusercontent.com/u/3335181?v=4", + "contributions": [ + "bug", + "code", + "maintenance", + "review", + "tool", + "doc", + "infra", + "test", + "ideas" + ], + "login": "JoshuaKGoldberg", + "name": "Josh Goldberg", + "profile": "http://www.joshuakgoldberg.com" + }, + { + "login": "sinchang", + "name": "Jeff Wen", + "avatar_url": "https://avatars.githubusercontent.com/u/3297859?v=4", + "profile": "https://sinchang.me", + "contributions": [ + "code" + ] + }, + { + "login": "Pinjasaur", + "name": "Paul Esch-Laurent", + "avatar_url": "https://avatars.githubusercontent.com/u/6335792?v=4", + "profile": "https://paulisaweso.me/", + "contributions": [ + "code" + ] + }, + { + "login": "NazCodeland", + "name": "NazCodeland", + "avatar_url": "https://avatars.githubusercontent.com/u/113494366?v=4", + "profile": "https://github.com/NazCodeland", + "contributions": [ + "code" + ] + }, + { + "login": "johnnyreilly", + "name": "John Reilly", + "avatar_url": "https://avatars.githubusercontent.com/u/1010525?v=4", + "profile": "https://blog.johnnyreilly.com/", + "contributions": [ + "code", + "ideas", + "bug", + "maintenance", + "doc" + ] + }, + { + "login": "webpro", + "name": "Lars Kappert", + "avatar_url": "https://avatars.githubusercontent.com/u/456426?v=4", + "profile": "https://webpro.nl", + "contributions": [ + "code" + ] + }, + { + "login": "RebeccaStevens", + "name": "Rebecca Stevens", + "avatar_url": "https://avatars.githubusercontent.com/u/7224206?v=4", + "profile": "https://github.com/RebeccaStevens", + "contributions": [ + "code", + "infra" + ] + }, + { + "login": "ronthetech", + "name": "Ron Jean-Francois", + "avatar_url": "https://avatars.githubusercontent.com/u/105710107?v=4", + "profile": "http://ronjeanfrancois.com", + "contributions": [ + "code", + "infra" + ] + }, + { + "login": "nowyDEV", + "name": "Dominik Nowik", + "avatar_url": "https://avatars.githubusercontent.com/u/12304307?v=4", + "profile": "https://github.com/nowyDEV", + "contributions": [ + "tool", + "code", + "ideas" + ] + }, + { + "login": "TAKANOME-DEV", + "name": "takanomedev", + "avatar_url": "https://avatars.githubusercontent.com/u/79809121?v=4", + "profile": "https://github.com/TAKANOME-DEV", + "contributions": [ + "code" + ] + }, + { + "login": "emday4prez", + "name": "Emerson", + "avatar_url": "https://avatars.githubusercontent.com/u/35363144?v=4", + "profile": "https://github.com/emday4prez", + "contributions": [ + "code" + ] + }, + { + "login": "jsjoeio", + "name": "Joe Previte", + "avatar_url": "https://avatars.githubusercontent.com/u/3806031?v=4", + "profile": "https://typescriptcourse.com/tutorials", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "navin-moorthy", + "name": "Navin Moorthy", + "avatar_url": "https://avatars.githubusercontent.com/u/39694575?v=4", + "profile": "https://navinmoorthy.me/", + "contributions": [ + "bug", + "code", + "ideas" + ] + }, + { + "login": "garuna-m6", + "name": "Anurag", + "avatar_url": "https://avatars.githubusercontent.com/u/23234342?v=4", + "profile": "https://github.com/garuna-m6", + "contributions": [ + "code" + ] + }, + { + "login": "danielroe", + "name": "Daniel Roe", + "avatar_url": "https://avatars.githubusercontent.com/u/28706372?v=4", + "profile": "https://roe.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "the-lazy-learner", + "name": "Sudhansu", + "avatar_url": "https://avatars.githubusercontent.com/u/13695177?v=4", + "profile": "https://github.com/the-lazy-learner", + "contributions": [ + "code" + ] + }, + { + "login": "RNR1", + "name": "Ron Braha", + "avatar_url": "https://avatars.githubusercontent.com/u/45559220?v=4", + "profile": "https://linktr.ee/ronbraha", + "contributions": [ + "code", + "design", + "test" + ] + }, + { + "login": "tungbq", + "name": "Tung Bui (Leo)", + "avatar_url": "https://avatars.githubusercontent.com/u/85242618?v=4", + "profile": "https://github.com/tungbq", + "contributions": [ + "code" + ] + }, + { + "login": "orta", + "name": "Orta Therox", + "avatar_url": "https://avatars.githubusercontent.com/u/49038?v=4", + "profile": "https://orta.io", + "contributions": [ + "code" + ] + }, + { + "login": "promise-dash", + "name": "Promise Dash", + "avatar_url": "https://avatars.githubusercontent.com/u/86062880?v=4", + "profile": "https://github.com/promise-dash", + "contributions": [ + "code" + ] + }, + { + "login": "jolg42", + "name": "Joël Galeran", + "avatar_url": "https://avatars.githubusercontent.com/u/1328733?v=4", + "profile": "https://twitter.com/Jolg42", + "contributions": [ + "code" + ] + }, + { + "login": "kristo-baricevic", + "name": "Kristo Baricevic", + "avatar_url": "https://avatars.githubusercontent.com/u/108290619?v=4", + "profile": "https://kristo-baricevic.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "ryota-murakami", + "name": "Ryota Murakami", + "avatar_url": "https://avatars.githubusercontent.com/u/5501268?v=4", + "profile": "https://ryota-murakami.github.io/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "ruthwikreddy09", + "name": "Ruthwik", + "avatar_url": "https://avatars.githubusercontent.com/u/126862059?v=4", + "profile": "https://github.com/RuthwikReddy09", + "contributions": [ + "code" + ] + }, + { + "login": "jdwilkin4", + "name": "Jessica Wilkins ", + "avatar_url": "https://avatars.githubusercontent.com/u/67210629?v=4", + "profile": "https://jessicawilkins.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "vasanth9", + "name": "Vasanth Kumar Cheepurupalli", + "avatar_url": "https://avatars.githubusercontent.com/u/42891954?v=4", + "profile": "https://github.com/vasanth9", + "contributions": [ + "code" + ] + }, + { + "login": "conrmahr", + "name": "Conor Meagher", + "avatar_url": "https://avatars.githubusercontent.com/u/363781?v=4", + "profile": "https://conormeagher.com/", + "contributions": [ + "code" + ] + }, + { + "login": "DanexQ", + "name": "Daniel", + "avatar_url": "https://avatars.githubusercontent.com/u/72567464?v=4", + "profile": "https://github.com/DanexQ", + "contributions": [ + "infra" + ] + }, + { + "login": "jaas666", + "name": "Juan A.", + "avatar_url": "https://avatars.githubusercontent.com/u/30204147?v=4", + "profile": "https://github.com/jaas666", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "katt", + "name": "Alex / KATT", + "avatar_url": "https://avatars.githubusercontent.com/u/459267?v=4", + "profile": "https://katt.dev", + "contributions": [ + "bug" + ] + }, + { + "login": "dertimonius", + "name": "Timon Jurschitsch", + "avatar_url": "https://avatars.githubusercontent.com/u/103483059?v=4", + "profile": "https://www.linkedin.com/in/timonjurschitsch/", + "contributions": [ + "code" + ] + }, + { + "login": "biplobsd", + "name": "Biplob Sutradhar", + "avatar_url": "https://avatars.githubusercontent.com/u/43641536?v=4", + "profile": "http://biplobsd.me", + "contributions": [ + "code" + ] + }, + { + "login": "mrswastik-robot", + "name": "Swastik Patel", + "avatar_url": "https://avatars.githubusercontent.com/u/107865087?v=4", + "profile": "https://github.com/mrswastik-robot", + "contributions": [ + "doc" + ] + }, + { + "login": "gv14982", + "name": "Graham Vasquez", + "avatar_url": "https://avatars.githubusercontent.com/u/7041175?v=4", + "profile": "https://gvasquez.dev", + "contributions": [ + "code" + ] + }, + { + "login": "dominicduffin1", + "name": "Dominic Duffin", + "avatar_url": "https://avatars.githubusercontent.com/u/26224873?v=4", + "profile": "https://dominicduffin.uk", + "contributions": [ + "code" + ] + }, + { + "login": "5hraddha", + "name": "Shraddha", + "avatar_url": "https://avatars.githubusercontent.com/u/27571141?v=4", + "profile": "https://www.shraddha.tech", + "contributions": [ + "code" + ] + }, + { + "login": "xl4624", + "name": "Xiaomin Liu", + "avatar_url": "https://avatars.githubusercontent.com/u/116298054?v=4", + "profile": "https://github.com/xl4624", + "contributions": [ + "code" + ] + }, + { + "login": "jamiemagee", + "name": "Jamie Magee", + "avatar_url": "https://avatars.githubusercontent.com/u/1358764?v=4", + "profile": "https://jamiemagee.co.uk", + "contributions": [ + "ideas" + ] + }, + { + "login": "praveenshinde3", + "name": "Praveen Shinde", + "avatar_url": "https://avatars.githubusercontent.com/u/107350270?v=4", + "profile": "https://praveenshinde.vercel.app/", + "contributions": [ + "code" + ] + }, + { + "login": "aslemammad", + "name": "Mohammad Bagher Abiyat", + "avatar_url": "https://avatars.githubusercontent.com/u/37929992?v=4", + "profile": "https://github.com/Aslemammad", + "contributions": [ + "code" + ] + }, + { + "login": "lcforbes", + "name": "lcforbes", + "avatar_url": "https://avatars.githubusercontent.com/u/42080532?v=4", + "profile": "https://github.com/lcforbes", + "contributions": [ + "bug" + ] + }, + { + "login": "danvk", + "name": "Dan Vanderkam", + "avatar_url": "https://avatars.githubusercontent.com/u/98301?v=4", + "profile": "https://effectivetypescript.com", + "contributions": [ + "bug", + "ideas", + "tool" + ] + }, + { + "login": "nandertga", + "name": "nandertga", + "avatar_url": "https://avatars.githubusercontent.com/u/65074195?v=4", + "profile": "http://nandertga.ddns.net", + "contributions": [ + "code" + ] + }, + { + "login": "demianparkhomenko", + "name": "Demian Parkhomenko", + "avatar_url": "https://avatars.githubusercontent.com/u/95881717?v=4", + "profile": "https://linktr.ee/DemianParkhomenko", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "niklas-wortmann", + "name": "Jan-Niklas W.", + "avatar_url": "https://avatars.githubusercontent.com/u/6104311?v=4", + "profile": "https://niklas-wortmann.com", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7, + "contributorsSortAlphabetically": true, + "files": [ + "README.md" + ], + "imageSize": 100, + "projectName": "create-typescript-app", + "projectOwner": "JoshuaKGoldberg", + "repoHost": "https://github.com", + "repoType": "github" +} diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..8e556f3 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,184 @@ +/* +👋 Hi! This ESLint configuration contains a lot more stuff than many repos'! +You can read from it to see all sorts of linting goodness, but don't worry - +it's not something you need to exhaustively understand immediately. 💙 + +If you're interested in learning more, see the 'getting started' docs on: +- ESLint: https://eslint.org +- typescript-eslint: https://typescript-eslint.io +*/ + +/** @type {import("@types/eslint").Linter.Config} */ +module.exports = { + env: { + es2022: true, + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:eslint-comments/recommended", + "plugin:n/recommended", + "plugin:perfectionist/recommended-natural", + "plugin:regexp/recommended", + "plugin:vitest/recommended", + ], + ignorePatterns: ["!.*", "coverage*", "lib", "node_modules", "pnpm-lock.yaml"], + overrides: [ + { + extends: ["plugin:markdown/recommended"], + files: ["**/*.md"], + processor: "markdown/markdown", + }, + { + extends: [ + "plugin:jsdoc/recommended-typescript-error", + "plugin:@typescript-eslint/strict", + "plugin:@typescript-eslint/stylistic", + ], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + rules: { + // These off-by-default rules work well for this repo and we like them on. + "jsdoc/informative-docs": "error", + "logical-assignment-operators": [ + "error", + "always", + { enforceForIfStatements: true }, + ], + "operator-assignment": "error", + + // These on-by-default rules don't work well for this repo and we like them off. + "jsdoc/require-jsdoc": "off", + "jsdoc/require-param": "off", + "jsdoc/require-property": "off", + "jsdoc/require-returns": "off", + }, + }, + { + files: "**/*.md/*.ts", + rules: { + "n/no-missing-import": [ + "error", + { allowModules: ["create-typescript-app"] }, + ], + }, + }, + { + excludedFiles: ["**/*.md/*.ts"], + extends: [ + "plugin:@typescript-eslint/strict-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + ], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.eslint.json", + }, + rules: { + // These off-by-default rules work well for this repo and we like them on. + "deprecation/deprecation": "error", + + // These more-strict-by-default rules don't work well for this repo and we like them less strict. + "@typescript-eslint/no-unnecessary-condition": [ + "error", + { + allowConstantLoopConditions: true, + }, + ], + "@typescript-eslint/prefer-nullish-coalescing": [ + "error", + { ignorePrimitives: true }, + ], + }, + }, + { + excludedFiles: ["package.json"], + extends: ["plugin:jsonc/recommended-with-json"], + files: ["*.json", "*.jsonc"], + parser: "jsonc-eslint-parser", + rules: { + "jsonc/comma-dangle": "off", + "jsonc/sort-keys": "error", + }, + }, + { + files: ["*.jsonc"], + rules: { + "jsonc/no-comments": "off", + }, + }, + { + extends: ["plugin:package-json/recommended"], + files: ["package.json"], + parser: "jsonc-eslint-parser", + plugins: ["package-json"], + }, + { + files: "**/*.test.ts", + rules: { + // These on-by-default rules aren't useful in test files. + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + }, + }, + { + extends: ["plugin:yml/standard", "plugin:yml/prettier"], + files: ["**/*.{yml,yaml}"], + parser: "yaml-eslint-parser", + rules: { + "yml/file-extension": ["error", { extension: "yml" }], + "yml/sort-keys": [ + "error", + { + order: { type: "asc" }, + pathPattern: "^.*$", + }, + ], + "yml/sort-sequence-values": [ + "error", + { + order: { type: "asc" }, + pathPattern: "^.*$", + }, + ], + }, + }, + ], + parser: "@typescript-eslint/parser", + plugins: [ + "@typescript-eslint", + "deprecation", + "jsdoc", + "perfectionist", + "regexp", + "vitest", + ], + reportUnusedDisableDirectives: true, + root: true, + rules: { + // These off/less-strict-by-default rules work well for this repo and we like them on. + "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }], + + // These on-by-default rules don't work well for this repo and we like them off. + "no-case-declarations": "off", + "no-constant-condition": "off", + "no-inner-declarations": "off", + "no-mixed-spaces-and-tabs": "off", + + // Stylistic concerns that don't interfere with Prettier + "@typescript-eslint/padding-line-between-statements": [ + "error", + { blankLine: "always", next: "*", prev: "block-like" }, + ], + "no-useless-rename": "error", + "object-shorthand": "error", + "perfectionist/sort-objects": [ + "error", + { + order: "asc", + "partition-by-comment": true, + type: "natural", + }, + ], + }, +}; diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fa256b8 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +github@joshuakgoldberg.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..4049fea --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing + +Thanks for your interest in contributing to `create-typescript-app`! 💖 + +> After this page, see [DEVELOPMENT.md](./DEVELOPMENT.md) for local development instructions. + +## Code of Conduct + +This project contains a [Contributor Covenant code of conduct](./CODE_OF_CONDUCT.md) all contributors are expected to follow. + +## Reporting Issues + +Please do [report an issue on the issue tracker](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/new/choose) if there's any bugfix, documentation improvement, or general enhancement you'd like to see in the repository! Please fully fill out all required fields in the most appropriate issue form. + +## Sending Contributions + +Sending your own changes as contribution is always appreciated! +There are two steps involved: + +1. [Finding an Issue](#finding-an-issue) +2. [Sending a Pull Request](#sending-a-pull-request) + +### Finding an Issue + +With the exception of very small typos, all changes to this repository generally need to correspond to an [unassigned open issue marked as `status: accepting prs` on the issue tracker](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). +If this is your first time contributing, consider searching for [unassigned issues that also have the `good first issue` label](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). +If the issue you'd like to fix isn't found on the issue, see [Reporting Issues](#reporting-issues) for filing your own (please do!). + +#### Issue Claiming + +We don't use any kind of issue claiming system. +We've found in the past that they result in accidental ["licked cookie"](https://devblogs.microsoft.com/oldnewthing/20091201-00/?p=15843) situations where contributors claim an issue but run out of time or energy trying before sending a PR. + +If an unassigned issue has been marked as `status: accepting prs` and an open PR does not exist, feel free to send a PR. +Please don't post comments asking for permission or stating you will work on an issue. + +### Sending a Pull Request + +Once you've identified an open issue accepting PRs that doesn't yet have a PR sent, you're free to send a pull request. +Be sure to fill out the pull request template's requested information -- otherwise your PR will likely be closed. + +PRs are also expected to have a title that adheres to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0). +Only PR titles need to be in that format, not individual commits. +Don't worry if you get this wrong: you can always change the PR title after sending it. +Check [previously merged PRs](https://github.com/JoshuaKGoldberg/create-typescript-app/pulls?q=is%3Apr+is%3Amerged+-label%3Adependencies+) for reference. + +#### Draft PRs + +If you don't think your PR is ready for review, [set it as a draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#converting-a-pull-request-to-a-draft). +Draft PRs won't be reviewed. + +#### Granular PRs + +Please keep pull requests single-purpose: in other words, don't attempt to solve multiple unrelated problems in one pull request. +Send one PR per area of concern. +Multi-purpose pull requests are harder and slower to review, block all changes from being merged until the whole pull request is reviewed, and are difficult to name well with semantic PR titles. + +#### Pull Request Reviews + +When a PR is not in draft, it's considered ready for review. +Please don't manually `@` tag anybody to request review. +A maintainer will look at it when they're next able to. + +PRs should have passing [GitHub status checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks) before review is requested (unless there are explicit questions asked in the PR about any failures). + +#### Asking Questions + +If you need help and/or have a question, posting a comment in the PR is a great way to do so. +There's no need to tag anybody individually. +One of us will drop by and help when we can. + +Please post comments as [line comments](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#adding-line-comments-to-a-pull-request) when possible, so that they can be threaded. +You can [resolve conversations](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#resolving-conversations) on your own when you feel they're resolved - no need to comment explicitly and/or wait for a maintainer. + +#### Requested Changes + +After a maintainer reviews your PR, they may request changes on it. +Once you've made those changes, [re-request review on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews#re-requesting-a-review). + +Please try not to force-push commits to PRs that have already been reviewed. +Doing so makes it harder to review the changes. +We squash merge all commits so there's no need to try to preserve Git history within a PR branch. + +Once you've addressed all our feedback by making code changes and/or started a followup discussion, [re-request review](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews#re-requesting-a-review) from each maintainer whose feedback you addressed. + +Once all feedback is addressed and the PR is approved, we'll ensure the branch is up to date with `main` and merge it for you. + +#### Post-Merge Recognition + +Once your PR is merged, if you haven't yet been added to the [_Contributors_ table in the README.md](../README.md#contributors) for its [type of contribution](https://allcontributors.org/docs/en/emoji-key "Allcontributors emoji key"), you should be soon. +Please do ping the maintainer who merged your PR if that doesn't happen within 24 hours - it was likely an oversight on our end! + +## Emojis & Appreciation + +If you made it all the way to the end, bravo dear user, we love you. +Please include your favorite emoji in the bottom of your issues and PRs to signal to us that you did in fact read this file and are trying to conform to it as best as possible. +💖 is a good starter if you're not sure which to use. diff --git a/.github/DEVELOPMENT.md b/.github/DEVELOPMENT.md new file mode 100644 index 0000000..e827350 --- /dev/null +++ b/.github/DEVELOPMENT.md @@ -0,0 +1,276 @@ +# Development + +> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository). +> It'll walk you through the common activities you'll need to contribute. + +After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + +```shell +git clone https://github.com//create-typescript-app +cd create-typescript-app +pnpm install +``` + +> This repository includes a list of suggested VS Code extensions. +> It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + +## Building + +Run [**tsup**](https://tsup.egoist.dev) locally to build source files from `src/` into output files in `lib/`: + +```shell +pnpm build +``` + +Add `--watch` to run the builder in a watch mode that continuously cleans and recreates `lib/` as you save files: + +```shell +pnpm build --watch +``` + +## Formatting + +[Prettier](https://prettier.io) is used to format code. +It should be applied automatically when you save files in VS Code or make a Git commit. + +To manually reformat all files, you can run: + +```shell +pnpm format --write +``` + +## Linting + +This package includes several forms of linting to enforce consistent code quality and styling. +Each should be shown in VS Code, and can be run manually on the command-line: + +- `pnpm lint` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files +- `pnpm lint:knip` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports +- `pnpm lint:md` ([Markdownlint](https://github.com/DavidAnson/markdownlint)): Checks Markdown source files +- `pnpm lint:packages` ([pnpm dedupe --check](https://pnpm.io/cli/dedupe)): Checks for unnecessarily duplicated packages in the `pnpm-lock.yml` file +- `pnpm lint:spelling` ([cspell](https://cspell.org)): Spell checks across all source files + +Read the individual documentation for each linter to understand how it can be configured and used best. + +For example, ESLint can be run with `--fix` to auto-fix some lint rule complaints: + +```shell +pnpm run lint --fix +``` + +Note that you'll likely need to run `pnpm build` before `pnpm lint` so that lint rules which check the file system can pick up on any built files. + +## Testing + +[Vitest](https://vitest.dev) is used for tests. +You can run it locally on the command-line: + +```shell +pnpm run test +``` + +Add the `--coverage` flag to compute test coverage and place reports in the `coverage/` directory: + +```shell +pnpm run test --coverage +``` + +Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. +Calls to `console.log`, `console.warn`, and other console methods will cause a test to fail. + +### Debugging Tests + +This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. +To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). + +## Type Checking + +You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + +However, it can be useful to run the TypeScript command-line (`tsc`) to type check all files in `src/`: + +```shell +pnpm tsc +``` + +Add `--watch` to keep the type checker running in a watch mode that updates the display as you save files: + +```shell +pnpm tsc --watch +``` + +## Debugging + +This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging. +Depending upon the type of usage, it can include debugging for unit tests _and_ for executable (or "bin") apps. + +### Unit Tests + +To debug a unit test, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). + +### `bin` Apps + +To debug a `bin` app, add a breakpoint to your code, then run _Debug Program_ from the VS Code Debug panel (or press F5). + +## Setup Scripts + +As described in the `README.md` file and `docs/`, this template repository comes with three scripts that can set up an existing or new repository. + +Each follows roughly the same general flow: + +1. `bin/index.ts` uses `bin/mode.ts` to determine which of the three setup scripts to run +2. `readOptions` parses in options from local files, Git commands, npm APIs, and/or files on disk +3. `runOrRestore` wraps the setup script's main logic in a friendly prompt wrapper +4. The setup script wraps each portion of its main logic with `withSpinner` + - Each step of setup logic is generally imported from within `src/steps` +5. A call to `outro` summarizes the results for the user + +> **Warning** +> Each setup script overrides many files in the directory they're run in. +> Make sure to save any changes you want to preserve before running them. + +### The Creation Script + +> 📝 See [`docs/Creation.md`](../docs/Creation.md) for user documentation on the creation script. + +This template's "creation" script is located in `src/create/`. +You can run it locally with `node bin/index.js --mode create`. +Note that files need to be built with `pnpm run build` beforehand. + +#### Testing the Creation Script + +You can run the end-to-end test for creation locally on the command-line. +Note that the files need to be built with `pnpm run build` beforehand. + +```shell +pnpm run test:create +``` + +That end-to-end test executes `script/create-test-e2e.js`, which: + +1. Runs the creation script to create a new `test-repository` child directory and repository, capturing code coverage +2. Asserts that commands such as `build` and `lint` each pass + +The `pnpm run test:create` script is run in CI to ensure that templating changes are in sync with the template's actual files. +See `.github/workflows/test-create.yml`. + +### The Initialization Script + +> 📝 See [`docs/Initialization.md`](../docs/Initialization.md) for user documentation on the initialization script. + +This template's "initialization" script is located in `src/initialize/`. +You can run it locally with `pnpm run initialize`. +It uses [`tsx`](https://github.com/esbuild-kit/tsx) so you don't need to build files before running. + +```shell +pnpm run initialize +``` + +#### Testing the Initialization Script + +You can run the end-to-end test for initializing locally on the command-line. +Note that files need to be built with `pnpm run build` beforehand. + +```shell +pnpm run test:initialize +``` + +That end-to-end test executes `script/initialize-test-e2e.js`, which: + +1. Runs the initialization script using `--skip-github-api` and other skip flags +2. Checks that the local repository's files were changed correctly (e.g. removed initialization-only files) +3. Runs `pnpm run lint:knip` to make sure no excess dependencies or files were left over +4. Resets everything +5. Runs initialization a second time, capturing test coverage + +The `pnpm run test:initialize` script is run in CI to ensure that templating changes are in sync with the template's actual files. +See `.github/workflows/test-initialize.yml`. + +### The Migration Script + +> 📝 See [`docs/Migration.md`](../docs/Migration.md) for user documentation on the migration script. + +This template's "migration" script is located in `src/migrate/`. +Note that files need to be built with `pnpm run build` beforehand. + +To test out the script locally, run it from a different repository's directory: + +```shell +cd ../other-repo +node ../create-typescript-app/bin/migrate.js +``` + +The migration script will work on any directory. +You can try it out in a blank directory with scripts like: + +```shell +cd .. +mkdir temp +cd temp +node ../create-typescript-app/bin/migrate.js +``` + +#### Testing the Migration Script + +> 💡 Seeing `Oh no! Running the migrate script unexpectedly modified:` errors? +> _[Unexpected File Modifications](#unexpected-file-modifications)_ covers that below. + +You can run the end-to-end test for migrating locally on the command-line: + +```shell +pnpm run test:migrate +``` + +That end-to-end test executes `script/migrate-test-e2e.js`, which: + +1. Runs the migration script using `--skip-github-api` and other skip flags, capturing code coverage +2. Checks that only a small list of allowed files were changed +3. Checks that the local repository's files were changed correctly (e.g. removed initialization-only files) + +The `pnpm run test:migrate` script is run in CI to ensure that templating changes are in sync with the template's actual files. +See `.github/workflows/test-migrate.yml`. + +> Tip: if the migration test is failing in CI and you don't see any errors, try [downloading the full logs](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/using-workflow-run-logs#downloading-logs). + +##### Migration Snapshot Failures + +The migration test uses the [Vitest file snapshot](https://vitest.dev/guide/snapshot#file-snapshots) in `script/__snapshots__/migrate-test-e2e.js.snap` to store expected differences to this repository after running the migration script. +The end-to-end migration test will fail any changes that don't keep the same differences in that snapshot. + +You can update the snapshot file by: + +1. Committing any changes to your local repository +2. Running `pnpm i` and `pnpm build` if any updates have been made to the `package.json` or `src/` files, respectively +3. Running `pnpm run test:migrate -u` to update the snapshot + +At this point there will be some files changed: + +- `script/__snapshots__/migrate-test-e2e.js.snap` will have updates if any files mismatched templates +- The actual updated files on disk will be there too + +If the snapshot file changes are what you expected, then you can commit them. +The rest of the file changes can be reverted. + +> [🚀 Feature: Add a way to apply known file changes after migration #1184](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1184) tracks turning the test snapshot into a feature. + +##### Unexpected File Modifications + +The migration test also asserts that no files were unexpectedly changed. +If you see a failure like: + +```plaintext +Oh no! Running the migrate script unexpectedly modified: + - ... +``` + +...then that means the file generated from templates differs from what's checked into the repository. +This is most often caused by changes to templates not being applied to checked-in files too. + +Templates for files are generally stored in [`src/steps/writing/creation`] under a path roughly corresponding to the file they describe. +For example, the template for `tsup.config.ts` is stored in [`src/steps/writing/creation/createTsupConfig.ts`](../src/steps/writing/creation/createTsupConfig.ts). +If the `createTsupConfig` function were to be modified without an equivalent change to `tsup.config.ts` -or vice-versa- then the migration test would report: + +```plaintext +Oh no! Running the migrate script unexpectedly modified: + - tsup.config.ts +``` diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c51a73f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: JoshuaKGoldberg diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..e9c06c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ + + + + + + +## Overview + +... diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml new file mode 100644 index 0000000..179e277 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -0,0 +1,33 @@ +body: + - attributes: + description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you! + label: Bug Report Checklist + options: + - label: I have tried restarting my IDE and the issue persists. + required: true + - label: I have pulled the latest `main` branch of the repository. + required: true + - label: I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue. + required: true + type: checkboxes + - attributes: + description: What did you expect to happen? + label: Expected + type: textarea + validations: + required: true + - attributes: + description: What happened instead? + label: Actual + type: textarea + validations: + required: true + - attributes: + description: Any additional info you'd like to provide. + label: Additional Info + type: textarea +description: Report a bug trying to run the code +labels: + - "type: bug" +name: 🐛 Bug +title: "🐛 Bug: " diff --git a/.github/ISSUE_TEMPLATE/02-documentation.yml b/.github/ISSUE_TEMPLATE/02-documentation.yml new file mode 100644 index 0000000..5ee0fd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-documentation.yml @@ -0,0 +1,25 @@ +body: + - attributes: + description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you! + label: Bug Report Checklist + options: + - label: I have pulled the latest `main` branch of the repository. + required: true + - label: I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue. + required: true + type: checkboxes + - attributes: + description: What would you like to report? + label: Overview + type: textarea + validations: + required: true + - attributes: + description: Any additional info you'd like to provide. + label: Additional Info + type: textarea +description: Report a typo or missing area of documentation +labels: + - "area: documentation" +name: 📝 Documentation +title: "📝 Documentation: " diff --git a/.github/ISSUE_TEMPLATE/03-feature.yml b/.github/ISSUE_TEMPLATE/03-feature.yml new file mode 100644 index 0000000..a09557a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-feature.yml @@ -0,0 +1,25 @@ +body: + - attributes: + description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you! + label: Bug Report Checklist + options: + - label: I have pulled the latest `main` branch of the repository. + required: true + - label: I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue. + required: true + type: checkboxes + - attributes: + description: What did you expect to be able to do? + label: Overview + type: textarea + validations: + required: true + - attributes: + description: Any additional info you'd like to provide. + label: Additional Info + type: textarea +description: Request that a new feature be added or an existing feature improved +labels: + - "type: feature" +name: 🚀 Feature +title: "🚀 Feature: " diff --git a/.github/ISSUE_TEMPLATE/04-tooling.yml b/.github/ISSUE_TEMPLATE/04-tooling.yml new file mode 100644 index 0000000..8814d72 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04-tooling.yml @@ -0,0 +1,27 @@ +body: + - attributes: + description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you! + label: Bug Report Checklist + options: + - label: I have tried restarting my IDE and the issue persists. + required: true + - label: I have pulled the latest `main` branch of the repository. + required: true + - label: I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue. + required: true + type: checkboxes + - attributes: + description: What did you expect to be able to do? + label: Overview + type: textarea + validations: + required: true + - attributes: + description: Any additional info you'd like to provide. + label: Additional Info + type: textarea +description: Report a bug or request an enhancement in repository tooling +labels: + - "area: tooling" +name: 🛠 Tooling +title: "🛠 Tooling: " diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..dd02f44 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ + + +## PR Checklist + +- [ ] Addresses an existing open issue: fixes #000 +- [ ] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) +- [ ] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken + +## Overview + + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..5ea3d61 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +We take all security vulnerabilities seriously. +If you have a vulnerability or other security issues to disclose: + +- Thank you very much, please do! +- Please send them to us by emailing `github@joshuakgoldberg.com` + +We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml new file mode 100644 index 0000000..cb825e6 --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,14 @@ +description: Prepares the repo for a typical CI job + +name: Prepare + +runs: + steps: + - uses: pnpm/action-setup@v2 + - uses: actions/setup-node@v4 + with: + cache: pnpm + node-version: "20" + - run: pnpm install --frozen-lockfile + shell: bash + using: composite diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..7097638 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,5 @@ +codecov: + notify: + after_n_builds: 4 +comment: + after_n_builds: 4 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..639a146 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "automerge": true, + "internalChecksFilter": "strict", + "labels": ["dependencies"], + "minimumReleaseAge": "3 days", + "postUpdateOptions": ["pnpmDedupe"] +} diff --git a/.github/workflows/accessibility-alt-text-bot.yml b/.github/workflows/accessibility-alt-text-bot.yml new file mode 100644 index 0000000..96af67f --- /dev/null +++ b/.github/workflows/accessibility-alt-text-bot.yml @@ -0,0 +1,26 @@ +jobs: + accessibility_alt_text_bot: + if: ${{ !endsWith(github.actor, '[bot]') }} + runs-on: ubuntu-latest + steps: + - uses: github/accessibility-alt-text-bot@v1.4.0 + +name: Accessibility Alt Text Bot + +on: + issue_comment: + types: + - created + - edited + issues: + types: + - edited + - opened + pull_request: + types: + - edited + - opened + +permissions: + issues: write + pull-requests: write diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..057fc54 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,16 @@ +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build + - run: node ./lib/index.js + +name: Build + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml new file mode 100644 index 0000000..341d28d --- /dev/null +++ b/.github/workflows/compliance.yml @@ -0,0 +1,28 @@ +jobs: + compliance: + runs-on: ubuntu-latest + steps: + - uses: mtfoley/pr-compliance-action@main + with: + body-auto-close: false + ignore-authors: |- + allcontributors + allcontributors[bot] + renovate + renovate[bot] + ignore-team-members: false + +name: Compliance + +on: + pull_request: + branches: + - main + types: + - edited + - opened + - reopened + - synchronize + +permissions: + pull-requests: write diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 0000000..9d22aa1 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,18 @@ +jobs: + contributors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/prepare + - env: + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + uses: JoshuaKGoldberg/all-contributors-auto-action@v0.4.3 + +name: Contributors + +on: + push: + branches: + - main diff --git a/.github/workflows/lint-knip.yml b/.github/workflows/lint-knip.yml new file mode 100644 index 0000000..781d52e --- /dev/null +++ b/.github/workflows/lint-knip.yml @@ -0,0 +1,15 @@ +jobs: + lint_knip: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:knip + +name: Lint Knip + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml new file mode 100644 index 0000000..acac714 --- /dev/null +++ b/.github/workflows/lint-markdown.yml @@ -0,0 +1,15 @@ +jobs: + lint_markdown: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:md + +name: Lint Markdown + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/lint-packages.yml b/.github/workflows/lint-packages.yml new file mode 100644 index 0000000..87520ca --- /dev/null +++ b/.github/workflows/lint-packages.yml @@ -0,0 +1,15 @@ +jobs: + lint_packages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:packages + +name: Lint Packages + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/lint-spelling.yml b/.github/workflows/lint-spelling.yml new file mode 100644 index 0000000..ef020b6 --- /dev/null +++ b/.github/workflows/lint-spelling.yml @@ -0,0 +1,15 @@ +jobs: + lint_spelling: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:spelling + +name: Lint spelling + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ed6d2a9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,16 @@ +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build --no-dts + - run: pnpm lint + +name: Lint + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml new file mode 100644 index 0000000..4f649c4 --- /dev/null +++ b/.github/workflows/post-release.yml @@ -0,0 +1,27 @@ +jobs: + post_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: echo "npm_version=$(npm pkg get version | tr -d '"')" >> "$GITHUB_ENV" + - uses: apexskier/github-release-commenter@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + comment-template: | + :tada: This is included in version {release_link} :tada: + + The release is available on: + + * [GitHub releases](https://github.com/JoshuaKGoldberg/create-typescript-app/releases/tag/{release_tag}) + * [npm package (@latest dist-tag)](https://www.npmjs.com/package/create-typescript-app/v/${{ env.npm_version }}) + + Cheers! 📦🚀 + +name: Post Release + +on: + release: + types: + - published diff --git a/.github/workflows/pr-review-requested.yml b/.github/workflows/pr-review-requested.yml new file mode 100644 index 0000000..e2e518c --- /dev/null +++ b/.github/workflows/pr-review-requested.yml @@ -0,0 +1,21 @@ +jobs: + pr_review_requested: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: "status: waiting for author" + - if: failure() + run: | + echo "Don't worry if the previous step failed." + echo "See https://github.com/actions-ecosystem/action-remove-labels/issues/221." + +name: PR Review Requested + +on: + pull_request_target: + types: + - review_requested + +permissions: + pull-requests: write diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 0000000..ae43fc4 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,15 @@ +jobs: + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm format --list-different + +name: Prettier + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f1f8e39 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +concurrency: + group: ${{ github.workflow }} + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + - uses: ./.github/actions/prepare + - run: pnpm build + - env: + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + uses: JoshuaKGoldberg/release-it-action@v0.2.2 + +name: Release + +on: + push: + branches: + - main + +permissions: + contents: write + id-token: write diff --git a/.github/workflows/test-create.yml b/.github/workflows/test-create.yml new file mode 100644 index 0000000..a0fdedc --- /dev/null +++ b/.github/workflows/test-create.yml @@ -0,0 +1,23 @@ +jobs: + create: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm run build + - run: pnpm run test:create + - if: always() + name: Codecov + uses: codecov/codecov-action@v3 + with: + files: coverage-create/lcov.info + flags: create + +name: Test Creation Script + +on: + pull_request: ~ + + push: + branches: + - main diff --git a/.github/workflows/test-initialize.yml b/.github/workflows/test-initialize.yml new file mode 100644 index 0000000..3314db5 --- /dev/null +++ b/.github/workflows/test-initialize.yml @@ -0,0 +1,27 @@ +jobs: + initialize: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm run build + - run: pnpm run test:initialize + # The template's ESLint ignorePatterns only ignores coverage, not coverage-* + # https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1131 + - if: always() + run: mv coverage coverage-initialize + - if: always() + name: Codecov + uses: codecov/codecov-action@v3 + with: + files: coverage-initialize/lcov.info + flags: initialize + +name: Test Initialization Script + +on: + pull_request: ~ + + push: + branches: + - main diff --git a/.github/workflows/test-migrate.yml b/.github/workflows/test-migrate.yml new file mode 100644 index 0000000..38cfe71 --- /dev/null +++ b/.github/workflows/test-migrate.yml @@ -0,0 +1,27 @@ +jobs: + migrate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm run build + - run: pnpm run test:migrate + # The template's ESLint ignorePatterns only ignores coverage, not coverage-* + # https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1131 + - if: always() + run: mv coverage coverage-migrate + - if: always() + name: Codecov + uses: codecov/codecov-action@v3 + with: + files: coverage-migrate/lcov.info + flags: migrate + +name: Test Migration Script + +on: + pull_request: ~ + + push: + branches: + - main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dae947f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm run test --coverage + - name: Codecov + uses: codecov/codecov-action@v3 + with: + flags: unit + +name: Test + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.github/workflows/tsc.yml b/.github/workflows/tsc.yml new file mode 100644 index 0000000..3b20f24 --- /dev/null +++ b/.github/workflows/tsc.yml @@ -0,0 +1,15 @@ +jobs: + type_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm tsc + +name: Type Check + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32ffbca --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +coverage*/ +lib/ +node_modules/ diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..0ccfe48 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" +npx lint-staged diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..14f350b --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "extends": "markdownlint/style/prettier", + "first-line-h1": false, + "no-inline-html": false +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..d6ea7b6 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,4 @@ +.github/CODE_OF_CONDUCT.md +CHANGELOG.md +lib/ +node_modules/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8b0beab --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.11.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..613eedd --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +.all-contributorsrc +coverage*/ +lib/ +pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..e3efe60 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "overrides": [{ "files": ".nvmrc", "options": { "parser": "yaml" } }], + "plugins": ["prettier-plugin-curly", "prettier-plugin-packagejson"], + "useTabs": true +} diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 0000000..2b95fac --- /dev/null +++ b/.release-it.json @@ -0,0 +1,18 @@ +{ + "git": { + "commitMessage": "chore: release v${version}", + "requireCommits": true + }, + "github": { + "autoGenerate": true, + "release": true, + "releaseName": "v${version}" + }, + "npm": { "publishArgs": ["--access public", "--provenance"] }, + "plugins": { + "@release-it/conventional-changelog": { + "infile": "CHANGELOG.md", + "preset": "angular" + } + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..095512a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1d2f28d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "configurations": [ + { + "args": ["run", "${relativeFile}"], + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "name": "Debug Current Test File", + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "request": "launch", + "skipFiles": ["/**", "**/node_modules/**"], + "smartStep": true, + "type": "node" + }, + { + "name": "Debug Program", + "preLaunchTask": "build", + "program": "./bin/index.js", + "request": "launch", + "skipFiles": ["/**"], + "type": "node" + } + ], + "version": "0.2.0" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b8153ef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml" + ], + "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ea91cf7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "tasks": [ + { + "detail": "Build the project", + "label": "build", + "script": "build", + "type": "npm" + } + ], + "version": "2.0.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f4400d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,542 @@ +## [1.57.8](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.7...v1.57.8) (2024-02-15) + +### Bug Fixes + +- don't ask to restart IDE in feature template ([#1311](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1311)) ([c61946d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c61946de1525800b424b078a8e2c8ae3766b0489)), closes [#1287](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1287) + +## [1.57.7](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.6...v1.57.7) (2024-02-15) + +### Bug Fixes + +- gate branch protection settings on options exclusions ([#1310](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1310)) ([3a8c7c0](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/3a8c7c03723eca5a50998270b75f15a7998ca69c)), closes [#1171](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1171) + +## [1.57.6](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.5...v1.57.6) (2024-02-15) + +### Bug Fixes + +- don't suggest --skip options in rerun if --offline ([#1308](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1308)) ([373e1b7](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/373e1b79a3e746c21e6e67a773dd3a4b017cf8e1)), closes [#1128](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1128) [#1130](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1130) + +## [1.57.5](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.4...v1.57.5) (2024-02-15) + +### Bug Fixes + +- omit default --access value in rerun suggestions ([#1307](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1307)) ([150bce6](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/150bce6749aa30598bc42b5898f7efef2e9d9bc3)), closes [#1127](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1127) + +## [1.57.4](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.3...v1.57.4) (2024-02-14) + +### Bug Fixes + +- reset codecov-action back to v3 ([#1304](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1304)) ([ec238bf](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/ec238bf8e4a16eb974f3931d79da93c44945d422)), closes [#1303](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1303) + +## [1.57.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.2...v1.57.3) (2024-02-14) + +### Bug Fixes + +- include base in rerun commands ([#1301](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1301)) ([2dfb7b5](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/2dfb7b56a900ae59806cdb09825bf25d42671b29)), closes [#1276](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1276) [#699](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/699) + +## [1.57.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.57.0...v1.57.2) (2024-02-14) + +### Bug Fixes + +- empty commit to release new version ([ea63b2d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/ea63b2d0a811ed61df15544317c4ffe1a7bfe9f1)) +- empty commit to trigger release flow ([acf93cf](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/acf93cfbe2cde29a9dc91cd7d84bbd538754f031)) +- pin zod-validation-error version to 3.0.0 ([#1296](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1296)) ([275784d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/275784de3a951319811379cc08ef107de0e16487)), closes [#000](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/000) +- update remaining codecov-action references from v3 to v4 ([9736fcf](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/9736fcf45e10c0a5eac2033589cce1d247e4de2f)) +- update remaining pnpm references from 8.14.1 to 8.15.1 ([c5cac23](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c5cac236ba4fa3ca209a8675bef88e19ac08d417)) + +# [1.57.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.56.0...v1.57.0) (2024-02-13) + +### Features + +- remove eslint-plugin-no-only-tests ([#1297](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1297)) ([d7b43c3](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/d7b43c35d0b0917a1548cb207169c8442155ffb3)), closes [#1294](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1294) + +# [1.56.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.55.0...v1.56.0) (2024-01-20) + +### Features + +- onboard eslint-plugin-package-json ([#1269](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1269)) ([592b0c8](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/592b0c88a11e4cf38c1235614f69e3cbde3dd35a)), closes [#839](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/839) + +# [1.55.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.54.4...v1.55.0) (2024-01-17) + +### Features + +- move .eslintignore into .eslintrc.cjs ignorePatterns ([#1262](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1262)) ([287bc46](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/287bc462998ee3c073d034c6c01a22fb4b3042e6)), closes [#1261](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1261) + +## [1.54.4](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.54.3...v1.54.4) (2024-01-17) + +### Bug Fixes + +- disable jsonc/comma-dangle ([#1258](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1258)) ([ed53de1](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/ed53de119d71f21a6b957721ce7e8334b0b9b082)), closes [#1257](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1257) + +## [1.54.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.54.2...v1.54.3) (2024-01-17) + +### Bug Fixes + +- switch rimraf to full dependency ([#1256](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1256)) ([41ab495](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/41ab4958d227700cc82ff640052da8da8927ac1a)), closes [#1253](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1253) + +## [1.54.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.54.1...v1.54.2) (2024-01-17) + +### Performance Improvements + +- use --no-dts in cleanup and pre-lint builds ([#1255](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1255)) ([0fffae0](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/0fffae01a7888b696385d766cf10fc5e0a0c1488)), closes [#1162](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1162) + +## [1.54.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.54.0...v1.54.1) (2024-01-16) + +### Bug Fixes + +- preserve existing package.json scripts ([#1252](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1252)) ([fc4e7e5](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/fc4e7e518ffd39706fde670e390711338527ed18)), closes [#1226](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1226) + +# [1.54.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.7...v1.54.0) (2024-01-16) + +### Bug Fixes + +- all-contributors-auto-action@v0.4.3 ([45f4b78](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/45f4b78d8bda658b3a1b5b991c33ca07d4fb0ad8)) +- skip replacing existing tooling in --mode migrate ([#1223](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1223)) ([c4025fa](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c4025fac088a26039d78dab5448c1cec7d97b955)), closes [#1220](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1220) + +### Features + +- bump CI version of Node to 20 ([#1233](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1233)) ([1f395f5](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/1f395f5a7cd0f4231386e6add17f4268ed0250a0)), closes [#1204](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1204) +- debugging in vs code ([#1153](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1153)) ([308d44a](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/308d44a4ddde8af2d6153379d96471562f366592)), closes [#1145](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1145) +- populate existing cspell.json words during migration ([#1208](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1208)) ([dd4fc29](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/dd4fc2989f6a441b7e834d9733ff3c26f992b8f1)), closes [#702](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/702) +- preserve existing unknown .github/DEVELOPMENT.md sections ([#1173](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1173)) ([5cd4a95](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/5cd4a95e7dacabfc6c4cc1174d3fd8bf7c15b8dc)), closes [#1167](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1167) +- rename .prettierrc to .prettierrc.json ([#1215](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1215)) ([07c0abc](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/07c0abcee146346ba819ab444ddad298fb698c2a)), closes [#1214](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1214) + +### Performance Improvements + +- rimraf coverage directories in --mode migrate ([#1248](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1248)) ([6774141](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/67741411d8920021606ce4676c3ff55a3ffa59be)), closes [#1221](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1221) + +## [1.52.7](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.6...v1.52.7) (2024-01-06) + +### Bug Fixes + +- allow chmod to fail in setup ([#1197](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1197)) ([4568cb9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/4568cb92e1479a3c61588511254e9101d4098fa5)), closes [#1195](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1195) + +## [1.52.6](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.5...v1.52.6) (2024-01-03) + +### Bug Fixes + +- markdown typo - the case of the extra ')' ([#1176](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1176)) ([a76cea9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/a76cea9838d14fa6502acb8d1a27ec86a4b090d3)), closes [#1174](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1174) + +## [1.52.5](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.4...v1.52.5) (2023-12-31) + +### Bug Fixes + +- correct initialize script path ([#1170](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1170)) ([88524d3](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/88524d3ebea16d64191ac22110f7afe0094582b7)), closes [#1168](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1168) + +## [1.52.4](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.3...v1.52.4) (2023-12-31) + +### Bug Fixes + +- removed unnecessary knip.jsonc entries ([#1164](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1164)) ([51a426a](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/51a426ac7bbc5d21b9b0fabf8ac5fcf00c090646)), closes [#1163](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1163) + +## [1.52.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.2...v1.52.3) (2023-12-31) + +### Bug Fixes + +- print logo in README.md ([#1166](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1166)) ([8244e06](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/8244e0612568c706554cc8869583ac5cfec1d0b1)), closes [#1165](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1165) + +## [1.52.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.1...v1.52.2) (2023-12-31) + +### Bug Fixes + +- don't || exit 0 in cleanup build ([#1161](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1161)) ([c5e15e4](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c5e15e4f7ebd2ab83dec1d1dc1e9ebeb36489e55)), closes [#000](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/000) + +## [1.52.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.52.0...v1.52.1) (2023-12-31) + +### Bug Fixes + +- encode emojis in README.md badge images ([#1156](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1156)) ([378d1b0](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/378d1b0d03d3ce9de68ed1aaeda3575108926960)), closes [#1155](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1155) + +# [1.52.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.51.2...v1.52.0) (2023-12-31) + +### Features + +- streamlined and unified README.md badges ([#1154](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1154)) ([6daf043](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/6daf0433a166de89f995404d0bd9f9811e27b4ee)), closes [#1088](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1088) + +## [1.51.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.51.1...v1.51.2) (2023-12-30) + +### Bug Fixes + +- normalize whether build happens before lint/knip ([#1152](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1152)) ([01704e9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/01704e906969fc3cba8be9618e6e097ec3760cd4)), closes [#1087](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1087) + +## [1.51.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.51.0...v1.51.1) (2023-12-30) + +### Bug Fixes + +- include bin entry in package.json files ([#1149](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1149)) ([206c835](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/206c835753db822106291a4dae0ceaf851749c48)), closes [#1147](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1147) + +# [1.51.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.50.2...v1.51.0) (2023-12-30) + +### Features + +- clarify option selection mechanism ([#1122](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1122)) ([aaad5ae](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/aaad5ae41699e7c4e591462aced48fe9c3b07fc6)), closes [#1121](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1121) + +## [1.50.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.50.1...v1.50.2) (2023-12-30) + +### Bug Fixes + +- remove unnecessary boolean values in rerun suggestions ([#1144](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1144)) ([e560bfc](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e560bfce287d013c83ce40d118e2ce8c74cc978f)), closes [#1129](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1129) + +## [1.50.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.50.0...v1.50.1) (2023-12-30) + +### Bug Fixes + +- infer --guide-title ([#1138](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1138)) ([d316737](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/d316737863cb46c6ff47f36670711221351ea45a)), closes [#1137](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1137) + +# [1.50.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.49.4...v1.50.0) (2023-12-24) + +### Features + +- Updated source.fixAll.eslint to explicit ([#1110](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1110)) ([274ec71](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/274ec7133b0cd1997b7d12536e17a2aea6612302)), closes [#1105](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1105) + +## [1.49.4](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.49.3...v1.49.4) (2023-12-22) + +### Bug Fixes + +- use repository name in README.md npm package badge ([#1109](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1109)) ([728eb4b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/728eb4b6526f973901cd5fdaea25c8316558f9bd)), closes [#1108](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1108) + +## [1.49.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.49.2...v1.49.3) (2023-12-22) + +### Bug Fixes + +- prompt for values when auto is off, regardless of a default value ([#1107](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1107)) ([127108f](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/127108f4cde34d2f43e9cf6858b49c17cd8ec183)), closes [#1106](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1106) + +## [1.49.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.49.1...v1.49.2) (2023-12-07) + +### Bug Fixes + +- corrected readOptions using provided values ([#1084](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1084)) ([b3c7970](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/b3c7970c6fa10f82cf521bbe33276036317e5121)), closes [#1083](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1083) + +## [1.49.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.49.0...v1.49.1) (2023-12-07) + +### Bug Fixes + +- improve migration owner replacements ([#1078](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1078)) ([4e25327](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/4e25327f593c19c640f9adf5411c0676a70d9d5e)), closes [#1043](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1043) + +# [1.49.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.48.0...v1.49.0) (2023-12-01) + +### Features + +- add -v flag to display version ([#1063](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1063)) ([a29fc6a](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/a29fc6a07b5869640c7b27e4e62f020689fc9cc0)), closes [#704](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/704) [#704](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/704) + +# [1.48.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.47.0...v1.48.0) (2023-12-01) + +### Features + +- add --bin option ([#1068](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1068)) ([21c8529](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/21c8529d49c12cbe252fd985086e3207931a83e9)), closes [#938](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/938) + +# [1.47.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.46.0...v1.47.0) (2023-11-24) + +### Features + +- Add .eslintrc to clearUnnecessaryFiles.ts ([#1052](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1052)) ([d785711](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/d785711692b0a7b4023c2524af45d3b1206509ef)), closes [#1018](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1018) +- Add prettier.config to clearUnnecessaryFiles.ts ([#1051](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1051)) ([5559a68](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/5559a68f1504cf5f3d6e4e92fa0d5b0eda8b820f)), closes [#1019](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1019) + +# [1.46.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.45.0...v1.46.0) (2023-11-21) + +### Features + +- added --auto mode ([#1046](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1046)) ([082b0d9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/082b0d906cfa0f2237e81c1aef7e7460fac2a75b)), closes [#1042](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1042) + +# [1.45.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.44.0...v1.45.0) (2023-11-17) + +### Features + +- port README.md badge underlines fix to templates ([#1038](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1038)) ([54e3dee](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/54e3deed307f02556b46d25ef0cc73bdd2cc181e)), closes [#1032](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1032) [#1030](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1030) + +# [1.44.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.43.4...v1.44.0) (2023-11-15) + +### Features + +- add emoji to label descriptions ([#1013](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1013)) ([6371790](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/6371790018937969ef42a961210d046a282e44e2)), closes [#1004](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1004) + +## [1.43.4](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.43.3...v1.43.4) (2023-11-14) + +### Bug Fixes + +- remove visual underlines on badge links in README.md ([#1030](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1030)) ([e25a47f](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e25a47f2e9e4b587bd4db5e35d7f34079f56a99b)), closes [#1027](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1027) + +## [1.43.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.43.2...v1.43.3) (2023-11-12) + +### Bug Fixes + +- allow empty paths for updating local files ([#1022](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1022)) ([b2049ff](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/b2049ff49e8d97d4a2e2451d2fed5e785a7bdabf)), closes [#1015](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1015) + +## [1.43.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.43.1...v1.43.2) (2023-11-11) + +### Bug Fixes + +- graceful recovery for failed spinner step ([#1017](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1017)) ([e36f765](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e36f7655c72a67890b4912dc1cb3c533c99c1c88)), closes [#1016](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1016) + +## [1.43.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.43.0...v1.43.1) (2023-11-09) + +### Bug Fixes + +- account for base when suggesting rerun cmd ([#992](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/992)) ([5978879](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/5978879260840c175f11bb4537b77234f5d935c0)), closes [#905](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/905) + +# [1.43.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.42.1...v1.43.0) (2023-11-08) + +### Features + +- Add help cli ([#775](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/775)) ([6c15440](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/6c15440a23ecdff7b38bc39773fdcd4cccc75e5e)), closes [#703](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/703) + +## [1.42.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.42.0...v1.42.1) (2023-10-31) + +### Bug Fixes + +- typo in a11y bot creation ([#1003](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1003)) ([8918987](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/89189873fd1338461499bc34a69c3d9bd6c36d90)), closes [#999](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/999) + +# [1.42.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.41.0...v1.42.0) (2023-10-31) + +### Features + +- infer base from package json scripts during migration ([#993](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/993)) ([20afaf4](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/20afaf4c2be5afcde6f78d16b5d78f9d3227e74d)), closes [#933](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/933) + +# [1.41.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.40.0...v1.41.0) (2023-10-30) + +### Features + +- add a11y-alt-text bot ([#989](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/989)) ([8050c83](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/8050c838078db99b46393af452a5c5408de2641a)), closes [#825](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/825) + +# [1.40.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.39.0...v1.40.0) (2023-10-26) + +### Features + +- add --guide options ([#983](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/983)) ([7f1350e](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/7f1350e1a00e6a778101335d5cc9ae18474d650f)), closes [#982](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/982) + +# [1.39.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.38.0...v1.39.0) (2023-10-20) + +### Features + +- skip file cleanup if installation is skipped ([#975](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/975)) ([aef28f2](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/aef28f2f7c8edb102e650de449a54de61a3913dd)), closes [#706](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/706) + +# [1.38.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.37.1...v1.38.0) (2023-10-14) + +### Features + +- updated Node version to 20.8.0 ([#957](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/957)) ([981ad21](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/981ad21248dca7bf2bb6d4d88325ae313bd2dad3)), closes [#953](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/953) + +## [1.37.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.37.0...v1.37.1) (2023-10-11) + +### Bug Fixes + +- also uninstall parse-author packages ([7cc468b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/7cc468b3bda03b4be7025e2d9be3038bb0ea6f15)) + +# [1.37.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.36.0...v1.37.0) (2023-10-08) + +### Features + +- replace parsePackageAuthor with npm package ([#904](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/904)) ([d5d8d40](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/d5d8d401297714aace7bb344580396a76a2869d3)), closes [#789](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/789) [#789](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/789) + +# [1.36.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.35.0...v1.36.0) (2023-10-08) + +### Features + +- **lint:** add `no-useless-rename` and `object-shorthand` ([#944](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/944)) ([4184d1b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/4184d1be4834de0b0880aafd30eb5fc6792293e5)), closes [#939](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/939) + +# [1.35.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.34.0...v1.35.0) (2023-10-03) + +### Features + +- add opt-in --preserve-generated-from option ([#940](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/940)) ([fc1eda7](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/fc1eda7097ff2466bef59032a904bc2bfb1652f1)), closes [#913](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/913) + +# [1.34.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.33.2...v1.34.0) (2023-10-02) + +### Features + +- always run build || exit 0 before lint in CI ([#936](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/936)) ([827b4b4](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/827b4b408bc3f02972be2aa748ca6ddea2351b15)), closes [#935](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/935) + +## [1.33.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.33.1...v1.33.2) (2023-10-02) + +### Bug Fixes + +- two small .json template touchups ([9c45e46](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/9c45e4638ccd3426ae492da9c6a5c605abb9c164)) + +## [1.33.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.33.0...v1.33.1) (2023-10-02) + +### Bug Fixes + +- no, do not install -import and -prettier ([4a3c3f0](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/4a3c3f0e6a47db4fc1fd4aa5836d801d6378b752)) + +# [1.33.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.32.0...v1.33.0) (2023-10-02) + +### Features + +- explicit 'unassigned' under issue claiming notice, and snapshot updates ([76e8860](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/76e8860aedc4d2dde2cec439731878d3982fccc2)) + +# [1.32.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.29.52...v1.32.0) (2023-10-02) + +### Bug Fixes + +- base suggested next steps on options ([#906](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/906)) ([ddf1318](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/ddf1318ca0302b458c9dc853d6252c2072fac802)), closes [#903](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/903) +- correct create-repository-by-default logic ([#922](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/922)) ([ef2e348](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/ef2e3483d851c8df2d4fdb2f7847d921a79f29b6)), closes [#920](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/920) +- invert test.yml excludes logic ([#910](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/910)) ([bbb6760](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/bbb67605fa553f151a23128f858c5fc88e57459f)), closes [#909](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/909) +- release-it-action@v0.2.2 ([250d5fe](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/250d5fe52dd2f4cc479c9d8aecd5c247bd3b0f10)) +- release-it-action@v0.2.2, and a few package removals ([a4e2a45](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/a4e2a45858d4f9cfa9b4c724081953ded8995cff)) +- remove cancel-in-progress from release.yml ([#876](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/876)) ([f124518](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/f12451821d64b6e3ac00d0d1fdc307d4c992e162)), closes [#862](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/862) [#145](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/145) +- remove redundant format at end of finalizeDependencies ([#887](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/887)) ([973140d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/973140dae1e0fc24002b04afe4ace8ccda610e3f)), closes [#748](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/748) +- remove unnecessary gh workflow run ([#868](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/868)) ([cc0cbaf](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/cc0cbaf3754b0b7a9d8983fffbca3e7c487b8f8c)), closes [#863](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/863) +- replace existing description with string, not regex ([#861](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/861)) ([dcc6ad4](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/dcc6ad40ce4b5b5ac2af69f014567c8f9bc26c97)), closes [#803](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/803) +- set emoji for all-contributors badge to 👪 ([#930](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/930)) ([dc67d1f](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/dc67d1fbe7859df30da3967879962c085a1bbb63)), closes [#929](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/929) +- use options.repository for n/no-missing-import's allowedModules ([#924](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/924)) ([0b542be](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/0b542be2a2c16dd225b739dbcf987b28ccd04a6f)), closes [#923](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/923) + +### Features + +- add --offline mode ([#879](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/879)) ([715e79d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/715e79dfb1501797ed2a1ebaea980e1448ca2842)), closes [#878](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/878) +- Add @vitest/coverage-istanbul to devDependenciesToRemove ([#885](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/885)) ([36e236a](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/36e236aab308a3b9154fe2ed4c5c57c0aabbbf77)), closes [#759](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/759) [#759](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/759) +- add aged away label ([#881](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/881)) ([565b854](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/565b85449c97ad52864a3ad4394cc99cec97fa3f)), closes [#852](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/852) +- add back Usage example to create and initialize ([#882](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/882)) ([b465f78](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/b465f787923437929ebb2b1ce4474b8f0d2ee122)), closes [#785](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/785) +- add npm badge to readme ([#880](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/880)) ([e199ea9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e199ea95f15c78daa6d3cdaa773d87298b76795e)), closes [#730](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/730) +- add optional --access option ([#871](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/871)) ([bfcd795](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/bfcd795a077a9e4409dce9f99569a5325456bae2)), closes [#665](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/665) +- added --logo and --logo-alt ([#851](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/851)) ([72c6d99](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/72c6d99633b2298931577ebc6f6990a5600b733a)), closes [#739](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/739) +- allow specific GitHub and/or npm emails ([#858](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/858)) ([d112a31](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/d112a31310c8f6694181ff82ba02efef34a6cad3)), closes [#542](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/542) [#851](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/851) +- enable noEmit in tsconfig.json ([#888](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/888)) ([617dcc4](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/617dcc4bc40c5b346a3be84ae53a4f5f7339f253)), closes [#800](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/800) +- fix .nvmrc template to 18.18.0 ([b077fbc](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/b077fbcd549c0ce685cddb27c93ff94956b7fc77)) +- much more ESLint config configurability ([#867](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/867)) ([094b253](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/094b253411168038a2c7585e5172f8b773c18ea2)), closes [#864](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/864) [#866](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/866) +- remove --create-repository option ([#921](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/921)) ([dd69dd5](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/dd69dd531ddc769ff0403cd578f38f0bec479315)), closes [#920](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/920) +- run npm dedupe at end of finalizeDependencies ([#902](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/902)) ([8f7692a](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/8f7692ad544fad6558ea5daf39fcfd5f2feb27c0)), closes [#770](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/770) +- smarter initial mode prompt ([#919](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/919)) ([001882b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/001882b7c4224d885d2e03a4da1b8a2d6024502d)), closes [#884](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/884) +- split contributors option into exclude-all-contributors and --skip-all-contributors-api ([#899](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/899)) ([a51c624](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/a51c624da09df2fefe7e2b12615a07f0a2457538)), closes [#875](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/875) [#898](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/898) +- support package.json keywords ([#886](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/886)) ([1399071](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/139907146150ae4c3589d57148851ec975cb6dd7)), closes [#526](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/526) +- switch format:write to format --write ([#901](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/901)) ([4486186](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/44861860bc45479800f570b547372c2b0d77c909)), closes [#762](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/762) +- use release-it-action for release flow ([#927](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/927)) ([58c31de](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/58c31de9c2a8c35fd3feef8956905776a3a5a852)), closes [#145](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/145) + +## [1.29.52](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.29.51...v1.29.52) (2023-09-21) + +### Bug Fixes + +- generate CHANGELOG.md files again ([#854](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/854)) ([88fbfb9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/88fbfb92f2cb16a93fe7f7badbb40c8e069de468)), closes [#842](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/842) + +# Changelog + +### [1.27.12](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.11...v1.27.12) (2022-12-31) + +### [1.27.11](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.10...v1.27.11) (2022-12-29) + +### [1.27.10](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.9...v1.27.10) (2022-12-26) + +### [1.27.9](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.8...v1.27.9) (2022-12-26) + +### Bug Fixes + +- run build prior to release ([#166](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/166)) ([c83355b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c83355bafb5196dcb4716ced032ddc7e5b02d072)) + +### [1.27.8](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.7...v1.27.8) (2022-12-25) + +### [1.27.7](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.6...v1.27.7) (2022-12-24) + +### [1.27.6](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.5...v1.27.6) (2022-12-24) + +### [1.27.5](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.4...v1.27.5) (2022-12-24) + +### Bug Fixes + +- don't skipCi for allcontributors ([#152](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/152)) ([674e2c1](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/674e2c1ada116f0b04b7a137fa3bfe2f9f34d558)) + +### [1.27.4](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.3...v1.27.4) (2022-12-24) + +### [1.27.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.2...v1.27.3) (2022-12-23) + +### [1.27.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.1...v1.27.2) (2022-12-23) + +### [1.27.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.27.0...v1.27.1) (2022-12-23) + +## [1.27.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.26.0...v1.27.0) (2022-12-23) + +### Features + +- document 145, and re-add npm publishing ([4df06c6](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/4df06c6dd4092b041d29eefe573a021a62a40e0d)) + +## [1.26.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.22.1...v1.26.0) (2022-12-23) + +### Features + +- fix typo in pnpm description ([e1a21fc](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e1a21fc62b3ba9d2421099b3e4db3eab33b301f0)) +- re-add branch protection toggling in release.yml ([#144](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/144)) ([b55395a](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/b55395a962dca3cc24cfb5590a9107f527c94855)) +- skip npm publish (just for testing ([14dc939](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/14dc939aafd648cd6d36587b5378b540da87770d)) +- try adding back benjefferies/branch-protection-bot ([7e12306](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/7e12306dffb37fb1e8545ec2ee7c7db970d26434)) +- use Octokit to populate more settings ([#130](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/130)) ([ff33d79](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/ff33d796ee95f802262f413ba88af4e3237c1425)) + +### Bug Fixes + +- correct renovate config ([#133](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/133)) ([19f7666](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/19f766641d7d7cfa896134b398bc7431f200e087)) +- unlock base branch for PRs ([e0fa0c9](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e0fa0c9b19caf1f7a6fff12a64917a58851db5d4)) + +### [1.22.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.22.0...v1.22.1) (2022-12-20) + +### Bug Fixes + +- only append README.md notice once in setup ([#131](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/131)) ([bd2901d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/bd2901d11f9d1c942d3bea816208845c47deff58)) + +## [1.22.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.21.0...v1.22.0) (2022-12-15) + +### Features + +- add header to CHANGELOG.md ([#127](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/127)) ([4724643](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/4724643fb33de89b33e437af5e340d9fe2eb39c6)) + +## [1.21.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.20.2...v1.21.0) (2022-12-15) + +### Features + +- clear CHANGELOG.md and local Git tags in setup ([#125](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/125)) ([9ed5f45](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/9ed5f452f1de3841647cfff594676819a3e416e5)) + +## [1.20.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.20.1...v1.20.2) (2022-12-15) + +### Bug Fixes + +- corrected allcontributors setup import path with fs ([#124](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/124)) ([2506907](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/25069072073b3b26b039aaf2f6b6dea9cd1cd8b2)) + +## [1.20.1](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.20.0...v1.20.1) (2022-12-15) + +### Bug Fixes + +- corrected description quote in setup script ([#123](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/123)) ([1360510](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/1360510ee2173084499b5d4360d6213b8003e76c)) + +## [1.20.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.19.0...v1.20.0) (2022-12-15) + +### Features + +- add 'area: testing' label to setup data ([#120](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/120)) ([e0f9e0b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e0f9e0b34f44db996400932185cd2359cb29bba2)) + +## [1.19.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.8.0...v1.19.0) (2022-12-15) + +### Features + +- bind console.log always to the same value ([#111](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/111)) ([c62dc71](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c62dc7134408bb726af16550499a560fe117dee6)) +- remove branch protection bot from release.yml ([c8b5e2b](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c8b5e2b0eea3c57e0ce6d46e0f53b09eb20feed8)) +- set back release.yml ([27fa842](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/27fa842449a3ea119a5b90fe5648c104872b30e8)) +- testing out package version 1.14.0 bump ([69e2617](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/69e26170c7dc95bfcc5a9ab997156f1cdfb93a9c)) + +### Bug Fixes + +- temporarily disable branch protections for release-it ([#113](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/113)) ([e4c41b8](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/e4c41b8a18ea7d4fab1fff151213519c94596367)) + +## [1.8.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.7.0...v1.8.0) (2022-12-14) + +### Features + +- switch from semantic-release to release-it ([#110](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/110)) ([c0fcbf4](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c0fcbf46718ee6ad0e3582b1f01e2910a3da847d)) + +## [1.7.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.6.0...v1.7.0) (2022-12-14) + +### Features + +- used semantic-release/exec to clean changelog headings ([#109](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/109)) ([db88e12](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/db88e12afd863020d46f9aaec12a4ef1d824c94b)) + +## [1.6.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.5.0...v1.6.0) (2022-12-13) + +### Features + +- change GHANGELOG.md title to include hash ([297eb4e](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/297eb4edf9187d7f38d03e3be2daf169f05fe8a4)) + +## [1.5.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.4.0...v1.5.0) (2022-12-13) + +### Features + +- add test entry to CHANGELOG.md ([c39b3db](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/c39b3db1ad2bf8cd9eb2939ac1d3bba848a2f3d5)) + +## [1.4.0](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/v1.3.0...v1.4.0) (2022-12-13) + +### Features + +- bind console log for performance (not really) ([#105](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/105)) ([98155c5](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/98155c5fdb301c78bc9a04a5933b4843fb186692)) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..08520a1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +# MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2615c23 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +

Create TypeScript App

+ +

Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling. 💝

+ +

+ + + 👪 All Contributors: 45 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

+ +Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS' + +`create-typescript-app` is a one-stop-shop solution to set up a new or existing repository with the latest and greatest TypeScript tooling. +It includes options not just for building and testing but also GitHub repository templates, contributor recognition, automated release management, and more. + +## Getting Started + +First make sure you have the following installed: + +- [GitHub CLI](https://cli.github.com) _(you'll need to be logged in)_ +- [Node.js](https://nodejs.org) +- [pnpm](https://pnpm.io) + +Then in an existing repository or in your directory where you'd like to make a new repository: + +```shell +npx create-typescript-app +``` + +That setup script will walk you through using the template. +You can read more about the supported setup modes in their docs pages: + +- [**Creating from the terminal**](./docs/Creation.md): creating a new repository locally on the command-line _(recommended)_ +- [**Initializing from the template**](./docs/Initialization.md): creating a new repository with the [_Use this template_](https://github.com/JoshuaKGoldberg/create-typescript-app/generate) button on GitHub +- [**Migrating an existing repository**](./docs/Migration.md): adding this template's tooling on top of an existing repository + +## Documentation + +You can read more about `create-typescript-app` and the tooling it supports: + +1. [**Tooling**](./docs/Tooling.md): a breakdown of all the pieces this template can set up. +2. [**Options**](./docs/Options.md): granular options to customize how the template is run. +3. [**FAQs**](./docs/FAQs.md): frequently asked questions + +> [!NOTE] +> This template is early stage, opinionated, and not endorsed by the TypeScript team. +> It can be configured to set up a _lot_ of tooling out of the box. +> If you don't want to use any particular tool, you can always remove it manually. + +## Development + +See [`.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md), then [`.github/DEVELOPMENT.md`](./.github/DEVELOPMENT.md). +Thanks! 💖 + +## Contributors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Alex / KATT
Alex / KATT

🐛
Anurag
Anurag

💻
Biplob Sutradhar
Biplob Sutradhar

💻
Conor Meagher
Conor Meagher

💻
Dan Vanderkam
Dan Vanderkam

🐛 🤔 🚇
Daniel
Daniel

🚇
Daniel Roe
Daniel Roe

💻
Demian Parkhomenko
Demian Parkhomenko

🐛 💻
Dominic Duffin
Dominic Duffin

💻
Dominik Nowik
Dominik Nowik

🔧 💻 🤔
Emerson
Emerson

💻
Graham Vasquez
Graham Vasquez

💻
Jamie Magee
Jamie Magee

🤔
Jan-Niklas W.
Jan-Niklas W.

💻
Jeff Wen
Jeff Wen

💻
Jessica Wilkins
Jessica Wilkins

💻
Joe Previte
Joe Previte

🐛 💻
John Reilly
John Reilly

💻 🤔 🐛 🚧 📖
Josh Goldberg
Josh Goldberg

🐛 💻 🚧 👀 🔧 📖 🚇 ⚠️ 🤔
Joël Galeran
Joël Galeran

💻
Juan A.
Juan A.

💻 📖
Kristo Baricevic
Kristo Baricevic

💻
Lars Kappert
Lars Kappert

💻
Mohammad Bagher Abiyat
Mohammad Bagher Abiyat

💻
Navin Moorthy
Navin Moorthy

🐛 💻 🤔
NazCodeland
NazCodeland

💻
Orta Therox
Orta Therox

💻
Paul Esch-Laurent
Paul Esch-Laurent

💻
Praveen Shinde
Praveen Shinde

💻
Promise Dash
Promise Dash

💻
Rebecca Stevens
Rebecca Stevens

💻 🚇
Ron Braha
Ron Braha

💻 🎨 ⚠️
Ron Jean-Francois
Ron Jean-Francois

💻 🚇
Ruthwik
Ruthwik

💻
Ryota Murakami
Ryota Murakami

💻 🐛
Shraddha
Shraddha

💻
Sudhansu
Sudhansu

💻
Swastik Patel
Swastik Patel

📖
Timon Jurschitsch
Timon Jurschitsch

💻
Tung Bui (Leo)
Tung Bui (Leo)

💻
Vasanth Kumar Cheepurupalli
Vasanth Kumar Cheepurupalli

💻
Xiaomin Liu
Xiaomin Liu

💻
lcforbes
lcforbes

🐛
nandertga
nandertga

💻
takanomedev
takanomedev

💻
+ + + + + + diff --git a/bin/index.js b/bin/index.js new file mode 100755 index 0000000..1b7a284 --- /dev/null +++ b/bin/index.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { bin } from "../lib/bin/index.js"; + +process.exitCode = await bin(process.argv.slice(2)); diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..cbaeff5 --- /dev/null +++ b/cspell.json @@ -0,0 +1,33 @@ +{ + "dictionaries": ["typescript"], + "ignorePaths": [ + "./coverage*", + "./script/__snapshots__", + ".github", + "CHANGELOG.md", + "lib", + "node_modules", + "pnpm-lock.yaml" + ], + "words": [ + "allcontributors", + "apexskier", + "arethetypeswrong", + "codespace", + "contributorsrc", + "execa", + "infile", + "knip", + "markdownlintignore", + "mtfoley", + "npmignore", + "npmpackagejsonlintrc", + "outro", + "packagejson", + "precommit", + "quickstart", + "tada", + "tsup", + "vitest" + ] +} diff --git a/docs/Creation.md b/docs/Creation.md new file mode 100644 index 0000000..5572d9d --- /dev/null +++ b/docs/Creation.md @@ -0,0 +1,40 @@ +# Creating from the Terminal + +You can run `npx create-typescript-app` in your terminal to interactively create a new repository in a child directory: + +```shell +npx create-typescript-app +``` + +The creation script will by default: + +1. Create a new directory with the given repository name +2. Initialize that new directory as a local Git repository +3. Copy the template's files to that directory +4. Create a new repository on GitHub and set it as the local repository's upstream +5. Configure relevant settings on the GitHub repository + +You'll then need to manually go through the following two steps to set up tooling on GitHub: + +1. Create two tokens in [repository secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) _(unless you chose to opt out of releases)_: + - `ACCESS_TOKEN`: A [GitHub PAT](https://github.com/settings/tokens/new) with _repo_ and _workflow_ permissions + - `NPM_TOKEN`: An [npm access token](https://docs.npmjs.com/creating-and-viewing-access-tokens/) with _Automation_ permissions +2. Install two GitHub apps: + - [Codecov](https://github.com/marketplace/codecov) _(unless you chose to opt out of tests)_ + - [Renovate](https://github.com/marketplace/renovate) _(unless you chose to opt out of renovate)_ + +Your new repository will then be ready for development! +Hooray! 🥳 + +## Options + +You can explicitly provide some or all of the options the script would prompt for as command-line flags. +See [Options.md](./Options.md). + +For example, running the creation script and skipping all GitHub-related APIs: + +```shell +npx create-typescript-app --mode create --skip-all-contributors-api --skip-github-api +``` + +See [Tooling.md](./Tooling.md) for details on the tooling pieces and which bases they're included in. diff --git a/docs/FAQs.md b/docs/FAQs.md new file mode 100644 index 0000000..1ce013c --- /dev/null +++ b/docs/FAQs.md @@ -0,0 +1,220 @@ +# FAQs + +## Do you have a guide to working with a `create-typescript-app` repository? + +Yes! +See [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository). +It'll walk you through the common activities you'll need to contribute to a repository scaffolded with `create-typescript-app`. + +## Can I use _(insert tool here)_ with this template? + +Yes! +After you set up a repository, you can substitute in any tools you'd like. + +If you think the tool would be broadly useful to most consumers of this template, feel free to [file a feature request](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/new?assignees=&labels=type%3A+feature&projects=&template=03-feature.yml&title=%F0%9F%9A%80+Feature%3A+%3Cshort+description+of+the+feature%3E) to add it in. + +## Can I create a GitHub action? + +Yes! If you want to read the [GitHub Actions documentation](https://docs.github.com/en/actions/creating-actions) in detail. +Here we'll outline the steps required to migrate a CTA app to a GitHub Action: + +1. GitHub Actions store built output on a GitHub branch rather than in a published package on npm. + As a consequence we should: + + - delete `.github/workflows/release.yml` and `.github/workflows/post-release.yml`. + - update `.github/workflows/build.yml` to ensure `dist` is up to date: + +
+ .github/workflows/build.yml + + ```yml + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build + + - name: Compare dist/index.js + run: | + if [ "$(git diff --ignore-space-at-eol --text dist/index.js | wc -l)" -gt "0" ]; then + echo "Detected uncommitted changes after build." + echo "You may need to run 'pnpm run build' locally and commit the changes." + echo "" + echo "See diff below:" + echo "" + git diff --ignore-space-at-eol --text dist/index.js + echo "" + # say this again in case the diff is long + echo "You may need to run 'pnpm run build' locally and commit the changes." + echo "" + exit 1 + fi + + name: Build + + on: + pull_request: ~ + push: + branches: + - main + + permissions: + contents: read + ``` + +
+ + - GitHub Actions run without installing package dependencies. + Replace `tsup` with [`ncc`](https://github.com/vercel/ncc) to build source files and dependencies into a single JS file. + Delete `tsup.config.ts` then execute the following commands: + + ```bash + pnpm remove tsup + pnpm add @vercel/ncc -D + ``` + + - Now we need to update the `build` script in our `package.json`: + + ```diff + -"build": "tsup", + +"build": "ncc build src/index.ts -o dist --license licenses.txt", + ``` + + - Our build now emits to the `dist` directory; so we'll want to avoid linting that directory by adding the following to `.eslintignore` and our `.prettierignore`: + + ```diff + +dist + ``` + + - Rather than having to remember to compile each time, we'll update our precommit hook in `.husky/precommit` to build for us on each commit: + + ```diff + +pnpm run build + +git add dist + npx lint-staged + ``` + +2. Create an [`action.yml` metadata file](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action#creating-an-action-metadata-file). + +It's worth reading the [GitHub Actions documentation](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action#writing-the-action-code). + +## How can I add dual CommonJS / ECMAScript Modules emit? + +First, I'd suggest reading [TypeScript Handbook > Modules - Introduction](https://www.typescriptlang.org/docs/handbook/modules/introduction.html) to understand how CommonJS (CJS) and ECMAScript (ESM) came to be. + +Then: + +1. In `tsup.config.ts`, change the [tsup `format` option](https://tsup.egoist.dev/#bundle-formats) from `["esm"]` to `["cjs", "esm"]` +2. Add a [`package.json` `"exports"` entry](https://nodejs.org/api/packages.html#subpath-exports) like: + + + + ```jsonc package.json + { + "exports": { + ".": { + "types": { + "import": "./lib/index.d.ts", + "require": "./lib/index.d.cts" + }, + "import": "./lib/index.js", + "require": "./lib/index.cjs" + } + } + } + ``` + + + +That should be it! + +To be safe, consider checking with [arethetypeswrong](https://arethetypeswrong.github.io): + +1. Run `pnpm build` +2. Run `npm pack` +3. Upload that generated `.tgz` file to [arethetypeswrong.github.io](https://arethetypeswrong.github.io) + +### Why doesn't `create-typescript-app` have an option to dual emit CJS and ESM? + +Dual CJS/ESM emit is a stopgap solution while the JavaScript ecosystem migrates towards full ESM support in most-to-all popular user packages. +Most packages newly created with `create-typescript-app` should target just ESM by default. + +Some packages published with `create-typescript` legitimately need dual CJS/ESM output because they're used by frameworks that don't yet fully support ESM. +That's reasonable. + +Unless you know a package needs to support a CJS consumer, please strongly consider keeping it ESM-only (the `create-typescript-app` default). +ESM-only packages have a smaller footprint by virtue of including fewer files. + +## Is there a way to pull in template updates to previously created repositories? + +Not directly. +You can always copy & paste them in manually, and/or re-run `npx create-typescript-app --mode migrate`. + +See [🚀 Feature: Add a script to sync the tooling updates from forked template repo #498](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/498): it will likely eventually be possible. + +## What about `eslint-config-prettier`? + +[`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier) is an ESLint plugin that serves only to turn off all rules that are unnecessary or might conflict with formatters such as Prettier. +None of the ESLint configs enabled by this repository's tooling leave any rules enabled that would need to be disabled. +Using `eslint-config-prettier` would be redundant. + +## What determines which "base" a tool goes into? + +The four bases correspond to what have seemed to be the most common user needs of template consumers: + +1. **Minimum**: Developers who just want the barest of starting templates. + - They may be very new to TypeScript tooling, or they may have made an informed decision that the additional tooling isn't worth the complexity and/or time investment. + - Tooling in this base is only what would be essential for a small TypeScript package that can be built, formatted, linted, and released. +2. **Common**: The common case of users who want the minimum tooling along with common repository management. + - Tooling added in this base should be essential for a TypeScript repository that additionally automates useful GitHub tasks: contributor recognition, release management, and testing. +3. **Everything**: Power users (including this repository) who want as much of the latest and greatest safety checks and standardization as possible. + +Note that users can always customize exactly with portions are kept in with `--base` **`prompt`**. + +## Which tools can't I remove? + +The following pieces of this template's tooling don't have options to be removed: + +- Linting with ESLint and `pnpm run lint` +- GitHub repository metadata such as the code of conduct and issue templates +- Prettier and `pnpm run format` +- tsup and `pnpm run build` +- TypeScript and `pnpm run tsc` + +If you have a strong desire to add an `--exclude-*` flag for any of them, please do [file a feature request](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/new?assignees=&labels=type%3A+feature&projects=&template=03-feature.yml&title=%F0%9F%9A%80+Feature%3A+%3Cshort+description+of+the+feature%3E). + +## Why does this package include so many tools? + +This repository is meant to serve as a starter that includes all the general tooling a modern TypeScript/Node repository could possibly need. +Each of the included tools exists for a good reason and provides real value. + +If you don't want to use any particular tool, you can always remove it manually. + +### What tooling does this package use that isn't part of created repositories? + +Glad you asked! +These are the projects used across many parts of `create-typescript-app`: + +- [Chalk](https://github.com/chalk/chalk): Makes it easier to print colored characters in the terminal. +- [Clack](https://www.clack.cc): Provides interactive terminal prompts and loading spinners. +- [execa](https://github.com/sindresorhus/execa): Makes it easier to run child processes. +- [Octokit](https://github.com/octokit/octokit.js#octokitjs): +- [tsx](https://github.com/esbuild-kit/tsx): Quickly runs TypeScript files with ESBuild. +- [Zod](https://zod.dev): TypeScript-first schema validation with static type inference. + +## Why tabs? + +This repository template configures `"useTabs": true` in the root-level `.prettierrc.json`. +It does so because tabs have been phrased by the community as generally better for accessibility: + +- +- + +Note that those points on tabs over spaces have generally been made by accessibility-experienced _individuals_ rather than accessibility-focused _organizations_. +If you know of any accessibility organization that's published more formal recommendations or research, please do file an issue here for this FAQ entry to be updated. + +You can adjust the tab size that GitHub uses to display files from your [account settings page](https://github.com/settings/appearance#tab-size-heading) (the default is 8 spaces). + +If you really want spaces in your project you can always remove the `"useTabs": true`. diff --git a/docs/Initialization.md b/docs/Initialization.md new file mode 100644 index 0000000..f2f17d3 --- /dev/null +++ b/docs/Initialization.md @@ -0,0 +1,46 @@ +# Initializing from the Template + +As an alternative to [creating with `npx create-typescript-app`](./Creation.md), the [_Use this template_](https://github.com/JoshuaKGoldberg/create-typescript-app/generate) button on GitHub can be used to quickly create a new repository from the template. +You can set up the new repository locally by cloning it and installing packages: + +```shell +git clone https://github.com/YourUsername/YourRepositoryName +cd YourRepositoryName +pnpm i +``` + +> 💡 If you don't want to clone it locally, you can always [develop in a codespace](https://docs.github.com/en/codespaces/developing-in-codespaces/developing-in-a-codespace) instead. + +Once the repository's packages are installed, you can run `pnpm run initialize` to fill out your repository's details and install necessary packages. +It will then remove itself and uninstall dependencies only used for initialization. + +```shell +pnpm run initialize +``` + +You'll then need to manually go through the following two steps to set up tooling on GitHub: + +1. Create two tokens in [repository secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) _(unless you chose to opt out of releases)_: + - `ACCESS_TOKEN`: A [GitHub PAT](https://github.com/settings/tokens/new) with _repo_ and _workflow_ permissions + - `NPM_TOKEN`: An [npm access token](https://docs.npmjs.com/creating-and-viewing-access-tokens/) with _Automation_ permissions +2. Install two GitHub apps: + - [Codecov](https://github.com/marketplace/codecov) _(unless you chose to opt out of tests)_ + - [Renovate](https://github.com/marketplace/renovate) _(unless you chose to opt out of renovate)_ + +Your new repository will then be ready for development! +Hooray! 🥳 + +## Options + +You can explicitly provide some or all of the options the script would prompt for as command-line flags. +See [Options.md](./Options.md). + +`pnpm run initialize` will set `--mode` to `initialize`. + +For example, running the initialization script and skipping all GitHub-related APIs: + +```shell +pnpm run initialize --skip-all-contributors-api --skip-github-api +``` + +See [Tooling.md](./Tooling.md) for details on the tooling pieces and which bases they're included in. diff --git a/docs/Migration.md b/docs/Migration.md new file mode 100644 index 0000000..262f7cb --- /dev/null +++ b/docs/Migration.md @@ -0,0 +1,59 @@ +# Migrating an Existing Repository + +If you have an existing repository that you'd like to give the files from this repository, you can run `npx create-typescript-app` in it to "migrate" its tooling to this template's. + +```shell +npx create-typescript-app +``` + +The migration script will: + +- Uninstall any known old packages that conflict with this template's tooling +- Delete configuration files used with those old packages +- Install any packages needed for this template's tooling +- Create or rewrite configuration files for the new tooling +- Run ESLint and Prettier auto-fixers to align formatting and style to the new settings + +For example, if the repository previously using Jest for testing: + +- `eslint-plugin-jest`, `jest`, and other Jest-related packages will be uninstalled +- Any Jest config file like `jest.config.js` will be deleted +- `eslint-plugin-vitest`, `vitest`, and other Vitest-related packages will be installed +- A `vitest.config.ts` file will be created + +You'll then need to manually go through the following two steps to set up tooling on GitHub: + +1. Create two tokens in [repository secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) _(unless you chose to opt out of releases)_: + - `ACCESS_TOKEN`: A [GitHub PAT](https://github.com/settings/tokens/new) with _repo_ and _workflow_ permissions + - `NPM_TOKEN`: An [npm access token](https://docs.npmjs.com/creating-and-viewing-access-tokens/) with _Automation_ permissions +2. Install two GitHub apps: + - [Codecov](https://github.com/marketplace/codecov) _(unless you chose to opt out of tests)_ + - [Renovate](https://github.com/marketplace/renovate) _(unless you chose to opt out of renovate)_ + +Your repository will then have an approximate copy of this template's tooling ready for you to review! +Hooray! 🥳 + +> [!WARNING] +> Migration will override many files in your repository. +> You'll want to review each of the changes. +> There will almost certainly be some incorrect changes you'll need to fix. + +## Options + +`create-typescript-app` will detect whether it's being run in an existing repository. +It also allows specifying `--mode migrate` if that detection misinterprets the current directory: + +```shell +npx create-typescript-app --mode migrate +``` + +You can explicitly provide some or all of the options the script would prompt for as command-line flags. +See [Options.md](./Options.md). + +For example, running the migration script and skipping all GitHub-related APIs: + +```shell +npx create-typescript-app --skip-all-contributors-api --skip-github-api +``` + +See [Tooling.md](./Tooling.md) for details on the tooling pieces and which bases they're included in. diff --git a/docs/Options.md b/docs/Options.md new file mode 100644 index 0000000..9749581 --- /dev/null +++ b/docs/Options.md @@ -0,0 +1,161 @@ +# Options + +All three of `create-typescript-app`'s setup scripts -[creation](./Creation.md), [initialization](./Initialization.md), and [migration](./Migration.md)- support a shared set of input options. + +> This page uses `npx create-typescript-app` in its code examples, but initialization's `pnpm run initialize` works the same. + +## Required Options + +The following required options will be prompted for interactively if not provided as CLI flags. + +### Base and Mode + +These required options determine how the creation script will set up and scaffold the repository: + +- `--base`: Whether to scaffold the repository with: + - `minimum`: Just the bare starter tooling most repositories should ideally include + - `common`: Important additions to the minimum starters such as releases and tests + - `everything`: The most thorough tooling imaginable: sorting, spellchecking, and more! + - `prompt`: Fine-grained control over which tooling pieces to use +- `--mode`: Whether to: + - `create` a new repository in a child directory + - `initialize` a freshly repository in the current directory + - `migrate` an existing repository in the current directory + +For example, scaffolding a full new repository under the current directory and also linking it to a new repository on github.com: + +```shell +npx create-typescript-app --base everything --mode create +``` + +See [Tooling.md](./Tooling.md) for details on the tooling pieces and which bases they're included in. + +### Core Options + +These required options determine the options that will be substituted into the template's files: + +- `--description` _(`string`)_: Sentence case description of the repository (e.g. `Quickstart-friendly TypeScript package with lots of great repository tooling. ✨`) +- `--owner` _(`string`)_: GitHub organization or user the repository is underneath (e.g. `JoshuaKGoldberg`) +- `--repository` _(`string`)_: The kebab-case name of the repository (e.g. `create-typescript-app`) +- `--title` _(`string`)_: Title Case title for the repository to be used in documentation (e.g. `Create TypeScript App`) + +For example, pre-populating all core required options and also creating a new repository: + +```shell +npx create-typescript-app --base everything --mode create --repository testing-repository --title "Testing Title" --owner TestingOwner --description "Test Description" +``` + +That script will run completely autonomously, no prompted inputs required. ✨ + +## Optional Options + +The setup scripts also allow for optional overrides of the following inputs whose defaults are based on other options: + +- `--access` _(`"public" | "restricted"`)_: Which [`npm publish --access`](https://docs.npmjs.com/cli/commands/npm-publish#access) to release npm packages with (by default, `"public"`) +- `--author` _(`string`)_: Username on npm to publish packages under (by default, an existing npm author, or the currently logged in npm user, or `owner.toLowerCase()`) +- `--bin` _(`string`)_: `package.json` bin value to include for npx-style running. +- `--directory` _(`string`)_: Directory to create the repository in (by default, the same name as the repository) +- `--email` _(`string`)_: Email address to be listed as the point of contact in docs and packages (e.g. `example@joshuakgoldberg.com`) + - Optionally, `--email-github` _(`string`)_ and/or `--email-npm` _(`string`)_ may be provided to use different emails in `.md` files and `package.json`, respectively +- `--funding` _(`string`)_: GitHub organization or username to mention in `funding.yml` (by default, `owner`) +- `--guide` _(`string`)_: Link to a contribution guide to place at the top of the development docs + - `--guide-title` _(`string`)_: If `--guide` is provided or detected from an existing DEVELOPMENT.md, the text title to place in the guide link +- `--keywords` _(`string[]`)_: Any number of keywords to include in `package.json` (by default, none) + - This can be specified any number of times, like `--keywords apple --keywords "banana cherry"` +- `--logo` _(`string`)_: Local image file in the repository to display near the top of the README.md as a logo + - `--logo-alt` _(`string`)_: If `--logo` is provided or detected from an existing README.md, alt text that describes the image (will be prompted for if not provided) +- `--preserve-generated-from` _(`boolean`)_: Whether to keep the GitHub repository _generated from_ notice (by default, `false`) + +For example, customizing the ownership and users associated with a new repository: + +```shell +npx create-typescript-app --author my-npm-username --email example@joshuakgoldberg.com --funding MyGitHubOrganization +``` + +> 💡 You can always manually edit files such as `package.json` after running a setup script. + +## Opt-Outs + +The setup scripts can be directed with CLI flags to opt out tooling portions and/or using API calls. + +### Excluding Tooling Portions + +The setup scripts normally will prompt you to select how much of the tooling you'd like to enable in a new repository. +Alternately, you can bypass that prompt by providing any number of the following CLI flags: + +- `--exclude-all-contributors`: Don't add all-contributors to track contributions and display them in a README.md table. +- `--exclude-compliance`: Don't add a GitHub Actions workflow to verify that PRs match an expected format. +- `--exclude-lint-json`: Don't apply linting and sorting to `*.json` and `*.jsonc` files. +- `--exclude-lint-knip`: Don't add Knip to detect unused files, dependencies, and code exports. +- `--exclude-lint-md`: Don't apply linting to `*.md` files. +- `--exclude-lint-package-json`: Don't add eslint-plugin-package-json to lint for package.json correctness. +- `--exclude-lint-deprecation`: Don't use eslint-plugin-deprecation to report on usage of code marked as `@deprecated`. +- `--exclude-lint-eslint`: Don't use eslint-plugin-eslint-comment to enforce good practices around ESLint comment directives. +- `--exclude-lint-jsdoc`: Don't use eslint-plugin-jsdoc to enforce good practices around JSDoc comments. +- `--exclude-lint-packages`: Don't add a pnpm dedupe workflow to ensure packages aren't duplicated unnecessarily. +- `--exclude-lint-perfectionist`: Don't apply eslint-plugin-perfectionist to ensure imports, keys, and so on are in sorted order. +- `--exclude-lint-regex`: Don't add eslint-plugin-regex to enforce good practices around regular expressions. +- `--exclude-lint-strict`: Don't augment the recommended logical lint rules with typescript-eslint's strict config. +- `--exclude-lint-stylistic`: Don't add stylistic rules such as typescript-eslint's stylistic config. +- `--exclude-lint-spelling`: Don't add cspell to spell check against dictionaries of known words. +- `--exclude-lint-yml`: Don't apply linting and sorting to `*.yaml` and `*.yml` files. +- `--exclude-releases`: Don't add release-it to generate changelogs, package bumps, and publishes based on conventional commits. +- `--exclude-renovate`: Don't add a Renovate config to dependencies up-to-date with PRs. +- `--exclude-tests`: Don't add Vitest tooling for fast unit tests, configured with coverage tracking. + +For example, initializing with all tooling except for `package.json` checks and Renovate: + +```shell +npx create-typescript-app --exclude-lint-package-json --exclude-lint-packages --exclude-renovate +``` + +> **Warning** +> Specifying any `--exclude-*` flag on the command-line will cause the setup script to skip prompting for more excludes. + +See [Tooling.md](./Tooling.md) for details on the tooling pieces and which bases they're included in. + +### Skipping API Calls + +> Alternately, see [Offline Mode](#offline-mode) to skip API calls without disabling features + +You can prevent the migration script from making some network-based changes using any or all of the following CLI flags: + +- `--skip-all-contributors-api` _(`boolean`)_: Skips network calls that fetch all-contributors data from GitHub + - This flag does nothing if `--exclude-all-contributors` was specified. +- `--skip-github-api` _(`boolean`)_: Skips calling to GitHub APIs. +- `--skip-install` _(`boolean`)_: Skips installing all the new template packages with `pnpm`. + +For example, providing all three flags will completely skip all network requests: + +```shell +npx create-typescript-app --skip-all-contributors-api --skip-github-api --skip-install +``` + +> 💡 Tip: To temporarily preview what the script would apply without making changes on GitHub, you can run with all `--skip-*-api` flags, then `git add -A; git reset --hard HEAD` to completely reset all changes. + +### Skipping Local Changes + +You can prevent the migration script from making some changes on disk using any or all of the following CLI flags: + +- `--skip-removal` _(`boolean`)_: Skips removing setup docs and scripts, including this `docs/` directory +- `--skip-restore` _(`boolean`)_: Skips the prompt offering to restore the repository if an error occurs during setup +- `--skip-uninstall` _(`boolean`)_: Skips uninstalling packages only used for setup scripts + +For example, providing all local change skip flags: + +```shell +npx create-typescript-app --skip-removal --skip-restore --skip-uninstall +``` + +## Offline Mode + +You can run `create-typescript-app` in an "offline" mode with `--offline`. +Doing so will: + +- Enable `--exclude-all-contributors-api` and `--skip-github-api` +- Skip network calls when setting up contributors +- Run pnpm commands with pnpm's `--offline` mode + +```shell +npx create-typescript-app --offline +``` diff --git a/docs/Tooling.md b/docs/Tooling.md new file mode 100644 index 0000000..a2c80a1 --- /dev/null +++ b/docs/Tooling.md @@ -0,0 +1,311 @@ +# Tooling + +`create-typescript-app` provides over two dozen pieces of tooling, ranging from code building and formatting to various forms of GitHub repository management. +Most can be individually turned off or on. + +The `create-typescript-app` setup scripts -[creation](./Creation.md), [initialization](./Initialization.md), and [migration](./Migration.md)- will prompt you to choose a "base" template level to initialize from. +Those template levels provide common presets of which tooling pieces to enable. + +```plaintext +◆ How much tooling would you like the template to set up for you? +│ ○ minimum Just the bare starter tooling most repositories should ideally include. +│ ○ common Important additions to the minimum starters such as releases and tests. +│ ○ everything The most thorough tooling imaginable: sorting, spellchecking, and more! +│ ○ prompt (allow me to customize) +└ +``` + +This table summarizes each tooling piece and which base levels they're included in: + +| Tooling Piece | Exclusion Flag | Minimum | Common | Everything | +| --------------------------------------------- | ------------------------------ | ------- | ------ | ---------- | +| [Building](#building) | | ✔️ | ✅ | 💯 | +| [Compliance](#compliance) | `--exclude-compliance` | | | 💯 | +| [Contributors](#contributors) | `--exclude-contributors` | | ✅ | 💯 | +| [Formatting](#formatting) | | ✔️ | ✅ | 💯 | +| [Lint Deprecation](#lint-deprecation) | `--exclude-lint-deprecation` | | | 💯 | +| [Lint ESLint](#lint-eslint) | `--exclude-lint-eslint` | | | 💯 | +| [Lint JSDoc](#lint-jsdoc) | `--exclude-lint-jsdoc` | | | 💯 | +| [Lint JSON](#lint-json) | `--exclude-lint-json` | | | 💯 | +| [Lint Knip](#lint-knip) | `--exclude-lint-knip` | | ✅ | 💯 | +| [Lint MD](#lint-md) | `--exclude-lint-md` | | | 💯 | +| [Lint Package JSON](#lint-package-json) | `--exclude-lint-package-json` | | | 💯 | +| [Lint Packages](#lint-packages) | `--exclude-lint-packages` | | | 💯 | +| [Lint Perfectionist](#lint-perfectionist) | `--exclude-lint-perfectionist` | | | 💯 | +| [Lint Regex](#lint-regex) | `--exclude-lint-regex` | | | 💯 | +| [Lint Spelling](#lint-spelling) | `--exclude-lint-spelling` | | | 💯 | +| [Lint Strict](#lint-strict) | `--exclude-lint-strict` | | | 💯 | +| [Lint Stylistic](#lint-stylistic) | `--exclude-lint-stylistic` | | | 💯 | +| [Lint YML](#lint-yml) | `--exclude-lint-yml` | | | 💯 | +| [Linting](#linting) | | ✔️ | ✅ | 💯 | +| [Package Management](#package-management) | | ✔️ | ✅ | 💯 | +| [Releases](#releases) | `--exclude-releases` | | ✅ | 💯 | +| [Renovate](#renovate) | `--exclude-renovate` | | ✅ | 💯 | +| [Repository Templates](#repository-templates) | | ✔️ | ✅ | 💯 | +| [Testing](#testing) | `--exclude-tests` | | ✅ | 💯 | +| [Type Checking](#type-checking) | | ✔️ | ✅ | 💯 | + +See also [Options](./Options.md) for how to customize the way template is run. + +## "Minimum" Base Level + +These tooling pieces are the ones that most repositories should generally always have enabled. +Other pieces of tooling are likely to not work as well (or at all) if these are removed. + +The _"minimum"_ base is best suited for projects that are very small and not likely to change very frequently. +However, they'll be missing out on many of the great tooling pieces enabled in more comprehensive bases. +We strongly recommend using at least the [_"common"_ base level](#common-base-level) instead for most repositories. + +- [Building](#building) +- [Formatting](#formatting) +- [Linting](#linting) +- [Package Management](#package-management) +- [Repository Templates](#repository-templates) +- [Type Checking](#type-checking) + +### Building + +[**tsup**](https://tsup.egoist.dev): Builds output definitions and JavaScript files using [esbuild](https://esbuild.github.io). +Each `*.ts` source file within `src/` is built into `.d.ts`, `.js`, and `.js.map` output files in `lib/`. + +Building once: + +```shell +pnpm run build +``` + +Building in watch mode: + +```shell +pnpm run build --watch +``` + +### Formatting + +[**Prettier**](https://prettier.io): Formats code for developers and enforces a consistent formatting style. +It's run on file save per [VS Code](https://code.visualstudio.com/docs/getstarted/settings) settings and as a Git commit hook via [husky](https://typicode.github.io/husky) and [lint-staged](https://github.com/okonet/lint-staged). +[prettier-plugin-curly](https://github.com/JoshuaKGoldberg/prettier-plugin-curly) and [prettier-plugin-packagejson](https://github.com/matzkoh/prettier-plugin-packagejson) add in more formatting as well. + +Auto-formatting all files: + +```shell +pnpm run format --write +``` + +### Linting + +[**ESLint**](https://eslint.org): Static analysis for JavaScript code that detects likely logical issues and helps enforce consistent code style. +Uses [**typescript-eslint**](https://typescript-eslint.io) to understand TypeScript syntax and include TypeScript-specific rules. + +Linting all files: + +```shell +pnpm run lint +``` + +Linting all files, auto-fixing fixable rule reports: + +```shell +pnpm run lint --fix +``` + +### Package Management + +[**pnpm**](https://pnpm.io): Disk-efficient package manager alternative to npm. +It caches packages in a computer-wide registry and symlinks installed packages within that registry. + +Installing packages: + +```shell +pnpm run install +``` + +### Repository Templates + +In code, assorted repository documentation files for GitHub are created: + +- [Code of conduct](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project) +- [Contributing guide](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors) +- [Issue and PR templates](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/about-issue-and-pull-request-templates) +- [Security policy](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository) + +[GitHub Actions workflows](https://github.com/features/actions) are added for each of the enabled tooling pieces to run them in CI. + +On the GitHub repository, metadata will be populated: + +- [Issue labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) for issue areas, statuses, and types. +- [Repository settings](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features) such as [branch protections](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) and [squash merging PRs](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges) + +### Type Checking + +[**TypeScript**](https://typescriptlang.org): A typed superset of JavaScript that ensures runtime behavior matches the code's intent. +Configured with strict compiler options, including [`noImplicitAny`](https://aka.ms#noImplicitAny) and [`strict`](https://aka.ms#strict). + +Type checking once: + +```shell +pnpm run tsc +``` + +Type checking in watch mode: + +```shell +pnpm run tsc --watch +``` + +## "Common" Base Level + +These added tooling pieces are those that aren't quite essential for a repository, but are still very commonly useful. +This is recommended for most users of `create-typescript-app` to start with. + +- [Contributors](#contributors) +- [Lint Knip](#lint-knip) +- [Releases](#releases) +- [Renovate](#renovate) +- [Testing](#testing) + +### Contributors + +[**All Contributors**](https://allcontributors.org): Tracks various kinds of contributions and displays them in a nicely formatted table in the README.md. +Contributions will be auto-detected when possible using [all-contributors-auto-action](https://github.com/JoshuaKGoldberg/all-contributors-auto-action). + +### Lint Knip + +[**Knip**](https://github.com/webpro/knip): Detects unused files, dependencies, and code exports. + +Running Knip: + +```shell +pnpm run lint:knip +``` + +### Releases + +[**release-it**](https://github.com/release-it/release-it): Generates changelogs, bumps the package version, and publishes to GitHub and npm based on [conventional commits](https://www.conventionalcommits.org). + +### Renovate + +[**Renovate**](https://docs.renovatebot.com): Keeps dependencies up-to-date with PRs, configured to wait a few days after each update for safety. + +### Testing + +[**Vitest**](https://vitest.dev): Fast unit tests, configured with coverage tracking. + +Additionally: + +- [`console-fail-test`](https://github.com/JoshuaKGoldberg/console-fail-test) will also be added to ensure tests don't accidentally log to the console. +- [`eslint-plugin-vitest`](https://github.com/veritem/eslint-plugin-vitest) will be added to the ESLint config to lint for Vitest-specific issues + +Running tests in watch mode: + +```shell +pnpm run test +``` + +Running tests in watch mode and auto-updating [Vitest snapshots](https://vitest.dev/guide/snapshot.html): + +```shell +pnpm run test -u +``` + +Running tests once with coverage tracking: + +```shell +pnpm run test run --coverage +``` + +## "Everything" Base Level + +This level is for developers who are eager to get the maximum tooling benefits in a repository. +Using the _"everything"_ level will gain you comprehensive, strict coverage of all sorts of repository issues, including auto-sorting of properties and strict ESLint configs. + +- [Compliance](#compliance) +- [Lint Deprecation](#lint-deprecation) +- [Lint ESLint](#lint-eslint) +- [Lint JSDoc](#lint-jsdoc) +- [Lint JSON](#lint-json) +- [Lint MD](#lint-md) +- [Lint Package JSON](#lint-package-json) +- [Lint Packages](#lint-packages) +- [Lint Perfectionist](#lint-perfectionist) +- [Lint Regex](#lint-regex) +- [Lint Spelling](#lint-spelling) +- [Lint Strict](#lint-strict) +- [Lint Stylistic](#lint-stylistic) +- [Lint YML](#lint-yml) + +### Compliance + +[**PR Compliance Action**](https://github.com/mtfoley/pr-compliance-action): Checks PRs for compliance such as addressing a linked issue and proper title formatting. + +### Lint Deprecation + +[`eslint-plugin-deprecation`](https://github.com/gund/eslint-plugin-deprecation): Reports on usage of code marked with `@deprecated`. + +### Lint ESLint + +[`eslint-plugin-eslint-comments`](https://github.com/mysticatea/eslint-plugin-eslint-comments): Enforces proper usage of [ESLint configuration comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments). + +### Lint JSDoc + +[`eslint-plugin-jsdoc`](https://github.com/gajus/eslint-plugin-jsdoc): Enforces good practices around JSDoc comments. + +### Lint JSON + +[`eslint-plugin-jsonc`](https://github.com/ota-meshi/eslint-plugin-jsonc): Adds linting for `.json` and `.jsonc` files. + +### Lint MD + +[**Markdownlint**](https://github.com/DavidAnson/markdownlint): Linting for Markdown code. + +```shell +pnpm lint:md +``` + +> This is a separate linter from ESLint, but will likely eventually be switched to an ESLint plugin ([#567](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/567)). + +### Lint Package JSON + +[`eslint-plugin-package-json`](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json): Linting for `package.json` files. + +### Lint Packages + +Uses [`pnpm dedupe`](https://pnpm.io/cli/dedupe) to deduplicate package dependencies. + +```shell +pnpm lint:packages +``` + +> This is grouped with _"Lint"_ tooling pieces, but will likely eventually be renamed ([#896](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/896)). + +### Lint Perfectionist + +[`eslint-plugin-perfectionist`](https://eslint-plugin-perfectionist.azat.io): Lints for sorting properties, imports, etc. +This plugin is quite particular -perfectionist, even- but all its rules include auto-fixers that can correct complaints for you. + +### Lint Regex + +[`eslint-plugin-regex`](https://github.com/gmullerb/eslint-plugin-regex): Detects issues with JavaScript regular expressions, such as potential exponential complexity. + +### Lint Spelling + +[**CSpell**](https://cspell.org): Spell checking for code. +Helps detect typos based on a configurable user dictionary (`cspell.json`). + +```shell +pnpm lint:spelling +``` + +> This is a separate linter from ESLint, but will likely eventually be switched to an ESLint plugin ([#897](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/897)). + +### Lint Strict + +Enables [typescript-eslint's strict configs](https://typescript-eslint.io/linting/configs/#strict) for increased scrutiny around code logic. + +### Lint Stylistic + +Enables [typescript-eslint's stylistic configs](https://typescript-eslint.io/linting/configs/#stylistic) for increased scrutiny around consistent code style. + +### Lint YML + +[`eslint-plugin-yml`](https://ota-meshi.github.io/eslint-plugin-yml): Adds linting for `yaml` and `.yml` files, such as sorting keys. diff --git a/docs/create-typescript-app.png b/docs/create-typescript-app.png new file mode 100644 index 0000000000000000000000000000000000000000..592fb48ace6bca0d502eb22df34cd1ca3ea9992c GIT binary patch literal 3108 zcmcguZEVzJ9Pj3W2+Yk8$sFjKlHiQ=`f|OyUEPgscVj!@#@L3u2)O6kr@Ll%ZEd^T z?O?E=LBOHJFD7n=#6`s~1ZMy<0wW~GyxuqgcXC2d5DBv9$j0*6oDroW{ZcYupHv6 z0(H1o)gkV*(WF4rEbemK7$?U$>4i8;vou9JD262&Cr@)cbqv;`kD1YOolSygb$A_%Ij zriVb0CR2!L8MBI!q6ra3#ehaJHrh%l0ThJ{PHWY}3Fo3f!7z+K#WWC>$zTynfj|a6 zq1i|zgEq{nD2gbR+k|MNv0Z~yJv7u-U4T_lix;ny6;W+7Ua~?=O@KaNLJxtKoF!@N*T}ed z&c!L<>K5VaB(bYXM=zSk^=@IOwaeJLaVj>*8F58 zh^l~WWO(E4oV&!uIspkpWQ`LzNCty6L%K@r42QlBflKC&qFJWPs2D+byj3eI5m^+2 zPROzxKqi8KbUL^o>2@KDf&H$?moZH8b`@P;eU6tf8dSKL7 zSK|IEuaj{*og`~xteQz^m#>ixw8rhJDr|snrmn&=o{%dK1goMvgkY%wibQHyo`BG) zg`PCq7J|s=15}cP8LDKq0UcH}Bk~`rLTa+$4G?{B=z_!mNw6dfSc!B75&tD3E0O{m zWbHs;U7RzRPJN=#k!ERETC!s7!2dWIK~)?(Y!EdtZ{t#n##+ zUP=5!y0f=^c5106k@&bhM0yBqOsvbyjtJdk_Eb`H}Z8UK+r9wjU|{;>PXwPUJnYFWkc(em+P0x%hnlNZ!%0O^1(Ezj));$>YT% z;p~{Wt(S%}iS724L< z;m4?1%MaYl+1G^F@MCsFz+jH{Q*YCEo2ovaT^=35&&PKiyqfjxXu+pTi#`$F!cOcT zm^16tZQIQ&qj`Auj!lJoP6o!#Zp<%_GHaN|3v1rpJ8N`9LFf9h-ep_o{&i&k=4&4p zL@zAI`Wl9JhM4%X9ZJ!2t>#GA`syXaYdQC&fqu4 literal 0 HcmV?d00001 diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..50718f2 --- /dev/null +++ b/knip.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://unpkg.com/knip@latest/schema.json", + "entry": ["src/index.ts!", "script/*e2e.js"], + "ignoreBinaries": ["gh"], + "ignoreExportsUsedInFile": { "interface": true, "type": true }, + "project": ["src/**/*.ts!", "script/**/*.js"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67beba8 --- /dev/null +++ b/package.json @@ -0,0 +1,110 @@ +{ + "name": "create-typescript-app", + "version": "1.57.8", + "description": "Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling. 💝", + "repository": "JoshuaKGoldberg/create-typescript-app", + "license": "MIT", + "author": { + "name": "Josh Goldberg", + "email": "npm@joshuakgoldberg.com" + }, + "type": "module", + "main": "./lib/index.js", + "bin": "./bin/index.js", + "files": [ + "bin/index.js", + "lib/", + "package.json", + "LICENSE.md", + "README.md" + ], + "scripts": { + "build": "tsup", + "format": "prettier .", + "initialize": "tsx ./bin/index.js --mode initialize", + "lint": "eslint . .*js --max-warnings 0", + "lint:knip": "knip", + "lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\" --rules sentences-per-line", + "lint:packages": "pnpm dedupe --check", + "lint:spelling": "cspell \"**\" \".github/**/*\"", + "prepare": "husky install", + "test": "vitest", + "test:create": "node script/create-test-e2e.js", + "test:initialize": "node script/initialize-test-e2e.js", + "test:migrate": "vitest run -r script/", + "tsc": "tsc" + }, + "lint-staged": { + "*": "prettier --ignore-unknown --write" + }, + "dependencies": { + "@clack/prompts": "^0.7.0", + "all-contributors-for-repository": "^0.2.1", + "chalk": "^5.3.0", + "execa": "^8.0.1", + "git-remote-origin-url": "^4.0.0", + "git-url-parse": "^14.0.0", + "js-yaml": "^4.1.0", + "lazy-value": "^3.0.0", + "npm-user": "^6.0.0", + "octokit": "^3.1.2", + "parse-author": "^2.0.0", + "prettier": "^3.1.0", + "replace-in-file": "^7.0.2", + "rimraf": "^5.0.5", + "title-case": "^4.1.2", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.2" + }, + "devDependencies": { + "@octokit/request-error": "^6.0.0", + "@release-it/conventional-changelog": "^8.0.1", + "@types/eslint": "^8.44.8", + "@types/git-url-parse": "^9.0.3", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.10.4", + "@types/parse-author": "^2.0.3", + "@types/prettier": "^2.7.3", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^7.0.0", + "@vitest/coverage-v8": "^1.0.2", + "c8": "^9.0.0", + "console-fail-test": "^0.2.3", + "cspell": "^8.1.3", + "eslint": "^8.55.0", + "eslint-plugin-deprecation": "^2.0.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-jsdoc": "^48.0.0", + "eslint-plugin-jsonc": "^2.10.0", + "eslint-plugin-markdown": "^3.0.1", + "eslint-plugin-n": "^16.3.1", + "eslint-plugin-package-json": "^0.10.0", + "eslint-plugin-perfectionist": "^2.5.0", + "eslint-plugin-regexp": "^2.1.2", + "eslint-plugin-vitest": "^0.3.10", + "eslint-plugin-yml": "^1.10.0", + "globby": "^14.0.0", + "husky": "^9.0.0", + "jsonc-eslint-parser": "^2.4.0", + "knip": "5.0.2", + "lint-staged": "^15.2.0", + "markdownlint": "^0.33.0", + "markdownlint-cli": "^0.39.0", + "prettier-plugin-curly": "^0.1.3", + "prettier-plugin-packagejson": "^2.4.7", + "release-it": "^17.0.0", + "sentences-per-line": "^0.2.1", + "tsup": "^8.0.1", + "tsx": "^4.6.2", + "typescript": "^5.3.3", + "vitest": "^1.0.2", + "yaml-eslint-parser": "^1.2.2" + }, + "packageManager": "pnpm@8.15.1", + "engines": { + "node": ">=18" + }, + "publishConfig": { + "provenance": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..656f87e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7937 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 + all-contributors-for-repository: + specifier: ^0.2.1 + version: 0.2.2 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + execa: + specifier: ^8.0.1 + version: 8.0.1 + git-remote-origin-url: + specifier: ^4.0.0 + version: 4.0.0 + git-url-parse: + specifier: ^14.0.0 + version: 14.0.0 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + lazy-value: + specifier: ^3.0.0 + version: 3.0.0 + npm-user: + specifier: ^6.0.0 + version: 6.1.1 + octokit: + specifier: ^3.1.2 + version: 3.1.2 + parse-author: + specifier: ^2.0.0 + version: 2.0.0 + prettier: + specifier: ^3.1.0 + version: 3.1.0 + replace-in-file: + specifier: ^7.0.2 + version: 7.1.0 + rimraf: + specifier: ^5.0.5 + version: 5.0.5 + title-case: + specifier: ^4.1.2 + version: 4.3.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 + zod-validation-error: + specifier: ^3.0.2 + version: 3.0.3(zod@3.22.4) + +devDependencies: + '@octokit/request-error': + specifier: ^6.0.0 + version: 6.0.2 + '@release-it/conventional-changelog': + specifier: ^8.0.1 + version: 8.0.1(release-it@17.1.1) + '@types/eslint': + specifier: ^8.44.8 + version: 8.56.4 + '@types/git-url-parse': + specifier: ^9.0.3 + version: 9.0.3 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^20.10.4 + version: 20.11.21 + '@types/parse-author': + specifier: ^2.0.3 + version: 2.0.3 + '@types/prettier': + specifier: ^2.7.3 + version: 2.7.3 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.2 + version: 6.13.2(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^7.0.0 + version: 7.1.0(eslint@8.57.0)(typescript@5.3.3) + '@vitest/coverage-v8': + specifier: ^1.0.2 + version: 1.3.1(vitest@1.3.1) + c8: + specifier: ^9.0.0 + version: 9.1.0 + console-fail-test: + specifier: ^0.2.3 + version: 0.2.3 + cspell: + specifier: ^8.1.3 + version: 8.1.3 + eslint: + specifier: ^8.55.0 + version: 8.57.0 + eslint-plugin-deprecation: + specifier: ^2.0.0 + version: 2.0.0(eslint@8.57.0)(typescript@5.3.3) + eslint-plugin-eslint-comments: + specifier: ^3.2.0 + version: 3.2.0(eslint@8.57.0) + eslint-plugin-jsdoc: + specifier: ^48.0.0 + version: 48.2.0(eslint@8.57.0) + eslint-plugin-jsonc: + specifier: ^2.10.0 + version: 2.13.0(eslint@8.57.0) + eslint-plugin-markdown: + specifier: ^3.0.1 + version: 3.0.1(eslint@8.57.0) + eslint-plugin-n: + specifier: ^16.3.1 + version: 16.6.2(eslint@8.57.0) + eslint-plugin-package-json: + specifier: ^0.10.0 + version: 0.10.4(eslint@8.57.0)(jsonc-eslint-parser@2.4.0) + eslint-plugin-perfectionist: + specifier: ^2.5.0 + version: 2.5.0(eslint@8.57.0)(typescript@5.3.3) + eslint-plugin-regexp: + specifier: ^2.1.2 + version: 2.2.0(eslint@8.57.0) + eslint-plugin-vitest: + specifier: ^0.3.10 + version: 0.3.22(@typescript-eslint/eslint-plugin@6.13.2)(eslint@8.57.0)(typescript@5.3.3)(vitest@1.3.1) + eslint-plugin-yml: + specifier: ^1.10.0 + version: 1.12.2(eslint@8.57.0) + globby: + specifier: ^14.0.0 + version: 14.0.1 + husky: + specifier: ^9.0.0 + version: 9.0.11 + jsonc-eslint-parser: + specifier: ^2.4.0 + version: 2.4.0 + knip: + specifier: 5.0.2 + version: 5.0.2(@types/node@20.11.21)(typescript@5.3.3) + lint-staged: + specifier: ^15.2.0 + version: 15.2.2 + markdownlint: + specifier: ^0.33.0 + version: 0.33.0 + markdownlint-cli: + specifier: ^0.39.0 + version: 0.39.0 + prettier-plugin-curly: + specifier: ^0.1.3 + version: 0.1.3(prettier@3.1.0) + prettier-plugin-packagejson: + specifier: ^2.4.7 + version: 2.4.12(prettier@3.1.0) + release-it: + specifier: ^17.0.0 + version: 17.1.1(typescript@5.3.3) + sentences-per-line: + specifier: ^0.2.1 + version: 0.2.1 + tsup: + specifier: ^8.0.1 + version: 8.0.2(typescript@5.3.3) + tsx: + specifier: ^4.6.2 + version: 4.7.1 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + vitest: + specifier: ^1.0.2 + version: 1.3.1(@types/node@20.11.21) + yaml-eslint-parser: + specifier: ^1.2.2 + version: 1.2.2 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.4 + '@jridgewell/trace-mapping': 0.3.23 + dev: true + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/generator@7.22.5: + resolution: {integrity: sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + '@jridgewell/gen-mapping': 0.3.4 + '@jridgewell/trace-mapping': 0.3.23 + jsesc: 2.5.2 + dev: true + + /@babel/helper-environment-visitor@7.22.5: + resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.22.5: + resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.5 + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-split-export-declaration@7.22.5: + resolution: {integrity: sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.9: + resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/template@7.22.5: + resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + dev: true + + /@babel/traverse@7.22.5: + resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.5 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.9: + resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@clack/core@0.3.3: + resolution: {integrity: sha512-5ZGyb75BUBjlll6eOa1m/IZBxwk91dooBWhPSL67sWcLS0zt9SnswRL0l26TVdBhb0wnWORRxUn//uH6n4z7+A==} + dependencies: + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: false + + /@clack/prompts@0.7.0: + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + dependencies: + '@clack/core': 0.3.3 + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: false + bundledDependencies: + - is-unicode-supported + + /@cspell/cspell-bundled-dicts@8.1.3: + resolution: {integrity: sha512-TwLyL2bCtetXGhMudjOIgFPAsWF2UkT0E7T+DAZG8aUBfHoC/eco/sTmR6UJVpi6Crjs0YOQkFUBGrQ2pxJPcA==} + engines: {node: '>=18'} + dependencies: + '@cspell/dict-ada': 4.0.2 + '@cspell/dict-aws': 4.0.0 + '@cspell/dict-bash': 4.1.3 + '@cspell/dict-companies': 3.0.28 + '@cspell/dict-cpp': 5.0.10 + '@cspell/dict-cryptocurrencies': 4.0.0 + '@cspell/dict-csharp': 4.0.2 + '@cspell/dict-css': 4.0.12 + '@cspell/dict-dart': 2.0.3 + '@cspell/dict-django': 4.1.0 + '@cspell/dict-docker': 1.1.7 + '@cspell/dict-dotnet': 5.0.0 + '@cspell/dict-elixir': 4.0.3 + '@cspell/dict-en-common-misspellings': 1.0.2 + '@cspell/dict-en-gb': 1.1.33 + '@cspell/dict-en_us': 4.3.12 + '@cspell/dict-filetypes': 3.0.3 + '@cspell/dict-fonts': 4.0.0 + '@cspell/dict-fsharp': 1.0.1 + '@cspell/dict-fullstack': 3.1.5 + '@cspell/dict-gaming-terms': 1.0.4 + '@cspell/dict-git': 2.0.0 + '@cspell/dict-golang': 6.0.5 + '@cspell/dict-haskell': 4.0.1 + '@cspell/dict-html': 4.0.5 + '@cspell/dict-html-symbol-entities': 4.0.0 + '@cspell/dict-java': 5.0.6 + '@cspell/dict-k8s': 1.0.2 + '@cspell/dict-latex': 4.0.0 + '@cspell/dict-lorem-ipsum': 4.0.0 + '@cspell/dict-lua': 4.0.3 + '@cspell/dict-makefile': 1.0.0 + '@cspell/dict-node': 4.0.3 + '@cspell/dict-npm': 5.0.13 + '@cspell/dict-php': 4.0.4 + '@cspell/dict-powershell': 5.0.2 + '@cspell/dict-public-licenses': 2.0.5 + '@cspell/dict-python': 4.1.10 + '@cspell/dict-r': 2.0.1 + '@cspell/dict-ruby': 5.0.1 + '@cspell/dict-rust': 4.0.1 + '@cspell/dict-scala': 5.0.0 + '@cspell/dict-software-terms': 3.3.12 + '@cspell/dict-sql': 2.1.2 + '@cspell/dict-svelte': 1.0.2 + '@cspell/dict-swift': 2.0.1 + '@cspell/dict-typescript': 3.1.2 + '@cspell/dict-vue': 3.0.0 + dev: true + + /@cspell/cspell-json-reporter@8.1.3: + resolution: {integrity: sha512-9iOU0Y733XuF0cqC7xwzJkOKFdJ65rYGnHFdUHzr5lxEqeG9X/jhlkzyHuGGOhPxkUeFP1x9XoLhXo1isMDbKA==} + engines: {node: '>=18'} + dependencies: + '@cspell/cspell-types': 8.1.3 + dev: true + + /@cspell/cspell-pipe@8.1.3: + resolution: {integrity: sha512-/dcnyLDeyFuoX4seZv7VsDQyRpt3ZY0vjZiDpqFul8hPydM8czLyRPPMD6Za+Gqg6dZmh9+VsQWK52hVsqc0QA==} + engines: {node: '>=18'} + dev: true + + /@cspell/cspell-resolver@8.1.3: + resolution: {integrity: sha512-bGyJYqkHRilqhyKGL/NvODN5U+UvCuQo7kxgt0i3Vd7m7k6XYLsSLYZ4w6r1S5IQ/ybU8I5lh6/6fNqKwvo9eg==} + engines: {node: '>=18'} + dependencies: + global-directory: 4.0.1 + dev: true + + /@cspell/cspell-service-bus@8.1.3: + resolution: {integrity: sha512-8E5ZveQKneNfK+cuFMy0y6tDsho71UPppEHNoLZsEFDbIxDdtQcAfs0pk4nwEzxPBt+dBB+Yl8KExQ6x2FAYQw==} + engines: {node: '>=18'} + dev: true + + /@cspell/cspell-types@8.1.3: + resolution: {integrity: sha512-j14FENj+DzWu6JjzTl+0X5/OJv9AEckpEp6Jaw9YglxirrBBzTkZGfoLePe/AWo/MlIYp0asl92C1UHEjgz+FQ==} + engines: {node: '>=18'} + dev: true + + /@cspell/dict-ada@4.0.2: + resolution: {integrity: sha512-0kENOWQeHjUlfyId/aCM/mKXtkEgV0Zu2RhUXCBr4hHo9F9vph+Uu8Ww2b0i5a4ZixoIkudGA+eJvyxrG1jUpA==} + dev: true + + /@cspell/dict-aws@4.0.0: + resolution: {integrity: sha512-1YkCMWuna/EGIDN/zKkW+j98/55mxigftrSFgsehXhPld+ZMJM5J9UuBA88YfL7+/ETvBdd7mwW6IwWsC+/ltQ==} + dev: true + + /@cspell/dict-bash@4.1.3: + resolution: {integrity: sha512-tOdI3QVJDbQSwPjUkOiQFhYcu2eedmX/PtEpVWg0aFps/r6AyjUQINtTgpqMYnYuq8O1QUIQqnpx21aovcgZCw==} + dev: true + + /@cspell/dict-companies@3.0.28: + resolution: {integrity: sha512-UinHkMYB/1pUkLKm1PGIm9PBFYxeAa6YvbB1Rq/RAAlrs0WDwiDBr3BAYdxydukG1IqqwT5z9WtU+8D/yV/5lw==} + dev: true + + /@cspell/dict-cpp@5.0.10: + resolution: {integrity: sha512-WCRuDrkFdpmeIR6uXQYKU9loMQKNFS4bUhtHdv5fu4qVyJSh3k/kgmtTm1h1BDTj8EwPRc/RGxS+9Z3b2mnabA==} + dev: true + + /@cspell/dict-cryptocurrencies@4.0.0: + resolution: {integrity: sha512-EiZp91ATyRxTmauIQfOX9adLYCunKjHEh092rrM7o2eMXP9n7zpXAL9BK7LviL+LbB8VDOm21q+s83cKrrRrsg==} + dev: true + + /@cspell/dict-csharp@4.0.2: + resolution: {integrity: sha512-1JMofhLK+4p4KairF75D3A924m5ERMgd1GvzhwK2geuYgd2ZKuGW72gvXpIV7aGf52E3Uu1kDXxxGAiZ5uVG7g==} + dev: true + + /@cspell/dict-css@4.0.12: + resolution: {integrity: sha512-vGBgPM92MkHQF5/2jsWcnaahOZ+C6OE/fPvd5ScBP72oFY9tn5GLuomcyO0z8vWCr2e0nUSX1OGimPtcQAlvSw==} + dev: true + + /@cspell/dict-dart@2.0.3: + resolution: {integrity: sha512-cLkwo1KT5CJY5N5RJVHks2genFkNCl/WLfj+0fFjqNR+tk3tBI1LY7ldr9piCtSFSm4x9pO1x6IV3kRUY1lLiw==} + dev: true + + /@cspell/dict-data-science@1.0.11: + resolution: {integrity: sha512-TaHAZRVe0Zlcc3C23StZqqbzC0NrodRwoSAc8dis+5qLeLLnOCtagYQeROQvDlcDg3X/VVEO9Whh4W/z4PAmYQ==} + dev: true + + /@cspell/dict-django@4.1.0: + resolution: {integrity: sha512-bKJ4gPyrf+1c78Z0Oc4trEB9MuhcB+Yg+uTTWsvhY6O2ncFYbB/LbEZfqhfmmuK/XJJixXfI1laF2zicyf+l0w==} + dev: true + + /@cspell/dict-docker@1.1.7: + resolution: {integrity: sha512-XlXHAr822euV36GGsl2J1CkBIVg3fZ6879ZOg5dxTIssuhUOCiV2BuzKZmt6aIFmcdPmR14+9i9Xq+3zuxeX0A==} + dev: true + + /@cspell/dict-dotnet@5.0.0: + resolution: {integrity: sha512-EOwGd533v47aP5QYV8GlSSKkmM9Eq8P3G/eBzSpH3Nl2+IneDOYOBLEUraHuiCtnOkNsz0xtZHArYhAB2bHWAw==} + dev: true + + /@cspell/dict-elixir@4.0.3: + resolution: {integrity: sha512-g+uKLWvOp9IEZvrIvBPTr/oaO6619uH/wyqypqvwpmnmpjcfi8+/hqZH8YNKt15oviK8k4CkINIqNhyndG9d9Q==} + dev: true + + /@cspell/dict-en-common-misspellings@1.0.2: + resolution: {integrity: sha512-jg7ZQZpZH7+aAxNBlcAG4tGhYF6Ksy+QS5Df73Oo+XyckBjC9QS+PrRwLTeYoFIgXy5j3ICParK5r3MSSoL4gw==} + dev: true + + /@cspell/dict-en-gb@1.1.33: + resolution: {integrity: sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g==} + dev: true + + /@cspell/dict-en_us@4.3.12: + resolution: {integrity: sha512-1bsUxFjgxF30FTzcU5uvmCvH3lyqVKR9dbwsJhomBlUM97f0edrd6590SiYBXDm7ruE68m3lJd4vs0Ev2D6FtQ==} + dev: true + + /@cspell/dict-filetypes@3.0.3: + resolution: {integrity: sha512-J9UP+qwwBLfOQ8Qg9tAsKtSY/WWmjj21uj6zXTI9hRLD1eG1uUOLcfVovAmtmVqUWziPSKMr87F6SXI3xmJXgw==} + dev: true + + /@cspell/dict-fonts@4.0.0: + resolution: {integrity: sha512-t9V4GeN/m517UZn63kZPUYP3OQg5f0OBLSd3Md5CU3eH1IFogSvTzHHnz4Wqqbv8NNRiBZ3HfdY/pqREZ6br3Q==} + dev: true + + /@cspell/dict-fsharp@1.0.1: + resolution: {integrity: sha512-23xyPcD+j+NnqOjRHgW3IU7Li912SX9wmeefcY0QxukbAxJ/vAN4rBpjSwwYZeQPAn3fxdfdNZs03fg+UM+4yQ==} + dev: true + + /@cspell/dict-fullstack@3.1.5: + resolution: {integrity: sha512-6ppvo1dkXUZ3fbYn/wwzERxCa76RtDDl5Afzv2lijLoijGGUw5yYdLBKJnx8PJBGNLh829X352ftE7BElG4leA==} + dev: true + + /@cspell/dict-gaming-terms@1.0.4: + resolution: {integrity: sha512-hbDduNXlk4AOY0wFxcDMWBPpm34rpqJBeqaySeoUH70eKxpxm+dvjpoRLJgyu0TmymEICCQSl6lAHTHSDiWKZg==} + dev: true + + /@cspell/dict-git@2.0.0: + resolution: {integrity: sha512-n1AxyX5Kgxij/sZFkxFJlzn3K9y/sCcgVPg/vz4WNJ4K9YeTsUmyGLA2OQI7d10GJeiuAo2AP1iZf2A8j9aj2w==} + dev: true + + /@cspell/dict-golang@6.0.5: + resolution: {integrity: sha512-w4mEqGz4/wV+BBljLxduFNkMrd3rstBNDXmoX5kD4UTzIb4Sy0QybWCtg2iVT+R0KWiRRA56QKOvBsgXiddksA==} + dev: true + + /@cspell/dict-haskell@4.0.1: + resolution: {integrity: sha512-uRrl65mGrOmwT7NxspB4xKXFUenNC7IikmpRZW8Uzqbqcu7ZRCUfstuVH7T1rmjRgRkjcIjE4PC11luDou4wEQ==} + dev: true + + /@cspell/dict-html-symbol-entities@4.0.0: + resolution: {integrity: sha512-HGRu+48ErJjoweR5IbcixxETRewrBb0uxQBd6xFGcxbEYCX8CnQFTAmKI5xNaIt2PKaZiJH3ijodGSqbKdsxhw==} + dev: true + + /@cspell/dict-html@4.0.5: + resolution: {integrity: sha512-p0brEnRybzSSWi8sGbuVEf7jSTDmXPx7XhQUb5bgG6b54uj+Z0Qf0V2n8b/LWwIPJNd1GygaO9l8k3HTCy1h4w==} + dev: true + + /@cspell/dict-java@5.0.6: + resolution: {integrity: sha512-kdE4AHHHrixyZ5p6zyms1SLoYpaJarPxrz8Tveo6gddszBVVwIUZ+JkQE1bWNLK740GWzIXdkznpUfw1hP9nXw==} + dev: true + + /@cspell/dict-k8s@1.0.2: + resolution: {integrity: sha512-tLT7gZpNPnGa+IIFvK9SP1LrSpPpJ94a/DulzAPOb1Q2UBFwdpFd82UWhio0RNShduvKG/WiMZf/wGl98pn+VQ==} + dev: true + + /@cspell/dict-latex@4.0.0: + resolution: {integrity: sha512-LPY4y6D5oI7D3d+5JMJHK/wxYTQa2lJMSNxps2JtuF8hbAnBQb3igoWEjEbIbRRH1XBM0X8dQqemnjQNCiAtxQ==} + dev: true + + /@cspell/dict-lorem-ipsum@4.0.0: + resolution: {integrity: sha512-1l3yjfNvMzZPibW8A7mQU4kTozwVZVw0AvFEdy+NcqtbxH+TvbSkNMqROOFWrkD2PjnKG0+Ea0tHI2Pi6Gchnw==} + dev: true + + /@cspell/dict-lua@4.0.3: + resolution: {integrity: sha512-lDHKjsrrbqPaea13+G9s0rtXjMO06gPXPYRjRYawbNmo4E/e3XFfVzeci3OQDQNDmf2cPOwt9Ef5lu2lDmwfJg==} + dev: true + + /@cspell/dict-makefile@1.0.0: + resolution: {integrity: sha512-3W9tHPcSbJa6s0bcqWo6VisEDTSN5zOtDbnPabF7rbyjRpNo0uHXHRJQF8gAbFzoTzBBhgkTmrfSiuyQm7vBUQ==} + dev: true + + /@cspell/dict-node@4.0.3: + resolution: {integrity: sha512-sFlUNI5kOogy49KtPg8SMQYirDGIAoKBO3+cDLIwD4MLdsWy1q0upc7pzGht3mrjuyMiPRUV14Bb0rkVLrxOhg==} + dev: true + + /@cspell/dict-npm@5.0.13: + resolution: {integrity: sha512-uPb3DlQA/FvlmzT5RjZoy7fy91mxMRZW1B+K3atVM5A/cmP1QlDaSW/iCtde5kHET1MOV7uxz+vy0Yha2OI5pQ==} + dev: true + + /@cspell/dict-php@4.0.4: + resolution: {integrity: sha512-fRlLV730fJbulDsLIouZxXoxHt3KIH6hcLFwxaupHL+iTXDg0lo7neRpbqD5MScr/J3idEr7i9G8XWzIikKFug==} + dev: true + + /@cspell/dict-powershell@5.0.2: + resolution: {integrity: sha512-IHfWLme3FXE7vnOmMncSBxOsMTdNWd1Vcyhag03WS8oANSgX8IZ+4lMI00mF0ptlgchf16/OU8WsV4pZfikEFw==} + dev: true + + /@cspell/dict-public-licenses@2.0.5: + resolution: {integrity: sha512-91HK4dSRri/HqzAypHgduRMarJAleOX5NugoI8SjDLPzWYkwZ1ftuCXSk+fy8DLc3wK7iOaFcZAvbjmnLhVs4A==} + dev: true + + /@cspell/dict-python@4.1.10: + resolution: {integrity: sha512-ErF/Ohcu6Xk4QVNzFgo8p7CxkxvAKAmFszvso41qOOhu8CVpB35ikBRpGVDw9gsCUtZzi15Yl0izi4do6WcLkA==} + dependencies: + '@cspell/dict-data-science': 1.0.11 + dev: true + + /@cspell/dict-r@2.0.1: + resolution: {integrity: sha512-KCmKaeYMLm2Ip79mlYPc8p+B2uzwBp4KMkzeLd5E6jUlCL93Y5Nvq68wV5fRLDRTf7N1LvofkVFWfDcednFOgA==} + dev: true + + /@cspell/dict-ruby@5.0.1: + resolution: {integrity: sha512-rruTm7Emhty/BSYavSm8ZxRuVw0OBqzJkwIFXcV0cX7To8D1qbmS9HFHRuRg8IL11+/nJvtdDz+lMFBSmPUagQ==} + dev: true + + /@cspell/dict-rust@4.0.1: + resolution: {integrity: sha512-xJSSzHDK2z6lSVaOmMxl3PTOtfoffaxMo7fTcbZUF+SCJzfKbO6vnN9TCGX2sx1RHFDz66Js6goz6SAZQdOwaw==} + dev: true + + /@cspell/dict-scala@5.0.0: + resolution: {integrity: sha512-ph0twaRoV+ylui022clEO1dZ35QbeEQaKTaV2sPOsdwIokABPIiK09oWwGK9qg7jRGQwVaRPEq0Vp+IG1GpqSQ==} + dev: true + + /@cspell/dict-software-terms@3.3.12: + resolution: {integrity: sha512-6aa4T9VqOMc0SFNBt6gxp0CWjvRqMg/uxvgpRbil+ToHWcU+Q+As0WKhPLaOniuTdCM85WWzRouD0O1XUGqg5Q==} + dev: true + + /@cspell/dict-sql@2.1.2: + resolution: {integrity: sha512-Pi0hAcvsSGtZZeyyAN1VfGtQJbrXos5x2QjJU0niAQKhmITSOrXU/1II1Gogk+FYDjWyV9wP2De0U2f7EWs6oQ==} + dev: true + + /@cspell/dict-svelte@1.0.2: + resolution: {integrity: sha512-rPJmnn/GsDs0btNvrRBciOhngKV98yZ9SHmg8qI6HLS8hZKvcXc0LMsf9LLuMK1TmS2+WQFAan6qeqg6bBxL2Q==} + dev: true + + /@cspell/dict-swift@2.0.1: + resolution: {integrity: sha512-gxrCMUOndOk7xZFmXNtkCEeroZRnS2VbeaIPiymGRHj5H+qfTAzAKxtv7jJbVA3YYvEzWcVE2oKDP4wcbhIERw==} + dev: true + + /@cspell/dict-typescript@3.1.2: + resolution: {integrity: sha512-lcNOYWjLUvDZdLa0UMNd/LwfVdxhE9rKA+agZBGjL3lTA3uNvH7IUqSJM/IXhJoBpLLMVEOk8v1N9xi+vDuCdA==} + dev: true + + /@cspell/dict-vue@3.0.0: + resolution: {integrity: sha512-niiEMPWPV9IeRBRzZ0TBZmNnkK3olkOPYxC1Ny2AX4TGlYRajcW0WUtoSHmvvjZNfWLSg2L6ruiBeuPSbjnG6A==} + dev: true + + /@cspell/dynamic-import@8.1.3: + resolution: {integrity: sha512-/lXFLa92v4oOcZ2PbdRpOqBvnqWlYmGaV7iCy8+QhIWlMdzi+7tBX3LVTm9Jzvt/rJseVHQQ6RvfTsSmhbUMFQ==} + engines: {node: '>=18.0'} + dependencies: + import-meta-resolve: 4.0.0 + dev: true + + /@cspell/strong-weak-map@8.1.3: + resolution: {integrity: sha512-GhWyximzk8tumo0zhrDV3+nFYiETYefiTBWAEVbXJMibuvitFocVZwddqN85J0UdZ2M7q6tvBleEaI9ME/16gA==} + engines: {node: '>=18'} + dev: true + + /@ericcornelissen/bash-parser@0.5.2: + resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} + engines: {node: '>=4'} + dependencies: + array-last: 1.3.0 + babylon: 6.18.0 + compose-function: 3.0.3 + deep-freeze: 0.0.1 + filter-iterator: 0.0.1 + filter-obj: 1.1.0 + has-own-property: 0.1.0 + identity-function: 1.0.0 + is-iterable: 1.1.1 + iterable-lookahead: 1.0.0 + lodash.curry: 4.1.1 + magic-string: 0.16.0 + map-obj: 2.0.0 + object-pairs: 0.1.0 + object-values: 1.0.0 + reverse-arguments: 1.0.0 + shell-quote-word: 1.0.1 + to-pascal-case: 1.0.0 + unescape-js: 1.1.4 + dev: true + + /@es-joy/jsdoccomment@0.42.0: + resolution: {integrity: sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==} + engines: {node: '>=16'} + dependencies: + comment-parser: 1.4.1 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + dev: true + + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.2 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + dev: true + + /@hutson/parse-repository-url@5.0.0: + resolution: {integrity: sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==} + engines: {node: '>=10.13.0'} + dev: true + + /@iarna/toml@2.2.5: + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jridgewell/gen-mapping@0.3.4: + resolution: {integrity: sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.23 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.23: + resolution: {integrity: sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@ljharb/through@2.3.12: + resolution: {integrity: sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.scandir@3.0.0: + resolution: {integrity: sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg==} + engines: {node: '>=16.14.0'} + dependencies: + '@nodelib/fs.stat': 3.0.0 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.stat@3.0.0: + resolution: {integrity: sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ==} + engines: {node: '>=16.14.0'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@nodelib/fs.walk@2.0.0: + resolution: {integrity: sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A==} + engines: {node: '>=16.14.0'} + dependencies: + '@nodelib/fs.scandir': 3.0.0 + fastq: 1.15.0 + dev: true + + /@npmcli/git@5.0.3: + resolution: {integrity: sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@npmcli/promise-spawn': 7.0.0 + lru-cache: 10.2.0 + npm-pick-manifest: 9.0.0 + proc-log: 3.0.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.6.0 + which: 4.0.0 + transitivePeerDependencies: + - bluebird + dev: true + + /@npmcli/map-workspaces@3.0.4: + resolution: {integrity: sha512-Z0TbvXkRbacjFFLpVpV0e2mheCh+WzQpcqL+4xp49uNJOxOnIAPZyXtUxZ5Qn3QBTGKA11Exjd9a5411rBrhDg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + '@npmcli/name-from-folder': 2.0.0 + glob: 10.3.10 + minimatch: 9.0.3 + read-package-json-fast: 3.0.2 + dev: true + + /@npmcli/name-from-folder@2.0.0: + resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /@npmcli/package-json@5.0.0: + resolution: {integrity: sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@npmcli/git': 5.0.3 + glob: 10.3.10 + hosted-git-info: 7.0.1 + json-parse-even-better-errors: 3.0.0 + normalize-package-data: 6.0.0 + proc-log: 3.0.0 + semver: 7.6.0 + transitivePeerDependencies: + - bluebird + dev: true + + /@npmcli/promise-spawn@7.0.0: + resolution: {integrity: sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + which: 4.0.0 + dev: true + + /@octokit/app@14.0.2: + resolution: {integrity: sha512-NCSCktSx+XmjuSUVn2dLfqQ9WIYePGP95SDJs4I9cn/0ZkeXcPkaoCLl64Us3dRKL2ozC7hArwze5Eu+/qt1tg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-app': 6.0.0 + '@octokit/auth-unauthenticated': 5.0.0 + '@octokit/core': 5.1.0 + '@octokit/oauth-app': 6.0.0 + '@octokit/plugin-paginate-rest': 9.2.0(@octokit/core@5.1.0) + '@octokit/types': 12.6.0 + '@octokit/webhooks': 12.0.10 + dev: false + + /@octokit/auth-app@6.0.0: + resolution: {integrity: sha512-OKct7Rukf3g9DjpzcpdacQsdmd6oPrJ7fZND22JkjzhDvfhttUOnmh+qPS4kHhaNNyTxqSThnfrUWvkqNLd1nw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.0.0 + '@octokit/auth-oauth-user': 4.0.0 + '@octokit/request': 8.2.0 + '@octokit/request-error': 5.0.1 + '@octokit/types': 11.1.0 + deprecation: 2.3.1 + lru-cache: 10.2.0 + universal-github-app-jwt: 1.1.1 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-app@7.0.0: + resolution: {integrity: sha512-8JvJEXGoEqrbzLwt3SwIUvkDd+1wrM8up0KawvDIElB8rbxPbvWppGO0SLKAWSJ0q8ILcVq+mWck6pDcZ3a9KA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.0.0 + '@octokit/auth-oauth-user': 4.0.0 + '@octokit/request': 8.2.0 + '@octokit/types': 11.1.0 + '@types/btoa-lite': 1.0.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-device@6.0.0: + resolution: {integrity: sha512-Zgf/LKhwWk54rJaTGYVYtbKgUty+ouil6VQeRd+pCw7Gd0ECoSWaZuHK6uDGC/HtnWHjpSWFhzxPauDoHcNRtg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-methods': 4.0.0 + '@octokit/request': 8.2.0 + '@octokit/types': 11.1.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-user@4.0.0: + resolution: {integrity: sha512-VOm5aIkVGHaOhIvsF/4YmSjoYDzzrKbbYkdSEO0KqHK7I8SlO3ZndSikQ1fBlNPUEH0ve2BOTxLrVvI1qBf9/Q==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.0.0 + '@octokit/oauth-methods': 4.0.0 + '@octokit/request': 8.2.0 + '@octokit/types': 11.1.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-token@4.0.0: + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + + /@octokit/auth-unauthenticated@5.0.0: + resolution: {integrity: sha512-AjOI6FNB2dweJ85p6rf7D4EhE4y6VBcwYfX/7KJkR5Q9fD9ET6NABAjajUTSNFfCxmNIaQgISggZ3pkgwtTqsA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.0.1 + '@octokit/types': 11.1.0 + dev: false + + /@octokit/core@5.1.0: + resolution: {integrity: sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.0.2 + '@octokit/request': 8.2.0 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.6.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + + /@octokit/endpoint@9.0.4: + resolution: {integrity: sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 12.6.0 + universal-user-agent: 6.0.1 + + /@octokit/graphql@7.0.2: + resolution: {integrity: sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request': 8.2.0 + '@octokit/types': 12.6.0 + universal-user-agent: 6.0.1 + + /@octokit/oauth-app@6.0.0: + resolution: {integrity: sha512-bNMkS+vJ6oz2hCyraT9ZfTpAQ8dZNqJJQVNaKjPLx4ue5RZiFdU1YWXguOPR8AaSHS+lKe+lR3abn2siGd+zow==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.0.0 + '@octokit/auth-oauth-user': 4.0.0 + '@octokit/auth-unauthenticated': 5.0.0 + '@octokit/core': 5.1.0 + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/oauth-methods': 4.0.0 + '@types/aws-lambda': 8.10.114 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/oauth-authorization-url@6.0.2: + resolution: {integrity: sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/oauth-methods@4.0.0: + resolution: {integrity: sha512-dqy7BZLfLbi3/8X8xPKUKZclMEK9vN3fK5WF3ortRvtplQTszFvdAGbTo71gGLO+4ZxspNiLjnqdd64Chklf7w==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/request': 8.2.0 + '@octokit/request-error': 5.0.1 + '@octokit/types': 11.1.0 + btoa-lite: 1.0.0 + dev: false + + /@octokit/openapi-types@18.0.0: + resolution: {integrity: sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==} + dev: false + + /@octokit/openapi-types@20.0.0: + resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} + + /@octokit/plugin-paginate-graphql@4.0.0(@octokit/core@5.1.0): + resolution: {integrity: sha512-7HcYW5tP7/Z6AETAPU14gp5H5KmCPT3hmJrS/5tO7HIgbwenYmgw4OY9Ma54FDySuxMwD+wsJlxtuGWwuZuItA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.1.0 + dev: false + + /@octokit/plugin-paginate-rest@9.2.0(@octokit/core@5.1.0): + resolution: {integrity: sha512-NKi0bJEZqOSbBLMv9kdAcuocpe05Q2xAXNLTGi0HN2GSMFJHNZuSoPNa0tcQFTOFCKe+ZaYBZ3lpXh1yxgUDCA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.1.0 + '@octokit/types': 12.6.0 + + /@octokit/plugin-request-log@4.0.0(@octokit/core@5.1.0): + resolution: {integrity: sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.1.0 + dev: true + + /@octokit/plugin-rest-endpoint-methods@10.4.0(@octokit/core@5.1.0): + resolution: {integrity: sha512-INw5rGXWlbv/p/VvQL63dhlXr38qYTHkQ5bANi9xofrF9OraqmjHsIGyenmjmul1JVRHpUlw5heFOj1UZLEolA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.1.0 + '@octokit/types': 12.6.0 + + /@octokit/plugin-retry@6.0.0(@octokit/core@5.1.0): + resolution: {integrity: sha512-a1/A4A+PB1QoAHQfLJxGHhLfSAT03bR1jJz3GgQJZvty2ozawFWs93MiBQXO7SL2YbO7CIq0Goj4qLOBj8JeMQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.1.0 + '@octokit/request-error': 5.0.1 + '@octokit/types': 11.1.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/plugin-throttling@8.1.3(@octokit/core@5.1.0): + resolution: {integrity: sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5.0.0 + dependencies: + '@octokit/core': 5.1.0 + '@octokit/types': 12.6.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/request-error@5.0.1: + resolution: {integrity: sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 12.6.0 + deprecation: 2.3.1 + once: 1.4.0 + + /@octokit/request-error@6.0.2: + resolution: {integrity: sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 12.6.0 + dev: true + + /@octokit/request@8.2.0: + resolution: {integrity: sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/endpoint': 9.0.4 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.6.0 + universal-user-agent: 6.0.1 + + /@octokit/rest@20.0.2: + resolution: {integrity: sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/core': 5.1.0 + '@octokit/plugin-paginate-rest': 9.2.0(@octokit/core@5.1.0) + '@octokit/plugin-request-log': 4.0.0(@octokit/core@5.1.0) + '@octokit/plugin-rest-endpoint-methods': 10.4.0(@octokit/core@5.1.0) + dev: true + + /@octokit/types@11.1.0: + resolution: {integrity: sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==} + dependencies: + '@octokit/openapi-types': 18.0.0 + dev: false + + /@octokit/types@12.6.0: + resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} + dependencies: + '@octokit/openapi-types': 20.0.0 + + /@octokit/webhooks-methods@4.0.0: + resolution: {integrity: sha512-M8mwmTXp+VeolOS/kfRvsDdW+IO0qJ8kYodM/sAysk093q6ApgmBXwK1ZlUvAwXVrp/YVHp6aArj4auAxUAOFw==} + engines: {node: '>= 18'} + dev: false + + /@octokit/webhooks-types@7.1.0: + resolution: {integrity: sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==} + dev: false + + /@octokit/webhooks@12.0.10: + resolution: {integrity: sha512-Q8d26l7gZ3L1SSr25NFbbP0B431sovU5r0tIqcvy8Z4PrD1LBv0cJEjvDLOieouzPSTzSzufzRIeXD7S+zAESA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.0.1 + '@octokit/webhooks-methods': 4.0.0 + '@octokit/webhooks-types': 7.1.0 + aggregate-error: 3.1.0 + dev: false + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + optional: true + + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true + + /@pnpm/config.env-replace@1.1.0: + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + dev: true + + /@pnpm/constants@7.1.1: + resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==} + engines: {node: '>=16.14'} + dev: true + + /@pnpm/core-loggers@9.0.6(@pnpm/logger@5.0.0): + resolution: {integrity: sha512-iK67SGbp+06bA/elpg51wygPFjNA7JKHtKkpLxqXXHw+AjFFBC3f2OznJsCIuDK6HdGi5UhHLYqo5QxJ2gMqJQ==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 + dependencies: + '@pnpm/logger': 5.0.0 + '@pnpm/types': 9.4.2 + dev: true + + /@pnpm/error@5.0.2: + resolution: {integrity: sha512-0TEm+tWNYm+9uh6DSKyRbv8pv/6b4NL0PastLvMxIoqZbBZ5Zj1cYi332R9xsSUi31ZOsu2wpgn/bC7DA9hrjg==} + engines: {node: '>=16.14'} + dependencies: + '@pnpm/constants': 7.1.1 + dev: true + + /@pnpm/fetching-types@5.0.0: + resolution: {integrity: sha512-o9gdO1v8Uc5P2fBBuW6GSpfTqIivQmQlqjQJdFiQX0m+tgxlrMRneIg392jZuc6fk7kFqjLheInlslgJfwY+4Q==} + engines: {node: '>=16.14'} + dependencies: + '@zkochan/retry': 0.2.0 + node-fetch: 3.0.0-beta.9 + transitivePeerDependencies: + - domexception + dev: true + + /@pnpm/graceful-fs@3.2.0: + resolution: {integrity: sha512-vRoXJxscDpHak7YE9SqCkzfrayn+Lw+YueOeHIPEqkgokrHeYgYeONoc2kGh0ObHaRtNSsonozVfJ456kxLNvA==} + engines: {node: '>=16.14'} + dependencies: + graceful-fs: 4.2.11 + dev: true + + /@pnpm/logger@5.0.0: + resolution: {integrity: sha512-YfcB2QrX+Wx1o6LD1G2Y2fhDhOix/bAY/oAnMpHoNLsKkWIRbt1oKLkIFvxBMzLwAEPqnYWguJrYC+J6i4ywbw==} + engines: {node: '>=12.17'} + dependencies: + bole: 5.0.8 + ndjson: 2.0.0 + dev: true + + /@pnpm/network.ca-file@1.0.2: + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + dependencies: + graceful-fs: 4.2.10 + dev: true + + /@pnpm/npm-conf@2.2.2: + resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} + engines: {node: '>=12'} + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + dev: true + + /@pnpm/npm-package-arg@1.0.0: + resolution: {integrity: sha512-oQYP08exi6mOPdAZZWcNIGS+KKPsnNwUBzSuAEGWuCcqwMAt3k/WVCqVIXzBxhO5sP2b43og69VHmPj6IroKqw==} + engines: {node: '>=14.6'} + dependencies: + hosted-git-info: 4.1.0 + semver: 7.6.0 + validate-npm-package-name: 4.0.0 + dev: true + + /@pnpm/npm-resolver@18.1.0(@pnpm/logger@5.0.0): + resolution: {integrity: sha512-fUYKX/iHiHldL0VRVvkQI35YK2jWhZEkPO6rrGke8309+LKAo12v833nBttMDpQrtHefmqhB4mhCzQq6L2Xqmg==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 + dependencies: + '@pnpm/core-loggers': 9.0.6(@pnpm/logger@5.0.0) + '@pnpm/error': 5.0.2 + '@pnpm/fetching-types': 5.0.0 + '@pnpm/graceful-fs': 3.2.0 + '@pnpm/logger': 5.0.0 + '@pnpm/resolve-workspace-range': 5.0.1 + '@pnpm/resolver-base': 11.1.0 + '@pnpm/types': 9.4.2 + '@zkochan/retry': 0.2.0 + encode-registry: 3.0.1 + load-json-file: 6.2.0 + lru-cache: 10.2.0 + normalize-path: 3.0.0 + p-limit: 3.1.0 + p-memoize: 4.0.1 + parse-npm-tarball-url: 3.0.0 + path-temp: 2.1.0 + ramda: /@pnpm/ramda@0.28.1 + rename-overwrite: 5.0.0 + semver: 7.6.0 + ssri: 10.0.5 + version-selector-type: 3.0.0 + transitivePeerDependencies: + - domexception + dev: true + + /@pnpm/ramda@0.28.1: + resolution: {integrity: sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==} + dev: true + + /@pnpm/resolve-workspace-range@5.0.1: + resolution: {integrity: sha512-yQ0pMthlw8rTgS/C9hrjne+NEnnSNevCjtdodd7i15I59jMBYciHifZ/vjg0NY+Jl+USTc3dBE+0h/4tdYjMKg==} + engines: {node: '>=16.14'} + dependencies: + semver: 7.6.0 + dev: true + + /@pnpm/resolver-base@11.1.0: + resolution: {integrity: sha512-y2qKaj18pwe1VWc3YXEitdYFo+WqOOt60aqTUuOVkJAirUzz0DzuYh3Ifct4znYWPdgUXHaN5DMphNF5iL85rA==} + engines: {node: '>=16.14'} + dependencies: + '@pnpm/types': 9.4.2 + dev: true + + /@pnpm/types@9.4.2: + resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} + engines: {node: '>=16.14'} + dev: true + + /@pnpm/workspace.pkgs-graph@2.0.14(@pnpm/logger@5.0.0): + resolution: {integrity: sha512-SBXXyWDkPEoaLTjLRyQzRHoBYH+P0NLcIjX1yPUxuJiMTvGOMzjpLWTuxYNVe/P0V0VQMrjpJFaJPjlViNLhzg==} + engines: {node: '>=16.14'} + dependencies: + '@pnpm/npm-package-arg': 1.0.0 + '@pnpm/npm-resolver': 18.1.0(@pnpm/logger@5.0.0) + '@pnpm/resolve-workspace-range': 5.0.1 + ramda: /@pnpm/ramda@0.28.1 + transitivePeerDependencies: + - '@pnpm/logger' + - domexception + dev: true + + /@release-it/conventional-changelog@8.0.1(release-it@17.1.1): + resolution: {integrity: sha512-pwc9jaBYDaSX5TXw6rEnPfqDkKJN2sFBhYpON1kBi9T3sA9EOBncC4ed0Bv3L1ciNb6eqEJXPfp+tQMqVlv/eg==} + engines: {node: '>=18'} + peerDependencies: + release-it: ^17.0.0 + dependencies: + concat-stream: 2.0.0 + conventional-changelog: 5.1.0 + conventional-recommended-bump: 9.0.0 + release-it: 17.1.1(typescript@5.3.3) + semver: 7.6.0 + dev: true + + /@rollup/rollup-android-arm-eabi@4.12.0: + resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.12.0: + resolution: {integrity: sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.12.0: + resolution: {integrity: sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.12.0: + resolution: {integrity: sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.12.0: + resolution: {integrity: sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.12.0: + resolution: {integrity: sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.12.0: + resolution: {integrity: sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.12.0: + resolution: {integrity: sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.12.0: + resolution: {integrity: sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.12.0: + resolution: {integrity: sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.12.0: + resolution: {integrity: sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.12.0: + resolution: {integrity: sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.12.0: + resolution: {integrity: sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sindresorhus/is@5.6.0: + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + dev: true + + /@sindresorhus/merge-streams@2.3.0: + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + dev: true + + /@snyk/github-codeowners@1.1.0: + resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==} + engines: {node: '>=8.10'} + hasBin: true + dependencies: + commander: 4.1.1 + ignore: 5.3.1 + p-map: 4.0.0 + dev: true + + /@szmarczak/http-timer@5.0.1: + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + dependencies: + defer-to-connect: 2.0.1 + dev: true + + /@tootallnate/quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + dev: true + + /@types/aws-lambda@8.10.114: + resolution: {integrity: sha512-M8WpEGfC9iQ6V2Ccq6nGIXoQgeVc6z0Ngk8yCOL5V/TYIxshvb0MWQYLFFTZDesL0zmsoBc4OBjG9DB/4rei6w==} + dev: false + + /@types/btoa-lite@1.0.0: + resolution: {integrity: sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==} + dev: false + + /@types/eslint@8.56.4: + resolution: {integrity: sha512-lG1GLUnL5vuRBGb3MgWUWLdGMH2Hps+pERuyQXCfWozuGKdnhf9Pbg4pkcrVUHjKrU7Rl+GCZ/299ObBXZFAxg==} + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + dev: true + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/git-url-parse@9.0.3: + resolution: {integrity: sha512-Wrb8zeghhpKbYuqAOg203g+9YSNlrZWNZYvwxJuDF4dTmerijqpnGbI79yCuPtHSXHPEwv1pAFUB4zsSqn82Og==} + dev: true + + /@types/glob@7.2.0: + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 20.11.21 + dev: true + + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: true + + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: true + + /@types/js-yaml@4.0.9: + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/jsonwebtoken@9.0.1: + resolution: {integrity: sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==} + dependencies: + '@types/node': 20.11.21 + dev: false + + /@types/mdast@3.0.10: + resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /@types/minimatch@5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + dev: true + + /@types/node@20.11.21: + resolution: {integrity: sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==} + dependencies: + undici-types: 5.26.5 + + /@types/normalize-package-data@2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + + /@types/parse-author@2.0.3: + resolution: {integrity: sha512-pgRW2K/GVQoogylrGJXDl7PBLW9A6T4OOc9Hy9MLT5f7vgufK2GQ8FcfAbjFHR5HjcN9ByzuCczAORk49REqoA==} + dev: true + + /@types/picomatch@2.3.3: + resolution: {integrity: sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==} + dev: true + + /@types/prettier@2.7.3: + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + dev: true + + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + + /@types/unist@2.0.6: + resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + dev: true + + /@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.13.2 + '@typescript-eslint/type-utils': 6.13.2(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.13.2(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.13.2 + debug: 4.3.4 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.0 + ts-api-utils: 1.2.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 7.1.0 + '@typescript-eslint/types': 7.1.0 + '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.1.0 + debug: 4.3.4 + eslint: 8.57.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.13.2: + resolution: {integrity: sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/visitor-keys': 6.13.2 + dev: true + + /@typescript-eslint/scope-manager@6.21.0: + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + dev: true + + /@typescript-eslint/scope-manager@7.1.0: + resolution: {integrity: sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.1.0 + '@typescript-eslint/visitor-keys': 7.1.0 + dev: true + + /@typescript-eslint/type-utils@6.13.2(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) + '@typescript-eslint/utils': 6.13.2(eslint@8.57.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.57.0 + ts-api-utils: 1.2.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.13.2: + resolution: {integrity: sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/types@6.21.0: + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/types@7.1.0: + resolution: {integrity: sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.13.2(typescript@5.3.3): + resolution: {integrity: sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/visitor-keys': 6.13.2 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.0 + ts-api-utils: 1.2.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.0 + ts-api-utils: 1.2.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@7.1.0(typescript@5.3.3): + resolution: {integrity: sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 7.1.0 + '@typescript-eslint/visitor-keys': 7.1.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.0 + ts-api-utils: 1.2.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.13.2(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.13.2 + '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) + eslint: 8.57.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) + eslint: 8.57.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.13.2: + resolution: {integrity: sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.2 + eslint-visitor-keys: 3.4.3 + dev: true + + /@typescript-eslint/visitor-keys@6.21.0: + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@typescript-eslint/visitor-keys@7.1.0: + resolution: {integrity: sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.1.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitest/coverage-v8@1.3.1(vitest@1.3.1): + resolution: {integrity: sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==} + peerDependencies: + vitest: 1.3.1 + dependencies: + '@ampproject/remapping': 2.2.1 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + magic-string: 0.30.7 + magicast: 0.3.3 + picocolors: 1.0.0 + std-env: 3.7.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + vitest: 1.3.1(@types/node@20.11.21) + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/expect@1.3.1: + resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} + dependencies: + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.3.1: + resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==} + dependencies: + '@vitest/utils': 1.3.1 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.3.1: + resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} + dependencies: + magic-string: 0.30.7 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.3.1: + resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} + dependencies: + tinyspy: 2.2.1 + dev: true + + /@vitest/utils@1.3.1: + resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + + /@zkochan/retry@0.2.0: + resolution: {integrity: sha512-WhB+2B/ZPlW2Xy/kMJBrMbqecWXcbDDgn0K0wKBAgO2OlBTz1iLJrRWduo+DGGn0Akvz1Lu4Xvls7dJojximWw==} + engines: {node: '>=10'} + dev: true + + /@zkochan/rimraf@2.1.3: + resolution: {integrity: sha512-mCfR3gylCzPC+iqdxEA6z5SxJeOgzgbwmyxanKriIne5qZLswDe/M43aD3p5MNzwzXRhbZg/OX+MpES6Zk1a6A==} + engines: {node: '>=12.10'} + dependencies: + rimraf: 3.0.2 + dev: true + + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + dev: true + + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /all-contributors-for-repository@0.2.2: + resolution: {integrity: sha512-6+CJv+yI7wzPowGdwdIYLO2KNUnQ5eWiRZKTKanxDgg/lB+hBQzxPbXwjTLnlR0MwwPMNJzb8owx1Bo4OtG7hg==} + engines: {node: '>=18'} + dependencies: + co-author-to-username: 0.1.0 + commit-to-co-authors: 0.1.0 + conventional-commits-parser: 5.0.0 + octokit: 3.1.2 + dev: false + + /ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + dependencies: + string-width: 4.2.3 + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-escapes@6.2.0: + resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} + engines: {node: '>=14.16'} + dependencies: + type-fest: 3.13.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /arity-n@1.0.4: + resolution: {integrity: sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ==} + dev: true + + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + + /array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + dev: true + + /array-last@1.3.0: + resolution: {integrity: sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 4.0.0 + dev: true + + /array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.map@1.0.6: + resolution: {integrity: sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + es-array-method-boxes-properly: 1.0.0 + is-string: 1.0.7 + dev: true + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + dependencies: + tslib: 2.6.2 + dev: true + + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + dependencies: + retry: 0.13.1 + dev: true + + /author-regex@1.0.0: + resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} + engines: {node: '>=0.8'} + dev: false + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /basic-ftp@5.0.4: + resolution: {integrity: sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==} + engines: {node: '>=10.0.0'} + dev: true + + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /bole@5.0.8: + resolution: {integrity: sha512-Upz2bX9d+gwNWcTikpVWiv2WP+A1vNsw/aRmlAzFgzlGkh/heckidRSUDt3xBRV49B1Cl9gtR2ijjvL8tC8v1g==} + dependencies: + fast-safe-stringify: 2.1.1 + individual: 3.0.0 + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + + /bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + dev: false + + /boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.3.0 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /btoa-lite@1.0.0: + resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==} + dev: false + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /builtins@5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + dependencies: + semver: 7.6.0 + dev: true + + /bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + dependencies: + run-applescript: 7.0.0 + dev: true + + /bundle-require@4.0.2(esbuild@0.19.12): + resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + dependencies: + esbuild: 0.19.12 + load-tsconfig: 0.2.5 + dev: true + + /c8@9.1.0: + resolution: {integrity: sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==} + engines: {node: '>=14.14.0'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 3.1.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.1.7 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + dev: true + + /cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.0 + responselike: 3.0.0 + dev: true + + /cached-factory@0.0.1: + resolution: {integrity: sha512-nlhbhyJwz++jwl5wJR9oV3E69eCfNuWbtTia+zZslORBDBCk7hwRO1EoUbHsA1c5kOGP6vWIPPSYAxA5y5VcMw==} + engines: {node: '>=18'} + dev: false + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.1 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + dev: true + + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk-template@1.1.0: + resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==} + engines: {node: '>=14.16'} + dependencies: + chalk: 5.3.0 + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + /character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + dev: true + + /character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + dev: true + + /character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + dev: true + + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: true + + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: false + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: false + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + dev: true + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + /clear-module@4.1.2: + resolution: {integrity: sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==} + engines: {node: '>=8'} + dependencies: + parent-module: 2.0.0 + resolve-from: 5.0.0 + dev: true + + /cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + dev: true + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: true + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: true + + /cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + dependencies: + slice-ansi: 5.0.0 + string-width: 7.1.0 + dev: true + + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + requiresBuild: true + dev: true + + /co-author-to-username@0.1.0: + resolution: {integrity: sha512-tpRatfclGzJO2LEsszgOasgOc4+OOp6kPHpCPnxpQMxdRJEMnpTGh4YtWnxWfuQEVt5wVHGMq4laaj+x6/zB7g==} + engines: {node: '>=18'} + dependencies: + cached-factory: 0.0.1 + octokit: 3.1.2 + dev: false + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /comment-json@4.2.3: + resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} + engines: {node: '>= 6'} + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + dev: true + + /comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + dev: true + + /commit-to-co-authors@0.1.0: + resolution: {integrity: sha512-5v1M+gBD0H7G2++D1L3z8ekVkTsIYNTuUFmL8KgIumuJdZ1CFjRulgUvYA7dFDmZNENu0G//yp+krWGcXb2WYA==} + engines: {node: '>=18'} + dev: false + + /compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + dev: true + + /compose-function@3.0.3: + resolution: {integrity: sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg==} + dependencies: + arity-n: 1.0.4 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + dev: true + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: true + + /configstore@6.0.0: + resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} + engines: {node: '>=12'} + dependencies: + dot-prop: 6.0.1 + graceful-fs: 4.2.11 + unique-string: 3.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 5.1.0 + dev: true + + /console-fail-test@0.2.3: + resolution: {integrity: sha512-p1xfi9ubVM2X3NpjPH1Gm8Gxc7vXyTwcOIquU8rpUDA+ISSitAOiBDnT/Bti1We5YzZKRxgI0JU+fFvOmOXtvA==} + dev: true + + /conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + dependencies: + compare-func: 2.0.0 + dev: true + + /conventional-changelog-atom@4.0.0: + resolution: {integrity: sha512-q2YtiN7rnT1TGwPTwjjBSIPIzDJCRE+XAUahWxnh+buKK99Kks4WLMHoexw38GXx9OUxAsrp44f9qXe5VEMYhw==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-codemirror@4.0.0: + resolution: {integrity: sha512-hQSojc/5imn1GJK3A75m9hEZZhc3urojA5gMpnar4JHmgLnuM3CUIARPpEk86glEKr3c54Po3WV/vCaO/U8g3Q==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + dependencies: + compare-func: 2.0.0 + dev: true + + /conventional-changelog-core@7.0.0: + resolution: {integrity: sha512-UYgaB1F/COt7VFjlYKVE/9tTzfU3VUq47r6iWf6lM5T7TlOxr0thI63ojQueRLIpVbrtHK4Ffw+yQGduw2Bhdg==} + engines: {node: '>=16'} + dependencies: + '@hutson/parse-repository-url': 5.0.0 + add-stream: 1.0.0 + conventional-changelog-writer: 7.0.1 + conventional-commits-parser: 5.0.0 + git-raw-commits: 4.0.0 + git-semver-tags: 7.0.1 + hosted-git-info: 7.0.1 + normalize-package-data: 6.0.0 + read-pkg: 8.1.0 + read-pkg-up: 10.1.0 + dev: true + + /conventional-changelog-ember@4.0.0: + resolution: {integrity: sha512-D0IMhwcJUg1Y8FSry6XAplEJcljkHVlvAZddhhsdbL1rbsqRsMfGx/PIkPYq0ru5aDgn+OxhQ5N5yR7P9mfsvA==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-eslint@5.0.0: + resolution: {integrity: sha512-6JtLWqAQIeJLn/OzUlYmzd9fKeNSWmQVim9kql+v4GrZwLx807kAJl3IJVc3jTYfVKWLxhC3BGUxYiuVEcVjgA==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-express@4.0.0: + resolution: {integrity: sha512-yWyy5c7raP9v7aTvPAWzqrztACNO9+FEI1FSYh7UP7YT1AkWgv5UspUeB5v3Ibv4/o60zj2o9GF2tqKQ99lIsw==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-jquery@5.0.0: + resolution: {integrity: sha512-slLjlXLRNa/icMI3+uGLQbtrgEny3RgITeCxevJB+p05ExiTgHACP5p3XiMKzjBn80n+Rzr83XMYfRInEtCPPw==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-jshint@4.0.0: + resolution: {integrity: sha512-LyXq1bbl0yG0Ai1SbLxIk8ZxUOe3AjnlwE6sVRQmMgetBk+4gY9EO3d00zlEt8Y8gwsITytDnPORl8al7InTjg==} + engines: {node: '>=16'} + dependencies: + compare-func: 2.0.0 + dev: true + + /conventional-changelog-preset-loader@4.1.0: + resolution: {integrity: sha512-HozQjJicZTuRhCRTq4rZbefaiCzRM2pr6u2NL3XhrmQm4RMnDXfESU6JKu/pnKwx5xtdkYfNCsbhN5exhiKGJA==} + engines: {node: '>=16'} + dev: true + + /conventional-changelog-writer@7.0.1: + resolution: {integrity: sha512-Uo+R9neH3r/foIvQ0MKcsXkX642hdm9odUp7TqgFS7BsalTcjzRlIfWZrZR1gbxOozKucaKt5KAbjW8J8xRSmA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + conventional-commits-filter: 4.0.0 + handlebars: 4.7.8 + json-stringify-safe: 5.0.1 + meow: 12.1.1 + semver: 7.6.0 + split2: 4.2.0 + dev: true + + /conventional-changelog@5.1.0: + resolution: {integrity: sha512-aWyE/P39wGYRPllcCEZDxTVEmhyLzTc9XA6z6rVfkuCD2UBnhV/sgSOKbQrEG5z9mEZJjnopjgQooTKxEg8mAg==} + engines: {node: '>=16'} + dependencies: + conventional-changelog-angular: 7.0.0 + conventional-changelog-atom: 4.0.0 + conventional-changelog-codemirror: 4.0.0 + conventional-changelog-conventionalcommits: 7.0.2 + conventional-changelog-core: 7.0.0 + conventional-changelog-ember: 4.0.0 + conventional-changelog-eslint: 5.0.0 + conventional-changelog-express: 4.0.0 + conventional-changelog-jquery: 5.0.0 + conventional-changelog-jshint: 4.0.0 + conventional-changelog-preset-loader: 4.1.0 + dev: true + + /conventional-commits-filter@4.0.0: + resolution: {integrity: sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==} + engines: {node: '>=16'} + dev: true + + /conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + /conventional-recommended-bump@9.0.0: + resolution: {integrity: sha512-HR1yD0G5HgYAu6K0wJjLd7QGRK8MQDqqj6Tn1n/ja1dFwBCE6QmV+iSgQ5F7hkx7OUR/8bHpxJqYtXj2f/opPQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + conventional-changelog-preset-loader: 4.1.0 + conventional-commits-filter: 4.0.0 + conventional-commits-parser: 5.0.0 + git-raw-commits: 4.0.0 + git-semver-tags: 7.0.1 + meow: 12.1.1 + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /cosmiconfig@9.0.0(typescript@5.3.3): + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + typescript: 5.3.3 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + + /crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + dependencies: + type-fest: 1.4.0 + dev: true + + /cspell-config-lib@8.1.3: + resolution: {integrity: sha512-whzJYxcxos3vnywn0alCFZ+Myc0K/C62pUurfOGhgvIba7ArmlXhNRaL2r5noBxWARtpBOtzz3vrzSBK7Lq6jg==} + engines: {node: '>=18'} + dependencies: + '@cspell/cspell-types': 8.1.3 + comment-json: 4.2.3 + yaml: 2.4.0 + dev: true + + /cspell-dictionary@8.1.3: + resolution: {integrity: sha512-nkRQDPNnA6tw+hJFBqq26M0nK306q5rtyv/AUIWa8ZHhQkwzACnpMSpuJA7/DV5GVvPKltMK5M4A6vgfpoaFHw==} + engines: {node: '>=18'} + dependencies: + '@cspell/cspell-pipe': 8.1.3 + '@cspell/cspell-types': 8.1.3 + cspell-trie-lib: 8.1.3 + fast-equals: 5.0.1 + gensequence: 6.0.0 + dev: true + + /cspell-gitignore@8.1.3: + resolution: {integrity: sha512-NHx5lg44eCKb6yJmUPOCz4prcuYowzoo5GJ5hOcCfbk7ZEBWV1E2/kDRuQMOK2W0y1hNGr45CSxO3UxWJlYg7w==} + engines: {node: '>=18'} + hasBin: true + dependencies: + cspell-glob: 8.1.3 + find-up-simple: 1.0.0 + dev: true + + /cspell-glob@8.1.3: + resolution: {integrity: sha512-Likr7UVUXBpthQnM5r6yao3X0YBNRbJ9AHWXTC2RJfzwZOFKF+pKPfeo3FU+Px8My96M4RC2bVMbrbZUwN5NJw==} + engines: {node: '>=18'} + dependencies: + micromatch: 4.0.5 + dev: true + + /cspell-grammar@8.1.3: + resolution: {integrity: sha512-dTOwNq6a5wcVzOsi4xY5/tq2r2w/+wLVU+WfyySTsPe66Rjqx/QceFl4OinImks/ZMKF7Zyjd3WGyQ5TcSsJFQ==} + engines: {node: '>=18'} + hasBin: true + dependencies: + '@cspell/cspell-pipe': 8.1.3 + '@cspell/cspell-types': 8.1.3 + dev: true + + /cspell-io@8.1.3: + resolution: {integrity: sha512-QkcFeYd79oIl7PgSqFSZyvwXnZQhXmdCI733n54IN2+iXDcf7W0mwptxoC/cE19RkEwAwEFLG81UAy6L/BXI6A==} + engines: {node: '>=18'} + dependencies: + '@cspell/cspell-service-bus': 8.1.3 + dev: true + + /cspell-lib@8.1.3: + resolution: {integrity: sha512-Kk8bpHVkDZO4MEiPkDvRf/LgJ0h5mufbKLTWModq6k0Ca8EkZ/qgQlZ0ve0rIivbleSqebuWjpJHKDM+IHmzHA==} + engines: {node: '>=18'} + dependencies: + '@cspell/cspell-bundled-dicts': 8.1.3 + '@cspell/cspell-pipe': 8.1.3 + '@cspell/cspell-resolver': 8.1.3 + '@cspell/cspell-types': 8.1.3 + '@cspell/dynamic-import': 8.1.3 + '@cspell/strong-weak-map': 8.1.3 + clear-module: 4.1.2 + comment-json: 4.2.3 + configstore: 6.0.0 + cspell-config-lib: 8.1.3 + cspell-dictionary: 8.1.3 + cspell-glob: 8.1.3 + cspell-grammar: 8.1.3 + cspell-io: 8.1.3 + cspell-trie-lib: 8.1.3 + fast-equals: 5.0.1 + gensequence: 6.0.0 + import-fresh: 3.3.0 + resolve-from: 5.0.0 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + dev: true + + /cspell-trie-lib@8.1.3: + resolution: {integrity: sha512-EDSYU9MCtzPSJDrfvDrTKmc0rzl50Ehjg1c5rUCqn33p2LCRe/G8hW0FxXe0mxrZxrMO2b8l0PVSGlrCXCQ8RQ==} + engines: {node: '>=18'} + dependencies: + '@cspell/cspell-pipe': 8.1.3 + '@cspell/cspell-types': 8.1.3 + gensequence: 6.0.0 + dev: true + + /cspell@8.1.3: + resolution: {integrity: sha512-SU4Su6002bPoJYaiMeNV4wwLoS8TwaOgIwaTxhys3GDbJIxZV6CrDgwksezHcG7TZrC4yrveDVsdpnrzmQ7T5Q==} + engines: {node: '>=18'} + hasBin: true + dependencies: + '@cspell/cspell-json-reporter': 8.1.3 + '@cspell/cspell-pipe': 8.1.3 + '@cspell/cspell-types': 8.1.3 + '@cspell/dynamic-import': 8.1.3 + chalk: 5.3.0 + chalk-template: 1.1.0 + commander: 11.1.0 + cspell-gitignore: 8.1.3 + cspell-glob: 8.1.3 + cspell-io: 8.1.3 + cspell-lib: 8.1.3 + fast-glob: 3.3.2 + fast-json-stable-stringify: 2.1.0 + file-entry-cache: 7.0.2 + get-stdin: 9.0.0 + semver: 7.6.0 + strip-ansi: 7.1.0 + vscode-uri: 3.0.8 + dev: true + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + + /dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + dev: true + + /data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + dev: true + + /data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + dev: true + + /data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + + /deep-freeze@0.0.1: + resolution: {integrity: sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==} + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + dev: true + + /default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + dev: true + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + requiresBuild: true + dependencies: + clone: 1.0.4 + dev: true + + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: true + + /define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + dev: true + + /degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + dev: true + + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + + /detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + dev: true + + /detect-indent@7.0.1: + resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} + engines: {node: '>=12.20'} + dev: true + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: true + + /detect-newline@4.0.1: + resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: true + + /dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dependencies: + is-obj: 2.0.0 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + /easy-table@1.2.0: + resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + dependencies: + ansi-regex: 5.0.1 + optionalDependencies: + wcwidth: 1.0.1 + dev: true + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + /encode-registry@3.0.1: + resolution: {integrity: sha512-6qOwkl1g0fv0DN3Y3ggr2EaZXN71aoAqPp3p/pVaWSBSIo+YjLOWN61Fva43oVyQNPf7kgm8lkudzlzojwE2jw==} + engines: {node: '>=10'} + dependencies: + mem: 8.1.1 + dev: true + + /entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + /env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: true + + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es-abstract@1.22.4: + resolution: {integrity: sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.1 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.0 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.5 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.14 + dev: true + + /es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.1 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + /escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-compat-utils@0.1.2(eslint@8.57.0): + resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-compat-utils@0.4.1(eslint@8.57.0): + resolution: {integrity: sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + eslint: 8.57.0 + semver: 7.6.0 + dev: true + + /eslint-plugin-deprecation@2.0.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-OAm9Ohzbj11/ZFyICyR5N6LbOIvQMp7ZU2zI7Ej0jIc8kiGUERXPNMfw2QqqHD1ZHtjMub3yPZILovYEYucgoQ==} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: ^4.2.4 || ^5.0.0 + dependencies: + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + eslint: 8.57.0 + tslib: 2.6.2 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-es-x@7.5.0(eslint@8.57.0): + resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + eslint: 8.57.0 + eslint-compat-utils: 0.1.2(eslint@8.57.0) + dev: true + + /eslint-plugin-eslint-comments@3.2.0(eslint@8.57.0): + resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} + engines: {node: '>=6.5.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + escape-string-regexp: 1.0.5 + eslint: 8.57.0 + ignore: 5.3.1 + dev: true + + /eslint-plugin-jsdoc@48.2.0(eslint@8.57.0): + resolution: {integrity: sha512-O2B1XLBJnUCRkggFzUQ+PBYJDit8iAgXdlu8ucolqGrbmOWPvttZQZX8d1sC0MbqDMSLs8SHSQxaNPRY1RQREg==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + dependencies: + '@es-joy/jsdoccomment': 0.42.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.3.4 + escape-string-regexp: 4.0.0 + eslint: 8.57.0 + esquery: 1.5.0 + is-builtin-module: 3.2.1 + semver: 7.6.0 + spdx-expression-parse: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-jsonc@2.13.0(eslint@8.57.0): + resolution: {integrity: sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + eslint: 8.57.0 + eslint-compat-utils: 0.4.1(eslint@8.57.0) + espree: 9.6.1 + graphemer: 1.4.0 + jsonc-eslint-parser: 2.4.0 + natural-compare: 1.4.0 + synckit: 0.6.2 + dev: true + + /eslint-plugin-markdown@3.0.1(eslint@8.57.0): + resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.57.0 + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-n@16.6.2(eslint@8.57.0): + resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + builtins: 5.0.1 + eslint: 8.57.0 + eslint-plugin-es-x: 7.5.0(eslint@8.57.0) + get-tsconfig: 4.7.2 + globals: 13.24.0 + ignore: 5.3.1 + is-builtin-module: 3.2.1 + is-core-module: 2.13.1 + minimatch: 3.1.2 + resolve: 1.22.8 + semver: 7.6.0 + dev: true + + /eslint-plugin-package-json@0.10.4(eslint@8.57.0)(jsonc-eslint-parser@2.4.0): + resolution: {integrity: sha512-dape6w9G7hCXDkoVWoPwrQjUK0V6636qAgUPewduXLH9RdEw/y4XGqCyvD5KujlMBQuagadMkxuGNluW0GC4vQ==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=8.0.0' + jsonc-eslint-parser: ^2.0.0 + dependencies: + eslint: 8.57.0 + jsonc-eslint-parser: 2.4.0 + package-json-validator: 0.6.3 + semver: 7.6.0 + sort-package-json: 1.57.0 + validate-npm-package-name: 5.0.0 + dev: true + + /eslint-plugin-perfectionist@2.5.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-F6XXcq4mKKUe/SREoMGQqzgw6cgCgf3pFzkFfQVIGtqD1yXVpQjnhTepzhBeZfxZwgMzR9HO4yH4CUhIQ2WBcQ==} + peerDependencies: + astro-eslint-parser: ^0.16.0 + eslint: '>=8.0.0' + svelte: '>=3.0.0' + svelte-eslint-parser: ^0.33.0 + vue-eslint-parser: '>=9.0.0' + peerDependenciesMeta: + astro-eslint-parser: + optional: true + svelte: + optional: true + svelte-eslint-parser: + optional: true + vue-eslint-parser: + optional: true + dependencies: + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + eslint: 8.57.0 + minimatch: 9.0.3 + natural-compare-lite: 1.4.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /eslint-plugin-regexp@2.2.0(eslint@8.57.0): + resolution: {integrity: sha512-0kwpiWiLRVBkVr3oIRQLl196sXP/NF6DQFefv9jtR4ZOgQR+6WID2pIZ0I+wIt54qgBPwBB7Gm2a+ueh8/WsFQ==} + engines: {node: ^18 || >=20} + peerDependencies: + eslint: '>=8.44.0' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + comment-parser: 1.4.1 + eslint: 8.57.0 + jsdoc-type-pratt-parser: 4.0.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + dev: true + + /eslint-plugin-vitest@0.3.22(@typescript-eslint/eslint-plugin@6.13.2)(eslint@8.57.0)(typescript@5.3.3)(vitest@1.3.1): + resolution: {integrity: sha512-atkFGQ7aVgcuSeSMDqnyevIyUpfBPMnosksgEPrKE7Y8xQlqG/5z2IQ6UDau05zXaaFv7Iz8uzqvIuKshjZ0Zw==} + engines: {node: ^18.0.0 || >= 20.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': '*' + eslint: '>=8.0.0' + vitest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + vitest: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 6.13.2(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + eslint: 8.57.0 + vitest: 1.3.1(@types/node@20.11.21) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /eslint-plugin-yml@1.12.2(eslint@8.57.0): + resolution: {integrity: sha512-hvS9p08FhPT7i/ynwl7/Wt7ke7Rf4P2D6fT8lZlL43peZDTsHtH2A0SIFQ7Kt7+mJ6if6P+FX3iJhMkdnxQwpg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4 + eslint: 8.57.0 + eslint-compat-utils: 0.4.1(eslint@8.57.0) + lodash: 4.17.21 + natural-compare: 1.4.0 + yaml-eslint-parser: 1.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fetch-blob@2.1.2: + resolution: {integrity: sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==} + engines: {node: ^10.17.0 || >=12.3.0} + peerDependencies: + domexception: '*' + peerDependenciesMeta: + domexception: + optional: true + dev: true + + /fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + dev: true + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /file-entry-cache@7.0.2: + resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==} + engines: {node: '>=12.0.0'} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /filter-iterator@0.0.1: + resolution: {integrity: sha512-v4lhL7Qa8XpbW3LN46CEnmhGk3eHZwxfNl5at20aEkreesht4YKb/Ba3BUIbnPhAC/r3dmu7ABaGk6MAvh2alA==} + dev: true + + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: true + + /find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + /form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + dev: true + + /formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.2.0 + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gensequence@6.0.0: + resolution: {integrity: sha512-8WwuywE9pokJRAcg2QFR/plk3cVPebSUqRPzpGQh3WQ0wIiHAw+HyOQj5IuHyUTQBHpBKFoB2JUMu9zT3vJ16Q==} + engines: {node: '>=16'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + dev: true + + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.1 + dev: true + + /get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + + /get-uri@6.0.3: + resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + engines: {node: '>= 14'} + dependencies: + basic-ftp: 5.0.4 + data-uri-to-buffer: 6.0.2 + debug: 4.3.4 + fs-extra: 11.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /git-hooks-list@1.0.3: + resolution: {integrity: sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==} + dev: true + + /git-hooks-list@3.1.0: + resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} + dev: true + + /git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + dev: true + + /git-remote-origin-url@4.0.0: + resolution: {integrity: sha512-EAxDksNdjuWgmVW9pVvA9jQDi/dmTaiDONktIy7qiRRhBZUI4FQK1YvBvteuTSX24aNKg9lfgxNYJEeeSXe6DA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + gitconfiglocal: 2.1.0 + dev: false + + /git-semver-tags@7.0.1: + resolution: {integrity: sha512-NY0ZHjJzyyNXHTDZmj+GG7PyuAKtMsyWSwh07CR2hOZFa+/yoTsXci/nF2obzL8UDhakFNkD9gNdt/Ed+cxh2Q==} + engines: {node: '>=16'} + hasBin: true + dependencies: + meow: 12.1.1 + semver: 7.6.0 + dev: true + + /git-up@7.0.0: + resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} + dependencies: + is-ssh: 1.4.0 + parse-url: 8.1.0 + + /git-url-parse@14.0.0: + resolution: {integrity: sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==} + dependencies: + git-up: 7.0.0 + + /gitconfiglocal@2.1.0: + resolution: {integrity: sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==} + dependencies: + ini: 1.3.8 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: false + + /global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + dependencies: + ini: 4.1.1 + dev: true + + /global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + dependencies: + ini: 2.0.0 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /globby@10.0.0: + resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==} + engines: {node: '>=8'} + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + + /globby@14.0.1: + resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==} + engines: {node: '>=18'} + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.1 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: true + + /got@13.0.0: + resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==} + engines: {node: '>=16'} + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: true + + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + 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==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + dev: true + + /has-own-property@0.1.0: + resolution: {integrity: sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw==} + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: true + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /hasown@2.0.1: + resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /hosted-git-info@7.0.1: + resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + lru-cache: 10.2.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + + /https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + /husky@9.0.11: + resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==} + engines: {node: '>=18'} + hasBin: true + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /identity-function@1.0.0: + resolution: {integrity: sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw==} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: true + + /import-meta-resolve@4.0.0: + resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==} + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + /individual@3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + /ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + dev: true + + /ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /inquirer@9.2.14: + resolution: {integrity: sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==} + engines: {node: '>=18'} + dependencies: + '@ljharb/through': 2.3.12 + ansi-escapes: 4.3.2 + chalk: 5.3.0 + cli-cursor: 3.1.0 + cli-width: 4.1.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.1 + side-channel: 1.0.5 + dev: true + + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: true + + /ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: true + + /is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + dev: true + + /is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + dev: true + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + dependencies: + ci-info: 3.9.0 + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.1 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + dev: true + + /is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + + /is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + dependencies: + get-east-asian-width: 1.2.0 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + dev: true + + /is-in-ci@0.1.0: + resolution: {integrity: sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==} + engines: {node: '>=18'} + hasBin: true + dev: true + + /is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + dependencies: + is-docker: 3.0.0 + dev: true + + /is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + dev: true + + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: true + + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: true + + /is-iterable@1.1.1: + resolution: {integrity: sha512-EdOZCr0NsGE00Pot+x1ZFx9MJK3C6wy91geZpXwvwexDLJvA4nzYyZf7r+EIwSeVsOLDdBz7ATg9NqKTzuNYuQ==} + engines: {node: '>= 4'} + dev: true + + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: true + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-npm@6.0.0: + resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-number@4.0.0: + resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: true + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-ssh@1.4.0: + resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} + dependencies: + protocols: 2.0.1 + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + dependencies: + text-extensions: 2.4.0 + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.14 + dev: true + + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: true + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: true + + /is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + dependencies: + is-inside-container: 1.0.0 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + dev: true + + /issue-parser@6.0.0: + resolution: {integrity: sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==} + engines: {node: '>=10.13'} + dependencies: + lodash.capitalize: 4.2.1 + lodash.escaperegexp: 4.1.2 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.uniqby: 4.7.0 + dev: true + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /iterable-lookahead@1.0.0: + resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==} + engines: {node: '>=4'} + dev: true + + /iterate-iterator@1.0.2: + resolution: {integrity: sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==} + dev: true + + /iterate-value@1.0.2: + resolution: {integrity: sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==} + dependencies: + es-get-iterator: 1.1.3 + iterate-iterator: 1.0.2 + dev: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: true + + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + + /jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + dev: true + + /jsdoc-type-pratt-parser@4.0.0: + resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} + engines: {node: '>=12.0.0'} + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-parse-even-better-errors@3.0.0: + resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /jsonc-eslint-parser@2.4.0: + resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.6.0 + dev: true + + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + /jsonwebtoken@9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.6.0 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /knip@5.0.2(@types/node@20.11.21)(typescript@5.3.3): + resolution: {integrity: sha512-ylmXi/CaR1rjOl8KqLsei7075cCsHttMNFwoYlBM7WMvd2Rd4oOfic9KqJFkDONXedsEsiiwnI3+u6GT6KOUuw==} + engines: {node: '>=18.6.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + dependencies: + '@ericcornelissen/bash-parser': 0.5.2 + '@nodelib/fs.walk': 2.0.0 + '@npmcli/map-workspaces': 3.0.4 + '@npmcli/package-json': 5.0.0 + '@pnpm/logger': 5.0.0 + '@pnpm/workspace.pkgs-graph': 2.0.14(@pnpm/logger@5.0.0) + '@snyk/github-codeowners': 1.1.0 + '@types/node': 20.11.21 + '@types/picomatch': 2.3.3 + easy-table: 1.2.0 + fast-glob: 3.3.2 + jiti: 1.21.0 + js-yaml: 4.1.0 + micromatch: 4.0.5 + minimist: 1.2.8 + picocolors: 1.0.0 + picomatch: 4.0.1 + pretty-ms: 9.0.0 + semver: 7.6.0 + smol-toml: 1.1.4 + strip-json-comments: 5.0.1 + summary: 2.1.0 + typescript: 5.3.3 + zod: 3.22.4 + zod-validation-error: 3.0.2(zod@3.22.4) + transitivePeerDependencies: + - bluebird + - domexception + dev: true + + /ky@1.2.2: + resolution: {integrity: sha512-gYA2QOI3uIaImJPJjaBbLCdvKHzwxsuB03s7PjrXmoO6tcn6k53rwYoSRgqrmVsEV6wFFegOXDVjABxFZ0aRSg==} + engines: {node: '>=18'} + dev: false + + /latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + dependencies: + package-json: 8.1.1 + dev: true + + /lazy-value@3.0.0: + resolution: {integrity: sha512-BBcLu68yjVhYSqRLYbiDOCWOD7Q8pm39SNL+UfhSfroJScJKRZMaoDJMXLYA2wBA+JCY/5ICoVEvG3yQjqQtKw==} + engines: {node: '>=12'} + dev: false + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + + /lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + engines: {node: '>=14'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /linkify-it@2.2.0: + resolution: {integrity: sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==} + dependencies: + uc.micro: 1.0.6 + dev: true + + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.0.0 + dev: true + + /lint-staged@15.2.2: + resolution: {integrity: sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==} + engines: {node: '>=18.12.0'} + hasBin: true + dependencies: + chalk: 5.3.0 + commander: 11.1.0 + debug: 4.3.4 + execa: 8.0.1 + lilconfig: 3.0.0 + listr2: 8.0.1 + micromatch: 4.0.5 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /listr2@8.0.1: + resolution: {integrity: sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==} + engines: {node: '>=18.0.0'} + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.0.0 + rfdc: 1.3.1 + wrap-ansi: 9.0.0 + dev: true + + /load-json-file@6.2.0: + resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} + engines: {node: '>=8'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 5.2.0 + strip-bom: 4.0.0 + type-fest: 0.6.0 + dev: true + + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.6.1 + pkg-types: 1.0.3 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: true + + /lodash.capitalize@4.2.1: + resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} + dev: true + + /lodash.curry@4.1.1: + resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} + dev: true + + /lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + dev: true + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + + /lodash.uniqby@4.7.0: + resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + dev: true + + /log-update@6.0.0: + resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==} + engines: {node: '>=18'} + dependencies: + ansi-escapes: 6.2.0 + cli-cursor: 4.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + dev: true + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: true + + /macos-release@3.2.0: + resolution: {integrity: sha512-fSErXALFNsnowREYZ49XCdOHF8wOPWuFOGQrAhP7x5J/BqQv+B02cNsTykGpDgRVx43EKg++6ANmTaGTtW+hUA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /magic-string@0.16.0: + resolution: {integrity: sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==} + dependencies: + vlq: 0.2.3 + dev: true + + /magic-string@0.30.7: + resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /magicast@0.3.3: + resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + source-map-js: 1.0.2 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: true + + /map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + dependencies: + p-defer: 1.0.0 + dev: true + + /map-obj@2.0.0: + resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} + engines: {node: '>=4'} + dev: true + + /markdown-it@14.0.0: + resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.0.0 + dev: true + + /markdown-it@8.4.2: + resolution: {integrity: sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==} + hasBin: true + dependencies: + argparse: 1.0.10 + entities: 1.1.2 + linkify-it: 2.2.0 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true + + /markdownlint-cli@0.39.0: + resolution: {integrity: sha512-ZuFN7Xpsbn1Nbp0YYkeLOfXOMOfLQBik2lKRy8pVI/llmKQ2uW7x+8k5OMgF6o7XCsTDSYC/OOmeJ+3qplvnJQ==} + engines: {node: '>=18'} + hasBin: true + dependencies: + commander: 11.1.0 + get-stdin: 9.0.0 + glob: 10.3.10 + ignore: 5.3.1 + js-yaml: 4.1.0 + jsonc-parser: 3.2.1 + markdownlint: 0.33.0 + minimatch: 9.0.3 + run-con: 1.3.2 + dev: true + + /markdownlint-micromark@0.1.8: + resolution: {integrity: sha512-1ouYkMRo9/6gou9gObuMDnvZM8jC/ly3QCFQyoSPCS2XV1ZClU0xpKbL1Ar3bWWRT1RnBZkWUEiNKrI2CwiBQA==} + engines: {node: '>=16'} + dev: true + + /markdownlint@0.11.0: + resolution: {integrity: sha512-wE5WdKD6zW2DQaPQ5TFBTXh5j76DnWd/IFffnDQgHmi6Y61DJXBDfLftZ/suJHuv6cwPjM6gKw2GaRLJMOR+Mg==} + engines: {node: '>=6'} + dependencies: + markdown-it: 8.4.2 + dev: true + + /markdownlint@0.33.0: + resolution: {integrity: sha512-4lbtT14A3m0LPX1WS/3d1m7Blg+ZwiLq36WvjQqFGsX3Gik99NV+VXp/PW3n+Q62xyPdbvGOCfjPqjW+/SKMig==} + engines: {node: '>=18'} + dependencies: + markdown-it: 14.0.0 + markdownlint-micromark: 0.1.8 + dev: true + + /mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + dependencies: + '@types/mdast': 3.0.10 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + dev: true + + /mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + dev: true + + /mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + dev: true + + /mem@6.1.1: + resolution: {integrity: sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==} + engines: {node: '>=8'} + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 3.1.0 + dev: true + + /mem@8.1.1: + resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} + engines: {node: '>=10'} + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 3.1.0 + dev: true + + /meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + dependencies: + debug: 4.3.4 + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + + /mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + + /minimist@0.0.10: + resolution: {integrity: sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==} + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + + /mlly@1.6.1: + resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.4.0 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /ndjson@2.0.0: + resolution: {integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + json-stringify-safe: 5.0.1 + minimist: 1.2.8 + readable-stream: 3.6.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + dev: true + + /new-github-release-url@2.0.0: + resolution: {integrity: sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + type-fest: 2.19.0 + dev: true + + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: true + + /node-fetch@3.0.0-beta.9: + resolution: {integrity: sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==} + engines: {node: ^10.17 || >=12.3} + dependencies: + data-uri-to-buffer: 3.0.1 + fetch-blob: 2.1.2 + transitivePeerDependencies: + - domexception + dev: true + + /node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: true + + /normalize-package-data@6.0.0: + resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + hosted-git-info: 7.0.1 + is-core-module: 2.13.1 + semver: 7.6.0 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-url@8.0.0: + resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} + engines: {node: '>=14.16'} + dev: true + + /npm-email@5.1.0: + resolution: {integrity: sha512-1fwpLd2ZuD7yZ8XaAfejB6fbKnDa+PwU3qzlkTy8sgy4C3KPjHpnBxo6fx3E3LjmcSKY34qEUD3ggx7HxxNvDQ==} + engines: {node: '>=18'} + dependencies: + ky: 1.2.2 + dev: false + + /npm-install-checks@6.3.0: + resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + semver: 7.6.0 + dev: true + + /npm-normalize-package-bin@3.0.0: + resolution: {integrity: sha512-g+DPQSkusnk7HYXr75NtzkIP4+N81i3RPsGFidF3DzHd9MT9wWngmqoeg/fnHFz5MNdtG4w03s+QnhewSLTT2Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /npm-package-arg@11.0.1: + resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + hosted-git-info: 7.0.1 + proc-log: 3.0.0 + semver: 7.6.0 + validate-npm-package-name: 5.0.0 + dev: true + + /npm-pick-manifest@9.0.0: + resolution: {integrity: sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + npm-install-checks: 6.3.0 + npm-normalize-package-bin: 3.0.0 + npm-package-arg: 11.0.1 + semver: 7.6.0 + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + + /npm-user@6.1.1: + resolution: {integrity: sha512-gfNQElX1LwT9ftOzlFe8GlsCn2zPL3Ee2+mv2+xAiPhXCiicYywt7mhRoIHLzBUW7FLp2s0PcKBBHtcpJw6sgQ==} + engines: {node: '>=18'} + dependencies: + cheerio: 1.0.0-rc.12 + ky: 1.2.2 + npm-email: 5.1.0 + dev: false + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object-pairs@0.1.0: + resolution: {integrity: sha512-3ECr6K831I4xX/Mduxr9UC+HPOz/d6WKKYj9p4cmC8Lg8p7g8gitzsxNX5IWlSIgFWN/a4JgrJaoAMKn20oKwA==} + dev: true + + /object-values@1.0.0: + resolution: {integrity: sha512-+8hwcz/JnQ9EpLIXzN0Rs7DLsBpJNT/xYehtB/jU93tHYr5BFEO8E+JGQNOSqE7opVzz5cGksKFHt7uUJVLSjQ==} + engines: {node: '>=0.10.0'} + dev: true + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /octokit@3.1.2: + resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==} + engines: {node: '>= 18'} + dependencies: + '@octokit/app': 14.0.2 + '@octokit/core': 5.1.0 + '@octokit/oauth-app': 6.0.0 + '@octokit/plugin-paginate-graphql': 4.0.0(@octokit/core@5.1.0) + '@octokit/plugin-paginate-rest': 9.2.0(@octokit/core@5.1.0) + '@octokit/plugin-rest-endpoint-methods': 10.4.0(@octokit/core@5.1.0) + '@octokit/plugin-retry': 6.0.0(@octokit/core@5.1.0) + '@octokit/plugin-throttling': 8.1.3(@octokit/core@5.1.0) + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.6.0 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + + /open@10.0.3: + resolution: {integrity: sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==} + engines: {node: '>=18'} + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.0 + dev: true + + /optimist@0.6.1: + resolution: {integrity: sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==} + dependencies: + minimist: 0.0.10 + wordwrap: 0.0.3 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: true + + /ora@8.0.1: + resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} + engines: {node: '>=18'} + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.0.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.1.0 + strip-ansi: 7.1.0 + dev: true + + /os-name@5.1.0: + resolution: {integrity: sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + macos-release: 3.2.0 + windows-release: 5.1.1 + dev: true + + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: true + + /p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + dev: true + + /p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-memoize@4.0.1: + resolution: {integrity: sha512-km0sP12uE0dOZ5qP+s7kGVf07QngxyG0gS8sYFvFWhqlgzOsSy+m71aUejf/0akxj5W7gE//2G74qTv6b4iMog==} + engines: {node: '>=10'} + dependencies: + mem: 6.1.1 + mimic-fn: 3.1.0 + dev: true + + /pac-proxy-agent@7.0.1: + resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} + engines: {node: '>= 14'} + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.0 + debug: 4.3.4 + get-uri: 6.0.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + dev: true + + /package-json-validator@0.6.3: + resolution: {integrity: sha512-juKiFboV4UKUvWQ+OSxstnyukhuluyuEoFmgZw1Rx21XzmwlgDWLcbl3qzjA3789IRORYhVFs7cmAO0YFGwHCg==} + hasBin: true + dependencies: + optimist: 0.6.1 + dev: true + + /package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + dependencies: + got: 12.6.1 + registry-auth-token: 5.0.2 + registry-url: 6.0.1 + semver: 7.6.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parent-module@2.0.0: + resolution: {integrity: sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==} + engines: {node: '>=8'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-author@2.0.0: + resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} + engines: {node: '>=0.10.0'} + dependencies: + author-regex: 1.0.0 + dev: false + + /parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parse-json@7.1.0: + resolution: {integrity: sha512-ihtdrgbqdONYD156Ap6qTcaGcGdkdAxodO1wLqQ/j7HP1u2sFYppINiq4jyC8F+Nm+4fVufylCV00QmkTHkSUg==} + engines: {node: '>=16'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 3.0.0 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + dev: true + + /parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + dev: true + + /parse-npm-tarball-url@3.0.0: + resolution: {integrity: sha512-InpdgIdNe5xWMEUcrVQUniQKwnggBtJ7+SCwh7zQAZwbbIYZV9XdgJyhtmDSSvykFyQXoe4BINnzKTfCwWLs5g==} + engines: {node: '>=8.15'} + dependencies: + semver: 6.3.1 + dev: true + + /parse-path@7.0.0: + resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} + dependencies: + protocols: 2.0.1 + + /parse-url@8.1.0: + resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} + dependencies: + parse-path: 7.0.0 + + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: false + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + + /path-temp@2.1.0: + resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==} + engines: {node: '>=8.15'} + dependencies: + unique-string: 2.0.0 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + dev: true + + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /picomatch@4.0.1: + resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} + engines: {node: '>=12'} + dev: true + + /pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.1 + mlly: 1.6.1 + pathe: 1.1.2 + dev: true + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.1 + yaml: 2.4.0 + dev: true + + /postcss@8.4.35: + resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-plugin-curly@0.1.3(prettier@3.1.0): + resolution: {integrity: sha512-NYr2BPex/0fFwDbiZZr91kfgBko1tmaorLOrVAkT5rN91mIYYJRiWabRxWGFqzRSO7J0eoEcxakY9NWvJWAh4w==} + engines: {node: '>=18'} + peerDependencies: + prettier: ^2 || ^3 + dependencies: + '@babel/parser': 7.23.9 + '@babel/traverse': 7.22.5 + prettier: 3.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /prettier-plugin-packagejson@2.4.12(prettier@3.1.0): + resolution: {integrity: sha512-hifuuOgw5rHHTdouw9VrhT8+Nd7UwxtL1qco8dUfd4XUFQL6ia3xyjSxhPQTsGnSYFraTWy5Omb+MZm/OWDTpQ==} + peerDependencies: + prettier: '>= 1.16.0' + peerDependenciesMeta: + prettier: + optional: true + dependencies: + prettier: 3.1.0 + sort-package-json: 2.8.0 + synckit: 0.9.0 + dev: true + + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /pretty-ms@9.0.0: + resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} + engines: {node: '>=18'} + dependencies: + parse-ms: 4.0.0 + dev: true + + /proc-log@3.0.0: + resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: true + + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: true + + /promise.allsettled@1.0.7: + resolution: {integrity: sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==} + engines: {node: '>= 0.4'} + dependencies: + array.prototype.map: 1.0.6 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + get-intrinsic: 1.2.4 + iterate-value: 1.0.2 + dev: true + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: true + + /protocols@2.0.1: + resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} + + /proxy-agent@6.4.0: + resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: true + + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + dependencies: + escape-goat: 4.0.0 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /read-package-json-fast@3.0.2: + resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + json-parse-even-better-errors: 3.0.0 + npm-normalize-package-bin: 3.0.0 + dev: true + + /read-pkg-up@10.1.0: + resolution: {integrity: sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==} + engines: {node: '>=16'} + dependencies: + find-up: 6.3.0 + read-pkg: 8.1.0 + type-fest: 4.10.3 + dev: true + + /read-pkg@8.1.0: + resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} + engines: {node: '>=16'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 6.0.0 + parse-json: 7.1.0 + type-fest: 4.10.3 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.8 + dev: true + + /refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dependencies: + '@eslint-community/regexpp': 4.10.0 + dev: true + + /regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dependencies: + '@eslint-community/regexpp': 4.10.0 + refa: 0.12.1 + dev: true + + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + + /registry-auth-token@5.0.2: + resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} + engines: {node: '>=14'} + dependencies: + '@pnpm/npm-conf': 2.2.2 + dev: true + + /registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + dependencies: + rc: 1.2.8 + dev: true + + /release-it@17.1.1(typescript@5.3.3): + resolution: {integrity: sha512-b+4Tu2eb5f2wIdIe5E9hre0evbMQrXp/kRq0natHsHYJVqu1Bd4/h2a+swFi0faGmC3cJdB16uYR6LscG9SchQ==} + engines: {node: '>=18'} + hasBin: true + dependencies: + '@iarna/toml': 2.2.5 + '@octokit/rest': 20.0.2 + async-retry: 1.3.3 + chalk: 5.3.0 + cosmiconfig: 9.0.0(typescript@5.3.3) + execa: 8.0.1 + git-url-parse: 14.0.0 + globby: 14.0.1 + got: 13.0.0 + inquirer: 9.2.14 + is-ci: 3.0.1 + issue-parser: 6.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + new-github-release-url: 2.0.0 + node-fetch: 3.3.2 + open: 10.0.3 + ora: 8.0.1 + os-name: 5.1.0 + promise.allsettled: 1.0.7 + proxy-agent: 6.4.0 + semver: 7.6.0 + shelljs: 0.8.5 + update-notifier: 7.0.0 + url-join: 5.0.0 + wildcard-match: 5.1.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /rename-overwrite@5.0.0: + resolution: {integrity: sha512-vSxE5Ww7Jnyotvaxi3Dj0vOMoojH8KMkBfs9xYeW/qNfJiLTcC1fmwTjrbGUq3mQSOCxkG0DbdcvwTUrpvBN4w==} + engines: {node: '>=12.10'} + dependencies: + '@zkochan/rimraf': 2.1.3 + fs-extra: 10.1.0 + dev: true + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: true + + /replace-in-file@7.1.0: + resolution: {integrity: sha512-1uZmJ78WtqNYCSuPC9IWbweXkGxPOtk2rKuar8diTw7naVIQZiE3Tm8ACx2PCMXDtVH6N+XxwaRY2qZ2xHPqXw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chalk: 4.1.2 + glob: 8.1.0 + yargs: 17.7.2 + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + dependencies: + lowercase-keys: 3.0.0 + dev: true + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: true + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /reverse-arguments@1.0.0: + resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==} + dev: true + + /rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: false + + /rollup@4.12.0: + resolution: {integrity: sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.12.0 + '@rollup/rollup-android-arm64': 4.12.0 + '@rollup/rollup-darwin-arm64': 4.12.0 + '@rollup/rollup-darwin-x64': 4.12.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.12.0 + '@rollup/rollup-linux-arm64-gnu': 4.12.0 + '@rollup/rollup-linux-arm64-musl': 4.12.0 + '@rollup/rollup-linux-riscv64-gnu': 4.12.0 + '@rollup/rollup-linux-x64-gnu': 4.12.0 + '@rollup/rollup-linux-x64-musl': 4.12.0 + '@rollup/rollup-win32-arm64-msvc': 4.12.0 + '@rollup/rollup-win32-ia32-msvc': 4.12.0 + '@rollup/rollup-win32-x64-msvc': 4.12.0 + fsevents: 2.3.3 + dev: true + + /run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + dev: true + + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + dev: true + + /run-con@1.3.2: + resolution: {integrity: sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 4.1.1 + minimist: 1.2.8 + strip-json-comments: 3.1.1 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + dev: true + + /safe-array-concat@1.1.0: + resolution: {integrity: sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + dependencies: + '@eslint-community/regexpp': 4.10.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + dev: true + + /semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + dependencies: + semver: 7.6.0 + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /sentences-per-line@0.2.1: + resolution: {integrity: sha512-6hlyKBwqoaZJ5+RBTKNNem2kBGAboh9e9KfFw5KYKA+64xaTYWbv5C6XnOudx8xk1Sg6f/4yalhJtCZFSLWIsQ==} + dependencies: + markdownlint: 0.11.0 + dev: true + + /set-function-length@1.2.1: + resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /shell-quote-word@1.0.1: + resolution: {integrity: sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg==} + dev: true + + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: true + + /side-channel@1.0.5: + resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: true + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + dev: true + + /slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + dev: true + + /slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + dev: true + + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: true + + /smol-toml@1.1.4: + resolution: {integrity: sha512-Y0OT8HezWsTNeEOSVxDnKOW/AyNXHQ4BwJNbAXlLTF5wWsBvrcHhIkE5Rf8kQMLmgf7nDX3PVOlgC6/Aiggu3Q==} + engines: {node: '>= 18', pnpm: '>= 8'} + dev: true + + /socks-proxy-agent@8.0.2: + resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + socks: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: true + + /socks@2.8.1: + resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: true + + /sort-object-keys@1.1.3: + resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} + dev: true + + /sort-package-json@1.57.0: + resolution: {integrity: sha512-FYsjYn2dHTRb41wqnv+uEqCUvBpK3jZcTp9rbz2qDTmel7Pmdtf+i2rLaaPMRZeSVM60V3Se31GyWFpmKs4Q5Q==} + hasBin: true + dependencies: + detect-indent: 6.1.0 + detect-newline: 3.1.0 + git-hooks-list: 1.0.3 + globby: 10.0.0 + is-plain-obj: 2.1.0 + sort-object-keys: 1.1.3 + dev: true + + /sort-package-json@2.8.0: + resolution: {integrity: sha512-PxeNg93bTJWmDGnu0HADDucoxfFiKkIr73Kv85EBThlI1YQPdc0XovBgg2llD0iABZbu2SlKo8ntGmOP9wOj/g==} + hasBin: true + dependencies: + detect-indent: 7.0.1 + detect-newline: 4.0.1 + get-stdin: 9.0.0 + git-hooks-list: 3.1.0 + globby: 13.2.2 + is-plain-obj: 4.1.0 + sort-object-keys: 1.1.3 + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + + /spdx-correct@3.1.1: + resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.17 + dev: true + + /spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.17 + dev: true + + /spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.17 + dev: true + + /spdx-license-ids@3.0.17: + resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} + dev: true + + /split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + dependencies: + readable-stream: 3.6.2 + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + dev: true + + /ssri@10.0.5: + resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + minipass: 7.0.4 + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + + /stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + dev: true + + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.7 + dev: true + + /string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + /string-width@7.1.0: + resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} + engines: {node: '>=18'} + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + dev: true + + /string.fromcodepoint@0.2.1: + resolution: {integrity: sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==} + dev: true + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strip-json-comments@5.0.1: + resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} + engines: {node: '>=14.16'} + dev: true + + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + dependencies: + js-tokens: 8.0.3 + dev: true + + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.4 + commander: 4.1.1 + glob: 10.3.10 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /summary@2.1.0: + resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /synckit@0.6.2: + resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} + engines: {node: '>=12.20'} + dependencies: + tslib: 2.6.2 + dev: true + + /synckit@0.9.0: + resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.6.2 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.2 + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + + /title-case@4.3.1: + resolution: {integrity: sha512-VnPxQ+/j0X2FZ4ceGq1oLruTLjtN5Ul4sam5ypd4mDZLm1eHwkwip1gLxqhON/j4qyTlUlfPKslE/t4NPSlxhg==} + dev: false + + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: true + + /to-pascal-case@1.0.0: + resolution: {integrity: sha512-QGMWHqM6xPrcQW57S23c5/3BbYb0Tbe9p+ur98ckRnGDwD4wbbtDiYI38CfmMKNB5Iv0REjs5SNDntTwvDxzZA==} + dependencies: + to-space-case: 1.0.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: true + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.1 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-api-utils@1.2.1(typescript@5.3.3): + resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.3.3 + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + + /tsup@8.0.2(typescript@5.3.3): + resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.2(esbuild@0.19.12) + cac: 6.7.14 + chokidar: 3.6.0 + debug: 4.3.4 + esbuild: 0.19.12 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2 + resolve-from: 5.0.0 + rollup: 4.12.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + + /tsutils@3.21.0(typescript@5.3.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.3.3 + dev: true + + /tsx@4.7.1: + resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + dev: true + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: true + + /type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + dev: true + + /type-fest@4.10.3: + resolution: {integrity: sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==} + engines: {node: '>=16'} + dev: true + + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-length@1.0.5: + resolution: {integrity: sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + dependencies: + is-typedarray: 1.0.0 + dev: true + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + dev: true + + /uc.micro@2.0.0: + resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==} + dev: true + + /ufo@1.4.0: + resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unescape-js@1.1.4: + resolution: {integrity: sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g==} + dependencies: + string.fromcodepoint: 0.2.1 + dev: true + + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: true + + /unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: true + + /unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + dependencies: + crypto-random-string: 4.0.0 + dev: true + + /unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /universal-github-app-jwt@1.1.1: + resolution: {integrity: sha512-G33RTLrIBMFmlDV4u4CBF7dh71eWwykck4XgaxaIVeZKOYZRAAxvcGMRFTUclVY6xoUPQvO4Ne5wKGxYm/Yy9w==} + dependencies: + '@types/jsonwebtoken': 9.0.1 + jsonwebtoken: 9.0.0 + dev: false + + /universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /update-notifier@7.0.0: + resolution: {integrity: sha512-Hv25Bh+eAbOLlsjJreVPOs4vd51rrtCrmhyOJtbpAojro34jS4KQaEp4/EvlHJX7jSO42VvEFpkastVyXyIsdQ==} + engines: {node: '>=18'} + dependencies: + boxen: 7.1.1 + chalk: 5.3.0 + configstore: 6.0.0 + import-lazy: 4.0.0 + is-in-ci: 0.1.0 + is-installed-globally: 0.4.0 + is-npm: 6.0.0 + latest-version: 7.0.0 + pupa: 3.1.0 + semver: 7.6.0 + semver-diff: 4.0.0 + xdg-basedir: 5.1.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /url-join@5.0.0: + resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.23 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 2.0.0 + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.1.1 + spdx-expression-parse: 3.0.1 + dev: true + + /validate-npm-package-name@4.0.0: + resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + builtins: 5.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: true + + /version-selector-type@3.0.0: + resolution: {integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==} + engines: {node: '>=10.13'} + dependencies: + semver: 7.6.0 + dev: true + + /vite-node@1.3.1(@types/node@20.11.21): + resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.1.4(@types/node@20.11.21) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite@5.1.4(@types/node@20.11.21): + resolution: {integrity: sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.21 + esbuild: 0.19.12 + postcss: 8.4.35 + rollup: 4.12.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.3.1(@types/node@20.11.21): + resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.3.1 + '@vitest/ui': 1.3.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.11.21 + '@vitest/expect': 1.3.1 + '@vitest/runner': 1.3.1 + '@vitest/snapshot': 1.3.1 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.7 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.1.4(@types/node@20.11.21) + vite-node: 1.3.1(@types/node@20.11.21) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vlq@0.2.3: + resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} + dev: true + + /vscode-languageserver-textdocument@1.0.11: + resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} + dev: true + + /vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + dev: true + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.4 + dev: true + + /web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + dev: true + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.14: + resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + isexe: 3.1.1 + dev: true + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + dev: true + + /wildcard-match@5.1.2: + resolution: {integrity: sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==} + dev: true + + /windows-release@5.1.1: + resolution: {integrity: sha512-NMD00arvqcq2nwqc5Q6KtrSRHK+fVD31erE5FEMahAw5PmVCgD7MUXodq3pdZSUkqA9Cda2iWx6s1XYwiJWRmw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + execa: 5.1.1 + dev: true + + /wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + dev: true + + /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==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + /wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + dependencies: + ansi-styles: 6.2.1 + string-width: 7.1.0 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + dev: true + + /xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml-eslint-parser@1.2.2: + resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} + engines: {node: ^14.17.0 || >=16.0.0} + dependencies: + eslint-visitor-keys: 3.4.3 + lodash: 4.17.21 + yaml: 2.4.0 + dev: true + + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + + /yaml@2.4.0: + resolution: {integrity: sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==} + engines: {node: '>= 14'} + hasBin: true + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + + /zod-validation-error@3.0.2(zod@3.22.4): + resolution: {integrity: sha512-21xGaDmnU7lJZ4J63n5GXWqi+rTzGy3gDHbuZ1jP6xrK/DEQGyOqs/xW7eH96tIfCOYm+ecCuT0bfajBRKEVUw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + dependencies: + zod: 3.22.4 + dev: true + + /zod-validation-error@3.0.3(zod@3.22.4): + resolution: {integrity: sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + dependencies: + zod: 3.22.4 + dev: false + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} diff --git a/script/__snapshots__/migrate-test-e2e.js.snap b/script/__snapshots__/migrate-test-e2e.js.snap new file mode 100644 index 0000000..69f2730 --- /dev/null +++ b/script/__snapshots__/migrate-test-e2e.js.snap @@ -0,0 +1,159 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`expected file changes > .eslintrc.cjs 1`] = ` +"--- a/.eslintrc.cjs ++++ b/.eslintrc.cjs +@@ ... @@ +-/* +-👋 Hi! This ESLint configuration contains a lot more stuff than many repos'! +-You can read from it to see all sorts of linting goodness, but don't worry - +-it's not something you need to exhaustively understand immediately. 💙 +- +-If you're interested in learning more, see the 'getting started' docs on: +-- ESLint: https://eslint.org +-- typescript-eslint: https://typescript-eslint.io +-*/ +- + /** @type {import("@types/eslint").Linter.Config} */ + module.exports = { + env: { +@@ ... @@ module.exports = { + "plugin:regexp/recommended", + "plugin:vitest/recommended", + ], +- ignorePatterns: ["!.*", "coverage*", "lib", "node_modules", "pnpm-lock.yaml"], ++ ignorePatterns: ["!.*", "coverage", "lib", "node_modules", "pnpm-lock.yaml"], + overrides: [ + { + extends: ["plugin:markdown/recommended"], +@@ ... @@ module.exports = { + rules: { + // These off-by-default rules work well for this repo and we like them on. + "deprecation/deprecation": "error", +- +- // These more-strict-by-default rules don't work well for this repo and we like them less strict. +- "@typescript-eslint/no-unnecessary-condition": [ +- "error", +- { +- allowConstantLoopConditions: true, +- }, +- ], +- "@typescript-eslint/prefer-nullish-coalescing": [ +- "error", +- { ignorePrimitives: true }, +- ], + }, + }, + {" +`; + +exports[`expected file changes > .github/workflows/test.yml 1`] = ` +"--- a/.github/workflows/test.yml ++++ b/.github/workflows/test.yml +@@ ... @@ jobs: + - run: pnpm run test --coverage + - name: Codecov + uses: codecov/codecov-action@v3 +- with: +- flags: unit + + name: Test + " +`; + +exports[`expected file changes > .gitignore 1`] = ` +"--- a/.gitignore ++++ b/.gitignore +@@ ... @@ +-coverage*/ ++coverage/ + lib/ + node_modules/" +`; + +exports[`expected file changes > .prettierignore 1`] = ` +"--- a/.prettierignore ++++ b/.prettierignore +@@ ... @@ + .all-contributorsrc +-coverage*/ ++coverage/ + lib/ + pnpm-lock.yaml" +`; + +exports[`expected file changes > README.md 1`] = ` +"--- a/README.md ++++ b/README.md +@@ ... @@ + + Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS' + +-\`create-typescript-app\` is a one-stop-shop solution to set up a new or existing repository with the latest and greatest TypeScript tooling. +-It includes options not just for building and testing but also GitHub repository templates, contributor recognition, automated release management, and more. +- + ## Getting Started + + First make sure you have the following installed: +@@ ... @@ Thanks! 💖 + + + ++ ++ ++ ++> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app)." +`; + +exports[`expected file changes > cspell.json 1`] = ` +"--- a/cspell.json ++++ b/cspell.json +@@ ... @@ + { + "dictionaries": ["typescript"], + "ignorePaths": [ +- "./coverage*", +- "./script/__snapshots__", + ".github", + "CHANGELOG.md", ++ "coverage", + "lib", + "node_modules", + "pnpm-lock.yaml" +@@ ... @@ + "contributorsrc", + "execa", + "infile", ++ "joshuakgoldberg", + "knip", ++ "markdownlint", + "markdownlintignore", + "mtfoley", + "npmignore", +@@ ... @@ + "precommit", + "quickstart", + "tada", ++ "templating", + "tsup", +- "vitest" ++ "vitest", ++ "wontfix" + ] + }" +`; + +exports[`expected file changes > knip.json 1`] = ` +"--- a/knip.json ++++ b/knip.json +@@ ... @@ + { + "$schema": "https://unpkg.com/knip@latest/schema.json", +- "entry": ["src/index.ts!", "script/*e2e.js"], +- "ignoreBinaries": ["gh"], ++ "entry": ["src/index.ts!"], + "ignoreExportsUsedInFile": { "interface": true, "type": true }, +- "project": ["src/**/*.ts!", "script/**/*.js"] ++ "project": ["src/**/*.ts!"] + }" +`; diff --git a/script/create-test-e2e.js b/script/create-test-e2e.js new file mode 100644 index 0000000..2419e8b --- /dev/null +++ b/script/create-test-e2e.js @@ -0,0 +1,44 @@ +import { $, execaCommand } from "execa"; +import { strict as assert } from "node:assert"; +import { rimraf } from "rimraf"; + +const author = "Test Author"; +const description = "Test description."; +const email = "test@email.com"; +const repository = "testing-repository"; +const owner = "TestOwner"; +const title = "Test Title"; + +await rimraf(["coverage*", repository]); + +// Fist we run with --mode create to create a new new local repository, +// asserting that pnpm i passes in that repository's directory. +await $({ + stdio: "inherit", +})`c8 -o ./coverage-create -r html -r lcov --src src node ./bin/index.js --base everything --mode create --author ${author} --email ${email} --description ${description} --owner ${owner} --title ${title} --repository ${repository} --skip-all-contributors-api --skip-github-api`; + +process.chdir(repository); + +const failures = []; + +// Then we run each of the CI commands to assert that they pass too. +for (const command of [ + `pnpm i`, + `pnpm run build`, + `pnpm run format --list-different`, + `pnpm run lint`, + `pnpm run lint:md`, + `pnpm run lint:packages`, + `pnpm run lint:spelling`, + `pnpm run lint:knip`, + `pnpm run test run`, + `pnpm run tsc`, +]) { + const result = await execaCommand(command, { stdio: "inherit" }); + + if (result.exitCode) { + failures.push({ command, result }); + } +} + +assert.deepEqual(failures, []); diff --git a/script/initialize-test-e2e.js b/script/initialize-test-e2e.js new file mode 100644 index 0000000..8d0354b --- /dev/null +++ b/script/initialize-test-e2e.js @@ -0,0 +1,59 @@ +import { $ } from "execa"; +import { globby } from "globby"; +import { strict as assert } from "node:assert"; +import * as fs from "node:fs/promises"; +import { rimraf } from "rimraf"; + +const description = "New Description Test"; +const owner = "RNR1"; +const title = "New Title Test"; +const repository = "new-repository-test"; + +await rimraf("coverage*"); + +// Fist we run with --mode initialize to modify the local repository files, +// asserting that the created package.json keeps the general description. +await $({ + stdio: "inherit", +})`pnpm run initialize --base everything --mode initialize --description ${description} --owner ${owner} --title ${title} --repository ${repository} --skip-all-contributors-api --skip-github-api --skip-restore`; + +const newPackageJson = JSON.parse( + (await fs.readFile("./package.json")).toString(), +); +console.log("New package JSON:", newPackageJson); + +assert.equal(newPackageJson.description, description); +assert.equal(newPackageJson.name, repository); + +// Assert that the initialize script used the provided values in files, +// except for the 'This package was templated with ...' attribution notice. +const files = await globby(["*.*", "**/*.*"], { + gitignore: true, + ignoreFiles: ["script/initialize-test-e2e.js"], +}); + +for (const search of [`/JoshuaKGoldberg/`, "create-typescript-app"]) { + const { stdout } = await $`grep -i ${search} ${files}`; + assert.equal( + stdout, + `README.md:> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).`, + ); +} + +// Use Knip to assert that none of the template-only dependencies remain. +// They should have been removed as part of initialization. +try { + await $`pnpm run lint:knip`; +} catch (error) { + throw new Error("Error running lint:knip:", { cause: error }); +} + +// Now that initialize has passed normal steps, we reset everything, +// then run again without removing files - so we can capture test coverage +await $`git add -A`; +await $`git reset --hard HEAD`; +await $`pnpm i`; +await $`pnpm run build`; +await $({ + stdio: "inherit", +})`c8 -o ./coverage -r html -r lcov --src src node ./bin/index.js --base everything --mode initialize --description ${description} --owner ${owner} --title ${title} --repository ${repository} --skip-all-contributors-api --skip-github-api --skip-removal --skip-restore`; diff --git a/script/migrate-test-e2e.js b/script/migrate-test-e2e.js new file mode 100644 index 0000000..595b7ab --- /dev/null +++ b/script/migrate-test-e2e.js @@ -0,0 +1,160 @@ +import chalk from "chalk"; +import { $, execaCommand } from "execa"; +import * as fs from "node:fs/promises"; +import { rimraf } from "rimraf"; +import { assert, describe, expect, test } from "vitest"; + +import packageData from "../package.json" assert { type: "json" }; + +const filesExpectedToBeChanged = [ + "README.md", + "knip.json", + ".eslintrc.cjs", + ".github/workflows/test.yml", + ".gitignore", + ".prettierignore", + "cspell.json", +]; + +const filesThatMightBeChanged = new Set([ + ...filesExpectedToBeChanged, + "script/__snapshots__/migrate-test-e2e.js.snap", +]); + +const { + author: { email: emailNpm, name: authorName }, + description, + name: repository, +} = packageData; +const emailGithub = "github@joshuakgoldberg.com"; +const bin = "./bin/index.js"; +const guide = + "https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository"; +const guideTitle = "Contributing to a create-typescript-app Repository"; +const logo = "./docs/create-typescript-app.png"; +const logoAlt = `Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'`; +const owner = "JoshuaKGoldberg"; +const title = "Create TypeScript App"; + +await rimraf("coverage*"); + +const originalReadme = (await fs.readFile("README.md")).toString(); + +const originalSnapshots = ( + await fs.readFile("script/__snapshots__/migrate-test-e2e.js.snap") +).toString(); + +await $({ + stdio: "inherit", +})`c8 -o ./coverage -r html -r lcov --src src node ${bin} --base everything --author ${authorName} --mode migrate --bin ${bin} --description ${description} --email-github ${emailGithub} --email-npm ${emailNpm} --guide ${guide} --guide-title ${guideTitle} --logo ${logo} --logo-alt ${logoAlt} --owner ${owner} --title ${title} --repository ${repository} --skip-all-contributors-api --skip-github-api --skip-install`; + +// All Contributors seems to not be using Prettier to format files... +await fs.writeFile( + ".all-contributorsrc", + JSON.stringify( + JSON.parse((await fs.readFile(".all-contributorsrc")).toString()), + null, + 2, + ) + "\n", +); + +// Ignore changes to the README.md all-contributor count and contributors table... +const updatedReadme = (await fs.readFile("README.md")).toString(); +await fs.writeFile( + "README.md", + [ + updatedReadme.slice(0, updatedReadme.indexOf("## Contributors")) + + originalReadme.slice( + originalReadme.indexOf("## Contributors"), + originalReadme.indexOf(""), + ), + updatedReadme.slice(updatedReadme.indexOf("")), + ] + .join("") + .replaceAll( + /All Contributors: \d+/g, + originalReadme.match(/All Contributors: \d+/)[0], + ) + .replaceAll( + /all_contributors-\d+/g, + originalReadme.match(/all_contributors-\d+/)[0], + ), +); + +// ...and even to the snapshot file, so diffs don't mind it. +await fs.writeFile( + "script/__snapshots__/migrate-test-e2e.js.snap", + originalSnapshots + .replaceAll( + /All Contributors: \d+/g, + originalReadme.match(/All Contributors: \d+/)[0], + ) + .replaceAll( + /all_contributors-\d+/g, + originalReadme.match(/all_contributors-\d+/)[0], + ), +); + +describe("expected file changes", () => { + test.each(filesExpectedToBeChanged)("%s", async (file) => { + const { stdout } = await execaCommand(`git diff HEAD -- ${file}`); + const contentsAfterGitMarkers = stdout + .split("\n") + .slice(2) + .join("\n") + .replaceAll(/@@ -\d+,\d+ \+\d+,\d+ @@/g, "@@ ... @@"); + + assert( + stdout, + `Looks like there were no changes to ${file} from migration?`, + ); + + // If this fails, see .github/DEVELOPMENT.md > Setup Scripts for context. + // Then see .github/DEVELOPMENT.md > Migration Snapshot Failures. + expect(contentsAfterGitMarkers).toMatchSnapshot(); + }); +}); + +// eslint-disable-next-line vitest/expect-expect +test("unexpected file changes", async () => { + const { stdout: gitStatus } = await $`git status`; + console.log(`Stdout from running \`git status\`:\n${gitStatus}`); + + const indexOfUnstagedFilesMessage = gitStatus.indexOf( + "Changes not staged for commit:", + ); + assert( + indexOfUnstagedFilesMessage !== -1, + `Looks like migrate didn't cause any file changes? That's ...probably incorrect? 😬`, + ); + + const unstagedModifiedFiles = gitStatus + .slice(indexOfUnstagedFilesMessage) + .match(/modified: {3}(\S+)\n/g) + .map((match) => match.split(/\s+/g)[1]) + .filter((filePath) => !filesThatMightBeChanged.has(filePath)); + + console.log("Unexpected modified files are:", unstagedModifiedFiles); + + if (unstagedModifiedFiles.length) { + const gitDiffCommand = `git diff HEAD -- ${unstagedModifiedFiles.join( + " ", + )}`; + const { stdout } = await execaCommand(gitDiffCommand); + + console.log(`Stdout from running \`${gitDiffCommand}\`:\n${stdout}`); + + throw new Error( + [ + "", + "Oh no! Running the migrate script unexpectedly modified:", + ...unstagedModifiedFiles.map((filePath) => ` - ${filePath}`), + "", + "See .github/DEVELOPMENT.md > Setup Scripts for context.", + "Then see .github/DEVELOPMENT.md > Unexpected File Modifications.", + ] + .map((line) => chalk.red(line)) + .join("\n"), + ); + } +}); diff --git a/script/vitest.config.ts b/script/vitest.config.ts new file mode 100644 index 0000000..2e458ef --- /dev/null +++ b/script/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ test: { include: ["./migrate-test-e2e.js"] } }); diff --git a/src/bin/help.test.ts b/src/bin/help.test.ts new file mode 100644 index 0000000..507cfa0 --- /dev/null +++ b/src/bin/help.test.ts @@ -0,0 +1,344 @@ +import chalk from "chalk"; +import { MockInstance, beforeEach, describe, expect, it, vi } from "vitest"; + +import { logHelpText } from "./help.js"; + +function makeProxy(receiver: T): T { + return new Proxy(receiver, { + get: () => makeProxy((input: string) => input), + }); +} + +vi.mock("chalk", () => ({ + default: makeProxy({}), +})); + +let mockConsoleLog: MockInstance; + +describe("logHelpText", () => { + beforeEach(() => { + mockConsoleLog = vi + .spyOn(console, "log") + .mockImplementation(() => undefined); + }); + + it("logs help text when called", () => { + logHelpText([ + chalk.yellow( + "⚠️ This template is early stage, opinionated, and not endorsed by the TypeScript team. ⚠️", + ), + chalk.yellow( + "⚠️ If any tooling it sets displeases you, you can always remove that portion manually. ⚠️", + ), + ]); + + expect(mockConsoleLog.mock.calls).toMatchInlineSnapshot(` + [ + [ + "⚠️ This template is early stage, opinionated, and not endorsed by the TypeScript team. ⚠️", + ], + [ + " ", + ], + [ + "⚠️ If any tooling it sets displeases you, you can always remove that portion manually. ⚠️", + ], + [ + " ", + ], + [ + " + A quickstart-friendly TypeScript template with comprehensive formatting, + linting, releases, testing, and other great tooling built-in. + ", + ], + [ + " ", + ], + [ + "Core options:", + ], + [ + " + --base (string): Whether to scaffold the repository with: + • everything: that comes with the template (recommended) + • minimum: amounts of tooling, essentially opting out of everything + • prompt: for which portions to exclude", + ], + [ + " + --create-repository: Whether to create a corresponding repository on github.com + (if it doesn't yet exist)", + ], + [ + " + --description (string): Sentence case description of the repository + (e.g. A quickstart-friendly TypeScript package with lots of great + repository tooling. ✨)", + ], + [ + " + --mode (string): Whether to: + • create: a new repository in a child directory + • initialize: a freshly repository in the current directory + • migrate: an existing repository in the current directory", + ], + [ + " + --owner (string): GitHub organization or user the repository is underneath + (e.g. JoshuaKGoldberg)", + ], + [ + " + --repository (string): The kebab-case name of the repository + (e.g. create-typescript-app)", + ], + [ + " + --title (string): Title Case title for the repository to be used in + documentation (e.g. Create TypeScript App)", + ], + [], + [ + " ", + ], + [ + "Optional options:", + ], + [ + " + --access (string): ("public" | "restricted"): Which npm publish --access to + release npm packages with (by default, "public")", + ], + [ + " + --author (string): Username on npm to publish packages under (by + default, an existing npm author, or the currently logged in npm user, or + owner.toLowerCase())", + ], + [ + " + --auto: Whether to infer all options from files on disk.", + ], + [ + " + --bin (string): package.json bin value to include for npx-style running.", + ], + [ + " + --directory (string): Directory to create the repository in (by default, the same + name as the repository)", + ], + [ + " + --email (string): Email address to be listed as the point of contact in docs + and packages (e.g. example@joshuakgoldberg.com)", + ], + [ + " + --email-github (string): Optionally, may be provided to use different emails in .md + files", + ], + [ + " + --email-npm (string): Optionally, may be provided to use different emails in + package.json", + ], + [ + " + --funding (string): GitHub organization or username to mention in funding.yml + (by default, owner)", + ], + [ + " + --guide (string): Link to a contribution guide to place at the top of the + development docs", + ], + [ + " + --guide-title (string): If --guide is provided or detected from an existing + DEVELOPMENT.md, the text title to place in the guide link", + ], + [ + " + --keywords (string): Any number of keywords to include in package.json (by default, + none). This can be specified any number of times, like + --keywords apple --keywords "banana cherry"", + ], + [ + " + --logo (string): Local image file in the repository to display near the top of + the README.md as a logo", + ], + [ + " + --logo-alt (string): If --logo is provided or detected from an existing README.md, + alt text that describes the image will be prompted for if not provided", + ], + [ + " + --preserve-generated-form: Whether to keep the GitHub repository generated from + notice (by default, false)", + ], + [], + [ + " ", + ], + [ + "Opt-outs:", + ], + [ + " + ⚠️ Warning: Specifying any --exclude-* flag on the command-line will + cause the setup script to skip prompting for more excludes. ⚠️", + ], + [ + " + --exclude-all-contributors: Don't add all-contributors to track contributions + and display them in a README.md table.", + ], + [ + " + --exclude-compliance: Don't add a GitHub Actions workflow to verify that PRs match + an expected format.", + ], + [ + " + --exclude-lint-deprecation: Don't use eslint-plugin-deprecation to report on usage + of code marked as @deprecated.", + ], + [ + " + --exclude-lint-jsdoc: Don't use eslint-plugin-jsdoc to enforce good practices around + JSDoc comments.", + ], + [ + " + --exclude-lint-json: Don't apply linting and sorting to *.json, and + *.jsonc files.", + ], + [ + " + --exclude-lint-knip: Don't add Knip to detect unused files, dependencies, and code + exports.", + ], + [ + " + --exclude-lint-md: Don't apply linting to *.md files.", + ], + [ + " + --exclude-lint-package-json: Don't add eslint-plugin-package-json to lint for + package.json correctness.", + ], + [ + " + --exclude-lint-packages: Don't add a pnpm dedupe workflow to ensure packages + aren't duplicated unnecessarily.", + ], + [ + " + --exclude-lint-perfectionist: Don't apply eslint-plugin-perfectionist to ensure + imports, keys, and so on are in sorted order.", + ], + [ + " + --exclude-lint-regex: Don't add eslint-plugin-regex to enforce good practices around + regular expressions.", + ], + [ + " + --exclude-lint-spelling: Don't add cspell to spell check against dictionaries + of known words.", + ], + [ + " + --exclude-lint-strict: Don't augment the recommended logical lint rules with + typescript-eslint's strict config.", + ], + [ + " + --exclude-lint-stylistic: Don't add stylistic rules such as typescript-eslint's + stylistic config.", + ], + [ + " + --exclude-lint-yml: Don't apply linting and sorting to *.yaml and *.yml files.", + ], + [ + " + --exclude-releases: Don't add release-it to generate changelogs, package bumps, + and publishes based on conventional commits.", + ], + [ + " + --exclude-renovate: Don't add a Renovate config to dependencies up-to-date with + PRs.", + ], + [ + " + --exclude-tests: Don't add Vitest tooling for fast unit tests, configured + with coverage tracking.", + ], + [ + " + You can prevent the migration script from making some network-based + changes using any or all of the following CLI flags:", + ], + [ + " + --exclude-contributors: Skips network calls that fetch all-contributors + data from GitHub", + ], + [ + " + --skip-all-contributors-api: Skips network calls that fetch all-contributors data from + GitHub. This flag does nothing if --exclude-all-contributors was specified.", + ], + [ + " + --skip-github-api: Skips calling to GitHub APIs.", + ], + [ + " + --skip-install: Skips installing all the new template packages with pnpm.", + ], + [ + " + You can prevent the migration script from making some changes on disk + using any or all of the following CLI flags:", + ], + [ + " + --skip-removal: Skips removing setup docs and scripts, including this docs/ + directory", + ], + [ + " + --skip-restore: Skips the prompt offering to restore the repository if an + error occurs during setup", + ], + [ + " + --skip-uninstall: Skips uninstalling packages only used for setup scripts", + ], + [], + [ + " ", + ], + [ + "Offline Mode:", + ], + [ + " + --offline: You can run create-typescript-app in an "offline" mode. + Doing so will: + • Enable --exclude-all-contributors-api and --skip-github-api + • Skip network calls when setting up contributors + • Run pnpm commands with pnpm's --offline mode", + ], + [], + ] + `); + }); +}); diff --git a/src/bin/help.ts b/src/bin/help.ts new file mode 100644 index 0000000..f9d0882 --- /dev/null +++ b/src/bin/help.ts @@ -0,0 +1,148 @@ +import chalk from "chalk"; + +import { allArgOptions } from "../shared/options/args.js"; + +interface HelpTextSection { + sectionHeading: string; + subsections: { + flags: SubsectionFlag[]; + subheading?: string; + warning?: string; + }[]; +} + +interface SubsectionFlag { + description: string; + flag: string; + type: string; +} + +function logHelpTextSection(section: HelpTextSection): void { + console.log(" "); + + console.log(`${chalk.black.bgGreenBright(section.sectionHeading)}`); + + for (const subsection of section.subsections) { + if (subsection.warning) { + console.log(chalk.yellow(subsection.warning)); + } + + if (subsection.subheading) { + console.log(chalk.green(subsection.subheading)); + } + + for (const { description, flag, type } of subsection.flags) { + console.log( + chalk.cyan( + ` + --${flag}${ + type !== "boolean" ? ` (${chalk.cyanBright(type)})` : "" + }: ${description}`, + ), + ); + } + } +} + +function createHelpTextSections(): HelpTextSection[] { + const core: HelpTextSection = { + sectionHeading: "Core options:", + subsections: [ + { + flags: [], + }, + ], + }; + + const optional: HelpTextSection = { + sectionHeading: "Optional options:", + subsections: [ + { + flags: [], + }, + ], + }; + + const optOut: HelpTextSection = { + sectionHeading: "Opt-outs:", + subsections: [ + { + flags: [], + warning: ` + ⚠️ Warning: Specifying any --exclude-* flag on the command-line will + cause the setup script to skip prompting for more excludes. ⚠️`, + }, + { + flags: [ + { + description: `Skips network calls that fetch all-contributors + data from GitHub`, + flag: "exclude-contributors", + type: "boolean", + }, + ], + subheading: ` +You can prevent the migration script from making some network-based +changes using any or all of the following CLI flags:`, + }, + { + flags: [], + subheading: ` +You can prevent the migration script from making some changes on disk +using any or all of the following CLI flags:`, + }, + ], + }; + + const offline: HelpTextSection = { + sectionHeading: "Offline Mode:", + subsections: [ + { + flags: [], + }, + ], + }; + + const subsections = { + core: core.subsections[0], + offline: offline.subsections[0], + "opt-out": optOut.subsections[0], + optional: optional.subsections[0], + "skip-disk": optOut.subsections[2], + "skip-net": optOut.subsections[1], + }; + + for (const [option, data] of Object.entries(allArgOptions)) { + subsections[data.docsSection].flags.push({ + description: data.description, + flag: option, + type: data.type, + }); + } + + return [core, optional, optOut, offline]; +} + +export function logHelpText(introLogs: string[]): void { + const helpTextSections = createHelpTextSections(); + + for (const log of introLogs) { + console.log(log); + console.log(" "); + } + + console.log( + chalk.cyan( + ` +A quickstart-friendly TypeScript template with comprehensive formatting, +linting, releases, testing, and other great tooling built-in. + `, + ), + ); + + for (const section of helpTextSections) { + logHelpTextSection(section); + + console.log(); + } +} diff --git a/src/bin/index.test.ts b/src/bin/index.test.ts new file mode 100644 index 0000000..8ed7fa0 --- /dev/null +++ b/src/bin/index.test.ts @@ -0,0 +1,205 @@ +import chalk from "chalk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import z from "zod"; + +import { bin } from "./index.js"; +import { getVersionFromPackageJson } from "./packageJson.js"; + +const mockCancel = vi.fn(); +const mockOutro = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + get cancel() { + return mockCancel; + }, + intro: vi.fn(), + log: { + info: vi.fn(), + }, + get outro() { + return mockOutro; + }, +})); + +const mockLogLine = vi.fn(); + +vi.mock("../shared/cli/lines.js", () => ({ + get logLine() { + return mockLogLine; + }, +})); + +const mockCreate = vi.fn(); + +vi.mock("../create/index.js", () => ({ + get create() { + return mockCreate; + }, +})); + +const mockInitialize = vi.fn(); + +vi.mock("../initialize/index.js", () => ({ + get initialize() { + return mockInitialize; + }, +})); + +const mockMigrate = vi.fn(); + +vi.mock("../migrate/index.js", () => ({ + get migrate() { + return mockMigrate; + }, +})); + +const mockPromptForMode = vi.fn(); + +vi.mock("./promptForMode.js", () => ({ + get promptForMode() { + return mockPromptForMode; + }, +})); + +describe("bin", () => { + beforeEach(() => { + vi.spyOn(console, "clear").mockImplementation(() => undefined); + vi.spyOn(console, "log").mockImplementation(() => undefined); + }); + + it("returns 1 when promptForMode returns an undefined mode", async () => { + mockPromptForMode.mockResolvedValue({}); + + const result = await bin([]); + + expect(mockOutro).toHaveBeenCalledWith( + chalk.red("Operation cancelled. Exiting - maybe another time? 👋"), + ); + expect(result).toBe(1); + }); + + it("returns 1 when promptForMode returns an error mode", async () => { + const error = new Error("Oh no!"); + mockPromptForMode.mockResolvedValue({ mode: error }); + + const result = await bin([]); + + expect(mockOutro).toHaveBeenCalledWith(chalk.red(error.message)); + expect(result).toBe(1); + }); + + it("returns the success result of the corresponding runner without cancel logging when promptForMode returns a mode that succeeds", async () => { + const mode = "create"; + const args = ["--owner", "abc123"]; + const code = 0; + const promptedOptions = { directory: "." }; + + mockPromptForMode.mockResolvedValue({ mode, options: promptedOptions }); + mockCreate.mockResolvedValue({ code, options: {} }); + + const result = await bin(args); + + expect(mockCreate).toHaveBeenCalledWith(args, promptedOptions); + expect(mockCancel).not.toHaveBeenCalled(); + expect(mockInitialize).not.toHaveBeenCalled(); + expect(mockMigrate).not.toHaveBeenCalled(); + expect(result).toEqual(code); + }); + + it("returns the cancel result of the corresponding runner and cancel logs when promptForMode returns a mode that cancels", async () => { + const mode = "create"; + const args = ["--owner", "abc123"]; + const code = 2; + const promptedOptions = { directory: "." }; + + mockPromptForMode.mockResolvedValue({ mode, options: promptedOptions }); + mockCreate.mockResolvedValue({ code, options: {} }); + + const result = await bin(args); + + expect(mockCreate).toHaveBeenCalledWith(args, promptedOptions); + expect(mockCancel).toHaveBeenCalledWith( + `Operation cancelled. Exiting - maybe another time? 👋`, + ); + expect(result).toEqual(code); + }); + + it("returns the cancel result containing a zod error of the corresponding runner and output plus cancel logs when promptForMode returns a mode that cancels with a string error", async () => { + const mode = "initialize"; + const args = ["--email", "abc123"]; + const code = 2; + const error = "Oh no!"; + + mockPromptForMode.mockResolvedValue({ mode }); + mockInitialize.mockResolvedValue({ + code: 2, + error, + options: {}, + }); + + const result = await bin(args); + + expect(mockInitialize).toHaveBeenCalledWith(args, undefined); + expect(mockLogLine).toHaveBeenCalledWith(chalk.red(error)); + expect(mockCancel).toHaveBeenCalledWith( + `Operation cancelled. Exiting - maybe another time? 👋`, + ); + expect(result).toEqual(code); + }); + + it("returns the cancel result containing a zod error of the corresponding runner and output plus cancel logs when promptForMode returns a mode that cancels with a zod error", async () => { + const mode = "initialize"; + const args = ["--email", "abc123"]; + const code = 2; + + const validationResult = z + .object({ email: z.string().email() }) + .safeParse({ email: "abc123" }); + + mockPromptForMode.mockResolvedValue({ mode }); + mockInitialize.mockResolvedValue({ + code: 2, + error: (validationResult as z.SafeParseError<{ email: string }>).error, + options: {}, + }); + + const result = await bin(args); + + expect(mockInitialize).toHaveBeenCalledWith(args, undefined); + expect(mockLogLine).toHaveBeenCalledWith( + chalk.red('Validation error: Invalid email at "email"'), + ); + expect(mockCancel).toHaveBeenCalledWith( + `Operation cancelled. Exiting - maybe another time? 👋`, + ); + expect(result).toEqual(code); + }); + + it("returns the cancel result of the corresponding runner and cancel logs when promptForMode returns a mode that fails", async () => { + const mode = "create"; + const args = ["--owner", "abc123"]; + const code = 1; + const promptedOptions = { directory: "." }; + + mockPromptForMode.mockResolvedValue({ mode, options: promptedOptions }); + mockCreate.mockResolvedValue({ code, options: {} }); + + const result = await bin(args); + + expect(mockCreate).toHaveBeenCalledWith(args, promptedOptions); + expect(mockCancel).toHaveBeenCalledWith( + `Operation failed. Exiting - maybe another time? 👋`, + ); + expect(result).toEqual(code); + }); + + it("prints the version when the --version flag is passed", async () => { + const args = ["--version"]; + const version = await getVersionFromPackageJson(); + + const result = await bin(args); + + expect(console.log).toHaveBeenCalledWith(version); + expect(result).toBe(0); + }); +}); diff --git a/src/bin/index.ts b/src/bin/index.ts new file mode 100644 index 0000000..9237277 --- /dev/null +++ b/src/bin/index.ts @@ -0,0 +1,108 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; +import { parseArgs } from "node:util"; +import { fromZodError } from "zod-validation-error"; + +import { createRerunSuggestion } from "../create/createRerunSuggestion.js"; +import { create } from "../create/index.js"; +import { initialize } from "../initialize/index.js"; +import { migrate } from "../migrate/index.js"; +import { logLine } from "../shared/cli/lines.js"; +import { StatusCodes } from "../shared/codes.js"; +import { logHelpText } from "./help.js"; +import { getVersionFromPackageJson } from "./packageJson.js"; +import { promptForMode } from "./promptForMode.js"; + +const operationMessage = (verb: string) => + `Operation ${verb}. Exiting - maybe another time? 👋`; + +export async function bin(args: string[]) { + console.clear(); + + const version = await getVersionFromPackageJson(); + + const introPrompts = [ + chalk.greenBright(`✨ Welcome to`), + chalk.bgGreenBright.black(`create-typescript-app`), + chalk.greenBright(`${version}! ✨`), + ].join(" "); + + const introWarnings = [ + chalk.yellow( + "⚠️ This template is early stage, opinionated, and not endorsed by the TypeScript team. ⚠️", + ), + chalk.yellow( + "⚠️ If any tooling it sets displeases you, you can always remove that portion manually. ⚠️", + ), + ]; + + const { values } = parseArgs({ + args, + options: { + help: { + short: "h", + type: "boolean", + }, + mode: { type: "string" }, + version: { + short: "v", + type: "boolean", + }, + }, + strict: false, + }); + + if (values.help) { + logHelpText([introPrompts, ...introWarnings]); + return 0; + } + + if (values.version) { + console.log(version); + return 0; + } + + prompts.intro(introPrompts); + + logLine(); + logLine(introWarnings[0]); + logLine(introWarnings[1]); + + const { mode, options: promptedOptions } = await promptForMode( + !!values.auto, + values.mode, + ); + if (typeof mode !== "string") { + prompts.outro(chalk.red(mode?.message ?? operationMessage("cancelled"))); + return 1; + } + + const runners = { create, initialize, migrate }; + const { code, error, options } = await runners[mode](args, promptedOptions); + + prompts.log.info( + [ + chalk.italic(`Tip: to run again with the same input values, use:`), + chalk.blue(createRerunSuggestion(options)), + ].join(" "), + ); + + if (code) { + logLine(); + + if (error) { + logLine( + chalk.red(typeof error === "string" ? error : fromZodError(error)), + ); + logLine(); + } + + prompts.cancel( + code === StatusCodes.Cancelled + ? operationMessage("cancelled") + : operationMessage("failed"), + ); + } + + return code; +} diff --git a/src/bin/packageJson.test.ts b/src/bin/packageJson.test.ts new file mode 100644 index 0000000..7e1be8d --- /dev/null +++ b/src/bin/packageJson.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getVersionFromPackageJson } from "./packageJson.js"; + +const mockReadFileSafeAsJson = vi.fn(); + +vi.mock("../shared/readFileSafeAsJson.js", () => ({ + get readFileSafeAsJson() { + return mockReadFileSafeAsJson; + }, +})); + +describe("getVersionFromPackageJson", () => { + it("returns the current version number from the package.json", async () => { + mockReadFileSafeAsJson.mockResolvedValue({ + version: "1.40.0", + }); + + const version = await getVersionFromPackageJson(); + + expect(version).toBe("1.40.0"); + }); + + it("throws an error when there is no version number", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + + await expect(() => getVersionFromPackageJson()).rejects.toEqual( + new Error("Cannot find version number"), + ); + }); +}); diff --git a/src/bin/packageJson.ts b/src/bin/packageJson.ts new file mode 100644 index 0000000..18d7004 --- /dev/null +++ b/src/bin/packageJson.ts @@ -0,0 +1,16 @@ +import { readFileSafeAsJson } from "../shared/readFileSafeAsJson.js"; + +interface PackageWithVersion { + version?: string; +} + +export async function getVersionFromPackageJson(): Promise { + const path = new URL("../../package.json", import.meta.url); + const data = (await readFileSafeAsJson(path)) as PackageWithVersion; + + if (typeof data === "object" && typeof data.version === "string") { + return data.version; + } + + throw new Error("Cannot find version number"); +} diff --git a/src/bin/promptForMode.test.ts b/src/bin/promptForMode.test.ts new file mode 100644 index 0000000..f508c6d --- /dev/null +++ b/src/bin/promptForMode.test.ts @@ -0,0 +1,129 @@ +import chalk from "chalk"; +import { describe, expect, it, vi } from "vitest"; + +import { promptForMode } from "./promptForMode.js"; + +const mockSelect = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + isCancel: () => false, + get select() { + return mockSelect; + }, +})); + +const mockReaddir = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readdir() { + return mockReaddir; + }, +})); + +const mockCwd = vi.fn(); + +vi.mock("node:process", () => ({ + get cwd() { + return mockCwd; + }, +})); + +const mockLogLine = vi.fn(); + +vi.mock("../shared/cli/lines.js", () => ({ + get logLine() { + return mockLogLine; + }, +})); +describe("promptForMode", () => { + it("returns an error when auto exists and input is not migrate", async () => { + const mode = await promptForMode(true, "create"); + + expect(mode).toMatchInlineSnapshot( + ` + { + "mode": [Error: --auto can only be used with --mode migrate.], + } + `, + ); + }); + + it("returns an error when the input exists and is not a mode", async () => { + const mode = await promptForMode(false, "other"); + + expect(mode).toMatchInlineSnapshot( + ` + { + "mode": [Error: Unknown --mode: other. Allowed modes are: create, initialize, migrate.], + } + `, + ); + }); + + it("returns the input when it is a mode", async () => { + const input = "create"; + + const mode = await promptForMode(false, input); + + expect(mode).toEqual({ mode: input }); + }); + + it("returns creating in the current directory when the current directory is empty and the user selects create-current", async () => { + mockSelect.mockResolvedValueOnce("create-current"); + const directory = "test-directory"; + + mockReaddir.mockResolvedValueOnce([]); + mockCwd.mockReturnValueOnce(`/path/to/${directory}`); + + const actual = await promptForMode(false, undefined); + + expect(actual).toEqual({ + mode: "create", + options: { directory: ".", repository: directory }, + }); + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("returns creating in a child directory when the current directory is empty and the user selects create-child", async () => { + mockSelect.mockResolvedValueOnce("create-child"); + const directory = "test-directory"; + + mockReaddir.mockResolvedValueOnce([]); + mockCwd.mockReturnValueOnce(`/path/to/${directory}`); + + const actual = await promptForMode(false, undefined); + + expect(actual).toEqual({ + mode: "create", + }); + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("returns the user selection when the current directory is a Git directory", async () => { + const mode = "initialize"; + mockSelect.mockResolvedValueOnce(mode); + + mockReaddir.mockResolvedValueOnce([".git"]); + + const actual = await promptForMode(false, undefined); + + expect(actual).toEqual({ mode }); + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("returns create without prompting when the current directory contains children but is not a Git directory", async () => { + const mode = "create"; + + mockReaddir.mockResolvedValueOnce(["file"]); + + const actual = await promptForMode(false, undefined); + + expect(actual).toEqual({ mode }); + expect(mockSelect).not.toHaveBeenCalled(); + expect(mockLogLine).toHaveBeenCalledWith( + chalk.gray( + "Defaulting to --mode create because the directory contains children and isn't a Git repository.", + ), + ); + }); +}); diff --git a/src/bin/promptForMode.ts b/src/bin/promptForMode.ts new file mode 100644 index 0000000..72d3f39 --- /dev/null +++ b/src/bin/promptForMode.ts @@ -0,0 +1,126 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; +import * as fs from "fs/promises"; +import path from "node:path"; +import * as process from "node:process"; + +import { logLine } from "../shared/cli/lines.js"; +import { filterPromptCancel } from "../shared/prompts.js"; +import { Mode, PromptedOptions } from "../shared/types.js"; + +const allowedModes = ["create", "initialize", "migrate"] satisfies Mode[]; + +function isMode(input: boolean | string): input is Mode { + return allowedModes.includes(input as Mode); +} + +function label(base: string, text: string) { + return `${chalk.bold(base)} ${text}`; +} + +export interface PromptedMode { + mode: Error | Mode | undefined; + options?: PromptedOptions; +} + +export async function promptForMode( + auto: boolean, + input: boolean | string | undefined, +): Promise { + if (auto && input !== "migrate") { + return { + mode: new Error("--auto can only be used with --mode migrate."), + }; + } + + if (input) { + if (!isMode(input)) { + return { + mode: new Error( + `Unknown --mode: ${input}. Allowed modes are: ${allowedModes.join( + ", ", + )}.`, + ), + }; + } + + return { mode: input }; + } + + const dir = await fs.readdir("."); + + if (dir.length === 0) { + const mode = filterPromptCancel( + (await prompts.select({ + message: chalk.blue("How would you like to use the template?"), + options: [ + { + label: label( + "create", + "a new repository in the current empty directory", + ), + value: "create-current", + }, + { + label: label("create", "a new repository in a new child directory"), + value: "create-child", + }, + ], + })) as string, + ); + + const directory = path.basename(process.cwd()); + + return { + mode: "create", + ...(mode === "create-current" && { + options: { + directory: ".", + repository: directory, + }, + }), + }; + } + + if (dir.includes(".git")) { + return { + mode: filterPromptCancel( + (await prompts.select({ + initialValue: "migrate" as Mode, + message: chalk.blue("How would you like to use the template?"), + options: [ + { + label: label("create", "a new repository in a child directory"), + value: "create", + }, + { + label: label( + "initialize", + "a freshly cloned repository in the current directory", + ), + value: "initialize", + }, + { + label: label( + "migrate", + "the existing repository in the current directory", + ), + value: "migrate", + }, + ], + })) as Mode | symbol, + ), + }; + } + + logLine(); + logLine( + chalk.gray( + "Defaulting to --mode create because the directory contains children and isn't a Git repository.", + ), + ); + + return { + mode: "create", + }; +} diff --git a/src/create/createAndEnterGitDirectory.test.ts b/src/create/createAndEnterGitDirectory.test.ts new file mode 100644 index 0000000..1ce7cd7 --- /dev/null +++ b/src/create/createAndEnterGitDirectory.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createAndEnterGitDirectory } from "./createAndEnterGitDirectory.js"; + +const mockMkdir = vi.fn(); +const mockReaddir = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get mkdir() { + return mockMkdir; + }, + get readdir() { + return mockReaddir; + }, +})); + +const mockChdir = vi.fn(); + +vi.mock("node:process", () => ({ + get chdir() { + return mockChdir; + }, +})); + +describe("createAndEnterGitDirectory", () => { + it("returns false and doesn't run fs.mkdir when the directory is '.' and has children", async () => { + mockReaddir.mockResolvedValueOnce(["file"]); + + const actual = await createAndEnterGitDirectory("."); + + expect(actual).toBe(false); + expect(mockMkdir).not.toHaveBeenCalled(); + }); + + it("returns true and doesn't run fs.mkdir when the directory is '.' and is empty", async () => { + mockReaddir.mockResolvedValueOnce([]); + + const actual = await createAndEnterGitDirectory("."); + + expect(actual).toBe(true); + expect(mockMkdir).not.toHaveBeenCalled(); + }); + + it("returns false and doesn't run fs.chdir when the directory is a child directory with children", async () => { + const directory = "dir"; + mockReaddir + .mockResolvedValueOnce([directory]) + .mockResolvedValueOnce(["file"]); + + const actual = await createAndEnterGitDirectory(directory); + + expect(actual).toBe(false); + expect(mockChdir).not.toHaveBeenCalled(); + }); + + it("returns true and runs fs.chdir when the directory is a child directory that doesn't exist", async () => { + const directory = "dir"; + mockReaddir.mockResolvedValueOnce([directory]).mockResolvedValueOnce([]); + + const actual = await createAndEnterGitDirectory(directory); + + expect(actual).toBe(true); + expect(mockChdir).toHaveBeenCalledWith(directory); + }); +}); diff --git a/src/create/createAndEnterGitDirectory.ts b/src/create/createAndEnterGitDirectory.ts new file mode 100644 index 0000000..dee1c71 --- /dev/null +++ b/src/create/createAndEnterGitDirectory.ts @@ -0,0 +1,19 @@ +import { $ } from "execa"; +import * as fs from "node:fs/promises"; +import * as process from "node:process"; + +export async function createAndEnterGitDirectory(directory: string) { + if (directory !== "." && !(await fs.readdir(".")).includes(directory)) { + await fs.mkdir(directory); + } else if ((await fs.readdir(directory)).length) { + return false; + } + + if (directory !== ".") { + process.chdir(directory); + } + + await $`git init -b main`; + + return true; +} diff --git a/src/create/createRerunSuggestion.test.ts b/src/create/createRerunSuggestion.test.ts new file mode 100644 index 0000000..b31b020 --- /dev/null +++ b/src/create/createRerunSuggestion.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, it } from "vitest"; + +import { getExclusions } from "../shared/options/exclusionKeys.js"; +import { Options } from "../shared/types.js"; +import { createRerunSuggestion } from "./createRerunSuggestion.js"; + +const options = { + access: "public", + author: "TestAuthor", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + excludeAllContributors: true, + excludeCompliance: true, + excludeLintJSDoc: true, + excludeLintJson: true, + excludeLintKnip: true, + excludeLintMd: false, + excludeLintPackageJson: true, + excludeLintPackages: false, + excludeLintPerfectionist: true, + excludeLintSpelling: false, + excludeLintYml: false, + excludeReleases: false, + excludeRenovate: undefined, + excludeTests: undefined, + funding: undefined, + keywords: ["abc", "def ghi", "jkl mno pqr"], + mode: "create", + owner: "TestOwner", + repository: "test-repository", + skipGitHubApi: true, + skipInstall: true, + skipRemoval: true, + skipRestore: undefined, + skipUninstall: undefined, + title: "Test Title", +} satisfies Options; + +describe("createRerunSuggestion", () => { + it("prints no options when no options are provided", () => { + const actual = createRerunSuggestion({}); + + expect(actual).toMatchInlineSnapshot(`"npx create-typescript-app"`); + }); + + it("prints only mode when no other options are provided", () => { + const actual = createRerunSuggestion({ + mode: "create", + }); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --mode create"`, + ); + }); + + it("includes key-value pairs with mixed truthy and falsy values", () => { + const actual = createRerunSuggestion(options); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --base everything --author TestAuthor --description "Test description." --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors --exclude-compliance --exclude-lint-jsdoc --exclude-lint-json --exclude-lint-knip --exclude-lint-package-json --exclude-lint-perfectionist --keywords "abc def ghi jkl mno pqr" --mode create --owner TestOwner --repository test-repository --skip-github-api --skip-install --skip-removal --title "Test Title""`, + ); + }); + + it("includes a non-default value when specified", () => { + const actual = createRerunSuggestion({ + ...options, + access: "restricted", + }); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --base everything --access restricted --author TestAuthor --description "Test description." --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors --exclude-compliance --exclude-lint-jsdoc --exclude-lint-json --exclude-lint-knip --exclude-lint-package-json --exclude-lint-perfectionist --keywords "abc def ghi jkl mno pqr" --mode create --owner TestOwner --repository test-repository --skip-github-api --skip-install --skip-removal --title "Test Title""`, + ); + }); + + it("includes stringified guide when it exists", () => { + const actual = createRerunSuggestion({ + ...options, + guide: { + href: "https://example.com", + title: "Test Title", + }, + mode: "initialize", + }); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --base everything --author TestAuthor --description "Test description." --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors --exclude-compliance --exclude-lint-jsdoc --exclude-lint-json --exclude-lint-knip --exclude-lint-package-json --exclude-lint-perfectionist --guide https://example.com --guide-title "Test Title" --keywords "abc def ghi jkl mno pqr" --mode initialize --owner TestOwner --repository test-repository --skip-github-api --skip-install --skip-removal --title "Test Title""`, + ); + }); + + it("includes stringified logo when it exists", () => { + const actual = createRerunSuggestion({ + ...options, + logo: { + alt: "Test alt.", + src: "test/src.png", + }, + mode: "initialize", + }); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --base everything --author TestAuthor --description "Test description." --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors --exclude-compliance --exclude-lint-jsdoc --exclude-lint-json --exclude-lint-knip --exclude-lint-package-json --exclude-lint-perfectionist --keywords "abc def ghi jkl mno pqr" --logo test/src.png --logo-alt "Test alt." --mode initialize --owner TestOwner --repository test-repository --skip-github-api --skip-install --skip-removal --title "Test Title""`, + ); + }); + + it("includes exclusions when they exist", () => { + const actual = createRerunSuggestion({ + ...options, + excludeCompliance: true, + excludeLintMd: true, + excludeLintSpelling: true, + mode: "initialize", + }); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --base everything --author TestAuthor --description "Test description." --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors --exclude-compliance --exclude-lint-jsdoc --exclude-lint-json --exclude-lint-knip --exclude-lint-md --exclude-lint-package-json --exclude-lint-perfectionist --exclude-lint-spelling --keywords "abc def ghi jkl mno pqr" --mode initialize --owner TestOwner --repository test-repository --skip-github-api --skip-install --skip-removal --title "Test Title""`, + ); + }); + + it("does not list all excludes when using common base", () => { + const common = createRerunSuggestion({ + base: "common", + ...getExclusions(options, "common"), + excludeLintKnip: undefined, + }); + + expect(common).toMatchInlineSnapshot( + `"npx create-typescript-app --base common"`, + ); + }); + + it("does not list all excludes when using minimum base", () => { + const minimum = createRerunSuggestion({ + base: "minimum", + ...getExclusions(options, "minimum"), + excludeLintKnip: undefined, + }); + + expect(minimum).toMatchInlineSnapshot( + `"npx create-typescript-app --base minimum"`, + ); + }); + + it("does not list API skip flags when --offline is true", () => { + const actual = createRerunSuggestion({ + ...options, + offline: true, + skipAllContributorsApi: true, + skipGitHubApi: true, + }); + + expect(actual).toMatchInlineSnapshot( + `"npx create-typescript-app --base everything --author TestAuthor --description "Test description." --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors --exclude-compliance --exclude-lint-jsdoc --exclude-lint-json --exclude-lint-knip --exclude-lint-package-json --exclude-lint-perfectionist --keywords "abc def ghi jkl mno pqr" --mode create --offline --owner TestOwner --repository test-repository --skip-install --skip-removal --title "Test Title""`, + ); + }); +}); diff --git a/src/create/createRerunSuggestion.ts b/src/create/createRerunSuggestion.ts new file mode 100644 index 0000000..b73e113 --- /dev/null +++ b/src/create/createRerunSuggestion.ts @@ -0,0 +1,79 @@ +import { allArgOptions } from "../shared/options/args.js"; +import { + ExclusionKey, + getExclusions, +} from "../shared/options/exclusionKeys.js"; +import { Options } from "../shared/types.js"; + +function getFirstMatchingArg(key: string) { + return Object.keys(allArgOptions).find( + (arg) => arg.replaceAll("-", "") === key.toLowerCase(), + ); +} + +const defaultValues = new Map([["access", "public"]]); + +export function createRerunSuggestion(options: Partial): string { + const optionsNormalized = { + ...options, + email: undefined, + ...(options.email && { + emailGitHub: options.email.github, + emailNpm: options.email.npm, + }), + ...(options.guide + ? { + guide: options.guide.href, + guideTitle: options.guide.title, + } + : { guide: undefined }), + ...(options.logo + ? { + logo: options.logo.src, + logoAlt: options.logo.alt, + } + : { logo: undefined }), + ...(options.offline && { + skipAllContributorsApi: undefined, + skipGitHubApi: undefined, + }), + }; + + const args = Object.entries(optionsNormalized) + // Sort so the base is first, then the rest are sorted alphabetically + .sort(([a], [b]) => + a === "base" ? -1 : b === "base" ? 1 : a.localeCompare(b), + ) + // Filter out entries with an excluded key or a default or falsy value + .filter( + ([key, value]) => + getExclusions(options, optionsNormalized.base)[key as ExclusionKey] == + undefined && + !!value && + value !== defaultValues.get(key), + ) + .map(([key, value]) => { + return `--${getFirstMatchingArg(key)}${stringifyValue(value)}`; + }) + .join(" "); + + return ["npx create-typescript-app", args].filter(Boolean).join(" "); +} + +function stringifyValue( + value: boolean | string | string[] | undefined, +): string { + if (Array.isArray(value)) { + return stringifyValue(value.join(" ")); + } + + if (typeof value === "boolean" && value) { + return ""; + } + + const valueStringified = `${value}`; + + return valueStringified.includes(" ") + ? ` "${valueStringified}"` + : ` ${valueStringified}`; +} diff --git a/src/create/createWithOptions.test.ts b/src/create/createWithOptions.test.ts new file mode 100644 index 0000000..6e7156a --- /dev/null +++ b/src/create/createWithOptions.test.ts @@ -0,0 +1,200 @@ +import { Octokit } from "octokit"; +import { describe, expect, it, vi } from "vitest"; + +import { SpinnerTask } from "../shared/cli/spinners.js"; +import { doesRepositoryExist } from "../shared/doesRepositoryExist.js"; +import { Options } from "../shared/types.js"; +import { addToolAllContributors } from "../steps/addToolAllContributors.js"; +import { finalizeDependencies } from "../steps/finalizeDependencies.js"; +import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { populateCSpellDictionary } from "../steps/populateCSpellDictionary.js"; +import { runCleanup } from "../steps/runCleanup.js"; +import { createWithOptions } from "./createWithOptions.js"; + +const optionsBase: Options = { + access: "public", + author: "Test Author", + base: "common", + description: "Test Description", + directory: "test-directory", + email: { github: "github@example.com", npm: "npm@example.com" }, + funding: "Test Funding", + keywords: ["test", "keywords"], + logo: { alt: "Test Alt", src: "test.png" }, + mode: "create", + owner: "Test Owner", + repository: "test-repo", + title: "Test Title", +}; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockOctokit = new Octokit(); +const github = { + auth: "auth-token", + octokit: mockOctokit, +}; + +vi.mock("../shared/cli/spinners.js", () => ({ + withSpinner: async (label: string, task: SpinnerTask) => { + return await task(); + }, + withSpinners: async ( + label: string, + tasks: [string, SpinnerTask][], + ) => { + for (const [, task] of tasks) { + await task(); + } + }, +})); + +vi.mock("../steps/writing/writeStructure.js"); + +vi.mock("../steps/writeReadme/index.js"); + +vi.mock("../steps/finalizeDependencies.js"); + +vi.mock("../steps/populateCSpellDictionary.js"); + +vi.mock("../steps/runCleanup.js"); + +vi.mock("../shared/doesRepositoryExist.js", () => ({ + doesRepositoryExist: vi.fn().mockResolvedValue(true), +})); + +vi.mock("../steps/initializeGitHubRepository/index.js", () => ({ + initializeGitHubRepository: vi.fn().mockResolvedValue(true), +})); + +vi.mock("../shared/getGitHubUserAsAllContributor.js", () => ({ + getGitHubUserAsAllContributor: vi.fn().mockResolvedValue({ + contributions: ["code", "doc"], + name: "Test User", + }), +})); + +vi.mock("../steps/addToolAllContributors.js"); + +describe("createWithOptions", () => { + it("calls addToolAllContributors with options when excludeAllContributors is false", async () => { + const options = { + ...optionsBase, + excludeAllContributors: false, + skipAllContributorsApi: false, + }; + + await createWithOptions({ github, options }); + expect(addToolAllContributors).toHaveBeenCalledWith(options); + }); + + it("does not call addToolAllContributors when excludeAllContributors is true", async () => { + const options = { + ...optionsBase, + excludeAllContributors: true, + }; + + await createWithOptions({ github, options }); + expect(addToolAllContributors).not.toHaveBeenCalled(); + }); + + it("does not call addToolAllContributors when skipAllContributorsApi is true", async () => { + const options = { + ...optionsBase, + skipAllContributorsApi: true, + }; + + await createWithOptions({ github, options }); + expect(addToolAllContributors).not.toHaveBeenCalled(); + }); + + it("does not call finalizeDependencies, populateCSpellDictionary, or runCleanup when skipInstall is true", async () => { + const options = { + ...optionsBase, + skipInstall: true, + }; + + await createWithOptions({ github, options }); + expect(finalizeDependencies).not.toHaveBeenCalled(); + expect(populateCSpellDictionary).not.toHaveBeenCalled(); + expect(runCleanup).not.toHaveBeenCalled(); + }); + + it("calls finalizeDependencies, populateCSpellDictionary, and runCleanup when skipInstall is false", async () => { + const options = { + ...optionsBase, + skipInstall: false, + }; + + await createWithOptions({ github, options }); + + expect(finalizeDependencies).toHaveBeenCalledWith(options); + expect(populateCSpellDictionary).toHaveBeenCalled(); + expect(runCleanup).toHaveBeenCalled(); + }); + + it("does not initialize GitHub repository if repository does not exist", async () => { + const options = optionsBase; + vi.mocked(doesRepositoryExist).mockResolvedValueOnce(false); + await createWithOptions({ github, options }); + expect(initializeGitHubRepository).not.toHaveBeenCalled(); + expect(mock$).not.toHaveBeenCalled(); + }); + + it("executes git commands when initializing GitHub repository when doesRepositoryExist is true", async () => { + const options = optionsBase; + + vi.mocked(doesRepositoryExist).mockResolvedValueOnce(true); + await createWithOptions({ github, options }); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "git remote add origin https://github.com/", + "/", + "", + ], + "Test Owner", + "test-repo", + ], + [ + [ + "git add -A", + ], + ], + [ + [ + "git commit --message ", + "", + ], + "feat: initialized repo ✨", + ], + [ + [ + "git push -u origin main --force", + ], + ], + ] + `); + }); + + it("calls doesRepositoryExist with github and options when doesRepositoryExist is true", async () => { + const options = optionsBase; + vi.mocked(doesRepositoryExist).mockResolvedValueOnce(true); + + await createWithOptions({ github, options }); + + expect(doesRepositoryExist).toHaveBeenCalledWith(github.octokit, options); + expect(initializeGitHubRepository).toHaveBeenCalledWith( + github.octokit, + options, + ); + }); +}); diff --git a/src/create/createWithOptions.ts b/src/create/createWithOptions.ts new file mode 100644 index 0000000..c8a9fd9 --- /dev/null +++ b/src/create/createWithOptions.ts @@ -0,0 +1,66 @@ +import { $ } from "execa"; + +import { withSpinner, withSpinners } from "../shared/cli/spinners.js"; +import { createCleanupCommands } from "../shared/createCleanupCommands.js"; +import { doesRepositoryExist } from "../shared/doesRepositoryExist.js"; +import { GitHubAndOptions } from "../shared/options/readOptions.js"; +import { addToolAllContributors } from "../steps/addToolAllContributors.js"; +import { finalizeDependencies } from "../steps/finalizeDependencies.js"; +import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { populateCSpellDictionary } from "../steps/populateCSpellDictionary.js"; +import { runCleanup } from "../steps/runCleanup.js"; +import { writeReadme } from "../steps/writeReadme/index.js"; +import { writeStructure } from "../steps/writing/writeStructure.js"; + +export async function createWithOptions({ github, options }: GitHubAndOptions) { + await withSpinners("Creating repository structure", [ + [ + "Writing structure", + async () => { + await writeStructure(options); + }, + ], + [ + "Writing README.md", + async () => { + await writeReadme(options); + }, + ], + ]); + + if (!options.excludeAllContributors && !options.skipAllContributorsApi) { + await withSpinner("Adding contributors to table", async () => { + await addToolAllContributors(options); + }); + } + + if (!options.skipInstall) { + await withSpinner("Installing packages", async () => + finalizeDependencies(options), + ); + + if (!options.excludeLintSpelling) { + await withSpinner( + "Populating CSpell dictionary", + populateCSpellDictionary, + ); + } + + await runCleanup(createCleanupCommands(options), options.mode); + } + + const sendToGitHub = + github && (await doesRepositoryExist(github.octokit, options)); + + if (sendToGitHub) { + await withSpinner("Initializing GitHub repository", async () => { + await $`git remote add origin https://github.com/${options.owner}/${options.repository}`; + await $`git add -A`; + await $`git commit --message ${"feat: initialized repo ✨"}`; + await $`git push -u origin main --force`; + await initializeGitHubRepository(github.octokit, options); + }); + } + + return { sentToGitHub: sendToGitHub }; +} diff --git a/src/create/index.test.ts b/src/create/index.test.ts new file mode 100644 index 0000000..d86ec6b --- /dev/null +++ b/src/create/index.test.ts @@ -0,0 +1,72 @@ +import chalk from "chalk"; +import { describe, expect, it, vi } from "vitest"; + +import { StatusCodes } from "../shared/codes.js"; +import { create } from "./index.js"; + +const mockOutro = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + get outro() { + return mockOutro; + }, + spinner: vi.fn(), +})); + +const mockReadOptions = vi.fn(); + +vi.mock("../shared/options/readOptions.js", () => ({ + get readOptions() { + return mockReadOptions; + }, +})); + +const mockCreateAndEnterGitDirectory = vi.fn(); + +vi.mock("./createAndEnterGitDirectory.js", () => ({ + get createAndEnterGitDirectory() { + return mockCreateAndEnterGitDirectory; + }, +})); + +const optionsBase = { + directory: "TestDirectory", + repository: "TestRepository", +}; + +describe("create", () => { + it("returns a cancellation code when readOptions cancels", async () => { + mockReadOptions.mockResolvedValue({ + cancelled: true, + options: optionsBase, + }); + + const result = await create([]); + + expect(result).toEqual({ + code: StatusCodes.Cancelled, + options: optionsBase, + }); + }); + + it("returns a failure code when createAndEnterGitDirectory returns false", async () => { + mockReadOptions.mockResolvedValue({ + cancelled: false, + options: optionsBase, + }); + + mockCreateAndEnterGitDirectory.mockResolvedValue(false); + + const result = await create([]); + + expect(result).toEqual({ + code: StatusCodes.Failure, + options: optionsBase, + }); + expect(mockOutro).toHaveBeenCalledWith( + chalk.red( + "The TestDirectory directory already exists and is not empty. Please clear the directory, run with --mode initialize, or try a different directory.", + ), + ); + }); +}); diff --git a/src/create/index.ts b/src/create/index.ts new file mode 100644 index 0000000..3ee49f8 --- /dev/null +++ b/src/create/index.ts @@ -0,0 +1,68 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; + +import { outro } from "../shared/cli/outro.js"; +import { StatusCodes } from "../shared/codes.js"; +import { generateNextSteps } from "../shared/generateNextSteps.js"; +import { readOptions } from "../shared/options/readOptions.js"; +import { runOrRestore } from "../shared/runOrRestore.js"; +import { ModeRunner } from "../shared/types.js"; +import { createAndEnterGitDirectory } from "./createAndEnterGitDirectory.js"; +import { createRerunSuggestion } from "./createRerunSuggestion.js"; +import { createWithOptions } from "./createWithOptions.js"; + +export const create: ModeRunner = async (args, promptedOptions) => { + const inputs = await readOptions(args, "create", promptedOptions); + if (inputs.cancelled) { + return { + code: StatusCodes.Cancelled, + error: inputs.error, + options: inputs.options, + }; + } + + if (!(await createAndEnterGitDirectory(inputs.options.directory))) { + prompts.outro( + chalk.red( + `The ${inputs.options.directory} directory already exists and is not empty. Please clear the directory, run with --mode initialize, or try a different directory.`, + ), + ); + return { code: StatusCodes.Failure, options: inputs.options }; + } + + return { + code: await runOrRestore({ + run: async () => { + const { sentToGitHub } = await createWithOptions(inputs); + const nextSteps = generateNextSteps(inputs.options); + + outro( + sentToGitHub + ? nextSteps + : [ + { + label: + "Consider creating a GitHub repository from the new directory:", + lines: [ + `cd ${inputs.options.repository}`, + createRerunSuggestion({ + ...inputs.options, + mode: "initialize", + skipGitHubApi: false, + skipInstall: false, + }), + `git add -A`, + `git commit -m "feat: initial commit ✨"`, + `git push -u origin main`, + ], + variant: "code", + }, + ...nextSteps, + ], + ); + }, + skipRestore: inputs.options.skipRestore, + }), + options: inputs.options, + }; +}; diff --git a/src/greet.test.ts b/src/greet.test.ts new file mode 100644 index 0000000..f729115 --- /dev/null +++ b/src/greet.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; + +import { greet } from "./greet.js"; + +const message = "Yay, testing!"; + +describe("greet", () => { + it("logs to the console once when message is provided as a string", () => { + const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); + + greet(message); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it("logs to the console once when message is provided as an object", () => { + const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); + + greet({ message }); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it("logs once when times is not provided in an object", () => { + const logger = vi.fn(); + + greet({ logger, message }); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it("logs a specified number of times when times is provided", () => { + const logger = vi.fn(); + const times = 7; + + greet({ logger, message, times }); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(7); + }); +}); diff --git a/src/greet.ts b/src/greet.ts new file mode 100644 index 0000000..a0d3b4c --- /dev/null +++ b/src/greet.ts @@ -0,0 +1,13 @@ +import { GreetOptions } from "./types.js"; + +export function greet(options: GreetOptions | string) { + const { + logger = console.log.bind(console), + message, + times = 1, + } = typeof options === "string" ? { message: options } : options; + + for (let i = 0; i < times; i += 1) { + logger(message); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a39b40f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from "./greet.js"; +export * from "./types.js"; diff --git a/src/initialize/index.test.ts b/src/initialize/index.test.ts new file mode 100644 index 0000000..675aaa6 --- /dev/null +++ b/src/initialize/index.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; + +import { StatusCodes } from "../shared/codes.js"; +import { initialize } from "./index.js"; + +const mockOutro = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + get outro() { + return mockOutro; + }, + spinner: vi.fn(), +})); + +const mockReadOptions = vi.fn(); + +vi.mock("../shared/options/readOptions.js", () => ({ + get readOptions() { + return mockReadOptions; + }, +})); + +const mockInitializeAndEnterRepository = vi.fn(); + +vi.mock("./initializeAndEnterRepository.js", () => ({ + get initializeAndEnterRepository() { + return mockInitializeAndEnterRepository; + }, +})); + +const optionsBase = { + repository: "TestRepository", +}; + +describe("initialize", () => { + it("returns a cancellation code when readOptions cancels", async () => { + mockReadOptions.mockResolvedValue({ + cancelled: true, + options: optionsBase, + }); + + const result = await initialize([]); + + expect(result).toEqual({ + code: StatusCodes.Cancelled, + options: optionsBase, + }); + }); +}); diff --git a/src/initialize/index.ts b/src/initialize/index.ts new file mode 100644 index 0000000..a2b4caa --- /dev/null +++ b/src/initialize/index.ts @@ -0,0 +1,44 @@ +import { outro } from "../shared/cli/outro.js"; +import { StatusCodes } from "../shared/codes.js"; +import { ensureGitRepository } from "../shared/ensureGitRepository.js"; +import { generateNextSteps } from "../shared/generateNextSteps.js"; +import { readOptions } from "../shared/options/readOptions.js"; +import { runOrRestore } from "../shared/runOrRestore.js"; +import { ModeRunner } from "../shared/types.js"; +import { initializeWithOptions } from "./initializeWithOptions.js"; + +export const initialize: ModeRunner = async (args) => { + const inputs = await readOptions(args, "initialize"); + if (inputs.cancelled) { + return { + code: StatusCodes.Cancelled, + error: inputs.error, + options: inputs.options, + }; + } + + await ensureGitRepository(); + + return { + code: await runOrRestore({ + run: async () => { + await initializeWithOptions(inputs); + + outro([ + { + label: "You may consider committing these changes:", + lines: [ + `git add -A`, + `git commit -m "feat: initialized repo ✨`, + `git push`, + ], + variant: "code", + }, + ...generateNextSteps(inputs.options), + ]); + }, + skipRestore: inputs.options.skipRestore, + }), + options: inputs.options, + }; +}; diff --git a/src/initialize/initializeWithOptions.ts b/src/initialize/initializeWithOptions.ts new file mode 100644 index 0000000..73f6592 --- /dev/null +++ b/src/initialize/initializeWithOptions.ts @@ -0,0 +1,65 @@ +import { withSpinner, withSpinners } from "../shared/cli/spinners.js"; +import { createCleanupCommands } from "../shared/createCleanupCommands.js"; +import { GitHubAndOptions } from "../shared/options/readOptions.js"; +import { addOwnerAsAllContributor } from "../steps/addOwnerAsAllContributor.js"; +import { clearChangelog } from "../steps/clearChangelog.js"; +import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { removeSetupScripts } from "../steps/removeSetupScripts.js"; +import { resetGitTags } from "../steps/resetGitTags.js"; +import { runCleanup } from "../steps/runCleanup.js"; +import { uninstallPackages } from "../steps/uninstallPackages.js"; +import { updateAllContributorsTable } from "../steps/updateAllContributorsTable.js"; +import { updateLocalFiles } from "../steps/updateLocalFiles.js"; +import { updateReadme } from "../steps/updateReadme.js"; + +export async function initializeWithOptions({ + github, + options, +}: GitHubAndOptions) { + await withSpinners("Initializing local files", [ + [ + "Updating local files", + async () => { + await updateLocalFiles(options); + }, + ], + [ + "Updating README.md", + async () => { + await updateReadme(options); + }, + ], + ["Clearing changelog", clearChangelog], + [ + "Updating all-contributors table", + async () => { + await updateAllContributorsTable(options); + }, + ], + ["Resetting Git tags", resetGitTags], + ]); + + if (!options.excludeAllContributors) { + await withSpinner("Updating existing contributor details", async () => { + await addOwnerAsAllContributor(options); + }); + } + + if (github) { + await withSpinner("Initializing GitHub repository", async () => { + await initializeGitHubRepository(github.octokit, options); + }); + } + + if (!options.skipRemoval) { + await withSpinner("Removing setup scripts", removeSetupScripts); + } + + if (!options.skipUninstall) { + await withSpinner("Uninstalling initialization-only packages", async () => + uninstallPackages(options.offline), + ); + } + + await runCleanup(createCleanupCommands(options), options.mode); +} diff --git a/src/migrate/index.test.ts b/src/migrate/index.test.ts new file mode 100644 index 0000000..8794809 --- /dev/null +++ b/src/migrate/index.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; + +import { StatusCodes } from "../shared/codes.js"; +import { migrate } from "./index.js"; + +const mockOutro = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + get outro() { + return mockOutro; + }, + spinner: vi.fn(), +})); + +const mockReadOptions = vi.fn(); + +vi.mock("../shared/options/readOptions.js", () => ({ + get readOptions() { + return mockReadOptions; + }, +})); + +const mockMigrateAndEnterRepository = vi.fn(); + +vi.mock("./migrateAndEnterRepository.js", () => ({ + get migrateAndEnterRepository() { + return mockMigrateAndEnterRepository; + }, +})); + +const optionsBase = { + repository: "TestRepository", +}; + +describe("migrate", () => { + it("returns a cancellation code when readOptions cancels", async () => { + mockReadOptions.mockResolvedValue({ + cancelled: true, + options: optionsBase, + }); + + const result = await migrate([]); + + expect(result).toEqual({ + code: StatusCodes.Cancelled, + options: optionsBase, + }); + }); +}); diff --git a/src/migrate/index.ts b/src/migrate/index.ts new file mode 100644 index 0000000..7f4fd02 --- /dev/null +++ b/src/migrate/index.ts @@ -0,0 +1,44 @@ +import { outro } from "../shared/cli/outro.js"; +import { StatusCodes } from "../shared/codes.js"; +import { ensureGitRepository } from "../shared/ensureGitRepository.js"; +import { generateNextSteps } from "../shared/generateNextSteps.js"; +import { readOptions } from "../shared/options/readOptions.js"; +import { runOrRestore } from "../shared/runOrRestore.js"; +import { ModeRunner } from "../shared/types.js"; +import { migrateWithOptions } from "./migrateWithOptions.js"; + +export const migrate: ModeRunner = async (args) => { + const inputs = await readOptions(args, "migrate"); + if (inputs.cancelled) { + return { + code: StatusCodes.Cancelled, + error: inputs.error, + options: inputs.options, + }; + } + + await ensureGitRepository(); + + return { + code: await runOrRestore({ + run: async () => { + await migrateWithOptions(inputs); + + outro([ + { + label: "You may consider committing these changes:", + lines: [ + `git add -A`, + `git commit -m "migrated repo to create-typescript-app ✨`, + `git push`, + ], + variant: "code", + }, + ...generateNextSteps(inputs.options), + ]); + }, + skipRestore: inputs.options.skipRestore, + }), + options: inputs.options, + }; +}; diff --git a/src/migrate/migrateWithOptions.ts b/src/migrate/migrateWithOptions.ts new file mode 100644 index 0000000..27fa80c --- /dev/null +++ b/src/migrate/migrateWithOptions.ts @@ -0,0 +1,70 @@ +import { withSpinner, withSpinners } from "../shared/cli/spinners.js"; +import { createCleanupCommands } from "../shared/createCleanupCommands.js"; +import { GitHubAndOptions } from "../shared/options/readOptions.js"; +import { clearUnnecessaryFiles } from "../steps/clearUnnecessaryFiles.js"; +import { detectExistingContributors } from "../steps/detectExistingContributors.js"; +import { finalizeDependencies } from "../steps/finalizeDependencies.js"; +import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { populateCSpellDictionary } from "../steps/populateCSpellDictionary.js"; +import { runCleanup } from "../steps/runCleanup.js"; +import { updateAllContributorsTable } from "../steps/updateAllContributorsTable.js"; +import { updateLocalFiles } from "../steps/updateLocalFiles.js"; +import { writeReadme } from "../steps/writeReadme/index.js"; +import { writeStructure } from "../steps/writing/writeStructure.js"; + +export async function migrateWithOptions({ + github, + options, +}: GitHubAndOptions) { + await withSpinners("Migrating repository structure", [ + ["Clearing unnecessary files", clearUnnecessaryFiles], + [ + "Writing structure", + async () => { + await writeStructure(options); + }, + ], + [ + "Writing README.md", + async () => { + await writeReadme(options); + }, + ], + [ + "Updating local files", + async () => { + await updateLocalFiles(options); + }, + ], + [ + "Updating all-contributors table", + async () => { + await updateAllContributorsTable(options); + }, + ], + ]); + + if (github) { + await withSpinner("Initializing GitHub repository", async () => { + await initializeGitHubRepository(github.octokit, options); + }); + } + + if (!options.excludeAllContributors && !options.skipAllContributorsApi) { + await withSpinner("Detecting existing contributors", async () => + detectExistingContributors(github?.auth, options), + ); + } + + if (!options.skipInstall) { + await withSpinner("Installing packages", async () => + finalizeDependencies(options), + ); + } + + if (!options.excludeLintSpelling) { + await withSpinner("Populating CSpell dictionary", populateCSpellDictionary); + } + + await runCleanup(createCleanupCommands(options), options.mode); +} diff --git a/src/shared/__snapshots__/generateNextSteps.test.ts.snap b/src/shared/__snapshots__/generateNextSteps.test.ts.snap new file mode 100644 index 0000000..df9a0ec --- /dev/null +++ b/src/shared/__snapshots__/generateNextSteps.test.ts.snap @@ -0,0 +1,183 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":false,"excludeRenovate":false,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the GitHub apps: + - Codecov (https://github.com/apps/codecov) + - Renovate (https://github.com/apps/renovate)", + "- populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":false,"excludeRenovate":false,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the Renovate GitHub app (https://github.com/apps/renovate).", + "- populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":false,"excludeRenovate":true,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the Codecov GitHub app (https://github.com/apps/codecov).", + "- populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":false,"excludeRenovate":true,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":true,"excludeRenovate":false,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the GitHub apps: + - Codecov (https://github.com/apps/codecov) + - Renovate (https://github.com/apps/renovate)", + "- populate the ACCESS_TOKEN secret (a GitHub PAT with repo and workflow permissions).", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":true,"excludeRenovate":false,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the Renovate GitHub app (https://github.com/apps/renovate).", + "- populate the ACCESS_TOKEN secret (a GitHub PAT with repo and workflow permissions).", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":true,"excludeRenovate":true,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the Codecov GitHub app (https://github.com/apps/codecov).", + "- populate the ACCESS_TOKEN secret (a GitHub PAT with repo and workflow permissions).", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":false,"excludeReleases":true,"excludeRenovate":true,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to populate the ACCESS_TOKEN secret (a GitHub PAT with repo and workflow permissions).", + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":false,"excludeRenovate":false,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the GitHub apps: + - Codecov (https://github.com/apps/codecov) + - Renovate (https://github.com/apps/renovate)", + "- populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":false,"excludeRenovate":false,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the Renovate GitHub app (https://github.com/apps/renovate).", + "- populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":false,"excludeRenovate":true,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to:", + "lines": [ + "- enable the Codecov GitHub app (https://github.com/apps/codecov).", + "- populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + ], + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":false,"excludeRenovate":true,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to populate the secrets: + - ACCESS_TOKEN (a GitHub PAT with repo and workflow permissions) + - NPM_TOKEN (an npm access token with automation permissions)", + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":true,"excludeRenovate":false,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to enable the GitHub apps: + - Codecov (https://github.com/apps/codecov) + - Renovate (https://github.com/apps/renovate)", + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":true,"excludeRenovate":false,"excludeTests":true} 1`] = ` +[ + { + "label": "Be sure to enable the Renovate GitHub app (https://github.com/apps/renovate).", + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":true,"excludeRenovate":true,"excludeTests":false} 1`] = ` +[ + { + "label": "Be sure to enable the Codecov GitHub app (https://github.com/apps/codecov).", + }, +] +`; + +exports[`generateNextSteps > {"excludeAllContributors":true,"excludeReleases":true,"excludeRenovate":true,"excludeTests":true} 1`] = `[]`; diff --git a/src/shared/cli/lines.ts b/src/shared/cli/lines.ts new file mode 100644 index 0000000..d1075ad --- /dev/null +++ b/src/shared/cli/lines.ts @@ -0,0 +1,14 @@ +import chalk from "chalk"; + +export function logLine(line?: string) { + console.log(makeLine(line)); +} + +export function logNewSection(line: string) { + logLine(); + console.log(`◇ ${line}`); +} + +export function makeLine(line: string | undefined) { + return [chalk.gray("│"), line].filter(Boolean).join(" "); +} diff --git a/src/shared/cli/lowerFirst.ts b/src/shared/cli/lowerFirst.ts new file mode 100644 index 0000000..3b96e98 --- /dev/null +++ b/src/shared/cli/lowerFirst.ts @@ -0,0 +1,3 @@ +export function lowerFirst(text: string) { + return text[0].toLowerCase() + text.slice(1); +} diff --git a/src/shared/cli/outro.test.ts b/src/shared/cli/outro.test.ts new file mode 100644 index 0000000..f46f8e9 --- /dev/null +++ b/src/shared/cli/outro.test.ts @@ -0,0 +1,61 @@ +import chalk from "chalk"; +import { MockInstance, beforeEach, describe, expect, it, vi } from "vitest"; + +import { outro } from "./outro.js"; + +const mockOutro = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + get outro() { + return mockOutro; + }, +})); + +let mockConsoleLog: MockInstance; + +describe("outro", () => { + beforeEach(() => { + mockConsoleLog = vi + .spyOn(console, "log") + .mockImplementation(() => undefined); + }); + + it("logs only basic statements when no lines are provided", () => { + outro([{ label: "Abc 123" }]); + + expect(mockConsoleLog.mock.calls).toEqual([ + [chalk.blue("Abc 123")], + [], + [chalk.greenBright(`See ya! 👋`)], + [], + ]); + }); + + it("also logs lines when provided", () => { + outro([{ label: "Abc 123", lines: ["one", "two"] }]); + + expect(mockConsoleLog.mock.calls).toEqual([ + [chalk.blue("Abc 123")], + [], + ["one"], + ["two"], + [], + [chalk.greenBright(`See ya! 👋`)], + [], + ]); + }); + + it("logs lines as code when variant is specified", () => { + outro([{ label: "Abc 123", lines: ["one", "two"], variant: "code" }]); + + expect(mockConsoleLog.mock.calls).toEqual([ + [chalk.blue("Abc 123")], + [], + [chalk.gray("one")], + [chalk.gray("two")], + [], + [chalk.greenBright(`See ya! 👋`)], + [], + ]); + }); +}); diff --git a/src/shared/cli/outro.ts b/src/shared/cli/outro.ts new file mode 100644 index 0000000..444891d --- /dev/null +++ b/src/shared/cli/outro.ts @@ -0,0 +1,28 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; + +export interface OutroGroup { + label: string; + lines?: string[]; + variant?: "code"; +} + +export function outro(groups: OutroGroup[]) { + prompts.outro(chalk.blue(`Great, looks like the script finished! 🎉`)); + + for (const { label, lines, variant } of groups) { + console.log(chalk.blue(label)); + console.log(); + + if (lines) { + for (const line of lines) { + console.log(variant === "code" ? chalk.gray(line) : line); + } + + console.log(); + } + } + + console.log(chalk.greenBright(`See ya! 👋`)); + console.log(); +} diff --git a/src/shared/cli/spinners.ts b/src/shared/cli/spinners.ts new file mode 100644 index 0000000..e3e65c5 --- /dev/null +++ b/src/shared/cli/spinners.ts @@ -0,0 +1,72 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; +import readline from "readline"; + +import { logLine, logNewSection, makeLine } from "./lines.js"; +import { lowerFirst } from "./lowerFirst.js"; +import { startLineWithDots } from "./startLineWithDots.js"; + +const s = prompts.spinner(); + +export type SpinnerTask = () => Promise; + +export type LabeledSpinnerTask = [string, SpinnerTask]; + +export async function withSpinner( + label: string, + task: SpinnerTask, +) { + s.start(`${label}...`); + + try { + const result = await task(); + + s.stop(chalk.green(`✅ Passed ${lowerFirst(label)}.`)); + + return result; + } catch (error) { + s.stop(chalk.red(`❌ Error ${lowerFirst(label)}.`)); + + throw new Error(`Failed ${lowerFirst(label)}`, { cause: error }); + } +} + +export async function withSpinners( + label: string, + tasks: LabeledSpinnerTask[], +) { + logNewSection(`${label}...`); + + let currentLabel!: string; + let lastLogged!: string; + + for (const [label, run] of tasks) { + currentLabel = label; + + const line = makeLine(chalk.gray(` - ${label}`)); + const stopWriting = startLineWithDots(line); + + try { + await run(); + } catch (error) { + const descriptor = `${lowerFirst(label)} > ${lowerFirst(currentLabel)}`; + + logLine(chalk.red(`❌ Error ${descriptor}.`)); + + throw new Error(`Failed ${descriptor}`, { cause: error }); + } finally { + const lineLength = stopWriting(); + readline.clearLine(process.stdout, -1); + readline.moveCursor(process.stdout, -lineLength, 0); + } + + lastLogged = chalk.gray(`${line} ✔️\n`); + + process.stdout.write(lastLogged); + } + + readline.moveCursor(process.stdout, -lastLogged.length, -tasks.length - 2); + readline.clearScreenDown(process.stdout); + + logNewSection(chalk.green(`✅ Passed ${lowerFirst(label)}.`)); +} diff --git a/src/shared/cli/startLineWithDots.ts b/src/shared/cli/startLineWithDots.ts new file mode 100644 index 0000000..e803781 --- /dev/null +++ b/src/shared/cli/startLineWithDots.ts @@ -0,0 +1,38 @@ +import readline from "readline"; + +export function startLineWithDots(line: string) { + const timer = [setTimeout(tick, 500)]; + let dots = 0; + let lastLogged!: string; + + function clearLine() { + readline.clearLine(process.stdout, -1); + readline.moveCursor(process.stdout, -lastLogged.length, 0); + } + + function writeLine() { + dots = (dots + 1) % 4; + + const toLog = `${line}${".".repeat(dots)}`; + + process.stdout.write(toLog); + + lastLogged = toLog; + + return toLog; + } + + function tick() { + clearLine(); + writeLine(); + timer[0] = setTimeout(tick, 500); + } + + writeLine(); + + return () => { + clearLine(); + clearInterval(timer[0]); + return lastLogged.length; + }; +} diff --git a/src/shared/codes.ts b/src/shared/codes.ts new file mode 100644 index 0000000..89b3957 --- /dev/null +++ b/src/shared/codes.ts @@ -0,0 +1,7 @@ +export const StatusCodes = { + Cancelled: 2, + Failure: 1, + Success: 0, +} as const; + +export type StatusCode = (typeof StatusCodes)[keyof typeof StatusCodes]; diff --git a/src/shared/createCleanupCommands.test.ts b/src/shared/createCleanupCommands.test.ts new file mode 100644 index 0000000..35ffbe3 --- /dev/null +++ b/src/shared/createCleanupCommands.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { createCleanupCommands } from "./createCleanupCommands.js"; + +describe("createCleanupCommands", () => { + it("only lints and formats when bin is not enabled and mode is initialize", () => { + const actual = createCleanupCommands({ + bin: undefined, + mode: "initialize", + }); + + expect(actual).toEqual(["pnpm lint --fix", "pnpm format --write"]); + }); + + it("runs dedupe and build before it lints and formats when bin is enabled and mode is create", () => { + const actual = createCleanupCommands({ + bin: "bin/index.js", + mode: "create", + }); + + expect(actual).toEqual([ + "pnpm dedupe", + "pnpm build --no-dts", + "pnpm lint --fix", + "pnpm format --write", + ]); + }); +}); diff --git a/src/shared/createCleanupCommands.ts b/src/shared/createCleanupCommands.ts new file mode 100644 index 0000000..43f24c2 --- /dev/null +++ b/src/shared/createCleanupCommands.ts @@ -0,0 +1,15 @@ +import { Options } from "./types.js"; + +export function createCleanupCommands({ + bin, + mode, +}: Pick) { + return [ + // There's no need to dedupe when initializing from the fixed template + ...(mode === "initialize" ? [] : ["pnpm dedupe"]), + // n/no-missing-import rightfully reports on a missing the bin .js file + ...(bin ? ["pnpm build --no-dts"] : []), + "pnpm lint --fix", + "pnpm format --write", + ]; +} diff --git a/src/shared/doesRepositoryExist.test.ts b/src/shared/doesRepositoryExist.test.ts new file mode 100644 index 0000000..d6a3db9 --- /dev/null +++ b/src/shared/doesRepositoryExist.test.ts @@ -0,0 +1,47 @@ +import { Octokit } from "octokit"; +import { MockInstance, describe, expect, it, vi } from "vitest"; + +import { doesRepositoryExist } from "./doesRepositoryExist.js"; + +const createMockOctokit = ( + repos: Partial>, +) => + ({ + rest: { + repos, + }, + }) as unknown as Octokit; + +const owner = "StubOwner"; +const repository = "stub-repository"; + +describe("doesRepositoryExist", () => { + it("returns true when the octokit GET resolves", async () => { + const octokit = createMockOctokit({ get: vi.fn().mockResolvedValue({}) }); + + const actual = await doesRepositoryExist(octokit, { owner, repository }); + + expect(actual).toBe(true); + }); + + it("returns false when the octokit GET rejects with a 404", async () => { + const octokit = createMockOctokit({ + get: vi.fn().mockRejectedValue({ status: 404 }), + }); + + const actual = await doesRepositoryExist(octokit, { owner, repository }); + + expect(actual).toBe(false); + }); + + it("throws the error when awaiting the octokit GET throws a non-404 error", async () => { + const error = new Error("Oh no!"); + const octokit = createMockOctokit({ + get: vi.fn().mockRejectedValue(error), + }); + + await expect( + async () => await doesRepositoryExist(octokit, { owner, repository }), + ).rejects.toEqual(error); + }); +}); diff --git a/src/shared/doesRepositoryExist.ts b/src/shared/doesRepositoryExist.ts new file mode 100644 index 0000000..8868a2f --- /dev/null +++ b/src/shared/doesRepositoryExist.ts @@ -0,0 +1,27 @@ +import { Octokit, RequestError } from "octokit"; + +export interface DoesRepositoryExistOptions { + owner: string; + repository: string; +} + +export async function doesRepositoryExist( + octokit: Octokit, + options: DoesRepositoryExistOptions, +) { + // Because the Octokit SDK throws on 404s (😡), + // we try/catch to check whether the repo exists. + try { + await octokit.rest.repos.get({ + owner: options.owner, + repo: options.repository, + }); + return true; + } catch (error) { + if ((error as RequestError).status !== 404) { + throw error; + } + + return false; + } +} diff --git a/src/shared/ensureGitRepository.test.ts b/src/shared/ensureGitRepository.test.ts new file mode 100644 index 0000000..a9b4b7e --- /dev/null +++ b/src/shared/ensureGitRepository.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it, vi } from "vitest"; + +import { ensureGitRepository } from "./ensureGitRepository.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +describe("ensureGitRepository", () => { + it("does not run git init when git status succeeds", async () => { + mock$.mockResolvedValue(0); + + await ensureGitRepository(); + + expect(mock$).toHaveBeenCalledTimes(1); + }); + + it("runs git init when git status fails", async () => { + mock$.mockRejectedValueOnce(1); + + await ensureGitRepository(); + + expect(mock$).toHaveBeenCalledWith(["git init"]); + }); +}); diff --git a/src/shared/ensureGitRepository.ts b/src/shared/ensureGitRepository.ts new file mode 100644 index 0000000..97ad707 --- /dev/null +++ b/src/shared/ensureGitRepository.ts @@ -0,0 +1,9 @@ +import { $ } from "execa"; + +export async function ensureGitRepository() { + try { + await $`git status`; + } catch { + await $`git init`; + } +} diff --git a/src/shared/generateNextSteps.test.ts b/src/shared/generateNextSteps.test.ts new file mode 100644 index 0000000..88c8381 --- /dev/null +++ b/src/shared/generateNextSteps.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "vitest"; + +import { generateNextSteps } from "./generateNextSteps.js"; +import { Options } from "./types.js"; + +const options = { + access: "public", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + mode: "create", + owner: "TestOwner", + repository: "test-repository", + title: "Test Title", +} satisfies Options; + +describe("generateNextSteps", () => { + for (const excludeAllContributors of [false, true]) { + for (const excludeReleases of [false, true]) { + for (const excludeRenovate of [false, true]) { + for (const excludeTests of [false, true]) { + test( + // eslint-disable-next-line vitest/valid-title + JSON.stringify({ + excludeAllContributors, + excludeReleases, + excludeRenovate, + excludeTests, + }), + () => { + expect( + generateNextSteps({ + ...options, + excludeAllContributors, + excludeReleases, + excludeRenovate, + excludeTests, + }), + ).toMatchSnapshot(); + }, + ); + } + } + } + } +}); diff --git a/src/shared/generateNextSteps.ts b/src/shared/generateNextSteps.ts new file mode 100644 index 0000000..c8eb8b9 --- /dev/null +++ b/src/shared/generateNextSteps.ts @@ -0,0 +1,69 @@ +import { OutroGroup } from "./cli/outro.js"; +import { Options } from "./types.js"; + +export function generateNextSteps(options: Options): OutroGroup[] { + const lines = [ + joinedMessage( + "enable the", + [ + options.excludeTests || ["Codecov", "https://github.com/apps/codecov"], + options.excludeRenovate || [ + "Renovate", + "https://github.com/apps/renovate", + ], + ], + "GitHub app", + ), + joinedMessage( + "populate the", + [ + (options.excludeAllContributors && options.excludeReleases) || [ + "ACCESS_TOKEN", + "a GitHub PAT with repo and workflow permissions", + ], + options.excludeReleases || [ + "NPM_TOKEN", + "an npm access token with automation permissions", + ], + ], + "secret", + ), + ].filter((line): line is string => !!line); + + switch (lines.length) { + case 0: + return []; + case 1: + return [{ label: `Be sure to ${lines[0]}` }]; + default: + return [ + { + label: "Be sure to:", + lines: lines.map((line) => `- ${line}`), + }, + ]; + } +} + +const itemBreak = "\n - "; + +function joinedMessage( + prefix: string, + entries: ([string, string] | true)[], + suffix: string, +) { + const realEntries = entries.filter( + (entry): entry is [string, string] => entry !== true, + ); + + switch (realEntries.length) { + case 0: + return undefined; + case 1: + return `${prefix} ${realEntries[0][0]} ${suffix} (${realEntries[0][1]}).`; + default: + return `${prefix} ${suffix}s: ${itemBreak}${realEntries + .map((entry) => `${entry[0]} (${entry[1]})`) + .join(itemBreak)}`; + } +} diff --git a/src/shared/getGitHubUserAsAllContributor.test.ts b/src/shared/getGitHubUserAsAllContributor.test.ts new file mode 100644 index 0000000..6b60918 --- /dev/null +++ b/src/shared/getGitHubUserAsAllContributor.test.ts @@ -0,0 +1,98 @@ +import chalk from "chalk"; +import { MockInstance, beforeEach, describe, expect, it, vi } from "vitest"; + +import { getGitHubUserAsAllContributor } from "./getGitHubUserAsAllContributor.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +let mockConsoleWarn: MockInstance; + +const owner = "TestOwner"; + +describe("getGitHubUserAsAllContributor", () => { + beforeEach(() => { + mockConsoleWarn = vi + .spyOn(console, "warn") + .mockImplementation(() => undefined); + }); + + it("defaults to owner with a log when options.offline is true", async () => { + const actual = await getGitHubUserAsAllContributor({ + offline: true, + owner, + }); + + expect(actual).toEqual(owner); + expect(mockConsoleWarn).toHaveBeenCalledWith( + chalk.gray( + `Skipping populating all-contributors contributions for TestOwner because in --offline mode.`, + ), + ); + }); + + it("uses the user from gh api user when it succeeds", async () => { + const login = "gh-api-user"; + + mock$.mockResolvedValueOnce({ + stdout: JSON.stringify({ login }), + }); + + await getGitHubUserAsAllContributor({ owner }); + + expect(mockConsoleWarn).not.toHaveBeenCalled(); + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh api user", + ], + ], + [ + [ + "npx -y all-contributors-cli@6.25 add ", + " ", + "", + ], + "gh-api-user", + "code,content,doc,ideas,infra,maintenance,projectManagement,tool", + ], + ] + `); + }); + + it("defaults the user to the owner when gh api user fails", async () => { + mock$.mockRejectedValueOnce({}); + + await getGitHubUserAsAllContributor({ owner }); + + expect(mockConsoleWarn).toHaveBeenCalledWith( + chalk.gray( + `Couldn't authenticate GitHub user, falling back to the provided owner name '${owner}'.`, + ), + ); + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh api user", + ], + ], + [ + [ + "npx -y all-contributors-cli@6.25 add ", + " ", + "", + ], + "TestOwner", + "code,content,doc,ideas,infra,maintenance,projectManagement,tool", + ], + ] + `); + }); +}); diff --git a/src/shared/getGitHubUserAsAllContributor.ts b/src/shared/getGitHubUserAsAllContributor.ts new file mode 100644 index 0000000..a290dde --- /dev/null +++ b/src/shared/getGitHubUserAsAllContributor.ts @@ -0,0 +1,48 @@ +import chalk from "chalk"; +import { $ } from "execa"; + +import { Options } from "./types.js"; + +interface GhUserOutput { + login: string; +} + +export async function getGitHubUserAsAllContributor( + options: Pick, +) { + if (options.offline) { + console.warn( + chalk.gray( + `Skipping populating all-contributors contributions for ${options.owner} because in --offline mode.`, + ), + ); + return options.owner; + } + + let user: string; + + try { + user = (JSON.parse((await $`gh api user`).stdout) as GhUserOutput).login; + } catch { + console.warn( + chalk.gray( + `Couldn't authenticate GitHub user, falling back to the provided owner name '${options.owner}'.`, + ), + ); + user = options.owner; + } + + const contributions = [ + "code", + "content", + "doc", + "ideas", + "infra", + "maintenance", + "projectManagement", + "tool", + ].join(","); + await $`npx -y all-contributors-cli@6.25 add ${user} ${contributions}`; + + return user; +} diff --git a/src/shared/options/args.ts b/src/shared/options/args.ts new file mode 100644 index 0000000..70c5978 --- /dev/null +++ b/src/shared/options/args.ts @@ -0,0 +1,316 @@ +import chalk from "chalk"; + +export const allArgOptions = { + access: { + description: `(${chalk.cyanBright( + '"public" | "restricted"', + )}): Which ${chalk.cyanBright("npm publish --access")} to + release npm packages with (by default, "public")`, + docsSection: "optional", + type: "string", + }, + author: { + description: `Username on npm to publish packages under (by + default, an existing npm author, or the currently logged in npm user, or + ${chalk.cyanBright("owner.toLowerCase()")})`, + docsSection: "optional", + type: "string", + }, + auto: { + description: `Whether to infer all options from files on disk.`, + docsSection: "optional", + type: "boolean", + }, + base: { + description: `Whether to scaffold the repository with: + • everything: that comes with the template (${chalk.cyanBright.bold( + "recommended", + )}) + • minimum: amounts of tooling, essentially opting out of everything + • prompt: for which portions to exclude`, + docsSection: "core", + type: "string", + }, + bin: { + description: `package.json bin value to include for npx-style running.`, + docsSection: "optional", + type: "string", + }, + "create-repository": { + description: `Whether to create a corresponding repository on github.com + (if it doesn't yet exist)`, + docsSection: "core", + type: "boolean", + }, + description: { + description: `Sentence case description of the repository + (e.g. A quickstart-friendly TypeScript package with lots of great + repository tooling. ✨)`, + docsSection: "core", + type: "string", + }, + directory: { + description: `Directory to create the repository in (by default, the same + name as the repository)`, + docsSection: "optional", + type: "string", + }, + email: { + description: `Email address to be listed as the point of contact in docs + and packages (e.g. example@joshuakgoldberg.com)`, + docsSection: "optional", + type: "string", + }, + "email-github": { + description: `Optionally, may be provided to use different emails in ${chalk.cyanBright( + ".md", + )} + files`, + docsSection: "optional", + type: "string", + }, + "email-npm": { + description: `Optionally, may be provided to use different emails in + ${chalk.cyanBright("package.json")}`, + docsSection: "optional", + type: "string", + }, + "exclude-all-contributors": { + description: `Don't add all-contributors to track contributions + and display them in a README.md table.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-compliance": { + description: `Don't add a GitHub Actions workflow to verify that PRs match + an expected format.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-deprecation": { + description: `Don't use eslint-plugin-deprecation to report on usage + of code marked as ${chalk.cyanBright("@deprecated")}.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-jsdoc": { + description: `Don't use eslint-plugin-jsdoc to enforce good practices around + JSDoc comments.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-json": { + description: `Don't apply linting and sorting to ${chalk.cyanBright( + "*.json", + )}, and + ${chalk.cyanBright("*.jsonc")} files.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-knip": { + description: `Don't add Knip to detect unused files, dependencies, and code + exports.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-md": { + description: `Don't apply linting to ${chalk.cyanBright("*.md")} files.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-package-json": { + description: `Don't add eslint-plugin-package-json to lint for + package.json correctness.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-packages": { + description: `Don't add a pnpm dedupe workflow to ensure packages + aren't duplicated unnecessarily.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-perfectionist": { + description: `Don't apply eslint-plugin-perfectionist to ensure + imports, keys, and so on are in sorted order.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-regex": { + description: `Don't add eslint-plugin-regex to enforce good practices around + regular expressions.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-spelling": { + description: `Don't add cspell to spell check against dictionaries + of known words.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-strict": { + description: `Don't augment the recommended logical lint rules with + typescript-eslint's strict config.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-stylistic": { + description: `Don't add stylistic rules such as typescript-eslint's + stylistic config.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-lint-yml": { + description: `Don't apply linting and sorting to ${chalk.cyanBright( + "*.yaml", + )} and ${chalk.cyanBright("*.yml")} files.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-releases": { + description: `Don't add release-it to generate changelogs, package bumps, + and publishes based on conventional commits.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-renovate": { + description: `Don't add a Renovate config to dependencies up-to-date with + PRs.`, + docsSection: "opt-out", + type: "boolean", + }, + "exclude-tests": { + description: `Don't add Vitest tooling for fast unit tests, configured + with coverage tracking.`, + docsSection: "opt-out", + type: "boolean", + }, + funding: { + description: `GitHub organization or username to mention in ${chalk.cyanBright( + "funding.yml", + )} + (by default, owner)`, + docsSection: "optional", + type: "string", + }, + guide: { + description: `Link to a contribution guide to place at the top of the + development docs`, + docsSection: "optional", + type: "string", + }, + "guide-title": { + description: `If ${chalk.cyanBright( + "--guide", + )} is provided or detected from an existing + DEVELOPMENT.md, the text title to place in the guide link`, + docsSection: "optional", + type: "string", + }, + keywords: { + description: `Any number of keywords to include in ${chalk.cyanBright( + "package.json", + )} (by default, + none). This can be specified any number of times, like + ${chalk.cyanBright('--keywords apple --keywords "banana cherry"')}`, + docsSection: "optional", + multiple: true, + type: "string", + }, + logo: { + description: `Local image file in the repository to display near the top of + the README.md as a logo`, + docsSection: "optional", + type: "string", + }, + "logo-alt": { + description: `If ${chalk.cyanBright( + "--logo", + )} is provided or detected from an existing README.md, + alt text that describes the image will be prompted for if not provided`, + docsSection: "optional", + type: "string", + }, + mode: { + description: `Whether to: + • create: a new repository in a child directory + • initialize: a freshly repository in the current directory + • migrate: an existing repository in the current directory`, + docsSection: "core", + type: "string", + }, + offline: { + description: `You can run ${chalk.cyanBright( + "create-typescript-app", + )} in an "offline" mode. + Doing so will: + • Enable ${chalk.cyanBright( + "--exclude-all-contributors-api", + )} and ${chalk.cyanBright("--skip-github-api")} + • Skip network calls when setting up contributors + • Run pnpm commands with pnpm's ${chalk.cyanBright("--offline")} mode`, + docsSection: "offline", + type: "boolean", + }, + owner: { + description: `GitHub organization or user the repository is underneath + (e.g. JoshuaKGoldberg)`, + docsSection: "core", + type: "string", + }, + "preserve-generated-form": { + description: `Whether to keep the GitHub repository generated from + notice (by default, false)`, + docsSection: "optional", + type: "boolean", + }, + repository: { + description: `The kebab-case name of the repository + (e.g. create-typescript-app)`, + docsSection: "core", + type: "string", + }, + "skip-all-contributors-api": { + description: `Skips network calls that fetch all-contributors data from + GitHub. This flag does nothing if ${chalk.cyanBright( + "--exclude-all-contributors", + )} was specified.`, + docsSection: "skip-net", + type: "boolean", + }, + "skip-github-api": { + description: `Skips calling to GitHub APIs.`, + docsSection: "skip-net", + type: "boolean", + }, + "skip-install": { + description: `Skips installing all the new template packages with pnpm.`, + docsSection: "skip-net", + type: "boolean", + }, + "skip-removal": { + description: `Skips removing setup docs and scripts, including this ${chalk.cyanBright( + "docs/", + )} + directory`, + docsSection: "skip-disk", + type: "boolean", + }, + "skip-restore": { + description: `Skips the prompt offering to restore the repository if an + error occurs during setup`, + docsSection: "skip-disk", + type: "boolean", + }, + "skip-uninstall": { + description: `Skips uninstalling packages only used for setup scripts`, + docsSection: "skip-disk", + type: "boolean", + }, + title: { + description: `Title Case title for the repository to be used in + documentation (e.g. Create TypeScript App)`, + docsSection: "core", + type: "string", + }, +} as const; diff --git a/src/shared/options/augmentOptionsWithExcludes.test.ts b/src/shared/options/augmentOptionsWithExcludes.test.ts new file mode 100644 index 0000000..1b6a107 --- /dev/null +++ b/src/shared/options/augmentOptionsWithExcludes.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../types.js"; +import { augmentOptionsWithExcludes } from "./augmentOptionsWithExcludes.js"; + +const mockMultiselect = vi.fn(); +const mockSelect = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + isCancel: () => false, + get multiselect() { + return mockMultiselect; + }, + get select() { + return mockSelect; + }, +})); + +const optionsBase = { + access: "public", + author: undefined, + base: undefined, + description: "", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + excludeAllContributors: undefined, + excludeCompliance: undefined, + excludeLintDeprecation: undefined, + excludeLintESLint: undefined, + excludeLintJSDoc: undefined, + excludeLintJson: undefined, + excludeLintKnip: undefined, + excludeLintMd: undefined, + excludeLintPackageJson: undefined, + excludeLintPackages: undefined, + excludeLintPerfectionist: undefined, + excludeLintRegex: undefined, + excludeLintSpelling: undefined, + excludeLintStrict: undefined, + excludeLintStylistic: undefined, + excludeLintYml: undefined, + excludeReleases: undefined, + excludeRenovate: undefined, + excludeTests: undefined, + funding: undefined, + logo: undefined, + mode: "create", + offline: true, + owner: "", + repository: "", + skipGitHubApi: false, + skipInstall: undefined, + skipRemoval: undefined, + skipRestore: undefined, + skipUninstall: undefined, + title: "", +} satisfies Options; + +describe("augmentOptionsWithExcludes", () => { + it("returns options directly when no exclusions are provided and 'base' is provided for the prompt", async () => { + const base = "everything"; + + mockSelect.mockResolvedValueOnce(base); + + const actual = await augmentOptionsWithExcludes(optionsBase); + + expect(actual).toEqual({ + ...optionsBase, + base, + }); + }); + + it("returns options based on the select when no exclusions are provided and 'prompt' is provided for the prompt", async () => { + const base = "prompt"; + + mockSelect.mockResolvedValueOnce(base); + mockMultiselect.mockResolvedValue([]); + + const actual = await augmentOptionsWithExcludes(optionsBase); + + expect(actual).toEqual({ + ...optionsBase, + base, + excludeAllContributors: true, + excludeCompliance: true, + excludeLintDeprecation: true, + excludeLintESLint: true, + excludeLintJSDoc: true, + excludeLintJson: true, + excludeLintKnip: true, + excludeLintMd: true, + excludeLintPackageJson: true, + excludeLintPackages: true, + excludeLintPerfectionist: true, + excludeLintRegex: true, + excludeLintSpelling: true, + excludeLintStrict: true, + excludeLintStylistic: true, + excludeLintYml: true, + excludeReleases: true, + excludeRenovate: true, + excludeTests: true, + }); + }); + + it("skips prompting returns options directly when exclusions are provided manually", async () => { + const options = { + ...optionsBase, + excludeCompliance: true, + } satisfies Options; + + const actual = await augmentOptionsWithExcludes(options); + + expect(actual).toBe(options); + }); + + it("uses the 'common' base without prompting when provided manually", async () => { + const options = { + ...optionsBase, + base: "common", + } satisfies Options; + + const actual = await augmentOptionsWithExcludes(options); + + expect(actual).toEqual({ + ...options, + excludeCompliance: true, + excludeLintDeprecation: true, + excludeLintESLint: true, + excludeLintJSDoc: true, + excludeLintJson: true, + excludeLintMd: true, + excludeLintPackageJson: true, + excludeLintPackages: true, + excludeLintPerfectionist: true, + excludeLintRegex: true, + excludeLintSpelling: true, + excludeLintStrict: true, + excludeLintStylistic: true, + excludeLintYml: true, + }); + }); + + it("uses the 'minimum' base without prompting when provided manually", async () => { + const options = { + ...optionsBase, + base: "minimum", + } satisfies Options; + + const actual = await augmentOptionsWithExcludes(options); + + expect(actual).toEqual({ + ...options, + excludeAllContributors: true, + excludeCompliance: true, + excludeLintDeprecation: true, + excludeLintESLint: true, + excludeLintJSDoc: true, + excludeLintJson: true, + excludeLintKnip: true, + excludeLintMd: true, + excludeLintPackageJson: true, + excludeLintPackages: true, + excludeLintPerfectionist: true, + excludeLintRegex: true, + excludeLintSpelling: true, + excludeLintStrict: true, + excludeLintStylistic: true, + excludeLintYml: true, + excludeReleases: true, + excludeRenovate: true, + excludeTests: true, + }); + }); + + it("uses the 'everything' base without prompting when provided manually", async () => { + const options = { + ...optionsBase, + base: "everything", + } satisfies Options; + + const actual = await augmentOptionsWithExcludes(options); + + expect(actual).toStrictEqual(options); + }); +}); diff --git a/src/shared/options/augmentOptionsWithExcludes.ts b/src/shared/options/augmentOptionsWithExcludes.ts new file mode 100644 index 0000000..4f42fa1 --- /dev/null +++ b/src/shared/options/augmentOptionsWithExcludes.ts @@ -0,0 +1,107 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; + +import { filterPromptCancel } from "../prompts.js"; +import { Options, OptionsBase } from "../types.js"; +import { + ExclusionKey, + exclusionDescriptions, + exclusionKeys, + getExclusions, +} from "./exclusionKeys.js"; + +export async function augmentOptionsWithExcludes( + options: Options, +): Promise { + if ( + Object.keys(options).some( + (key) => + key in exclusionDescriptions && + options[key as keyof typeof options] !== undefined, + ) + ) { + return options; + } + + const base = + options.base ?? + filterPromptCancel( + await prompts.select({ + initialValue: "common" as OptionsBase, + message: `How much tooling would you like the template to set up for you?`, + options: [ + { + label: makeLabel( + "everything", + "The most comprehensive tooling imaginable: sorting, spellchecking, and more!", + ), + value: "everything", + }, + { + hint: "recommended", + label: makeLabel( + "common", + "Bare starters plus testing and automation for all-contributors and releases.", + ), + value: "common", + }, + { + label: makeLabel( + "minimum", + "Just bare starter tooling: building, formatting, linting, and type checking.", + ), + value: "minimum", + }, + { + label: makeLabel("prompt", "(allow me to customize)"), + value: "prompt", + }, + ], + }), + ); + + switch (base) { + case undefined: + return undefined; + case "common": + case "minimum": + case "everything": + return { + ...options, + base, + ...getExclusions(options, base), + }; + case "prompt": + const exclusionsNotEnabled = new Set( + filterPromptCancel( + await prompts.multiselect({ + initialValues: exclusionKeys, + message: + "Select the tooling portions you'd like to remove. All are enabled by default. Press ↑ or ↓ to change the selected item, then space to select.", + options: Object.entries(exclusionDescriptions).map( + ([value, { hint, label }]) => ({ + hint, + label, + value: value as ExclusionKey, + }), + ), + }), + ), + ); + + return { + ...options, + base, + ...Object.fromEntries( + exclusionKeys.map( + (exclusionKey) => + [exclusionKey, !exclusionsNotEnabled.has(exclusionKey)] as const, + ), + ), + }; + } +} + +function makeLabel(label: string, message: string) { + return [chalk.bold(label), message].join("\t "); +} diff --git a/src/shared/options/createOptionDefaults/index.test.ts b/src/shared/options/createOptionDefaults/index.test.ts new file mode 100644 index 0000000..6876c25 --- /dev/null +++ b/src/shared/options/createOptionDefaults/index.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createOptionDefaults } from "./index.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockGitUrlParse = vi.fn(); + +vi.mock("git-url-parse", () => ({ + get default() { + return mockGitUrlParse; + }, +})); + +const mockNpmUser = vi.fn(); + +vi.mock("npm-user", () => ({ + get default() { + return mockNpmUser; + }, +})); + +const mockReadPackageData = vi.fn(); + +vi.mock("../../packages.js", () => ({ + get readPackageData() { + return mockReadPackageData; + }, +})); + +describe("createOptionDefaults", () => { + describe("bin", () => { + it("returns undefined when package data does not have a bin", async () => { + mockReadPackageData.mockResolvedValue({}); + + const actual = await createOptionDefaults().bin(); + + expect(actual).toBeUndefined(); + }); + + it("returns the bin when package data has a bin", async () => { + const bin = "./lib/index.js"; + + mockReadPackageData.mockResolvedValue({ bin }); + + const actual = await createOptionDefaults().bin(); + + expect(actual).toBe(bin); + }); + }); + + describe("email", () => { + it("returns the npm whoami email from npm when only an npm exists", async () => { + mock$.mockImplementation(([command]: string[]) => + command === "npm whoami" ? { stdout: "npm-username" } : undefined, + ); + mockNpmUser.mockImplementation((username: string) => ({ + email: `test@${username}.com`, + })); + + const actual = await createOptionDefaults().email(); + + expect(actual).toEqual({ + github: "test@npm-username.com", + npm: "test@npm-username.com", + }); + }); + + it("returns the npm whoami email from npm when only a package author email exists", async () => { + mock$.mockResolvedValue({ stdout: "" }); + mockReadPackageData.mockResolvedValue({ + author: { + email: "test@package.com", + }, + }); + + const actual = await createOptionDefaults().email(); + + expect(actual).toEqual({ + github: "test@package.com", + npm: "test@package.com", + }); + }); + + it("returns the git user email when only a git user email exists", async () => { + mock$.mockImplementation(([command]: string[]) => + command === "git config --get user.email" + ? { stdout: "test@git.com" } + : undefined, + ); + mockReadPackageData.mockResolvedValue({}); + + const actual = await createOptionDefaults().email(); + + expect(actual).toEqual({ + github: "test@git.com", + npm: "test@git.com", + }); + }); + + it("returns both the git user email and the npm user email when both exist", async () => { + mock$.mockImplementation(([command]: string[]) => ({ + stdout: + command === "git config --get user.email" + ? "test@git.com" + : "npm-username", + })); + mockReadPackageData.mockResolvedValue({}); + + const actual = await createOptionDefaults().email(); + + expect(actual).toEqual({ + github: "test@git.com", + npm: "test@npm-username.com", + }); + }); + + it("returns undefined when neither git nor npm emails exist", async () => { + mock$.mockResolvedValue({ stdout: "" }); + mockReadPackageData.mockResolvedValue({}); + + const actual = await createOptionDefaults().email(); + + expect(actual).toBeUndefined(); + }); + }); + + describe("repository", () => { + it("returns promptedOptions.repository when it exists", async () => { + const repository = "test-prompted-repository"; + const promptedOptions = { repository }; + const actual = await createOptionDefaults(promptedOptions).repository(); + + expect(actual).toBe(repository); + }); + + it("returns the Git name when it exists and promptedOptions.repository doesn't", async () => { + const name = "test-git-repository"; + mockGitUrlParse.mockResolvedValueOnce({ name }); + + const actual = await createOptionDefaults().repository(); + + expect(actual).toBe(name); + }); + + it("returns the package name when it exists and promptedOptions.repository a Git name don't", async () => { + const name = "test-package-name"; + mockReadPackageData.mockResolvedValueOnce({ name }); + + const actual = await createOptionDefaults().repository(); + + expect(actual).toBe(name); + }); + }); +}); diff --git a/src/shared/options/createOptionDefaults/index.ts b/src/shared/options/createOptionDefaults/index.ts new file mode 100644 index 0000000..a4f825a --- /dev/null +++ b/src/shared/options/createOptionDefaults/index.ts @@ -0,0 +1,65 @@ +import { $ } from "execa"; +import gitRemoteOriginUrl from "git-remote-origin-url"; +import gitUrlParse from "git-url-parse"; +import lazyValue from "lazy-value"; +import * as fs from "node:fs/promises"; +import npmUser from "npm-user"; + +import { readPackageData } from "../../packages.js"; +import { tryCatchAsync } from "../../tryCatchAsync.js"; +import { tryCatchLazyValueAsync } from "../../tryCatchLazyValueAsync.js"; +import { PromptedOptions } from "../../types.js"; +import { parsePackageAuthor } from "./parsePackageAuthor.js"; +import { readDefaultsFromDevelopment } from "./readDefaultsFromDevelopment.js"; +import { readDefaultsFromReadme } from "./readDefaultsFromReadme.js"; + +export function createOptionDefaults(promptedOptions?: PromptedOptions) { + const gitDefaults = tryCatchLazyValueAsync(async () => + gitUrlParse(await gitRemoteOriginUrl()), + ); + + const npmDefaults = tryCatchLazyValueAsync(async () => { + const whoami = (await $`npm whoami`).stdout; + return whoami ? await npmUser(whoami) : undefined; + }); + + const packageData = lazyValue(readPackageData); + const packageAuthor = lazyValue(async () => + parsePackageAuthor(await packageData()), + ); + + return { + author: async () => (await packageAuthor()).author ?? npmDefaults.name, + bin: async () => (await packageData()).bin, + description: async () => (await packageData()).description, + email: async () => { + const gitEmail = await tryCatchAsync( + async () => (await $`git config --get user.email`).stdout, + ); + const npmEmail = + (await npmDefaults())?.email ?? (await packageAuthor()).email; + + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + return gitEmail || npmEmail + ? { github: (gitEmail || npmEmail)!, npm: (npmEmail || gitEmail)! } + : undefined; + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + }, + funding: async () => + await tryCatchAsync( + async () => + (await fs.readFile(".github/FUNDING.yml")) + .toString() + .split(":")[1] + ?.trim(), + ), + owner: async () => + (await gitDefaults())?.organization ?? (await packageAuthor()).author, + repository: async () => + promptedOptions?.repository ?? + (await gitDefaults())?.name ?? + (await packageData()).name, + ...readDefaultsFromDevelopment(), + ...readDefaultsFromReadme(), + }; +} diff --git a/src/shared/options/createOptionDefaults/parsePackageAuthor.test.ts b/src/shared/options/createOptionDefaults/parsePackageAuthor.test.ts new file mode 100644 index 0000000..078768d --- /dev/null +++ b/src/shared/options/createOptionDefaults/parsePackageAuthor.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, vi } from "vitest"; + +import { parsePackageAuthor } from "./parsePackageAuthor.js"; + +const mockReadFileSafe = vi.fn(); + +vi.mock("../../shared/readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); + +describe("parsePackageAuthor", () => { + it.each([ + [{}, {}], + [{ author: "abc123" }, { author: "abc123" }], + [ + { author: "abc123", email: "def@ghi.com" }, + { author: "abc123 " }, + ], + [ + { author: "abc123", email: "def@ghi.com" }, + { author: "abc123 " }, + ], + [ + { author: "abc123", email: "def@ghi.com" }, + { author: { email: "def@ghi.com", name: "abc123" } }, + ], + ])("returns %s when given %s", (expected, packageData) => { + expect(parsePackageAuthor(packageData)).toEqual(expected); + }); +}); diff --git a/src/shared/options/createOptionDefaults/parsePackageAuthor.ts b/src/shared/options/createOptionDefaults/parsePackageAuthor.ts new file mode 100644 index 0000000..5fa833d --- /dev/null +++ b/src/shared/options/createOptionDefaults/parsePackageAuthor.ts @@ -0,0 +1,22 @@ +import parse from "parse-author"; + +import { PartialPackageData } from "../../types.js"; + +export function parsePackageAuthor(packageData: PartialPackageData) { + let packageAuthor: string | undefined; + let packageEmail: string | undefined; + + if (typeof packageData.author === "string") { + const parsedAuthor = parse(packageData.author); + packageAuthor = parsedAuthor.name; + packageEmail = parsedAuthor.email; + } else if (packageData.author) { + packageAuthor = packageData.author.name; + packageEmail = packageData.author.email; + } + + return { + author: packageAuthor, + email: packageEmail, + }; +} diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts new file mode 100644 index 0000000..e585f31 --- /dev/null +++ b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from "vitest"; + +import { readDefaultsFromDevelopment } from "./readDefaultsFromDevelopment.js"; + +const mockReadFileSafe = vi.fn(); + +vi.mock("../../readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); + +describe("readDefaultsFromDevelopment", () => { + describe("guide", () => { + it("defaults to undefined when .github/DEVELOPMENT.md cannot be found", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const guide = await readDefaultsFromDevelopment().guide(); + + expect(guide).toBeUndefined(); + }); + + it("reads guide when it exists", async () => { + mockReadFileSafe.mockResolvedValue(`# Development + +> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository). +> It'll walk you through the common activities you'll need to contribute. +`); + + const guide = await readDefaultsFromDevelopment().guide(); + + expect(guide).toBe( + "https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository", + ); + }); + }); + + describe("guideTitle", () => { + it("defaults to undefined when .github/DEVELOPMENT.md cannot be found", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const guideTitle = await readDefaultsFromDevelopment().guideTitle(); + + expect(guideTitle).toBeUndefined(); + }); + + it("reads guideTitle when it exists", async () => { + mockReadFileSafe.mockResolvedValue(`# Development + +> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository). +> It'll walk you through the common activities you'll need to contribute. +`); + + const guideTitle = await readDefaultsFromDevelopment().guideTitle(); + + expect(guideTitle).toBe( + "Contributing to a create-typescript-app Repository", + ); + }); + }); +}); diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts new file mode 100644 index 0000000..a682c96 --- /dev/null +++ b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts @@ -0,0 +1,20 @@ +import lazyValue from "lazy-value"; + +import { readFileSafe } from "../../readFileSafe.js"; + +export function readDefaultsFromDevelopment() { + const development = lazyValue( + async () => await readFileSafe(".github/DEVELOPMENT.md", ""), + ); + + const guideTag = lazyValue(async () => + (await development()).match( + /> .*guided walkthrough, see \[((?!\[).+)\]\((.+)\)/i, + ), + ); + + return { + guide: async () => (await guideTag())?.[2], + guideTitle: async () => (await guideTag())?.[1], + }; +} diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts new file mode 100644 index 0000000..ae708bc --- /dev/null +++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from "vitest"; + +import { readDefaultsFromReadme } from "./readDefaultsFromReadme.js"; + +const mockReadFileSafe = vi.fn(); + +vi.mock("../../readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); + +describe("readDefaultsFromReadme", () => { + describe("logo", () => { + it("defaults to undefined when it cannot be found", async () => { + mockReadFileSafe.mockResolvedValue("nothing."); + + const logo = await readDefaultsFromReadme().logo(); + + expect(logo).toBeUndefined(); + }); + + it("parses when found in an unquoted string", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const logo = await readDefaultsFromReadme().logo(); + + expect(logo).toBe("abc/def.jpg"); + }); + + it("parses when found in a single quoted string", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const logo = await readDefaultsFromReadme().logo(); + + expect(logo).toBe("abc/def.jpg"); + }); + + it("parses when found in a double quoted string", async () => { + mockReadFileSafe.mockResolvedValue(''); + + const logo = await readDefaultsFromReadme().logo(); + + expect(logo).toBe("abc/def.jpg"); + }); + }); + + describe("title", () => { + it("defaults to undefined when it cannot be found", async () => { + mockReadFileSafe.mockResolvedValue("nothing."); + + const title = await readDefaultsFromReadme().title(); + + expect(title).toBeUndefined(); + }); + + it('reads title as markdown from "README.md" when it exists', async () => { + mockReadFileSafe.mockResolvedValue("# My Awesome Package"); + + const title = await readDefaultsFromReadme().title(); + + expect(title).toBe("My Awesome Package"); + }); + + it('reads title as HTML from "README.md" when it exists', async () => { + mockReadFileSafe.mockResolvedValue( + '

My Awesome Package

', + ); + + const title = await readDefaultsFromReadme().title(); + + expect(title).toBe("My Awesome Package"); + }); + + it("returns undefined when title does not exist", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const title = await readDefaultsFromReadme().title(); + + expect(title).toBeUndefined(); + }); + }); +}); diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts new file mode 100644 index 0000000..27b339d --- /dev/null +++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts @@ -0,0 +1,23 @@ +import lazyValue from "lazy-value"; + +import { readFileSafe } from "../../readFileSafe.js"; + +export function readDefaultsFromReadme() { + const readme = lazyValue(async () => await readFileSafe("README.md", "")); + + const imageTag = lazyValue( + async () => (await readme()).match(//)?.[0], + ); + + return { + logo: async () => + (await imageTag()) + ?.match(/src\s*=(.+)?\/>/)?.[1] + ?.replaceAll(/^['"]|['"]$/g, ""), + title: async () => + (await readme()) + .match(/^(?:# |)(.*?)(?:<\/h1>)?$/i)?.[1] + ?.trim() + ?.replace(/<[^>]+(?:>|$)/g, ""), + }; +} diff --git a/src/shared/options/createRepositoryWithApi.test.ts b/src/shared/options/createRepositoryWithApi.test.ts new file mode 100644 index 0000000..5f4ca68 --- /dev/null +++ b/src/shared/options/createRepositoryWithApi.test.ts @@ -0,0 +1,71 @@ +import { Octokit } from "octokit"; +import { describe, expect, it, vi } from "vitest"; + +import { createRepositoryWithApi } from "./createRepositoryWithApi.js"; + +const options = { owner: "StubOwner", repository: "stub-repository" }; + +const mockCreateUsingTemplate = vi.fn(); +const mockCreateInOrg = vi.fn(); +const mockCreateForAuthenticatedUser = vi.fn(); +const mockGetAuthenticated = vi.fn(); + +const createMockOctokit = () => + ({ + rest: { + repos: { + createForAuthenticatedUser: mockCreateForAuthenticatedUser, + createInOrg: mockCreateInOrg, + createUsingTemplate: mockCreateUsingTemplate, + }, + users: { + getAuthenticated: mockGetAuthenticated, + }, + }, + }) as unknown as Octokit; + +describe("createRepositoryWithApi", () => { + it("creates using a template when preserveGeneratedFrom is true", async () => { + await createRepositoryWithApi(createMockOctokit(), { + ...options, + preserveGeneratedFrom: true, + }); + + expect(mockCreateForAuthenticatedUser).not.toHaveBeenCalled(); + expect(mockCreateInOrg).not.toHaveBeenCalled(); + expect(mockCreateUsingTemplate).toHaveBeenCalledWith({ + name: options.repository, + owner: options.owner, + template_owner: "JoshuaKGoldberg", + template_repo: "create-typescript-app", + }); + }); + + it("creates under the user when the user is the owner", async () => { + mockGetAuthenticated.mockResolvedValueOnce({ + data: { + login: options.owner, + }, + }); + await createRepositoryWithApi(createMockOctokit(), options); + + expect(mockCreateForAuthenticatedUser).toHaveBeenCalledWith({ + name: options.repository, + }); + expect(mockCreateInOrg).not.toHaveBeenCalled(); + expect(mockCreateUsingTemplate).not.toHaveBeenCalled(); + }); + + it("creates under an org when the user is not the owner", async () => { + const login = "other-user"; + mockGetAuthenticated.mockResolvedValueOnce({ data: { login } }); + await createRepositoryWithApi(createMockOctokit(), options); + + expect(mockCreateForAuthenticatedUser).not.toHaveBeenCalled(); + expect(mockCreateInOrg).toHaveBeenCalledWith({ + name: options.repository, + org: options.owner, + }); + expect(mockCreateUsingTemplate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/shared/options/createRepositoryWithApi.ts b/src/shared/options/createRepositoryWithApi.ts new file mode 100644 index 0000000..1a7a5bd --- /dev/null +++ b/src/shared/options/createRepositoryWithApi.ts @@ -0,0 +1,35 @@ +import { Octokit } from "octokit"; + +export interface CreateRepositoryWithApiOptions { + owner: string; + preserveGeneratedFrom?: boolean; + repository: string; +} + +export async function createRepositoryWithApi( + octokit: Octokit, + options: CreateRepositoryWithApiOptions, +) { + if (options.preserveGeneratedFrom) { + await octokit.rest.repos.createUsingTemplate({ + name: options.repository, + owner: options.owner, + template_owner: "JoshuaKGoldberg", + template_repo: "create-typescript-app", + }); + return; + } + + const currentUser = await octokit.rest.users.getAuthenticated(); + + if (currentUser.data.login === options.owner) { + await octokit.rest.repos.createForAuthenticatedUser({ + name: options.repository, + }); + } else { + await octokit.rest.repos.createInOrg({ + name: options.repository, + org: options.owner, + }); + } +} diff --git a/src/shared/options/detectEmailRedundancy.test.ts b/src/shared/options/detectEmailRedundancy.test.ts new file mode 100644 index 0000000..3091a60 --- /dev/null +++ b/src/shared/options/detectEmailRedundancy.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { detectEmailRedundancy } from "./detectEmailRedundancy.js"; + +describe("detectEmailRedundancy", () => { + it("returns undefined when only email is specified", () => { + expect(detectEmailRedundancy({ email: "test@email.com" })).toBeUndefined(); + }); + + it("returns undefined when email-github and email-npm are specified while email is not", () => { + expect( + detectEmailRedundancy({ + "email-github": "test@email.com", + "email-npm": "test@email.com", + }), + ).toBeUndefined(); + }); + + it("returns a complaint when email-github is specified while email and email-npm are not", () => { + expect( + detectEmailRedundancy({ + "email-github": "test@email.com", + }), + ).toBe( + "If --email-github is specified, either --email or --email-npm should be.", + ); + }); + + it("returns a complaint when email-npm is specified while email and email-github are not", () => { + expect( + detectEmailRedundancy({ + "email-npm": "test@email.com", + }), + ).toBe( + "If --email-npm is specified, either --email or --email-github should be.", + ); + }); +}); diff --git a/src/shared/options/detectEmailRedundancy.ts b/src/shared/options/detectEmailRedundancy.ts new file mode 100644 index 0000000..15044b4 --- /dev/null +++ b/src/shared/options/detectEmailRedundancy.ts @@ -0,0 +1,23 @@ +export interface EmailValues { + email?: boolean | string; + "email-github"?: boolean | string; + "email-npm"?: boolean | string; +} + +export function detectEmailRedundancy(values: EmailValues) { + if (values.email) { + return values["email-github"] && values["email-npm"] + ? "--email should not be specified if both --email-github and --email-npm are specified." + : undefined; + } + + if (values["email-github"] && !values["email-npm"]) { + return "If --email-github is specified, either --email or --email-npm should be."; + } + + if (values["email-npm"] && !values["email-github"]) { + return "If --email-npm is specified, either --email or --email-github should be."; + } + + return undefined; +} diff --git a/src/shared/options/ensureRepositoryExists.test.ts b/src/shared/options/ensureRepositoryExists.test.ts new file mode 100644 index 0000000..62de3f0 --- /dev/null +++ b/src/shared/options/ensureRepositoryExists.test.ts @@ -0,0 +1,169 @@ +import { Octokit } from "octokit"; +import { describe, expect, it, vi } from "vitest"; + +import { ensureRepositoryExists } from "./ensureRepositoryExists.js"; + +const mockSelect = vi.fn(); +const mockText = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + isCancel: vi.fn(), + outro: vi.fn(), + get select() { + return mockSelect; + }, + get text() { + return mockText; + }, +})); + +const mockDoesRepositoryExist = vi.fn(); + +vi.mock("../doesRepositoryExist.js", () => ({ + get doesRepositoryExist() { + return mockDoesRepositoryExist; + }, +})); + +const auth = "abc123"; +const owner = "StubOwner"; +const repository = "stub-repository"; + +const mockCreateRepositoryWithApi = vi.fn(); + +vi.mock("./createRepositoryWithApi.js", () => ({ + get createRepositoryWithApi() { + return mockCreateRepositoryWithApi; + }, +})); + +const createMockOctokit = () => ({}) as unknown as Octokit; + +describe("ensureRepositoryExists", () => { + it("returns the repository when octokit is undefined", async () => { + const actual = await ensureRepositoryExists(undefined, { + mode: "initialize", + owner, + repository, + }); + + expect(actual).toEqual({ octokit: undefined, repository }); + }); + + it("returns the repository when octokit is defined and the repository exists", async () => { + mockDoesRepositoryExist.mockResolvedValue(true); + const octokit = createMockOctokit(); + const actual = await ensureRepositoryExists( + { auth, octokit }, + { + mode: "initialize", + owner, + repository, + }, + ); + + expect(actual).toEqual({ github: { auth, octokit }, repository }); + }); + + it("creates a new repository when the prompt is 'create' and the repository does not exist", async () => { + const octokit = createMockOctokit(); + + mockDoesRepositoryExist.mockResolvedValue(false); + mockSelect.mockResolvedValue("create"); + + const actual = await ensureRepositoryExists( + { auth, octokit }, + { mode: "initialize", owner, repository }, + ); + + expect(actual).toEqual({ github: { auth, octokit }, repository }); + expect(mockCreateRepositoryWithApi).toHaveBeenCalledWith(octokit, { + owner, + preserveGeneratedFrom: undefined, + repository, + }); + }); + + it("defaults to creating a repository when mode is 'create'", async () => { + const octokit = createMockOctokit(); + + mockDoesRepositoryExist.mockResolvedValue(false); + + const actual = await ensureRepositoryExists( + { auth, octokit }, + { mode: "create", owner, repository }, + ); + + expect(actual).toEqual({ github: { auth, octokit }, repository }); + expect(mockCreateRepositoryWithApi).toHaveBeenCalledWith(octokit, { + owner, + preserveGeneratedFrom: undefined, + repository, + }); + expect(mockSelect).not.toHaveBeenCalled(); + }); + + it("returns the second repository when the prompt is 'different', the first repository does not exist, and the second repository exists", async () => { + const octokit = createMockOctokit(); + const newRepository = "new-repository"; + + mockDoesRepositoryExist + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + mockSelect.mockResolvedValueOnce("different"); + mockText.mockResolvedValue(newRepository); + + const actual = await ensureRepositoryExists( + { auth, octokit }, + { mode: "initialize", owner, repository }, + ); + + expect(actual).toEqual({ + github: { auth, octokit }, + repository: newRepository, + }); + expect(mockCreateRepositoryWithApi).not.toHaveBeenCalled(); + }); + + it("creates the second repository when the prompt is 'different', the first repository does not exist, and the second repository does not exist", async () => { + const octokit = createMockOctokit(); + const newRepository = "new-repository"; + + mockDoesRepositoryExist.mockResolvedValue(false); + mockSelect + .mockResolvedValueOnce("different") + .mockResolvedValueOnce("create"); + mockText.mockResolvedValue(newRepository); + + const actual = await ensureRepositoryExists( + { auth, octokit }, + { mode: "initialize", owner, repository }, + ); + + expect(actual).toEqual({ + github: { auth, octokit }, + repository: newRepository, + }); + expect(mockCreateRepositoryWithApi).toHaveBeenCalledWith(octokit, { + owner, + preserveGeneratedFrom: undefined, + repository: newRepository, + }); + }); + + it("switches octokit to undefined when the prompt is 'local' and the repository does not exist", async () => { + const octokit = createMockOctokit(); + + mockDoesRepositoryExist.mockResolvedValue(false); + mockSelect.mockResolvedValue("local"); + + const actual = await ensureRepositoryExists( + { auth, octokit }, + { mode: "initialize", owner, repository }, + ); + + expect(actual).toEqual({ octokit: undefined, repository }); + expect(mockCreateRepositoryWithApi).not.toHaveBeenCalled(); + }); +}); diff --git a/src/shared/options/ensureRepositoryExists.ts b/src/shared/options/ensureRepositoryExists.ts new file mode 100644 index 0000000..00644d1 --- /dev/null +++ b/src/shared/options/ensureRepositoryExists.ts @@ -0,0 +1,90 @@ +import * as prompts from "@clack/prompts"; + +import { doesRepositoryExist } from "../doesRepositoryExist.js"; +import { filterPromptCancel } from "../prompts.js"; +import { Options } from "../types.js"; +import { createRepositoryWithApi } from "./createRepositoryWithApi.js"; +import { GitHub } from "./getGitHub.js"; + +export type EnsureRepositoryExistsOptions = Pick< + Options, + "mode" | "owner" | "preserveGeneratedFrom" | "repository" +>; + +export interface RepositoryExistsResult { + github: GitHub | undefined; + repository: string; +} + +export async function ensureRepositoryExists( + github: GitHub | undefined, + options: EnsureRepositoryExistsOptions, +): Promise> { + // We'll only respect input options once before prompting for them + let { repository } = options; + let createRepository = options.mode === "create"; + + // We'll continuously pester the user for a repository + // until they bail, create a new one, or it exists. + while (github) { + if (await doesRepositoryExist(github.octokit, options)) { + return { github, repository }; + } + + const selection = createRepository + ? "create" + : filterPromptCancel( + (await prompts.select({ + message: `Repository ${options.repository} doesn't seem to exist under ${options.owner}. What would you like to do?`, + options: [ + { label: "Create a new repository", value: "create" }, + { + label: "Switch to a different repository name", + value: "different", + }, + { + label: "Keep changes local", + value: "local", + }, + { label: "Bail out and maybe try again later", value: "bail" }, + ], + })) as "bail" | "create" | "different" | "local", + ); + + createRepository = false; + + switch (selection) { + case undefined: + case "bail": + return {}; + + case "create": + await createRepositoryWithApi(github.octokit, { + owner: options.owner, + preserveGeneratedFrom: options.preserveGeneratedFrom, + repository, + }); + return { github, repository }; + + case "different": + const newRepository = filterPromptCancel( + await prompts.text({ + message: `What would you like to call the repository?`, + }), + ); + + if (!newRepository) { + return {}; + } + + repository = newRepository; + break; + + case "local": + github = undefined; + break; + } + } + + return { github, repository }; +} diff --git a/src/shared/options/exclusionKeys.ts b/src/shared/options/exclusionKeys.ts new file mode 100644 index 0000000..2789e80 --- /dev/null +++ b/src/shared/options/exclusionKeys.ts @@ -0,0 +1,148 @@ +import { Options, OptionsBase } from "../types.js"; + +export type ExclusionKey = keyof Options & `exclude${string}`; + +export interface ExclusionDescription { + hint: string; + label: string; + uncommon?: true; +} + +export const exclusionDescriptions: Record = + { + excludeAllContributors: { + hint: "--exclude-all-contributors", + label: + "Add all-contributors to track contributions and display them in a README.md table.", + }, + excludeCompliance: { + hint: "--exclude-compliance", + label: + "Add a GitHub Actions workflow to verify that PRs match an expected format.", + uncommon: true, + }, + excludeLintDeprecation: { + hint: "--exclude-lint-deprecation", + label: + "Include an eslint-plugin-deprecation to reports on usage of code marked as @deprecated.", + uncommon: true, + }, + excludeLintESLint: { + hint: "--exclude-lint-eslint", + label: + "Include eslint-plugin-eslint-comment to enforce good practices around ESLint comment directives.", + uncommon: true, + }, + excludeLintJSDoc: { + hint: "--exclude-lint-jsdoc", + label: + "Include eslint-plugin-jsdoc to enforce good practices around JSDoc comments.", + uncommon: true, + }, + excludeLintJson: { + hint: "--exclude-lint-json", + label: "Apply linting and sorting to *.json and *.jsonc files.", + uncommon: true, + }, + excludeLintKnip: { + hint: "--exclude-lint-knip", + label: "Add Knip to detect unused files, dependencies, and code exports.", + }, + excludeLintMd: { + hint: "--exclude-lint-md", + label: "Apply linting to *.md files.", + uncommon: true, + }, + excludeLintPackageJson: { + hint: "--exclude-lint-package-json", + label: + "Add eslint-plugin-package-json to lint for package.json correctness.", + uncommon: true, + }, + excludeLintPackages: { + hint: "--exclude-lint-packages", + label: + "Add a pnpm dedupe workflow to ensure packages aren't duplicated unnecessarily.", + uncommon: true, + }, + excludeLintPerfectionist: { + hint: "--exclude-lint-perfectionist", + label: + "Apply eslint-plugin-perfectionist to ensure imports, keys, and so on are in sorted order.", + uncommon: true, + }, + excludeLintRegex: { + hint: "--exclude-lint-regex", + label: + "Include eslint-plugin-regex to enforce good practices around regular expressions.", + uncommon: true, + }, + excludeLintSpelling: { + hint: "--exclude-lint-spelling", + label: "Add cspell to spell check against dictionaries of known words.", + uncommon: true, + }, + excludeLintStrict: { + hint: "--exclude-lint-strict", + label: + "Include strict logical lint rules such as typescript-eslint's strict config. ", + uncommon: true, + }, + excludeLintStylistic: { + hint: "--exclude-lint-stylistic", + label: + "Include stylistic lint rules such as typescript-eslint's stylistic config.", + uncommon: true, + }, + excludeLintYml: { + hint: "--exclude-lint-yml", + label: "Apply linting and sorting to *.yaml and *.yml files.", + uncommon: true, + }, + excludeReleases: { + hint: "--exclude-releases", + label: + "Add release-it to generate changelogs, package bumps, and publishes based on conventional commits.", + }, + excludeRenovate: { + hint: "--exclude-renovate", + label: "Add a Renovate config to keep dependencies up-to-date with PRs.", + }, + excludeTests: { + hint: "--exclude-tests", + label: + "Add Vitest tooling for fast unit tests, configured with coverage tracking.", + }, + }; + +export const exclusionKeys = Object.keys( + exclusionDescriptions, +) as ExclusionKey[]; + +export function getExclusions( + options: Partial, + base?: OptionsBase, +): Partial { + switch (base) { + case "common": + return { + ...Object.fromEntries( + exclusionKeys + .filter((exclusion) => exclusionDescriptions[exclusion].uncommon) + .map((exclusion) => [exclusion, options[exclusion] ?? true]), + ), + }; + case "minimum": + return { + ...Object.fromEntries( + exclusionKeys.map((exclusion) => [ + exclusion, + options[exclusion] ?? true, + ]), + ), + }; + // We only really care about exclusions on the common and minimum bases + default: + return {}; + } +} diff --git a/src/shared/options/getBase.test.ts b/src/shared/options/getBase.test.ts new file mode 100644 index 0000000..b131d33 --- /dev/null +++ b/src/shared/options/getBase.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getBase } from "./getBase.js"; + +const mockReadPackageData = vi.fn(); +vi.mock("../packages.js", () => ({ + get readPackageData() { + return mockReadPackageData; + }, +})); + +describe("getBase", () => { + it("should return minimum with minimum scripts", async () => { + mockReadPackageData.mockImplementationOnce(() => + Promise.resolve({ + scripts: { + build: "build", + lint: "lint", + test: "test", + }, + }), + ); + + expect(await getBase()).toBe("minimum"); + }); + + it("should return common with common scripts", async () => { + mockReadPackageData.mockImplementationOnce(() => + Promise.resolve({ + scripts: { + build: "build", + lint: "lint", + "lint:knip": "knip", + test: "test", + }, + }), + ); + + expect(await getBase()).toBe("common"); + }); + + it("should return everything with everything scripts", async () => { + mockReadPackageData.mockImplementationOnce(() => + Promise.resolve({ + scripts: { + build: "build", + lint: "lint", + "lint:knip": "knip", + "lint:md": "md", + "lint:packages": "packages", + "lint:spelling": "spelling", + test: "test", + }, + }), + ); + + expect(await getBase()).toBe("everything"); + }); +}); diff --git a/src/shared/options/getBase.ts b/src/shared/options/getBase.ts new file mode 100644 index 0000000..a092626 --- /dev/null +++ b/src/shared/options/getBase.ts @@ -0,0 +1,34 @@ +import { readPackageData } from "../packages.js"; +import { OptionsBase } from "../types.js"; + +const commonScripts = new Set(["lint:knip", "test"]); + +const everythingScripts = new Set([ + "lint:md", + "lint:packages", + "lint:spelling", +]); + +export async function getBase(): Promise { + const scripts = Object.keys((await readPackageData()).scripts ?? {}); + + if ( + scripts.reduce( + (acc, curr) => (everythingScripts.has(curr) ? acc + 1 : acc), + 0, + ) >= 3 + ) { + return "everything"; + } + + if ( + scripts.reduce( + (acc, curr) => (commonScripts.has(curr) ? acc + 1 : acc), + 0, + ) >= 2 + ) { + return "common"; + } + + return "minimum"; +} diff --git a/src/shared/options/getGitHub.test.ts b/src/shared/options/getGitHub.test.ts new file mode 100644 index 0000000..10dd11c --- /dev/null +++ b/src/shared/options/getGitHub.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getGitHub } from "./getGitHub.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +class MockOctokit { + readonly auth: string; + + constructor(options: { auth: string }) { + this.auth = options.auth; + } +} + +vi.mock("octokit", () => ({ + get Octokit() { + return MockOctokit; + }, +})); + +describe("getOctokit", () => { + it("throws an error when gh auth status fails", async () => { + mock$.mockRejectedValueOnce(new Error("Oh no!")); + + await expect(getGitHub).rejects.toMatchInlineSnapshot( + "[Error: GitHub authentication failed.]", + ); + }); + + it("returns a new Octokit when gh auth status succeeds", async () => { + const auth = "abc123"; + mock$.mockResolvedValueOnce({}).mockResolvedValueOnce({ stdout: auth }); + + const actual = await getGitHub(); + + expect(actual).toEqual({ auth, octokit: new MockOctokit({ auth }) }); + }); +}); diff --git a/src/shared/options/getGitHub.ts b/src/shared/options/getGitHub.ts new file mode 100644 index 0000000..1333690 --- /dev/null +++ b/src/shared/options/getGitHub.ts @@ -0,0 +1,22 @@ +import { $ } from "execa"; +import { Octokit } from "octokit"; + +export interface GitHub { + auth: string; + octokit: Octokit; +} + +export async function getGitHub(): Promise { + try { + await $`gh auth status`; + } catch (error) { + throw new Error("GitHub authentication failed.", { + cause: (error as Error).message, + }); + } + + const auth = (await $`gh auth token`).stdout.trim(); + const octokit = new Octokit({ auth }); + + return { auth, octokit }; +} diff --git a/src/shared/options/getPrefillOrPromptedOption.test.ts b/src/shared/options/getPrefillOrPromptedOption.test.ts new file mode 100644 index 0000000..ef190dd --- /dev/null +++ b/src/shared/options/getPrefillOrPromptedOption.test.ts @@ -0,0 +1,106 @@ +import { TextOptions } from "@clack/prompts"; +import { describe, expect, it, vi } from "vitest"; + +import { getPrefillOrPromptedOption } from "./getPrefillOrPromptedOption.js"; + +const mockText = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + isCancel: () => false, + get text() { + return mockText; + }, +})); + +describe("getPrefillOrPromptedValue", () => { + it("returns the provided when it exists", async () => { + const value = "Test Value"; + + const actual = await getPrefillOrPromptedOption({ + auto: true, + getDefaultValue: vi.fn().mockResolvedValue("default value"), + message: "Input message.", + name: "field", + provided: value, + }); + + expect(actual).toEqual({ error: undefined, value }); + }); + + it("returns the default value when auto is true and it exists", async () => { + const value = "Test Value"; + + const actual = await getPrefillOrPromptedOption({ + auto: true, + getDefaultValue: vi.fn().mockResolvedValue(value), + message: "Input message.", + name: "field", + }); + + expect(actual).toEqual({ error: undefined, value }); + }); + + it("returns an error when auto is true and no default value exists", async () => { + const actual = await getPrefillOrPromptedOption({ + auto: true, + getDefaultValue: vi.fn().mockResolvedValue(undefined), + message: "Input message.", + name: "field", + }); + + expect(actual).toEqual({ + error: "Could not infer a default value for field.", + value: undefined, + }); + }); + + it("provides no placeholder when one is not provided and auto is false", async () => { + const message = "Test message"; + + await getPrefillOrPromptedOption({ auto: false, message, name: "field" }); + + expect(mockText).toHaveBeenCalledWith({ + message, + placeholder: undefined, + validate: expect.any(Function), + }); + }); + + it("prompts with the default value as a placeholder when a placeholder function is provided and auto is false", async () => { + const message = "Test message"; + const placeholder = "Test placeholder"; + + await getPrefillOrPromptedOption({ + auto: false, + getDefaultValue: vi.fn().mockResolvedValue(placeholder), + message, + name: "field", + }); + + expect(mockText).toHaveBeenCalledWith({ + message, + placeholder, + validate: expect.any(Function), + }); + }); + + it("validates entered text when it's not blank and auto is false", async () => { + const message = "Test message"; + + await getPrefillOrPromptedOption({ auto: false, message, name: "field" }); + + const { validate } = (mockText.mock.calls[0] as [Required])[0]; + + expect(validate(message)).toBeUndefined(); + }); + + it("invalidates entered text when it's blank and auto is false", async () => { + const message = ""; + + await getPrefillOrPromptedOption({ auto: false, message, name: "field" }); + + const { validate } = (mockText.mock.calls[0] as [Required])[0]; + + expect(validate(message)).toBe("Please enter a value."); + }); +}); diff --git a/src/shared/options/getPrefillOrPromptedOption.ts b/src/shared/options/getPrefillOrPromptedOption.ts new file mode 100644 index 0000000..17384ba --- /dev/null +++ b/src/shared/options/getPrefillOrPromptedOption.ts @@ -0,0 +1,48 @@ +import * as prompts from "@clack/prompts"; + +import { filterPromptCancel } from "../prompts.js"; + +export interface GetPrefillOrPromptedOptionOptions { + auto: boolean; + getDefaultValue?: () => Promise; + message: string; + name: string; + provided?: string | undefined; +} + +export async function getPrefillOrPromptedOption({ + auto, + getDefaultValue, + message, + name, + provided, +}: GetPrefillOrPromptedOptionOptions) { + if (provided) { + return { value: provided }; + } + + const defaultValue = await getDefaultValue?.(); + + if (auto) { + return { + error: defaultValue + ? undefined + : `Could not infer a default value for ${name}.`, + value: defaultValue, + }; + } + + return { + value: filterPromptCancel( + await prompts.text({ + message, + placeholder: defaultValue, + validate: (val) => { + if (val.length === 0) { + return "Please enter a value."; + } + }, + }), + ), + }; +} diff --git a/src/shared/options/logInferredOptions.test.ts b/src/shared/options/logInferredOptions.test.ts new file mode 100644 index 0000000..f3a8ec9 --- /dev/null +++ b/src/shared/options/logInferredOptions.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it, vi } from "vitest"; + +import { InferredOptions, logInferredOptions } from "./logInferredOptions.js"; + +function makeProxy(receiver: T): T { + return new Proxy(receiver, { + get: () => makeProxy((input: string) => input), + }); +} + +vi.mock("chalk", () => ({ + default: makeProxy({}), +})); + +const mockLogLine = vi.fn(); + +vi.mock("../cli/lines.js", () => ({ + get logLine() { + return mockLogLine; + }, +})); + +const options = { + description: "Test description.", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + owner: "TestOwner", + repository: "test-repository", + title: "Test Title", +} satisfies InferredOptions; + +describe("logInferredOptions", () => { + it("logs the required inferred values when only they exist", () => { + logInferredOptions(options); + + expect(mockLogLine.mock.calls).toMatchInlineSnapshot(` + [ + [], + [ + "--auto inferred the following values:", + ], + [ + "- description: Test description.", + ], + [ + "- email-github: github@email.com", + ], + [ + "- email-npm: github@email.com", + ], + [ + "- owner: TestOwner", + ], + [ + "- repository: test-repository", + ], + [ + "- title: Test Title", + ], + ] + `); + }); + + it("logs additional and required inferred values when all they exist", () => { + logInferredOptions({ + ...options, + guide: { + href: "https://example.com/guide", + title: "Example Guide", + }, + logo: { + alt: "Logo text.", + src: "https://example.com/logo", + }, + }); + + expect(mockLogLine.mock.calls).toMatchInlineSnapshot(` + [ + [], + [ + "--auto inferred the following values:", + ], + [ + "- description: Test description.", + ], + [ + "- email-github: github@email.com", + ], + [ + "- email-npm: github@email.com", + ], + [ + "- guide: https://example.com/guide", + ], + [ + "- guide-title: Example Guide", + ], + [ + "- logo: https://example.com/logo", + ], + [ + "- logo-alt: Logo text.", + ], + [ + "- owner: TestOwner", + ], + [ + "- repository: test-repository", + ], + [ + "- title: Test Title", + ], + ] + `); + }); +}); diff --git a/src/shared/options/logInferredOptions.ts b/src/shared/options/logInferredOptions.ts new file mode 100644 index 0000000..dc0bacd --- /dev/null +++ b/src/shared/options/logInferredOptions.ts @@ -0,0 +1,31 @@ +import chalk from "chalk"; + +import { logLine } from "../cli/lines.js"; +import { Options } from "../types.js"; + +export type InferredOptions = Pick< + Options, + "description" | "email" | "guide" | "logo" | "owner" | "repository" | "title" +>; + +export function logInferredOptions(augmentedOptions: InferredOptions) { + logLine(); + logLine(chalk.gray("--auto inferred the following values:")); + logLine(chalk.gray(`- description: ${augmentedOptions.description}`)); + logLine(chalk.gray(`- email-github: ${augmentedOptions.email.github}`)); + logLine(chalk.gray(`- email-npm: ${augmentedOptions.email.github}`)); + + if (augmentedOptions.guide) { + logLine(chalk.gray(`- guide: ${augmentedOptions.guide.href}`)); + logLine(chalk.gray(`- guide-title: ${augmentedOptions.guide.title}`)); + } + + if (augmentedOptions.logo) { + logLine(chalk.gray(`- logo: ${augmentedOptions.logo.src}`)); + logLine(chalk.gray(`- logo-alt: ${augmentedOptions.logo.alt}`)); + } + + logLine(chalk.gray(`- owner: ${augmentedOptions.owner}`)); + logLine(chalk.gray(`- repository: ${augmentedOptions.repository}`)); + logLine(chalk.gray(`- title: ${augmentedOptions.title}`)); +} diff --git a/src/shared/options/optionsSchema.ts b/src/shared/options/optionsSchema.ts new file mode 100644 index 0000000..04f965d --- /dev/null +++ b/src/shared/options/optionsSchema.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +export const optionsSchemaShape = { + access: z.union([z.literal("public"), z.literal("restricted")]).optional(), + author: z.string().optional(), + auto: z.boolean().optional(), + base: z + .union([ + z.literal("common"), + z.literal("everything"), + z.literal("minimum"), + z.literal("prompt"), + ]) + .optional(), + bin: z.string().optional(), + description: z.string().optional(), + directory: z.string().optional(), + email: z + .object({ + github: z.string().email(), + npm: z.string().email(), + }) + .optional(), + excludeAllContributors: z.boolean().optional(), + excludeCompliance: z.boolean().optional(), + excludeLintDeprecation: z.boolean().optional(), + excludeLintESLint: z.boolean().optional(), + excludeLintJSDoc: z.boolean().optional(), + excludeLintJson: z.boolean().optional(), + excludeLintKnip: z.boolean().optional(), + excludeLintMd: z.boolean().optional(), + excludeLintPackageJson: z.boolean().optional(), + excludeLintPackages: z.boolean().optional(), + excludeLintPerfectionist: z.boolean().optional(), + excludeLintRegex: z.boolean().optional(), + excludeLintSpelling: z.boolean().optional(), + excludeLintStrict: z.boolean().optional(), + excludeLintStylistic: z.boolean().optional(), + excludeLintYml: z.boolean().optional(), + excludeReleases: z.boolean().optional(), + excludeRenovate: z.boolean().optional(), + excludeTests: z.boolean().optional(), + funding: z.string().optional(), + guide: z.string().url().optional(), + guideTitle: z.string().optional(), + keywords: z.array(z.string()).optional(), + logo: z.string().optional(), + logoAlt: z.string().optional(), + mode: z + .union([z.literal("create"), z.literal("initialize"), z.literal("migrate")]) + .optional(), + offline: z.boolean().optional(), + owner: z.string().optional(), + preserveGeneratedFrom: z.boolean().optional(), + repository: z.string().optional(), + skipAllContributorsApi: z.boolean().optional(), + skipGitHubApi: z.boolean().optional(), + skipInstall: z.boolean().optional(), + skipRemoval: z.boolean().optional(), + skipRestore: z.boolean().optional(), + skipUninstall: z.boolean().optional(), + title: z.string().optional(), +}; + +export const optionsSchema = z.object(optionsSchemaShape); diff --git a/src/shared/options/readOptions.test.ts b/src/shared/options/readOptions.test.ts new file mode 100644 index 0000000..7849dff --- /dev/null +++ b/src/shared/options/readOptions.test.ts @@ -0,0 +1,778 @@ +import { describe, expect, it, vi } from "vitest"; +import z from "zod"; + +import { Options } from "../types.js"; +import { GetPrefillOrPromptedOptionOptions } from "./getPrefillOrPromptedOption.js"; +import { optionsSchemaShape } from "./optionsSchema.js"; +import { readOptions } from "./readOptions.js"; + +const emptyOptions = { + access: undefined, + author: undefined, + auto: false, + base: undefined, + bin: undefined, + description: undefined, + directory: undefined, + email: undefined, + excludeAllContributors: undefined, + excludeCompliance: undefined, + excludeLintDeprecation: undefined, + excludeLintESLint: undefined, + excludeLintJSDoc: undefined, + excludeLintJson: undefined, + excludeLintKnip: undefined, + excludeLintMd: undefined, + excludeLintPackageJson: undefined, + excludeLintPackages: undefined, + excludeLintPerfectionist: undefined, + excludeLintRegex: undefined, + excludeLintSpelling: undefined, + excludeLintStrict: undefined, + excludeLintYml: undefined, + excludeReleases: undefined, + excludeRenovate: undefined, + excludeTests: undefined, + funding: undefined, + guide: undefined, + guideTitle: undefined, + logo: undefined, + logoAlt: undefined, + offline: undefined, + owner: undefined, + preserveGeneratedFrom: false, + repository: undefined, + skipAllContributorsApi: undefined, + skipGitHubApi: undefined, + skipInstall: undefined, + skipRemoval: undefined, + skipRestore: undefined, + skipUninstall: undefined, + title: undefined, +}; + +const mockOptions = { + base: "prompt", + github: "mock.git", + repository: "mock.repository", +}; + +vi.mock("../cli/spinners.ts", () => ({ + withSpinner() { + return () => ({}); + }, +})); + +const mockReadPackageData = vi.fn(); + +vi.mock("../packages.js", () => ({ + get readPackageData() { + return mockReadPackageData; + }, +})); + +const mockAugmentOptionsWithExcludes = vi.fn(); + +vi.mock("./augmentOptionsWithExcludes.js", () => ({ + get augmentOptionsWithExcludes() { + return mockAugmentOptionsWithExcludes; + }, +})); + +const mockDetectEmailRedundancy = vi.fn(); + +vi.mock("./detectEmailRedundancy.js", () => ({ + get detectEmailRedundancy() { + return mockDetectEmailRedundancy; + }, +})); + +const mockGetPrefillOrPromptedOption = vi.fn(); + +vi.mock("./getPrefillOrPromptedOption.js", () => ({ + get getPrefillOrPromptedOption() { + return mockGetPrefillOrPromptedOption; + }, +})); + +const mockEnsureRepositoryExists = vi.fn(); + +vi.mock("./ensureRepositoryExists.js", () => ({ + get ensureRepositoryExists() { + return mockEnsureRepositoryExists; + }, +})); + +vi.mock("./getGitHub.js", () => ({ + getGitHub() { + return undefined; + }, +})); + +vi.mock("./createOptionDefaults/index.js", () => ({ + createOptionDefaults() { + return { + author: vi.fn(), + description: vi.fn(), + email: vi.fn(), + funding: vi.fn(), + logo: vi.fn(), + owner: vi.fn(), + repository: vi.fn(), + title: vi.fn(), + }; + }, +})); + +const mockLogInferredOptions = vi.fn(); + +vi.mock("./logInferredOptions.js", () => ({ + get logInferredOptions() { + return mockLogInferredOptions; + }, +})); + +describe("readOptions", () => { + it("returns a cancellation when an arg is invalid", async () => { + const validationResult = z + .object({ base: optionsSchemaShape.base }) + .safeParse({ base: "b" }); + + const actual = await readOptions(["--base", "b"], "create"); + + expect(actual).toStrictEqual({ + cancelled: true, + error: (validationResult as z.SafeParseError<{ base: string }>).error, + options: { ...emptyOptions, base: "b" }, + }); + }); + + it("returns a cancellation when an email redundancy is detected", async () => { + const error = "Too many emails!"; + mockDetectEmailRedundancy.mockReturnValue(error); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: undefined, + })); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error, + options: { + ...emptyOptions, + }, + }); + }); + + it("returns a cancellation when the owner prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: undefined, + })); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + }, + }); + }); + + it("returns a cancellation when the repository prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementation(() => ({ value: undefined })); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + owner: "MockOwner", + }, + }); + }); + + it("returns a cancellation when ensureRepositoryPrompt does not return a repository", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({}); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + owner: "MockOwner", + repository: "MockRepository", + }, + }); + }); + + it("returns a cancellation when the description prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + owner: "MockOwner", + repository: "MockRepository", + }, + }); + }); + + it("returns a cancellation when the title prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + description: "Mock description.", + owner: "MockOwner", + repository: "MockRepository", + }, + }); + }); + + it("returns a cancellation when the guide title prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock Title" })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect( + await readOptions(["--guide", "https://example.com"], "create"), + ).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + description: "Mock description.", + guide: "https://example.com", + owner: "MockOwner", + repository: "MockRepository", + title: "Mock Title", + }, + }); + }); + + it("returns a cancellation when the guide alt prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock Title" })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect( + await readOptions(["--guide", "https://example.com"], "create"), + ).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + description: "Mock description.", + guide: "https://example.com", + owner: "MockOwner", + repository: "MockRepository", + title: "Mock Title", + }, + }); + }); + + it("returns a cancellation when the logo alt prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect(await readOptions(["--logo", "logo.svg"], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + description: "Mock description.", + logo: "logo.svg", + owner: "MockOwner", + repository: "MockRepository", + }, + }); + }); + + it("returns a cancellation when the email prompt is cancelled", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock title." })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + description: "Mock description.", + owner: "MockOwner", + repository: "MockRepository", + title: "Mock title.", + }, + }); + }); + + it("returns a cancellation when augmentOptionsWithExcludes returns undefined", async () => { + mockDetectEmailRedundancy.mockReturnValue(false); + mockGetPrefillOrPromptedOption + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock title." })) + .mockImplementation(() => ({ value: undefined })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + mockAugmentOptionsWithExcludes.mockResolvedValue(undefined); + + expect(await readOptions([], "create")).toStrictEqual({ + cancelled: true, + error: undefined, + options: { + ...emptyOptions, + description: "Mock description.", + owner: "MockOwner", + repository: "MockRepository", + title: "Mock title.", + }, + }); + }); + + it("returns success options when --base is valid", async () => { + mockAugmentOptionsWithExcludes.mockResolvedValue({ + ...emptyOptions, + ...mockOptions, + }); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect( + await readOptions(["--base", mockOptions.base], "create"), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: { + ...emptyOptions, + ...mockOptions, + }, + }); + }); + + it("returns success options when --base is valid with all optional options", async () => { + mockAugmentOptionsWithExcludes.mockResolvedValue({ + ...emptyOptions, + ...mockOptions, + }); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect( + await readOptions( + [ + "--base", + mockOptions.base, + "--guide", + "https://example.com", + "--logo", + "logo.svg", + ], + "create", + ), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: { + ...emptyOptions, + ...mockOptions, + }, + }); + }); + + it("returns cancelled options when augmentOptionsWithExcludes returns undefined", async () => { + mockAugmentOptionsWithExcludes.mockResolvedValue(undefined); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); + + expect( + await readOptions(["--base", mockOptions.base], "create"), + ).toStrictEqual({ + cancelled: true, + options: { + ...emptyOptions, + base: mockOptions.base, + description: "mock", + owner: "mock", + repository: "mock", + title: "mock", + }, + }); + }); + + it("defaults preserveGeneratedFrom to false when the owner is not JoshuaKGoldberg", async () => { + mockAugmentOptionsWithExcludes.mockImplementationOnce( + (options: Partial) => ({ + ...options, + ...mockOptions, + }), + ); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); + + expect( + await readOptions(["--base", mockOptions.base], "create"), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: expect.objectContaining({ + preserveGeneratedFrom: false, + }), + }); + }); + + it("defaults preserveGeneratedFrom to true when the owner is JoshuaKGoldberg", async () => { + mockAugmentOptionsWithExcludes.mockImplementationOnce( + (options: Partial) => ({ + ...options, + ...mockOptions, + }), + ); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); + + expect( + await readOptions( + ["--base", mockOptions.base, "--owner", "JoshuaKGoldberg"], + "create", + ), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: expect.objectContaining({ + preserveGeneratedFrom: true, + }), + }); + }); + + it("skips API calls when --offline is true", async () => { + mockAugmentOptionsWithExcludes.mockImplementation((options: Options) => ({ + ...emptyOptions, + ...mockOptions, + ...options, + })); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); + mockEnsureRepositoryExists.mockResolvedValue({ + github: mockOptions.github, + repository: mockOptions.repository, + }); + + expect( + await readOptions(["--base", mockOptions.base, "--offline"], "create"), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: { + ...emptyOptions, + ...mockOptions, + access: "public", + description: "mock", + directory: "mock", + email: { + github: "mock", + npm: "mock", + }, + guide: undefined, + logo: undefined, + mode: "create", + offline: true, + owner: "mock", + skipAllContributorsApi: true, + skipGitHubApi: true, + title: "mock", + }, + }); + }); + + it("infers base from package scripts during migration", async () => { + mockReadPackageData.mockImplementationOnce(() => + Promise.resolve({ + scripts: { + build: "build", + lint: "lint", + test: "test", + }, + }), + ); + expect(await readOptions(["--offline"], "migrate")).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: { + ...emptyOptions, + ...mockOptions, + access: "public", + base: "minimum", + description: "mock", + directory: "mock", + email: { + github: "mock", + npm: "mock", + }, + guide: undefined, + logo: undefined, + mode: "migrate", + offline: true, + owner: "mock", + skipAllContributorsApi: true, + skipGitHubApi: true, + title: "mock", + }, + }); + }); + + it("uses values when provided on the CLI", async () => { + mockReadPackageData.mockImplementationOnce(() => ({})); + mockGetPrefillOrPromptedOption.mockImplementation( + async ({ getDefaultValue }: GetPrefillOrPromptedOptionOptions) => ({ + value: (await getDefaultValue?.()) ?? "mock", + }), + ); + + const description = "Test description."; + const owner = "TestOwner"; + const repository = "test-repository"; + const title = "Test Title"; + + expect( + await readOptions( + [ + "--offline", + "--description", + description, + "--owner", + owner, + "--repository", + repository, + "--title", + title, + ], + "migrate", + ), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: { + ...emptyOptions, + ...mockOptions, + access: "public", + base: "minimum", + description, + directory: repository, + email: { + github: "mock", + npm: "mock", + }, + guide: undefined, + logo: undefined, + mode: "migrate", + offline: true, + owner, + skipAllContributorsApi: true, + skipGitHubApi: true, + title, + }, + }); + + expect(mockLogInferredOptions).not.toHaveBeenCalled(); + }); + + it("uses and logs values when provided on the CLI with auto", async () => { + mockReadPackageData.mockImplementationOnce(() => ({})); + mockGetPrefillOrPromptedOption.mockImplementation( + async ({ getDefaultValue }: GetPrefillOrPromptedOptionOptions) => ({ + value: (await getDefaultValue?.()) ?? "mock", + }), + ); + + const description = "Test description."; + const owner = "TestOwner"; + const repository = "test-repository"; + const title = "Test Title"; + + expect( + await readOptions( + [ + "--auto", + "--offline", + "--description", + description, + "--owner", + owner, + "--repository", + repository, + "--title", + title, + ], + "migrate", + ), + ).toStrictEqual({ + cancelled: false, + github: mockOptions.github, + options: { + ...emptyOptions, + ...mockOptions, + access: "public", + auto: true, + base: "minimum", + description, + directory: repository, + email: { + github: "mock", + npm: "mock", + }, + guide: undefined, + logo: undefined, + mode: "migrate", + offline: true, + owner, + skipAllContributorsApi: true, + skipGitHubApi: true, + title, + }, + }); + + expect(mockLogInferredOptions.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "access": "public", + "author": undefined, + "auto": true, + "base": "minimum", + "bin": undefined, + "description": "Test description.", + "directory": "test-repository", + "email": { + "github": "mock", + "npm": "mock", + }, + "excludeAllContributors": undefined, + "excludeCompliance": undefined, + "excludeLintDeprecation": undefined, + "excludeLintESLint": undefined, + "excludeLintJSDoc": undefined, + "excludeLintJson": undefined, + "excludeLintKnip": undefined, + "excludeLintMd": undefined, + "excludeLintPackageJson": undefined, + "excludeLintPackages": undefined, + "excludeLintPerfectionist": undefined, + "excludeLintRegex": undefined, + "excludeLintSpelling": undefined, + "excludeLintStrict": undefined, + "excludeLintYml": undefined, + "excludeReleases": undefined, + "excludeRenovate": undefined, + "excludeTests": undefined, + "funding": undefined, + "github": "mock.git", + "guide": undefined, + "guideTitle": undefined, + "logo": undefined, + "logoAlt": undefined, + "mode": "migrate", + "offline": true, + "owner": "TestOwner", + "preserveGeneratedFrom": false, + "repository": "mock.repository", + "skipAllContributorsApi": true, + "skipGitHubApi": true, + "skipInstall": undefined, + "skipRemoval": undefined, + "skipRestore": undefined, + "skipUninstall": undefined, + "title": "Test Title", + }, + ], + ] + `); + }); +}); diff --git a/src/shared/options/readOptions.ts b/src/shared/options/readOptions.ts new file mode 100644 index 0000000..f813cab --- /dev/null +++ b/src/shared/options/readOptions.ts @@ -0,0 +1,297 @@ +import { parseArgs } from "node:util"; +import { titleCase } from "title-case"; +import { z } from "zod"; + +import { withSpinner } from "../cli/spinners.js"; +import { Mode, OptionsGuide, PromptedOptions } from "../types.js"; +import { Options, OptionsLogo } from "../types.js"; +import { allArgOptions } from "./args.js"; +import { augmentOptionsWithExcludes } from "./augmentOptionsWithExcludes.js"; +import { createOptionDefaults } from "./createOptionDefaults/index.js"; +import { detectEmailRedundancy } from "./detectEmailRedundancy.js"; +import { ensureRepositoryExists } from "./ensureRepositoryExists.js"; +import { getBase } from "./getBase.js"; +import { GitHub, getGitHub } from "./getGitHub.js"; +import { getPrefillOrPromptedOption } from "./getPrefillOrPromptedOption.js"; +import { logInferredOptions } from "./logInferredOptions.js"; +import { optionsSchema } from "./optionsSchema.js"; + +export interface GitHubAndOptions { + github: GitHub | undefined; + options: Options; +} + +export interface OptionsParseCancelled { + cancelled: true; + error?: string | z.ZodError; + options: object; +} + +export interface OptionsParseSuccess extends GitHubAndOptions { + cancelled: false; +} + +export type OptionsParseResult = OptionsParseCancelled | OptionsParseSuccess; + +export async function readOptions( + args: string[], + mode: Mode, + promptedOptions: PromptedOptions = {}, +): Promise { + const defaults = createOptionDefaults(promptedOptions); + const { values } = parseArgs({ + args, + options: allArgOptions, + strict: false, + tokens: true, + }); + + if (mode === "migrate" && !values.base) { + values.base = await getBase(); + } + + const mappedOptions = { + access: values.access, + author: values.author, + auto: !!values.auto, + base: values.base, + bin: values.bin, + description: values.description, + directory: values.directory, + email: + values.email ?? values["email-github"] ?? values["email-npm"] + ? { + github: values.email ?? values["email-github"], + npm: values.email ?? values["email-npm"], + } + : undefined, + excludeAllContributors: values["exclude-all-contributors"], + excludeCompliance: values["exclude-compliance"], + excludeLintDeprecation: values["exclude-lint-deprecation"], + excludeLintESLint: values["exclude-lint-eslint"], + excludeLintJSDoc: values["exclude-lint-jsdoc"], + excludeLintJson: values["exclude-lint-json"], + excludeLintKnip: values["exclude-lint-knip"], + excludeLintMd: values["exclude-lint-md"], + excludeLintPackageJson: values["exclude-lint-package-json"], + excludeLintPackages: values["exclude-lint-packages"], + excludeLintPerfectionist: values["exclude-lint-perfectionist"], + excludeLintRegex: values["exclude-lint-regex"], + excludeLintSpelling: values["exclude-lint-spelling"], + excludeLintStrict: values["exclude-lint-strict"], + excludeLintYml: values["exclude-lint-yml"], + excludeReleases: values["exclude-releases"], + excludeRenovate: values["exclude-renovate"], + excludeTests: values["unit-tests"], + funding: values.funding, + guide: values.guide, + guideTitle: values["guide-title"], + logo: values.logo, + logoAlt: values["logo-alt"], + offline: values.offline, + owner: values.owner, + preserveGeneratedFrom: + values["preserve-generated-from"] ?? values.owner === "JoshuaKGoldberg", + repository: values.repository, + skipAllContributorsApi: + values["skip-all-contributors-api"] ?? values.offline, + skipGitHubApi: values["skip-github-api"] ?? values.offline, + skipInstall: values["skip-install"], + skipRemoval: values["skip-removal"], + skipRestore: values["skip-restore"], + skipUninstall: values["skip-uninstall"], + title: values.title, + }; + + const emailError = detectEmailRedundancy(values); + if (emailError) { + return { + cancelled: true, + error: emailError, + options: mappedOptions, + }; + } + + const optionsParseResult = optionsSchema.safeParse(mappedOptions); + + if (!optionsParseResult.success) { + return { + cancelled: true, + error: optionsParseResult.error, + options: mappedOptions, + }; + } + + const options = optionsParseResult.data; + + const ownerOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + getDefaultValue: defaults.owner, + message: "What organization or user will the repository be under?", + name: "owner", + provided: options.owner, + }); + + options.owner ??= ownerOption.value; + + if (!options.owner) { + return { + cancelled: true, + error: ownerOption.error, + options, + }; + } + + const repositoryOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + getDefaultValue: defaults.repository, + message: "What will the kebab-case name of the repository be?", + name: "repository", + provided: options.repository, + }); + + options.repository ??= repositoryOption.value; + + if (!options.repository) { + return { + cancelled: true, + error: repositoryOption.error, + options, + }; + } + + const { github, repository } = await ensureRepositoryExists( + options.skipGitHubApi + ? undefined + : await withSpinner("Checking GitHub authentication", getGitHub), + { + mode, + owner: options.owner, + repository: options.repository, + }, + ); + + if (!repository) { + return { cancelled: true, error: repositoryOption.error, options }; + } + + const descriptionOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + getDefaultValue: async () => + (await defaults.description()) ?? "A very lovely package. Hooray!", + message: "How would you describe the new package?", + name: "description", + provided: options.description, + }); + + options.description ??= descriptionOption.value; + + if (!options.description) { + return { cancelled: true, error: descriptionOption.error, options }; + } + + const titleOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + getDefaultValue: async () => + (await defaults.title()) ?? titleCase(repository).replaceAll("-", " "), + message: "What will the Title Case title of the repository be?", + name: "title", + provided: options.title, + }); + + options.title ??= titleOption.value; + + if (!options.title) { + return { cancelled: true, error: titleOption.error, options }; + } + + let guide: OptionsGuide | undefined; + + if (options.guide) { + if (options.guideTitle) { + guide = { href: options.guide, title: options.guideTitle }; + } else { + const titleOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + message: "What is the title text for the guide?", + name: "getPrefillOrPromptedOption", + }); + + if (!titleOption.value) { + return { cancelled: true, error: titleOption.error, options }; + } + + guide = { href: options.guide, title: titleOption.value }; + } + } + + let logo: OptionsLogo | undefined; + + if (options.logo) { + if (options.logoAlt) { + logo = { alt: options.logoAlt, src: options.logo }; + } else { + const logoAltOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + message: "What is the alt text (non-visual description) of the logo?", + name: "getPrefillOrPromptedOption", + }); + + if (!logoAltOption.value) { + return { cancelled: true, error: logoAltOption.error, options }; + } + + logo = { alt: logoAltOption.value, src: options.logo }; + } + } + + let email = options.email ?? (await defaults.email()); + + if (!email) { + const emailOption = await getPrefillOrPromptedOption({ + auto: !!mappedOptions.auto, + message: "What email should be used in package.json and .md files?", + name: "email", + }); + + if (!emailOption.value) { + return { cancelled: true, error: emailOption.error, options }; + } + + email = { github: emailOption.value, npm: emailOption.value }; + } + + const augmentedOptions = await augmentOptionsWithExcludes({ + ...options, + access: options.access ?? "public", + author: options.author ?? (await defaults.owner()), + description: options.description, + directory: + options.directory ?? promptedOptions.directory ?? options.repository, + email, + funding: options.funding ?? (await defaults.funding()), + guide, + logo, + mode, + owner: options.owner, + repository, + title: options.title, + }); + + if (!augmentedOptions) { + return { + cancelled: true, + options, + }; + } + + if (options.auto) { + logInferredOptions(augmentedOptions); + } + + return { + cancelled: false, + github, + options: augmentedOptions, + }; +} diff --git a/src/shared/packages.test.ts b/src/shared/packages.test.ts new file mode 100644 index 0000000..c8d6144 --- /dev/null +++ b/src/shared/packages.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from "vitest"; + +import { removeDependencies } from "./packages.js"; + +const mockExecaCommand = vi.fn(); + +vi.mock("execa", () => ({ + get execaCommand() { + return mockExecaCommand; + }, +})); + +describe("removeDependencies", () => { + it("removes all packages that already exist when all already exist", async () => { + await removeDependencies(["one", "two"], { + one: "1.2.3", + two: "4.5.6", + }); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot(` + [ + [ + "pnpm remove one two", + ], + ] + `); + }); + + it("removes only packages that already exist when some don't exist", async () => { + await removeDependencies(["exists", "missing"], { + exists: "1.2.3", + }); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot(` + [ + [ + "pnpm remove exists", + ], + ] + `); + }); + + it("adds a flag to removing packages when one is provided", async () => { + await removeDependencies( + ["exists", "missing"], + { + exists: "1.2.3", + }, + "-D", + ); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot(` + [ + [ + "pnpm remove exists -D", + ], + ] + `); + }); + + it("does nothing when no packages already exist", async () => { + await removeDependencies(["missing"]); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot("[]"); + }); +}); diff --git a/src/shared/packages.ts b/src/shared/packages.ts new file mode 100644 index 0000000..ae67839 --- /dev/null +++ b/src/shared/packages.ts @@ -0,0 +1,24 @@ +import { execaCommand } from "execa"; + +import { readFileSafe } from "./readFileSafe.js"; +import { PartialPackageData } from "./types.js"; + +export async function readPackageData() { + return JSON.parse( + await readFileSafe("./package.json", "{}"), + ) as PartialPackageData; +} + +export async function removeDependencies( + packageNames: string[], + existing: Record = {}, + flags = "", +) { + const present = packageNames.filter((packageName) => packageName in existing); + + if (present.length) { + await execaCommand( + `pnpm remove ${present.join(" ")}${flags ? ` ${flags}` : ""}`, + ); + } +} diff --git a/src/shared/prompts.ts b/src/shared/prompts.ts new file mode 100644 index 0000000..a886345 --- /dev/null +++ b/src/shared/prompts.ts @@ -0,0 +1,5 @@ +import * as prompts from "@clack/prompts"; + +export function filterPromptCancel(value: Value | symbol) { + return prompts.isCancel(value) ? undefined : value; +} diff --git a/src/shared/readFileAsJson.test.ts b/src/shared/readFileAsJson.test.ts new file mode 100644 index 0000000..9010589 --- /dev/null +++ b/src/shared/readFileAsJson.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from "vitest"; + +import { readFileAsJson } from "./readFileAsJson.js"; + +const mockReadFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readFile() { + return mockReadFile; + }, +})); + +describe("readFileAsJson", () => { + it("returns the file's parsed contents when it exists", async () => { + const data = { abc: 123 }; + + mockReadFile.mockResolvedValue(JSON.stringify(data)); + + const actual = await readFileAsJson("filePath.json"); + + expect(actual).toEqual(data); + }); + + it("throws an error when the file doesn't exist", async () => { + const error = new Error("Oh no!"); + + mockReadFile.mockRejectedValue(error); + + await expect(() => readFileAsJson("filePath.json")).rejects.toEqual( + new Error( + `Could not read file from filePath.json as JSON. Please ensure the file exists and is valid JSON.`, + { cause: error }, + ), + ); + }); +}); diff --git a/src/shared/readFileAsJson.ts b/src/shared/readFileAsJson.ts new file mode 100644 index 0000000..ea501df --- /dev/null +++ b/src/shared/readFileAsJson.ts @@ -0,0 +1,12 @@ +import * as fs from "node:fs/promises"; + +export async function readFileAsJson(filePath: string) { + try { + return JSON.parse((await fs.readFile(filePath)).toString()) as unknown; + } catch (error) { + throw new Error( + `Could not read file from ${filePath} as JSON. Please ensure the file exists and is valid JSON.`, + { cause: error }, + ); + } +} diff --git a/src/shared/readFileSafe.test.ts b/src/shared/readFileSafe.test.ts new file mode 100644 index 0000000..2722a19 --- /dev/null +++ b/src/shared/readFileSafe.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from "vitest"; + +import { readFileSafe } from "./readFileSafe.js"; + +const mockReadFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readFile() { + return mockReadFile; + }, +})); + +describe("readFundingIfExists", () => { + it("outputs the file content as string when it exists", async () => { + mockReadFile.mockResolvedValue("File content as string"); + const result = await readFileSafe("/path/to/file.ext", "fallback"); + expect(result).toBe("File content as string"); + }); + + it("returns fallback when readFile fails", async () => { + mockReadFile.mockRejectedValue("Oops"); + const result = await readFileSafe("/path/to/nowhere.ext", "fallback"); + expect(result).toBe("fallback"); + }); +}); diff --git a/src/shared/readFileSafe.ts b/src/shared/readFileSafe.ts new file mode 100644 index 0000000..652a25c --- /dev/null +++ b/src/shared/readFileSafe.ts @@ -0,0 +1,9 @@ +import * as fs from "node:fs/promises"; + +export async function readFileSafe(filePath: URL | string, fallback: string) { + try { + return (await fs.readFile(filePath)).toString(); + } catch { + return fallback; + } +} diff --git a/src/shared/readFileSafeAsJson.ts b/src/shared/readFileSafeAsJson.ts new file mode 100644 index 0000000..6bd18a9 --- /dev/null +++ b/src/shared/readFileSafeAsJson.ts @@ -0,0 +1,5 @@ +import { readFileSafe } from "./readFileSafe.js"; + +export async function readFileSafeAsJson(filePath: URL | string) { + return JSON.parse(await readFileSafe(filePath, "null")) as unknown; +} diff --git a/src/shared/runOrRestore.test.ts b/src/shared/runOrRestore.test.ts new file mode 100644 index 0000000..b7f9edc --- /dev/null +++ b/src/shared/runOrRestore.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { runOrRestore } from "./runOrRestore.js"; + +const mockConfirm = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + get confirm() { + return mockConfirm; + }, + intro: vi.fn(), + isCancel: vi.fn(), + outro: vi.fn(), +})); + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockGetInputValuesAndOctokit = vi.fn(); + +vi.mock("./inputs.js", () => ({ + get getInputValuesAndOctokit() { + return mockGetInputValuesAndOctokit; + }, +})); + +describe("runOrRestore", () => { + beforeEach(() => { + vi.spyOn(console, "clear").mockImplementation(() => undefined); + vi.spyOn(console, "log").mockImplementation(() => undefined); + }); + + it("returns 0 when run resolves", async () => { + mockGetInputValuesAndOctokit.mockResolvedValue({ + octokit: undefined, + values: {}, + }); + + const actual = await runOrRestore({ + run: vi.fn(), + skipRestore: true, + }); + + expect(actual).toBe(0); + }); + + it("returns 2 and does not restore the repository when run rejects and skipRestore is true", async () => { + mockGetInputValuesAndOctokit.mockResolvedValue({ + octokit: undefined, + values: { + skipRestore: false, + }, + }); + mockConfirm.mockResolvedValue(false); + + const actual = await runOrRestore({ + run: vi.fn().mockRejectedValue(new Error("Oh no!")), + skipRestore: true, + }); + + expect(actual).toBe(2); + expect(mock$).toHaveBeenCalledTimes(0); + }); + + it("returns 2 and restores the repository when run rejects and skipRestore is false", async () => { + mockGetInputValuesAndOctokit.mockResolvedValue({ + octokit: undefined, + values: { + skipRestore: false, + }, + }); + mockConfirm.mockResolvedValue(true); + + const actual = await runOrRestore({ + run: vi.fn().mockRejectedValue(new Error("Oh no!")), + skipRestore: false, + }); + + expect(actual).toBe(2); + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "git restore .", + ], + ], + ] + `); + }); +}); diff --git a/src/shared/runOrRestore.ts b/src/shared/runOrRestore.ts new file mode 100644 index 0000000..3fba6ff --- /dev/null +++ b/src/shared/runOrRestore.ts @@ -0,0 +1,42 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; +import { $ } from "execa"; + +import { logLine } from "./cli/lines.js"; + +export interface RunOrRestoreOptions { + run: () => Promise; + skipRestore: boolean | undefined; +} + +export async function runOrRestore({ run, skipRestore }: RunOrRestoreOptions) { + try { + await run(); + return 0; + } catch (error) { + logLine(); + console.log(error); + + if (skipRestore) { + logLine(); + logLine(chalk.gray`Leaving changes to the local directory on disk.`); + } else { + const shouldRestore = await prompts.confirm({ + message: "Do you want to restore the repository to how it was?", + }); + + if (shouldRestore) { + logLine(); + logLine( + [ + chalk.gray`Resetting repository using`, + chalk.reset`git restore .`, + ].join(" "), + ); + await $`git restore .`; + } + } + + return 2; + } +} diff --git a/src/shared/tryCatchAsync.ts b/src/shared/tryCatchAsync.ts new file mode 100644 index 0000000..7765510 --- /dev/null +++ b/src/shared/tryCatchAsync.ts @@ -0,0 +1,7 @@ +export async function tryCatchAsync(get: () => Promise) { + try { + return await get(); + } catch { + return undefined; + } +} diff --git a/src/shared/tryCatchLazyValueAsync.test.ts b/src/shared/tryCatchLazyValueAsync.test.ts new file mode 100644 index 0000000..e1fca14 --- /dev/null +++ b/src/shared/tryCatchLazyValueAsync.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vitest"; + +import { tryCatchLazyValueAsync } from "./tryCatchLazyValueAsync.js"; + +describe("tryCatchLazyValueAsync", () => { + it("does not run get when it has not been called", () => { + const get = vi.fn(); + + tryCatchLazyValueAsync(get); + + expect(get).not.toHaveBeenCalled(); + }); + + it("returns get's resolved value when it resolves", async () => { + const expected = "value"; + const get = vi.fn().mockResolvedValue(expected); + + const lazy = tryCatchLazyValueAsync(get); + + expect(await lazy()).toEqual(expected); + }); + + it("returns undefined when get rejects", async () => { + const get = vi.fn().mockRejectedValue(new Error("Oh no!")); + + const lazy = tryCatchLazyValueAsync(get); + + expect(await lazy()).toBeUndefined(); + }); +}); diff --git a/src/shared/tryCatchLazyValueAsync.ts b/src/shared/tryCatchLazyValueAsync.ts new file mode 100644 index 0000000..b8215d1 --- /dev/null +++ b/src/shared/tryCatchLazyValueAsync.ts @@ -0,0 +1,7 @@ +import lazyValue from "lazy-value"; + +import { tryCatchAsync } from "./tryCatchAsync.js"; + +export function tryCatchLazyValueAsync(get: () => Promise) { + return lazyValue(async () => await tryCatchAsync(get)); +} diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..01556d1 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,119 @@ +import { z } from "zod"; + +import { StatusCode } from "./codes.js"; + +export interface AllContributorContributor { + contributions: string[]; + login: string; +} + +export interface AllContributorsData { + contributors: AllContributorContributor[]; +} + +export interface PartialPackageData { + author?: { email: string; name: string } | string; + bin?: string; + dependencies?: Record; + description?: string; + devDependencies?: Record; + email?: string; + name?: string; + repository?: { type: string; url: string } | string; + scripts?: Record; +} + +export type OptionsAccess = "public" | "restricted"; + +export type OptionsBase = "common" | "everything" | "minimum" | "prompt"; + +export interface OptionsEmail { + github: string; + npm: string; +} + +export interface OptionsGuide { + href: string; + title: string; +} + +export interface OptionsLogo { + alt: string; + src: string; +} + +/** + * All runtime options that may (or must) be specified for setup. + */ +export interface Options { + access: OptionsAccess; + author?: string; + base?: OptionsBase; + bin?: string; + description: string; + directory: string; + email: OptionsEmail; + excludeAllContributors?: boolean; + excludeCompliance?: boolean; + excludeLintDeprecation?: boolean; + excludeLintESLint?: boolean; + excludeLintJSDoc?: boolean; + excludeLintJson?: boolean; + excludeLintKnip?: boolean; + excludeLintMd?: boolean; + excludeLintPackageJson?: boolean; + excludeLintPackages?: boolean; + excludeLintPerfectionist?: boolean; + excludeLintRegex?: boolean; + excludeLintSpelling?: boolean; + excludeLintStrict?: boolean; + excludeLintStylistic?: boolean; + excludeLintYml?: boolean; + excludeReleases?: boolean; + excludeRenovate?: boolean; + excludeTests?: boolean; + funding?: string; + guide?: OptionsGuide; + keywords?: string[]; + logo?: OptionsLogo; + mode: Mode; + offline?: boolean; + owner: string; + preserveGeneratedFrom?: boolean; + repository: string; + skipAllContributorsApi?: boolean; + skipGitHubApi?: boolean; + skipInstall?: boolean; + skipRemoval?: boolean; + skipRestore?: boolean; + skipUninstall?: boolean; + title: string; +} + +/** + * Options that might be suggested by how the user is running setup. + */ +export interface PromptedOptions { + /** + * Directory for the repository, if it may differ from the repository name. + */ + directory?: string; + + /** + * Repository name, if it may differ from the current directory. + */ + repository?: string; +} + +export interface ModeResult { + code: StatusCode; + error?: string | z.ZodError; + options: Partial; +} + +export type ModeRunner = ( + args: string[], + promptedOptions?: PromptedOptions, +) => Promise; + +export type Mode = "create" | "initialize" | "migrate"; diff --git a/src/steps/addOwnerAsAllContributor.test.ts b/src/steps/addOwnerAsAllContributor.test.ts new file mode 100644 index 0000000..ba62f85 --- /dev/null +++ b/src/steps/addOwnerAsAllContributor.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it, vi } from "vitest"; + +import { addOwnerAsAllContributor } from "./addOwnerAsAllContributor.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockWriteFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get writeFile() { + return mockWriteFile; + }, +})); + +const mockReadFileAsJson = vi.fn(); + +vi.mock("../shared/readFileAsJson.js", () => ({ + get readFileAsJson() { + return mockReadFileAsJson; + }, +})); + +const mockOwner = "TestOwner"; + +vi.mock("../shared/getGitHubUserAsAllContributor", () => ({ + getGitHubUserAsAllContributor: () => mockOwner, +})); + +describe("addOwnerAsAllContributor", () => { + it("throws an error when the .all-contributorsrc fails to read", async () => { + mock$.mockResolvedValueOnce({ + stdout: JSON.stringify({ login: "user" }), + }); + mockReadFileAsJson.mockResolvedValue("invalid"); + + await expect(async () => { + await addOwnerAsAllContributor({ owner: mockOwner }); + }).rejects.toMatchInlineSnapshot( + '[Error: Invalid .all-contributorsrc: "invalid"]', + ); + }); + + it("throws an error when the .all-contributorsrc is missing expected properties", async () => { + mock$.mockResolvedValueOnce({ + stdout: JSON.stringify({ login: "user" }), + }); + mockReadFileAsJson.mockResolvedValue({}); + + await expect(async () => { + await addOwnerAsAllContributor({ owner: mockOwner }); + }).rejects.toMatchInlineSnapshot( + "[Error: Invalid .all-contributorsrc: {}]", + ); + }); + + it("sets the running user to tool when no prior contributions exist", async () => { + mock$.mockResolvedValueOnce({ + stdout: JSON.stringify({ login: mockOwner }), + }); + mockReadFileAsJson.mockResolvedValue({ + contributors: [], + }); + + await addOwnerAsAllContributor({ owner: mockOwner }); + + expect(mockWriteFile).toHaveBeenCalledWith( + "./.all-contributorsrc", + await formatJson({ + contributors: [{ contributions: ["tool"], login: mockOwner }], + }), + ); + }); + + it("resets JoshuaKGoldberg to just tool and adds in the running user when both exist", async () => { + mock$.mockResolvedValueOnce({ + stdout: JSON.stringify({ login: mockOwner }), + }); + mockReadFileAsJson.mockResolvedValue({ + contributors: [ + { contributions: ["bug", "fix"], login: mockOwner }, + { contributions: ["bug", "fix"], login: "JoshuaKGoldberg" }, + ], + }); + + await addOwnerAsAllContributor({ owner: mockOwner }); + + expect(mockWriteFile).toHaveBeenCalledWith( + "./.all-contributorsrc", + await formatJson({ + contributors: [ + { contributions: ["bug", "fix", "tool"], login: mockOwner }, + { contributions: ["tool"], login: "JoshuaKGoldberg" }, + ], + }), + ); + }); +}); diff --git a/src/steps/addOwnerAsAllContributor.ts b/src/steps/addOwnerAsAllContributor.ts new file mode 100644 index 0000000..b4a7e12 --- /dev/null +++ b/src/steps/addOwnerAsAllContributor.ts @@ -0,0 +1,55 @@ +import * as fs from "node:fs/promises"; + +import { getGitHubUserAsAllContributor } from "../shared/getGitHubUserAsAllContributor.js"; +import { readFileAsJson } from "../shared/readFileAsJson.js"; +import { AllContributorsData, Options } from "../shared/types.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; + +export async function addOwnerAsAllContributor( + options: Pick, +) { + const user = await getGitHubUserAsAllContributor(options); + + const existingContributors = (await readFileAsJson( + "./.all-contributorsrc", + )) as AllContributorsData; + if (!isValidAllContributorsData(existingContributors)) { + throw new Error( + `Invalid .all-contributorsrc: ${JSON.stringify(existingContributors)}`, + ); + } + + const contributors = existingContributors.contributors + .filter(({ login }) => ["JoshuaKGoldberg", user].includes(login)) + .map((contributor) => + contributor.login === "JoshuaKGoldberg" + ? { ...contributor, contributions: ["tool"] } + : { + ...contributor, + contributions: Array.from( + new Set([...contributor.contributions, "tool"]), + ), + }, + ); + + if (!contributors.some((contributor) => contributor.login === user)) { + contributors.push({ + contributions: ["tool"], + login: user, + }); + } + + await fs.writeFile( + "./.all-contributorsrc", + await formatJson({ + ...existingContributors, + contributors, + }), + ); +} + +function isValidAllContributorsData( + value: unknown, +): value is AllContributorsData { + return !!value && typeof value === "object" && "contributors" in value; +} diff --git a/src/steps/addToolAllContributors.test.ts b/src/steps/addToolAllContributors.test.ts new file mode 100644 index 0000000..5ddedcb --- /dev/null +++ b/src/steps/addToolAllContributors.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from "vitest"; + +import { addToolAllContributors } from "./addToolAllContributors.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockGetGitHubUserAsAllContributor = vi.fn(); + +vi.mock("../shared/getGitHubUserAsAllContributor.js", () => ({ + get getGitHubUserAsAllContributor() { + return mockGetGitHubUserAsAllContributor; + }, +})); + +describe("addToolAllContributors", () => { + it("adds JoshuaKGoldberg when that is not the current github user", async () => { + mockGetGitHubUserAsAllContributor.mockResolvedValue("JoshuaKGoldberg"); + + await addToolAllContributors({ owner: "owner" }); + + expect(mock$).not.toHaveBeenCalled(); + }); + + it("does not add JoshuaKGoldberg when that not the current github user", async () => { + mockGetGitHubUserAsAllContributor.mockResolvedValue("other"); + + await addToolAllContributors({ owner: "owner" }); + + expect(mock$).toHaveBeenCalledWith([ + `npx -y all-contributors-cli add JoshuaKGoldberg tool`, + ]); + }); +}); diff --git a/src/steps/addToolAllContributors.ts b/src/steps/addToolAllContributors.ts new file mode 100644 index 0000000..907d15d --- /dev/null +++ b/src/steps/addToolAllContributors.ts @@ -0,0 +1,14 @@ +import { $ } from "execa"; + +import { getGitHubUserAsAllContributor } from "../shared/getGitHubUserAsAllContributor.js"; +import { Options } from "../shared/types.js"; + +export async function addToolAllContributors( + options: Pick, +) { + const login = await getGitHubUserAsAllContributor(options); + + if (login !== "JoshuaKGoldberg") { + await $`npx -y all-contributors-cli add JoshuaKGoldberg tool`; + } +} diff --git a/src/steps/clearChangelog.ts b/src/steps/clearChangelog.ts new file mode 100644 index 0000000..f2641cf --- /dev/null +++ b/src/steps/clearChangelog.ts @@ -0,0 +1,9 @@ +import * as fs from "node:fs/promises"; +import prettier from "prettier"; + +export async function clearChangelog() { + await fs.writeFile( + "./CHANGELOG.md", + await prettier.format(`# Changelog`, { parser: "markdown" }), + ); +} diff --git a/src/steps/clearUnnecessaryFiles.ts b/src/steps/clearUnnecessaryFiles.ts new file mode 100644 index 0000000..1fd57ed --- /dev/null +++ b/src/steps/clearUnnecessaryFiles.ts @@ -0,0 +1,37 @@ +import * as fs from "node:fs/promises"; + +const globPaths = [ + ...extensions(".babelrc", "cjs", "cts", "js", "json", "mjs"), + ...extensions(".eslintrc", "js", "json", "yml"), + ...extensions(".prettierrc", "json", "json5", "yaml", "yml"), + ...extensions("prettier.config", "js", "mjs", "cjs"), + ...extensions("babel.config", "cjs", "cts", "js", "json", "mjs"), + ...extensions("jest.config", "cjs", "js", "json", "mjs", "ts"), + "./src/**/*.js", + ".circleci/config.yml", + ".github/codecov.yml", + ".babelrc", + ".npmignore", + ".eslintignore", + ".eslintrc", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + ".npmpackagejsonlintrc.json", + "codecov.yml", + "DEVELOPMENT.md", + "dist", + "lib", + "package-lock.json", + "travis.yml", + "yarn.lock", +]; + +function extensions(base: string, ...extensions: string[]) { + return extensions.map((extension) => [base, extension].join(".")); +} + +export async function clearUnnecessaryFiles() { + for (const globPath of globPaths) { + await fs.rm(globPath, { force: true, recursive: true }); + } +} diff --git a/src/steps/createJoshuaKGoldbergReplacement.test.ts b/src/steps/createJoshuaKGoldbergReplacement.test.ts new file mode 100644 index 0000000..14bb7c0 --- /dev/null +++ b/src/steps/createJoshuaKGoldbergReplacement.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vitest"; + +import { createJoshuaKGoldbergReplacement } from "./createJoshuaKGoldbergReplacement.js"; + +const options = { + owner: "NewOwner", + repository: "new-repository", +}; + +describe("createJoshuaKGoldbergReplacement", () => { + test.each([ + [`JoshuaKGoldberg`, options.owner], + [ + `JoshuaKGoldberg/${options.repository}`, + `${options.owner}/${options.repository}`, + ], + [`JoshuaKGoldberg/other-repository`, `JoshuaKGoldberg/other-repository`], + ])("%s", (before, expected) => { + const [matcher, replacer] = createJoshuaKGoldbergReplacement(options); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const actual = replacer(before, matcher.exec(before)![1]); + + expect(actual).toBe(expected); + }); +}); diff --git a/src/steps/createJoshuaKGoldbergReplacement.ts b/src/steps/createJoshuaKGoldbergReplacement.ts new file mode 100644 index 0000000..ea1beae --- /dev/null +++ b/src/steps/createJoshuaKGoldbergReplacement.ts @@ -0,0 +1,21 @@ +import { Options } from "../shared/types.js"; + +/** + * Creates a replace-in-file replacement for JoshuaKGoldberg/... matches, + * keeping repository names not being migrated (e.g. for GitHub actions). + */ +export const createJoshuaKGoldbergReplacement = ( + options: Pick, +): [RegExp, ToCallback] => [ + /JoshuaKGoldberg(?:\/(.+))?/g, + (full: string, capture: string | undefined) => + capture + ? // If this was a "JoshuaKGoldberg/..." repository link, + // swap the owner if it's the repository being migrated. + capture.startsWith(options.repository) + ? `${options.owner}/${capture}` + : full + : // Otherwise it's just "JoshuaKGoldberg" standalone, + // so swap to the new owner. + options.owner, +]; diff --git a/src/steps/detectExistingContributors.test.ts b/src/steps/detectExistingContributors.test.ts new file mode 100644 index 0000000..c5aae0a --- /dev/null +++ b/src/steps/detectExistingContributors.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from "vitest"; + +import { detectExistingContributors } from "./detectExistingContributors.js"; + +const mockGetAllContributorsForRepository = vi.fn(); + +vi.mock("all-contributors-for-repository", () => ({ + get getAllContributorsForRepository() { + return mockGetAllContributorsForRepository; + }, +})); + +const mock$ = vi.fn().mockImplementation(() => mock$); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const options = { + owner: "TestOwner", + repository: "test-repository", +}; + +describe("detectExistingContributors", () => { + it("runs npx all-contributors add for each contributor and contribution type", async () => { + mockGetAllContributorsForRepository.mockResolvedValue({ + username: ["bug", "docs"], + }); + + await detectExistingContributors("auth-token", options); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "env": { + "PRIVATE_TOKEN": "auth-token", + }, + }, + ], + [ + [ + "npx -y all-contributors-cli add ", + " ", + "", + ], + "username", + "0,1", + ], + ] + `); + }); +}); diff --git a/src/steps/detectExistingContributors.ts b/src/steps/detectExistingContributors.ts new file mode 100644 index 0000000..c113482 --- /dev/null +++ b/src/steps/detectExistingContributors.ts @@ -0,0 +1,22 @@ +import { getAllContributorsForRepository } from "all-contributors-for-repository"; +import { $ } from "execa"; + +import { Options } from "../shared/types.js"; + +export async function detectExistingContributors( + auth: string | undefined, + options: Pick, +) { + const contributors = await getAllContributorsForRepository({ + auth, + owner: options.owner, + repo: options.repository, + }); + + for (const [contributor, contributions] of Object.entries(contributors)) { + const contributionTypes = Object.keys(contributions).join(","); + await $({ + env: { PRIVATE_TOKEN: auth }, + })`npx -y all-contributors-cli add ${contributor} ${contributionTypes}`; + } +} diff --git a/src/steps/finalizeDependencies.test.ts b/src/steps/finalizeDependencies.test.ts new file mode 100644 index 0000000..5d61bbb --- /dev/null +++ b/src/steps/finalizeDependencies.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../shared/types.js"; +import { finalizeDependencies } from "./finalizeDependencies.js"; + +const mockExecaCommand = vi.fn(); + +vi.mock("execa", () => ({ + get execaCommand() { + return mockExecaCommand; + }, +})); + +vi.mock("../shared/packages.js", () => ({ + readPackageData: () => [], + removeDependencies: vi.fn(), +})); + +const options = { + access: "public", + base: "everything", + description: "Stub description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + mode: "create", + owner: "StubOwner", + repository: "stub-repository", + title: "Stub Title", +} satisfies Options; + +describe("finalize", () => { + it("installs the full list of commands when no options are enabled", async () => { + await finalizeDependencies(options); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot(` + [ + [ + "pnpm add @release-it/conventional-changelog@latest @types/eslint@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest @vitest/coverage-v8@latest all-contributors-cli@latest console-fail-test@latest cspell@latest eslint@latest eslint-plugin-deprecation@latest eslint-plugin-eslint-comments@latest eslint-plugin-jsdoc@latest eslint-plugin-jsonc@latest eslint-plugin-markdown@latest eslint-plugin-n@latest eslint-plugin-package-json@latest eslint-plugin-perfectionist@latest eslint-plugin-regexp@latest eslint-plugin-vitest@latest eslint-plugin-yml@latest husky@latest jsonc-eslint-parser@latest knip@latest lint-staged@latest markdownlint@latest markdownlint-cli@latest prettier@latest prettier-plugin-curly@latest prettier-plugin-packagejson@latest release-it@latest sentences-per-line@latest tsup@latest typescript@latest vitest@latest yaml-eslint-parser@latest -D", + ], + [ + "npx all-contributors-cli generate", + ], + [ + "pnpm dedupe", + ], + ] + `); + }); + + it("installs in offline mode when options.offline is true", async () => { + await finalizeDependencies({ + ...options, + offline: true, + }); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot(` + [ + [ + "pnpm add @release-it/conventional-changelog@latest @types/eslint@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest @vitest/coverage-v8@latest all-contributors-cli@latest console-fail-test@latest cspell@latest eslint@latest eslint-plugin-deprecation@latest eslint-plugin-eslint-comments@latest eslint-plugin-jsdoc@latest eslint-plugin-jsonc@latest eslint-plugin-markdown@latest eslint-plugin-n@latest eslint-plugin-package-json@latest eslint-plugin-perfectionist@latest eslint-plugin-regexp@latest eslint-plugin-vitest@latest eslint-plugin-yml@latest husky@latest jsonc-eslint-parser@latest knip@latest lint-staged@latest markdownlint@latest markdownlint-cli@latest prettier@latest prettier-plugin-curly@latest prettier-plugin-packagejson@latest release-it@latest sentences-per-line@latest tsup@latest typescript@latest vitest@latest yaml-eslint-parser@latest -D --offline", + ], + [ + "npx all-contributors-cli generate", + ], + [ + "pnpm dedupe", + ], + ] + `); + }); + + it("installs the base list of commands when all options are enabled", async () => { + await finalizeDependencies({ + ...options, + excludeAllContributors: true, + excludeCompliance: true, + excludeLintJson: true, + excludeLintKnip: true, + excludeLintMd: true, + excludeLintPackageJson: true, + excludeLintPackages: true, + excludeLintPerfectionist: true, + excludeLintSpelling: true, + excludeLintYml: true, + excludeReleases: true, + excludeRenovate: undefined, + excludeTests: true, + }); + + expect(mockExecaCommand.mock.calls).toMatchInlineSnapshot(` + [ + [ + "pnpm add @types/eslint@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest eslint-plugin-deprecation@latest eslint-plugin-eslint-comments@latest eslint-plugin-jsdoc@latest eslint-plugin-n@latest eslint-plugin-regexp@latest husky@latest lint-staged@latest prettier@latest prettier-plugin-curly@latest prettier-plugin-packagejson@latest tsup@latest typescript@latest -D", + ], + [ + "pnpm dedupe", + ], + ] + `); + }); +}); diff --git a/src/steps/finalizeDependencies.ts b/src/steps/finalizeDependencies.ts new file mode 100644 index 0000000..1f1662e --- /dev/null +++ b/src/steps/finalizeDependencies.ts @@ -0,0 +1,77 @@ +import { execaCommand } from "execa"; + +import { readPackageData, removeDependencies } from "../shared/packages.js"; +import { Options } from "../shared/types.js"; + +export async function finalizeDependencies(options: Options) { + const devDependencies = [ + "@types/eslint", + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "eslint", + "eslint-plugin-deprecation", + "eslint-plugin-eslint-comments", + "eslint-plugin-jsdoc", + "eslint-plugin-n", + "eslint-plugin-regexp", + "husky", + "lint-staged", + "prettier", + "prettier-plugin-curly", + "prettier-plugin-packagejson", + "tsup", + "typescript", + ...(options.excludeAllContributors ? [] : ["all-contributors-cli"]), + ...(options.excludeLintJson ? [] : ["eslint-plugin-jsonc"]), + ...(options.excludeLintJson && options.excludeLintPackageJson + ? [] + : ["jsonc-eslint-parser"]), + ...(options.excludeLintKnip ? [] : ["knip"]), + ...(options.excludeLintMd + ? [] + : [ + "eslint-plugin-markdown", + "markdownlint", + "markdownlint-cli", + "sentences-per-line", + ]), + ...(options.excludeLintPackageJson ? [] : ["eslint-plugin-package-json"]), + ...(options.excludeLintPerfectionist + ? [] + : ["eslint-plugin-perfectionist"]), + ...(options.excludeLintSpelling ? [] : ["cspell"]), + ...(options.excludeLintYml + ? [] + : ["eslint-plugin-yml", "yaml-eslint-parser"]), + ...(options.excludeReleases + ? [] + : ["@release-it/conventional-changelog", "release-it"]), + ...(options.excludeTests + ? [] + : [ + "@vitest/coverage-v8", + "console-fail-test", + "eslint-plugin-vitest", + "vitest", + ]), + ] + .filter(Boolean) + .sort() + .map((packageName) => `${packageName}@latest`) + .join(" "); + + await execaCommand( + `pnpm add ${devDependencies} -D${options.offline ? " --offline" : ""}`, + ); + + if (!options.excludeAllContributors) { + await execaCommand(`npx all-contributors-cli generate`); + await removeDependencies( + ["all-contributors-cli", "all-contributors-for-repository"], + (await readPackageData()).devDependencies, + "-D", + ); + } + + await execaCommand(`pnpm dedupe`); +} diff --git a/src/steps/initializeBranchProtectionSettings.test.ts b/src/steps/initializeBranchProtectionSettings.test.ts new file mode 100644 index 0000000..0eec86a --- /dev/null +++ b/src/steps/initializeBranchProtectionSettings.test.ts @@ -0,0 +1,166 @@ +import { Octokit } from "octokit"; +import { MockInstance, describe, expect, it, vi } from "vitest"; + +import { Options } from "../shared/types.js"; +import { initializeBranchProtectionSettings } from "./initializeGitHubRepository/initializeBranchProtectionSettings.js"; + +const createMockOctokit = (request: MockInstance) => + ({ + request, + }) as unknown as Octokit; + +const stubOptions = { + access: "public", + description: "", + directory: "", + email: { + github: "", + npm: "", + }, + mode: "create", + owner: "", + repository: "", + title: "", +} satisfies Options; + +describe("migrateBranchProtectionSettings", () => { + it("does not throw when the request receives a non-error response", async () => { + const mockRequest = vi.fn().mockResolvedValue({ status: 200 }); + + await expect( + initializeBranchProtectionSettings( + createMockOctokit(mockRequest), + stubOptions, + ), + ).resolves.not.toThrow(); + + expect(mockRequest.mock.calls).toMatchInlineSnapshot(` + [ + [ + "PUT /repos///branches/main/protection", + { + "allow_deletions": false, + "allow_force_pushes": true, + "allow_fork_pushes": false, + "allow_fork_syncing": true, + "block_creations": false, + "branch": "main", + "enforce_admins": false, + "owner": "", + "repo": "", + "required_conversation_resolution": true, + "required_linear_history": false, + "required_pull_request_reviews": null, + "required_status_checks": { + "checks": [ + { + "context": "build", + }, + { + "context": "lint", + }, + { + "context": "prettier", + }, + { + "context": "compliance", + }, + { + "context": "lint_knip", + }, + { + "context": "lint_markdown", + }, + { + "context": "lint_packages", + }, + { + "context": "lint_spelling", + }, + { + "context": "test", + }, + ], + "strict": false, + }, + "restrictions": null, + }, + ], + ] + `); + }); + + it("returns false when the request receives a 403 response", async () => { + const mockRequest = vi.fn().mockRejectedValue({ status: 403 }); + + const actual = await initializeBranchProtectionSettings( + createMockOctokit(mockRequest), + stubOptions, + ); + + expect(actual).toBe(false); + }); + + it("throws the error when the request throws with a non-403 response", async () => { + const error = { status: 404 }; + const mockRequest = vi.fn().mockRejectedValue(error); + + await expect(() => + initializeBranchProtectionSettings( + createMockOctokit(mockRequest), + stubOptions, + ), + ).rejects.toBe(error); + }); + + it("doesn't create workflows for excluded options when specified", async () => { + const mockRequest = vi.fn().mockResolvedValue({ status: 200 }); + + await initializeBranchProtectionSettings(createMockOctokit(mockRequest), { + ...stubOptions, + excludeCompliance: true, + excludeLintKnip: true, + excludeLintMd: true, + excludeLintPackages: true, + excludeLintSpelling: true, + excludeTests: true, + }); + + expect(mockRequest.mock.calls).toMatchInlineSnapshot(` + [ + [ + "PUT /repos///branches/main/protection", + { + "allow_deletions": false, + "allow_force_pushes": true, + "allow_fork_pushes": false, + "allow_fork_syncing": true, + "block_creations": false, + "branch": "main", + "enforce_admins": false, + "owner": "", + "repo": "", + "required_conversation_resolution": true, + "required_linear_history": false, + "required_pull_request_reviews": null, + "required_status_checks": { + "checks": [ + { + "context": "build", + }, + { + "context": "lint", + }, + { + "context": "prettier", + }, + ], + "strict": false, + }, + "restrictions": null, + }, + ], + ] + `); + }); +}); diff --git a/src/steps/initializeGitHubRepository/index.ts b/src/steps/initializeGitHubRepository/index.ts new file mode 100644 index 0000000..5298e71 --- /dev/null +++ b/src/steps/initializeGitHubRepository/index.ts @@ -0,0 +1,17 @@ +import { Octokit } from "octokit"; + +import { Options } from "../../shared/types.js"; +import { initializeGitRemote } from "../initializeGitRemote.js"; +import { initializeRepositorySettings } from "../initializeRepositorySettings.js"; +import { initializeBranchProtectionSettings } from "./initializeBranchProtectionSettings.js"; +import { initializeRepositoryLabels } from "./labels/initializeRepositoryLabels.js"; + +export async function initializeGitHubRepository( + octokit: Octokit, + options: Options, +) { + await initializeGitRemote(options); + await initializeRepositorySettings(octokit, options); + await initializeBranchProtectionSettings(octokit, options); + await initializeRepositoryLabels(); +} diff --git a/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts b/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts new file mode 100644 index 0000000..45d9495 --- /dev/null +++ b/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts @@ -0,0 +1,54 @@ +import { RequestError } from "@octokit/request-error"; +import { Octokit } from "octokit"; + +import { Options } from "../../shared/types.js"; + +export async function initializeBranchProtectionSettings( + octokit: Octokit, + options: Options, +) { + try { + await octokit.request( + `PUT /repos/${options.owner}/${options.repository}/branches/main/protection`, + { + allow_deletions: false, + allow_force_pushes: true, + allow_fork_pushes: false, + allow_fork_syncing: true, + block_creations: false, + branch: "main", + enforce_admins: false, + owner: options.owner, + repo: options.repository, + required_conversation_resolution: true, + required_linear_history: false, + required_pull_request_reviews: null, + required_status_checks: { + checks: [ + { context: "build" }, + { context: "lint" }, + { context: "prettier" }, + ...(options.excludeCompliance ? [] : [{ context: "compliance" }]), + ...(options.excludeLintKnip ? [] : [{ context: "lint_knip" }]), + ...(options.excludeLintMd ? [] : [{ context: "lint_markdown" }]), + ...(options.excludeLintPackages + ? [] + : [{ context: "lint_packages" }]), + ...(options.excludeLintSpelling + ? [] + : [{ context: "lint_spelling" }]), + ...(options.excludeTests ? [] : [{ context: "test" }]), + ], + strict: false, + }, + restrictions: null, + }, + ); + } catch (error) { + if ((error as RequestError).status === 403) { + return false; + } + + throw error; + } +} diff --git a/src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.test.ts b/src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.test.ts new file mode 100644 index 0000000..66e747b --- /dev/null +++ b/src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; + +import { getExistingEquivalentLabels } from "./getExistingEquivalentLabels.js"; + +const createLabel = (name: string) => ({ + color: "#000000", + description: "A good label.", + name, +}); + +describe("getExistingEquivalentLabels", () => { + it("returns no labels when there are no existing labels", () => { + const actual = getExistingEquivalentLabels([], "abc"); + + expect(actual).toEqual([]); + }); + + it("returns no labels when no existing label matches", () => { + const actual = getExistingEquivalentLabels([createLabel("abc")], "def"); + + expect(actual).toEqual([]); + }); + + it("returns an existing un-prefixed label when it matches by name", () => { + const abcLabel = createLabel("abc"); + const actual = getExistingEquivalentLabels( + [createLabel("def"), abcLabel, createLabel("ghi")], + "abc", + ); + + expect(actual).toEqual([abcLabel]); + }); + + it("returns an existing prefixed label when it matches by name", () => { + const abcDefLabel = createLabel("abc: def"); + const actual = getExistingEquivalentLabels([abcDefLabel], "abc: def"); + + expect(actual).toEqual([abcDefLabel]); + }); + + it("returns the existing label when it matches excluding prefix", () => { + const abcLabel = createLabel("abc"); + const actual = getExistingEquivalentLabels( + [createLabel("abc: def"), abcLabel, createLabel("ghi")], + "type: abc", + ); + + expect(actual).toEqual([abcLabel]); + }); + + it("returns the existing label when it matches an alias", () => { + const enhancementLabel = createLabel("enhancement"); + const actual = getExistingEquivalentLabels( + [createLabel("abc: def"), enhancementLabel, createLabel("ghi")], + "type: feature", + ); + + expect(actual).toEqual([enhancementLabel]); + }); + + it("returns both existing labels when one matches on name and another matches an alias", () => { + const enhancementLabel = createLabel("enhancement"); + const typeFeatureLabel = createLabel("type: feature"); + const actual = getExistingEquivalentLabels( + [ + createLabel("abc: def"), + enhancementLabel, + createLabel("ghi"), + typeFeatureLabel, + ], + "type: feature", + ); + + expect(actual).toEqual([enhancementLabel, typeFeatureLabel]); + }); +}); diff --git a/src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.ts b/src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.ts new file mode 100644 index 0000000..ab78a95 --- /dev/null +++ b/src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.ts @@ -0,0 +1,26 @@ +const aliases = new Map([ + ["enhancement", "type: feature"], + ["help wanted", "status: accepting prs"], +]); + +export interface GhLabelData { + color: string; + description: string; + name: string; +} + +export function getExistingEquivalentLabels( + existingLabels: GhLabelData[], + outcomeLabelName: string, +) { + const outcomeTrimmed = outcomeLabelName.replace(/\w+: (\w+)/, "$1"); + + return existingLabels.filter(({ name: existingName }) => { + return ( + existingName === outcomeLabelName || + existingName === outcomeTrimmed || + aliases.get(existingName) === outcomeLabelName || + existingName.replace(/\w+: (\w+)/, "$1") === outcomeLabelName + ); + }); +} diff --git a/src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.test.ts b/src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.test.ts new file mode 100644 index 0000000..c750abd --- /dev/null +++ b/src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.test.ts @@ -0,0 +1,363 @@ +import { describe, expect, it, vi } from "vitest"; + +import { initializeRepositoryLabels } from "./initializeRepositoryLabels.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockOutcomeLabel = { + color: "000000", + description: "def ghi", + name: "area: abc", +}; + +vi.mock("./outcomeLabels.js", () => ({ + get outcomeLabels() { + return [mockOutcomeLabel]; + }, +})); + +describe("migrateRepositoryLabels", () => { + it("creates an outcome label when labels stdout is empty", async () => { + mock$.mockResolvedValue({ + stdout: "", + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label create ", + " --color ", + " --description ", + "", + ], + "area: abc", + "000000", + "def ghi", + ], + ] + `); + }); + + it("creates an outcome label when it doesn't already exist", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + color: "000000", + description: "def ghi", + name: "other", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label create ", + " --color ", + " --description ", + "", + ], + "area: abc", + "000000", + "def ghi", + ], + ] + `); + }); + + it("doesn't edit a outcome label when it already exists with the same information", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([mockOutcomeLabel]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + ] + `); + }); + + it("edits the outcome label when it already exists with different color", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + ...mockOutcomeLabel, + color: "111111", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label edit ", + " --color ", + " --description ", + " --name ", + "", + ], + "area: abc", + "000000", + "def ghi", + "area: abc", + ], + ] + `); + }); + + it("edits the outcome label when it already exists with a different description", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + ...mockOutcomeLabel, + description: "updated", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label edit ", + " --color ", + " --description ", + " --name ", + "", + ], + "area: abc", + "000000", + "def ghi", + "area: abc", + ], + ] + `); + }); + + it("deletes an existing non-outcome label when the equivalent outcome label already exists", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + color: "000000", + description: "def ghi", + name: "area: abc", + }, + { + color: "000000", + description: "def ghi", + name: "area: abc", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + ] + `); + }); + + it("deletes a pre-existing label when it isn't a outcome label", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + color: "000000", + description: "def ghi", + name: "unknown", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label create ", + " --color ", + " --description ", + "", + ], + "area: abc", + "000000", + "def ghi", + ], + ] + `); + }); + + it("deletes the existing duplicate outcome label and edits the label with the outcome name and different color when both exist", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + color: "000000", + description: "def ghi", + name: "abc", + }, + { + color: "111111", + description: "def ghi", + name: "area: abc", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label delete ", + " --yes", + ], + "abc", + ], + [ + [ + "gh label edit ", + " --color ", + " --description ", + " --name ", + "", + ], + "area: abc", + "000000", + "def ghi", + "area: abc", + ], + ] + `); + }); + + it("deletes the existing duplicate outcome label and does not edit the label with the outcome name and same information when both exist", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + color: "000000", + description: "def ghi", + name: "abc", + }, + { + color: "000000", + description: "def ghi", + name: "area: abc", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label delete ", + " --yes", + ], + "abc", + ], + ] + `); + }); + + it("doesn't delete a pre-existing label when it isn't a outcome label", async () => { + mock$.mockResolvedValue({ + stdout: JSON.stringify([ + { + color: "000000", + description: "def ghi", + name: "jkl", + }, + ]), + }); + + await initializeRepositoryLabels(); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "gh label list --json color,description,name", + ], + ], + [ + [ + "gh label create ", + " --color ", + " --description ", + "", + ], + "area: abc", + "000000", + "def ghi", + ], + ] + `); + }); +}); diff --git a/src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.ts b/src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.ts new file mode 100644 index 0000000..06cea95 --- /dev/null +++ b/src/steps/initializeGitHubRepository/labels/initializeRepositoryLabels.ts @@ -0,0 +1,49 @@ +import { $ } from "execa"; + +import { + GhLabelData, + getExistingEquivalentLabels, +} from "./getExistingEquivalentLabels.js"; +import { outcomeLabels } from "./outcomeLabels.js"; + +export async function initializeRepositoryLabels() { + const existingLabels = JSON.parse( + (await $`gh label list --json color,description,name`).stdout || "[]", + ) as GhLabelData[]; + + for (const outcome of outcomeLabels) { + const existingEquivalents = getExistingEquivalentLabels( + existingLabels, + outcome.name, + ); + + // Case: the repo has neither of the two label types + if (!existingEquivalents.length) { + await $`gh label create ${outcome.name} --color ${outcome.color} --description ${outcome.description}`; + continue; + } + + for (const existingEquivalent of existingEquivalents) { + // Case: the repo already has both prefixed and non-prefixed label name types + // E.g. both "area: documentation" and "documentation" + if ( + existingEquivalent.name !== outcome.name && + existingLabels.some((existing) => existing.name === outcome.name) + ) { + await $`gh label delete ${existingEquivalent.name} --yes`; + continue; + } + + // Case: the repo has one of the two label types, with >=1 different property + // E.g. "documentation" and the same color and description + // E.g. "area: documentation" but with a different color + if ( + outcome.color !== existingEquivalent.color || + outcome.description !== existingEquivalent.description || + outcome.name !== existingEquivalent.name + ) { + await $`gh label edit ${existingEquivalent.name} --color ${outcome.color} --description ${outcome.description} --name ${outcome.name}`; + } + } + } +} diff --git a/src/steps/initializeGitHubRepository/labels/outcomeLabels.ts b/src/steps/initializeGitHubRepository/labels/outcomeLabels.ts new file mode 100644 index 0000000..8b7ce63 --- /dev/null +++ b/src/steps/initializeGitHubRepository/labels/outcomeLabels.ts @@ -0,0 +1,89 @@ +/* spellchecker: disable */ +export const outcomeLabels = [ + { + color: "0075ca", + description: "Improvements or additions to docs 📝", + name: "area: documentation", + }, + { + color: "1177aa", + description: + "Improving how the repository's tests are run and/or code is tested 🧪", + name: "area: testing", + }, + { + color: "f9d0c4", + description: "Managing the repository's maintenance 🛠️", + name: "area: tooling", + }, + { + color: "5319E7", + description: "Good for newcomers, please hop on! 🙌", + name: "good first issue", + }, + { + color: "7a5901", + description: "This doesn't seem right", + name: "invalid", + }, + { + color: "0E8A16", + description: "Please, send a pull request to resolve this! 🙏", + name: "status: accepting prs", + }, + { + color: "eeeeee", + description: "Issue is stale and/or no longer valid", + name: "status: aged away", + }, + { + color: "#ddcccc", + description: "Waiting for something else to be resolved 🙅", + name: "status: blocked", + }, + { + color: "cfd3d7", + description: "This issue or pull request already exists", + name: "status: duplicate", + }, + { + color: "#05104F", + description: "Not yet ready for implementation or a pull request", + name: "status: in discussion", + }, + { + color: "D3F82D", + description: "Further research required...? 🔎", + name: "status: needs investigation", + }, + { + color: "E4BC82", + description: "Needs an action taken by the original poster", + name: "status: waiting for author", + }, + { + color: "ffffff", + description: "This will not be worked on", + name: "status: wontfix", + }, + { + color: "d73a4a", + description: "Something isn't working :( 🐛", + name: "type: bug", + }, + { + color: "a2eeef", + description: "New enhancement or request 🚀", + name: "type: feature", + }, + { + color: "d876e3", + description: "Further information is requested", + name: "type: question", + }, + { + color: "fde282", + description: "Tech debt or other code/repository cleanups 🧹", + name: "type: cleanup", + }, +]; diff --git a/src/steps/initializeGitRemote.test.ts b/src/steps/initializeGitRemote.test.ts new file mode 100644 index 0000000..65b67d9 --- /dev/null +++ b/src/steps/initializeGitRemote.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from "vitest"; + +import { initializeGitRemote } from "./initializeGitRemote.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const options = { + owner: "TestOwner", + repository: "test-repository", +}; + +describe("initializeGitRemote", () => { + it("does not add an origin or fetch when remotes already includes origin", async () => { + mock$.mockResolvedValue({ + stdout: "origin", + }); + + await initializeGitRemote(options); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "git remote", + ], + ], + ] + `); + }); + + it("add an origin and fetch when remotes does not include origin", async () => { + mock$.mockResolvedValue({ + stdout: "", + }); + + await initializeGitRemote(options); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "git remote", + ], + ], + [ + [ + "git remote add origin https://github.com/", + "/", + "", + ], + "TestOwner", + "test-repository", + ], + [ + [ + "git fetch", + ], + ], + ] + `); + }); +}); diff --git a/src/steps/initializeGitRemote.ts b/src/steps/initializeGitRemote.ts new file mode 100644 index 0000000..46c7a70 --- /dev/null +++ b/src/steps/initializeGitRemote.ts @@ -0,0 +1,17 @@ +import { $ } from "execa"; + +import { Options } from "../shared/types.js"; + +type InitializeRepositorySettings = Pick; + +export async function initializeGitRemote({ + owner, + repository, +}: InitializeRepositorySettings) { + const remotes = (await $`git remote`).stdout.trim().split("\n"); + + if (!remotes.includes("origin")) { + await $`git remote add origin https://github.com/${owner}/${repository}`; + await $`git fetch`; + } +} diff --git a/src/steps/initializeRepositorySettings.ts b/src/steps/initializeRepositorySettings.ts new file mode 100644 index 0000000..063648e --- /dev/null +++ b/src/steps/initializeRepositorySettings.ts @@ -0,0 +1,35 @@ +import { Octokit } from "octokit"; + +import { Options } from "../shared/types.js"; + +type InitializeRepositorySettings = Pick< + Options, + "description" | "owner" | "repository" +>; + +export async function initializeRepositorySettings( + octokit: Octokit, + { description, owner, repository }: InitializeRepositorySettings, +) { + await octokit.rest.repos.update({ + allow_auto_merge: true, + allow_rebase_merge: false, + allow_squash_merge: true, + default_branch: "main", + delete_branch_on_merge: true, + description, + has_wiki: false, + owner, + repo: repository, + security_and_analysis: { + secret_scanning: { + status: "enabled", + }, + secret_scanning_push_protection: { + status: "enabled", + }, + }, + squash_merge_commit_message: "PR_BODY", + squash_merge_commit_title: "PR_TITLE", + }); +} diff --git a/src/steps/populateCSpellDictionary.test.ts b/src/steps/populateCSpellDictionary.test.ts new file mode 100644 index 0000000..0d50a6a --- /dev/null +++ b/src/steps/populateCSpellDictionary.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it, vi } from "vitest"; + +import { populateCSpellDictionary } from "./populateCSpellDictionary.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockReadFile = vi.fn(); +const mockWriteFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readFile() { + return mockReadFile; + }, + get writeFile() { + return mockWriteFile; + }, +})); + +const mockFormatJson = vi.fn(); + +vi.mock("./writing/creation/formatters/formatJson.js", () => ({ + get formatJson() { + return mockFormatJson; + }, +})); + +describe("populateCSpellDictionary", () => { + it("works with no existing words when the existing cspell.json has no words", async () => { + const unknownWords = ["abc"]; + + mock$.mockResolvedValue({ + stdout: ` + file-1.ts Unknown word (${unknownWords[0]}) + `, + }); + + mockReadFile.mockResolvedValue(JSON.stringify({})); + + await populateCSpellDictionary(); + + expect(mockFormatJson.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "words": [ + "abc", + ], + }, + ], + ] + `); + }); + + it("adds unknown words to cspell.json", async () => { + const existingWords = ["abc", "ghi", "casing"]; + const unknownWords = ["def", "jkl", "Casing"]; + + mock$.mockResolvedValue({ + stdout: ` + file-1.ts Unknown word (${unknownWords[0]}) + file-2.ts Unknown word (${unknownWords[1]}) + file-2.ts Unknown word (${unknownWords[0]}) + `, + }); + + mockReadFile.mockResolvedValue(JSON.stringify({ words: existingWords })); + + await populateCSpellDictionary(); + + expect(mockFormatJson.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "words": [ + "abc", + "casing", + "def", + "ghi", + "jkl", + ], + }, + ], + ] + `); + }); + + it("doesn't add an upper-cased word when its lower-case will also be in the dictionary", async () => { + const existingWords = ["existing"]; + const unknownWords = ["abc", "Abc"]; + + mock$.mockResolvedValue({ + stdout: ` + file-1.ts Unknown word (${unknownWords[0]}) + file-2.ts Unknown word (${unknownWords[1]}) + `, + }); + + mockReadFile.mockResolvedValue(JSON.stringify({ words: existingWords })); + + await populateCSpellDictionary(); + + expect(mockFormatJson.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "words": [ + "abc", + "existing", + ], + }, + ], + ] + `); + }); +}); diff --git a/src/steps/populateCSpellDictionary.ts b/src/steps/populateCSpellDictionary.ts new file mode 100644 index 0000000..8633bec --- /dev/null +++ b/src/steps/populateCSpellDictionary.ts @@ -0,0 +1,42 @@ +import { $ } from "execa"; +import * as fs from "node:fs/promises"; + +import { readFileAsJson } from "../shared/readFileAsJson.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; + +async function getStdout() { + try { + return await $`pnpm run lint:spelling`; + } catch (error) { + return error as { stdout: string }; + } +} + +export async function populateCSpellDictionary() { + const { stdout } = await getStdout(); + const unknownWords = new Set( + Array.from(stdout.matchAll(/Unknown word \((.+)\)/g)).map( + ([, matched]) => matched, + ), + ); + + const existing = (await readFileAsJson("cspell.json")) as Partial< + typeof import("../../cspell.json") + >; + + const allWords = [...(existing.words ?? []), ...unknownWords]; + const allWordsUnique = new Set(allWords); + + await fs.writeFile( + "cspell.json", + await formatJson({ + ...existing, + words: allWords + .filter((word) => { + const wordLowerCase = word.toLowerCase(); + return word === wordLowerCase || !allWordsUnique.has(wordLowerCase); + }) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())), + }), + ); +} diff --git a/src/steps/removeSetupScripts.ts b/src/steps/removeSetupScripts.ts new file mode 100644 index 0000000..8a1e251 --- /dev/null +++ b/src/steps/removeSetupScripts.ts @@ -0,0 +1,21 @@ +import * as fs from "node:fs/promises"; + +const globPaths = [ + "./bin", + "./docs", + "./script", + "./src/bin", + "./src/create", + "./src/initialize", + "./src/migrate", + "./src/shared", + "./src/steps", + ".github/workflows/test-initialize.yml", + ".github/workflows/test-migrate.yml", +]; + +export async function removeSetupScripts() { + for (const globPath of globPaths) { + await fs.rm(globPath, { force: true, recursive: true }); + } +} diff --git a/src/steps/resetGitTags.ts b/src/steps/resetGitTags.ts new file mode 100644 index 0000000..59894fb --- /dev/null +++ b/src/steps/resetGitTags.ts @@ -0,0 +1,13 @@ +import { $ } from "execa"; + +export async function resetGitTags() { + const { stdout: allLocalTags } = await $`git tag -l`; + + // Create array of local tags by splitting the string at each new line and filtering out empty strings + const allLocalTagsArray = allLocalTags.split("\n").filter(Boolean); + + // Delete all local tags if there are any + if (allLocalTagsArray.length !== 0) { + await $`git tag -d ${allLocalTagsArray}`; + } +} diff --git a/src/steps/runCleanup.test.ts b/src/steps/runCleanup.test.ts new file mode 100644 index 0000000..70cc1fe --- /dev/null +++ b/src/steps/runCleanup.test.ts @@ -0,0 +1,71 @@ +import chalk from "chalk"; +import { describe, expect, it, vi } from "vitest"; + +import { runCleanup } from "./runCleanup.js"; + +const mockExecaCommand = vi.fn().mockRejectedValue("Oh no!"); + +vi.mock("execa", () => ({ + get execaCommand() { + return mockExecaCommand; + }, +})); + +const mockLogLine = vi.fn(); + +vi.mock("../shared/cli/lines.js", () => ({ + get logLine() { + return mockLogLine; + }, +})); + +vi.mock("../shared/cli/spinners.js", () => ({ + withSpinner: vi.fn((_label: string, callback: () => unknown) => callback()), +})); + +describe("runCleanup", () => { + it("does not log when the commands all succeed", async () => { + mockExecaCommand.mockResolvedValue(undefined); + + await runCleanup(["first", "second"], "create"); + + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("logs once when one command fails", async () => { + mockExecaCommand + .mockRejectedValueOnce(new Error("Oh no!")) + .mockResolvedValue(undefined); + + await runCleanup(["first", "second"], "create"); + + expect(mockLogLine).toHaveBeenCalledWith( + [ + chalk.yellow(`🟡 Running \``), + chalk.yellowBright("first"), + chalk.yellow(`\` failed. You should run it and fix its complaints.`), + ].join(""), + ); + }); + + it("logs twice when two commands fail", async () => { + mockExecaCommand.mockRejectedValue(new Error("Oh no!")); + + await runCleanup(["first", "second"], "create"); + + expect(mockLogLine).toHaveBeenCalledWith( + [ + chalk.yellow(`🟡 Running \``), + chalk.yellowBright("first"), + chalk.yellow(`\` failed. You should run it and fix its complaints.`), + ].join(""), + ); + expect(mockLogLine).toHaveBeenCalledWith( + [ + chalk.yellow(`🟡 Running \``), + chalk.yellowBright("second"), + chalk.yellow(`\` failed. You should run it and fix its complaints.`), + ].join(""), + ); + }); +}); diff --git a/src/steps/runCleanup.ts b/src/steps/runCleanup.ts new file mode 100644 index 0000000..79e4413 --- /dev/null +++ b/src/steps/runCleanup.ts @@ -0,0 +1,43 @@ +import chalk from "chalk"; +import { execaCommand } from "execa"; +import { rimraf } from "rimraf"; + +import { logLine } from "../shared/cli/lines.js"; +import { withSpinner } from "../shared/cli/spinners.js"; +import { Mode } from "../shared/types.js"; + +export async function runCleanup(commands: string[], mode: Mode) { + const failed: string[] = []; + + await withSpinner("Cleaning up files", async () => { + if (mode === "migrate") { + // Coverage folders can slow down format and lint times something awful. + // https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1221 + await rimraf("coverage*"); + } + + for (const command of commands) { + try { + await execaCommand(command); + } catch { + failed.push(command); + } + } + }); + + if (!failed.length) { + return; + } + + logLine(); + + for (const command of failed) { + logLine( + [ + chalk.yellow(`🟡 Running \``), + chalk.yellowBright(command), + chalk.yellow(`\` failed. You should run it and fix its complaints.`), + ].join(""), + ); + } +} diff --git a/src/steps/uninstallPackages.test.ts b/src/steps/uninstallPackages.test.ts new file mode 100644 index 0000000..28c0328 --- /dev/null +++ b/src/steps/uninstallPackages.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; + +import { uninstallPackages } from "./uninstallPackages.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +vi.mock("../shared/packages.js", () => ({ + readPackageData: () => ({}), + removeDependencies: vi.fn(), +})); + +describe("uninstallPackages", () => { + it("runs without --offline when offline is false", async () => { + await uninstallPackages(false); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "pnpm add prettier -D", + "", + ], + "", + ], + ] + `); + }); + + it("runs with --offline when offline is true", async () => { + await uninstallPackages(true); + + expect(mock$.mock.calls).toMatchInlineSnapshot(` + [ + [ + [ + "pnpm add prettier -D", + "", + ], + " --offline", + ], + ] + `); + }); +}); diff --git a/src/steps/uninstallPackages.ts b/src/steps/uninstallPackages.ts new file mode 100644 index 0000000..41ccbc5 --- /dev/null +++ b/src/steps/uninstallPackages.ts @@ -0,0 +1,49 @@ +import { $ } from "execa"; + +import { readPackageData, removeDependencies } from "../shared/packages.js"; + +export async function uninstallPackages(offline: boolean | undefined) { + const packageData = await readPackageData(); + + await removeDependencies( + [ + "@clack/prompts", + "all-contributors-for-repository", + "chalk", + "execa", + "git-remote-origin-url", + "git-url-parse", + "lazy-value", + "js-yaml", + "npm-user", + "parse-author", + "rimraf", + "octokit", + "prettier", + "replace-in-file", + "title-case", + "zod", + "zod-validation-error", + ], + packageData.dependencies, + ); + + await removeDependencies( + [ + "@octokit/request-error", + "@types/git-url-parse", + "@types/js-yaml", + "@types/parse-author", + "@types/prettier", + "all-contributors-cli", + "c8", + "eslint-config-prettier", + "globby", + "tsx", + ], + packageData.devDependencies, + "-D", + ); + + await $`pnpm add prettier -D${offline ? " --offline" : ""}`; +} diff --git a/src/steps/updateAllContributorsTable.ts b/src/steps/updateAllContributorsTable.ts new file mode 100644 index 0000000..47e51cd --- /dev/null +++ b/src/steps/updateAllContributorsTable.ts @@ -0,0 +1,24 @@ +import { $ } from "execa"; +import * as fs from "node:fs/promises"; + +import { readFileSafeAsJson } from "../shared/readFileSafeAsJson.js"; +import { AllContributorsData, Options } from "../shared/types.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; + +export async function updateAllContributorsTable({ + owner, + repository, +}: Pick) { + await fs.writeFile( + ".all-contributorsrc", + await formatJson({ + ...((await readFileSafeAsJson( + ".all-contributorsrc", + )) as AllContributorsData), + projectName: repository, + projectOwner: owner, + }), + ); + + await $`npx -y all-contributors-cli generate`; +} diff --git a/src/steps/updateLocalFiles.test.ts b/src/steps/updateLocalFiles.test.ts new file mode 100644 index 0000000..9ba2c2d --- /dev/null +++ b/src/steps/updateLocalFiles.test.ts @@ -0,0 +1,550 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../shared/types.js"; +import { updateLocalFiles } from "./updateLocalFiles.js"; + +const mockReplaceInFile = vi.fn(); + +vi.mock("replace-in-file", () => ({ + get default() { + return mockReplaceInFile; + }, +})); + +const mockReadFileSafeAsJson = vi.fn(); + +vi.mock("../shared/readFileSafeAsJson.js", () => ({ + get readFileSafeAsJson() { + return mockReadFileSafeAsJson; + }, +})); + +const options = { + access: "public", + base: "everything", + description: "Stub description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + mode: "create", + offline: true, + owner: "StubOwner", + repository: "stub-repository", + title: "Stub Title", +} satisfies Options; + +describe("updateLocalFiles", () => { + it("throws a wrapping error when replaceInFiles rejects", async () => { + const error = new Error("Oh no!"); + + mockReadFileSafeAsJson.mockResolvedValue({}); + mockReplaceInFile.mockRejectedValue(error); + + await expect(async () => { + await updateLocalFiles({ ...options, mode: "initialize" }); + }).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Failed to replace /Create TypeScript App/g with Stub Title in ./.github/**/*,./*.*]`, + ); + }); + + it("replaces using the common replacements when the existing package data is null", async () => { + mockReadFileSafeAsJson.mockResolvedValue(null); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "initialize" }); + + expect(mockReplaceInFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /Create TypeScript App/g, + "to": "Stub Title", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /JoshuaKGoldberg\\(\\?:\\\\/\\(\\.\\+\\)\\)\\?/g, + "to": [Function], + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "package.json", + "from": /JoshuaKGoldberg/g, + "to": "StubOwner", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /create-typescript-app/g, + "to": "stub-repository", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": ".eslintrc.cjs", + "from": /\\\\/\\\\\\*\\\\n\\.\\+\\\\\\*\\\\/\\\\n\\\\n/gs, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"author": "\\.\\+"/g, + "to": ""author": "undefined"", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"test:create": "\\.\\+\\\\n/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"test:initialize": "\\.\\*/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"initialize": "\\.\\*/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"test:migrate": "\\.\\+\\\\n/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./README.md", + "from": /## Getting Started\\.\\*## Development/gs, + "to": "## Development", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./.github/DEVELOPMENT.md", + "from": /\\\\n## Setup Scripts\\.\\*\\$/gs, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": " "src/initialize/index.ts", + ", + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": " "src/migrate/index.ts", + ", + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": "["src/index.ts!", "script/initialize*.js"]", + "to": ""src/index.ts!"", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": "["src/**/*.ts!", "script/**/*.js"]", + "to": ""src/**/*.ts!"", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./README.md", + "from": /> 💙 This package was templated with \\.\\+\\\\\\./g, + "to": "> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"bin": "\\.\\+\\\\n/g, + "to": "", + }, + ], + ] + `); + }); + + it("replaces using the common replacements when the existing package data is an empty object", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "initialize" }); + + expect(mockReplaceInFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /Create TypeScript App/g, + "to": "Stub Title", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /JoshuaKGoldberg\\(\\?:\\\\/\\(\\.\\+\\)\\)\\?/g, + "to": [Function], + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "package.json", + "from": /JoshuaKGoldberg/g, + "to": "StubOwner", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /create-typescript-app/g, + "to": "stub-repository", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": ".eslintrc.cjs", + "from": /\\\\/\\\\\\*\\\\n\\.\\+\\\\\\*\\\\/\\\\n\\\\n/gs, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"author": "\\.\\+"/g, + "to": ""author": "undefined"", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"test:create": "\\.\\+\\\\n/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"test:initialize": "\\.\\*/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"initialize": "\\.\\*/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"test:migrate": "\\.\\+\\\\n/g, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./README.md", + "from": /## Getting Started\\.\\*## Development/gs, + "to": "## Development", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./.github/DEVELOPMENT.md", + "from": /\\\\n## Setup Scripts\\.\\*\\$/gs, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": " "src/initialize/index.ts", + ", + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": " "src/migrate/index.ts", + ", + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": "["src/index.ts!", "script/initialize*.js"]", + "to": ""src/index.ts!"", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./knip.json", + "from": "["src/**/*.ts!", "script/**/*.js"]", + "to": ""src/**/*.ts!"", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./README.md", + "from": /> 💙 This package was templated with \\.\\+\\\\\\./g, + "to": "> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"bin": "\\.\\+\\\\n/g, + "to": "", + }, + ], + ] + `); + }); + + it("doesn't remove existing tooling when mode is migrate", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "migrate" }); + + expect(mockReplaceInFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /Create TypeScript App/g, + "to": "Stub Title", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /JoshuaKGoldberg\\(\\?:\\\\/\\(\\.\\+\\)\\)\\?/g, + "to": [Function], + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "package.json", + "from": /JoshuaKGoldberg/g, + "to": "StubOwner", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": [ + "./.github/**/*", + "./*.*", + ], + "from": /create-typescript-app/g, + "to": "stub-repository", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": ".eslintrc.cjs", + "from": /\\\\/\\\\\\*\\\\n\\.\\+\\\\\\*\\\\/\\\\n\\\\n/gs, + "to": "", + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "./package.json", + "from": /"author": "\\.\\+"/g, + "to": ""author": "undefined"", + }, + ], + ] + `); + }); + + it("does not replace an existing description when it does not exist", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "initialize" }); + + expect(mockReplaceInFile).not.toHaveBeenCalledWith({ + allowEmptyPaths: true, + files: ["./.github/**/*", "./*.*"], + from: expect.anything(), + to: options.description, + }); + }); + + it("replaces an existing description when it exists", async () => { + const existingDescription = "Existing description."; + + mockReadFileSafeAsJson.mockResolvedValue({ + description: existingDescription, + }); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "initialize" }); + + expect(mockReplaceInFile).toHaveBeenCalledWith({ + allowEmptyPaths: true, + files: ["./.github/**/*", "./*.*"], + from: existingDescription, + to: options.description, + }); + }); + + it("removes bin when the mode is initialize", async () => { + mockReadFileSafeAsJson.mockResolvedValue({ + version: "1.2.3", + }); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "initialize" }); + + expect(mockReplaceInFile).toHaveBeenCalledWith({ + allowEmptyPaths: true, + files: "./package.json", + from: /"bin": ".+\n/g, + to: "", + }); + }); + + it("does not remove bin when the mode is migrate", async () => { + mockReadFileSafeAsJson.mockResolvedValue({ + version: "1.2.3", + }); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "migrate" }); + + expect(mockReplaceInFile).not.toHaveBeenCalledWith({ + allowEmptyPaths: true, + files: "./package.json", + from: /"bin": ".+\n/g, + to: "", + }); + }); + + it("resets package version to 0.0.0 when mode is initialize", async () => { + mockReadFileSafeAsJson.mockResolvedValue({ + version: "1.2.3", + }); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "initialize" }); + + expect(mockReplaceInFile).toHaveBeenCalledWith({ + allowEmptyPaths: true, + files: "./package.json", + from: /"version": "1.2.3"/g, + to: '"version": "0.0.0"', + }); + }); + + it("does not reset package version to 0.0.0 when mode is migrate", async () => { + mockReadFileSafeAsJson.mockResolvedValue({ + version: "1.2.3", + }); + mockReplaceInFile.mockResolvedValue([]); + + await updateLocalFiles({ ...options, mode: "migrate" }); + + expect(mockReplaceInFile).not.toHaveBeenCalledWith({ + allowEmptyPaths: true, + files: "./package.json", + from: /"version": "1.2.3"/g, + to: '"version": "0.0.0"', + }); + }); +}); diff --git a/src/steps/updateLocalFiles.ts b/src/steps/updateLocalFiles.ts new file mode 100644 index 0000000..c9488fa --- /dev/null +++ b/src/steps/updateLocalFiles.ts @@ -0,0 +1,94 @@ +import replaceInFile, { From, To } from "replace-in-file"; + +import { readFileSafeAsJson } from "../shared/readFileSafeAsJson.js"; +import { Options } from "../shared/types.js"; +import { createJoshuaKGoldbergReplacement } from "./createJoshuaKGoldbergReplacement.js"; +import { endOfReadmeTemplateLine } from "./updateReadme.js"; + +interface ExistingPackageData { + description?: string; + version?: string; +} + +export async function updateLocalFiles(options: Options) { + const existingPackage = ((await readFileSafeAsJson("./package.json")) ?? + {}) as ExistingPackageData; + + const replacements: [From, To, (string | string[])?][] = [ + [/Create TypeScript App/g, options.title], + createJoshuaKGoldbergReplacement(options), + [/JoshuaKGoldberg/g, options.owner, "package.json"], + [/create-typescript-app/g, options.repository], + [/\/\*\n.+\*\/\n\n/gs, ``, ".eslintrc.cjs"], + [/"author": ".+"/g, `"author": "${options.author}"`, "./package.json"], + ...(options.mode === "migrate" + ? [] + : ([ + // Only creating a new repository should remove these parts. + // Existing ones might coincidentally have them too. + [/"test:create": ".+\n/g, ``, "./package.json"], + [/"test:initialize": ".*/g, ``, "./package.json"], + [/"initialize": ".*/g, ``, "./package.json"], + [/"test:migrate": ".+\n/g, ``, "./package.json"], + [ + /## Getting Started.*## Development/gs, + `## Development`, + "./README.md", + ], + [/\n## Setup Scripts.*$/gs, "", "./.github/DEVELOPMENT.md"], + [`\t\t"src/initialize/index.ts",\n`, ``, "./knip.json"], + [`\t\t"src/migrate/index.ts",\n`, ``, "./knip.json"], + [ + `["src/index.ts!", "script/initialize*.js"]`, + `"src/index.ts!"`, + "./knip.json", + ], + [ + `["src/**/*.ts!", "script/**/*.js"]`, + `"src/**/*.ts!"`, + "./knip.json", + ], + // Edge case: migration scripts will rewrite README.md attribution + [ + /> 💙 This package was templated with .+\./g, + endOfReadmeTemplateLine, + "./README.md", + ], + ] as typeof replacements)), + ]; + + if (existingPackage.description) { + replacements.push([existingPackage.description, options.description]); + } + + if (options.mode !== "migrate") { + replacements.push([/"bin": ".+\n/g, ``, "./package.json"]); + + if (options.mode === "initialize" && existingPackage.version) { + replacements.push([ + new RegExp(`"version": "${existingPackage.version}"`, "g"), + `"version": "0.0.0"`, + "./package.json", + ]); + } + } + + for (const [from, to, files = ["./.github/**/*", "./*.*"]] of replacements) { + try { + await replaceInFile({ + allowEmptyPaths: true, + files, + from, + to, + }); + } catch (error) { + const toString = typeof to === "function" ? "(function)" : to; + throw new Error( + `Failed to replace ${from.toString()} with ${toString} in ${files.toString()}`, + { + cause: error, + }, + ); + } + } +} diff --git a/src/steps/updateReadme.test.ts b/src/steps/updateReadme.test.ts new file mode 100644 index 0000000..135c456 --- /dev/null +++ b/src/steps/updateReadme.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi } from "vitest"; + +import { updateReadme } from "./updateReadme.js"; + +const mockWriteFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get writeFile() { + return mockWriteFile; + }, +})); + +const mockReadFileSafe = vi.fn(); + +vi.mock("../shared/readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); + +const options = { + owner: "NewOwner", +}; + +describe("updateReadme", () => { + it("adds a notice when the file does not contain it already", async () => { + mockReadFileSafe.mockResolvedValue( + "Existing JoshuaKGoldberg/create-typescript-app content.", + ); + + await updateReadme(options); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "./README.md", + "Existing NewOwner/create-typescript-app content. + + + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). + ", + ], + ] + `); + }); + + it("doesn't add a notice when the file contains it already", async () => { + mockReadFileSafe.mockResolvedValue(` + Existing JoshuaKGoldberg/create-typescript-app content. + + + + > 💙 This package was templated using [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + `); + + await updateReadme(options); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "./README.md", + " + Existing NewOwner/create-typescript-app content. + + + + > 💙 This package was templated using [create-typescript-app](https://github.com/NewOwner/create-typescript-app). + ", + ], + ] + `); + }); + + it("doesn't add a notice when the file contains an older version of it already", async () => { + mockReadFileSafe.mockResolvedValue(` + Existing JoshuaKGoldberg/create-typescript-app content. + + 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + `); + + await updateReadme(options); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "./README.md", + " + Existing NewOwner/create-typescript-app content. + + 💙 This package is based on [@NewOwner](https://github.com/NewOwner)'s [create-typescript-app](https://github.com/NewOwner/create-typescript-app). + ", + ], + ] + `); + }); +}); diff --git a/src/steps/updateReadme.ts b/src/steps/updateReadme.ts new file mode 100644 index 0000000..6032020 --- /dev/null +++ b/src/steps/updateReadme.ts @@ -0,0 +1,30 @@ +import * as fs from "node:fs/promises"; +import { EOL } from "node:os"; + +import { readFileSafe } from "../shared/readFileSafe.js"; +import { Options } from "../shared/types.js"; + +export const endOfReadmeTemplateLine = `> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).`; + +export const endOfReadmeNotice = [ + ``, + ``, + ``, + endOfReadmeTemplateLine, + ``, +].join(EOL); + +export const endOfReadmeMatcher = + /💙.+(?:based|built|templated).+(?:from|using|on|with).+create-typescript-app/; + +export async function updateReadme(options: Pick) { + let contents = await readFileSafe("./README.md", ""); + + contents = contents.replaceAll("JoshuaKGoldberg", options.owner); + + if (!endOfReadmeMatcher.test(contents)) { + contents += endOfReadmeNotice; + } + + await fs.writeFile("./README.md", contents); +} diff --git a/src/steps/writeReadme/findExistingBadges.test.ts b/src/steps/writeReadme/findExistingBadges.test.ts new file mode 100644 index 0000000..8d9c44e --- /dev/null +++ b/src/steps/writeReadme/findExistingBadges.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it, test } from "vitest"; + +import { findExistingBadges } from "./findExistingBadges.js"; + +describe("findExistingBadges", () => { + describe("no result cases", () => { + test.each([ + "", + "abc123", + "# Test Title", + "[]", + "[][]", + "[]()", + "[][]()()", + ``, + ])("%j", (input) => { + expect(findExistingBadges(input)).toEqual([]); + }); + }); + + describe("single result cases", () => { + test.each([ + `[![GitHub CI](https://github.com/ExampleOwner/console-fail-test/actions/workflows/compile.yml/badge.svg)](https://github.com/ExampleOwner/console-fail-test/actions/workflows/compile.yml)`, + `[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io)`, + `![TypeScript: Strict](https://img.shields.io/badge/typescript-strict-brightgreen.svg)`, + `[![NPM version](https://badge.fury.io/js/console-fail-test.svg)](http://badge.fury.io/js/console-fail-test)`, + `[![Downloads](http://img.shields.io/npm/dm/console-fail-test.svg)](https://npmjs.org/package/console-fail-test)`, + "badge", + "badge", + "badge", + "badge", + ` + + + All Contributors: 1 + + + `, + ` Codecov Test Coverage`, + ` Contributor Covenant`, + `📦 npm version`, + `🧪 Coverage`, + `🤝 Code of Conduct: kept`, + `📝 License: MIT`, + `License: MIT`, + ` + License: MIT + `, + `💪 TypeScript: Strict`, + `Style: Prettier`, + `TypeScript: Strict`, + ` + TypeScript: Strict + `, + ])("%s", (contents) => { + expect(findExistingBadges(contents)).toEqual([contents.trim()]); + }); + }); + + it("doesn't collect badges after a ##", () => { + expect( + findExistingBadges(` + test badge a + + ## Usage + + test badge b + `), + ).toEqual([`test badge a`]); + }); + + it("doesn't collect badges after an h2", () => { + expect( + findExistingBadges(` + test badge a + +

Usage

+ + test badge b + `), + ).toEqual([`test badge a`]); + }); + + test("real-world usage", () => { + expect( + findExistingBadges(` +

+ + + All Contributors: 1 + + + Codecov Test Coverage + Contributor Covenant + License: MIT + Style: Prettier + TypeScript: Strict +

+ `), + ).toMatchInlineSnapshot(` + [ + "All Contributors: 1", + "Codecov Test Coverage", + "Contributor Covenant", + "License: MIT", + "Style: Prettier", + "TypeScript: Strict", + ] + `); + }); +}); diff --git a/src/steps/writeReadme/findExistingBadges.ts b/src/steps/writeReadme/findExistingBadges.ts new file mode 100644 index 0000000..f916725 --- /dev/null +++ b/src/steps/writeReadme/findExistingBadges.ts @@ -0,0 +1,30 @@ +export const existingBadgeMatcherCreators = [ + () => /\[!\[.+\]\(.+\)\]\(.+\)/g, + () => /!\[.+\]\(.+\)/g, + () => /^\s*[\s\S]+?<\/a>/gm, + () => //g, +]; + +export function findExistingBadges(contents: string): string[] { + const badges: string[] = []; + let remaining = contents.split(/<\s*h2.*>|##/)[0]; + + for (const createMatcher of existingBadgeMatcherCreators) { + while (true) { + const matcher = createMatcher(); + const matched = matcher.exec(remaining); + + if (!matched) { + break; + } + + const [badge] = matched; + + badges.push(badge.trim()); + + remaining = remaining.replace(badge, ""); + } + } + + return badges; +} diff --git a/src/steps/writeReadme/findIntroSectionClose.test.ts b/src/steps/writeReadme/findIntroSectionClose.test.ts new file mode 100644 index 0000000..60da73e --- /dev/null +++ b/src/steps/writeReadme/findIntroSectionClose.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; + +import { findIntroSectionClose } from "./findIntroSectionClose.js"; + +describe("findIntroSectionClose", () => { + it.each([ + [ + `# First +## Second`, + 6, + ], + [ + `# First +

Second

`, + 6, + ], + [ + `# First +

Second

`, + 6, + ], + [ + `# First +\`\`\`js +... +\`\`\``, + 6, + ], + [ + `# First + +[![](https://img.shields.io/badge/abc-ffcc00.svg)](image.jpg) + +[![](https://img.shields.io/badge/abc-ffcc00.svg)](image.jpg) +`, + 135, + ], + [ + `Plain heading + +Next line. +`, + 14, + ], + ])("%s", (contents, expected) => { + expect(findIntroSectionClose(contents)).toEqual(expected); + }); +}); diff --git a/src/steps/writeReadme/findIntroSectionClose.ts b/src/steps/writeReadme/findIntroSectionClose.ts new file mode 100644 index 0000000..e910b5f --- /dev/null +++ b/src/steps/writeReadme/findIntroSectionClose.ts @@ -0,0 +1,22 @@ +import { existingBadgeMatcherCreators } from "./findExistingBadges.js"; + +export function findIntroSectionClose(contents: string) { + // Highest priority match: an h2, presumably following badges + const indexOfH2OrCodeBlock = contents.search(/## |<\s*h2|```/); + + if (indexOfH2OrCodeBlock !== -1) { + return indexOfH2OrCodeBlock - 2; + } + + // Failing that, if any badges are found, go after the last of them + for (const createMatcher of existingBadgeMatcherCreators) { + const lastMatch = [...contents.matchAll(createMatcher())].at(-1); + + if (lastMatch?.index) { + return lastMatch.index + lastMatch[0].length + 2; + } + } + + // Lastly, go for the second line altogether + return contents.indexOf("\n", 1) + 1; +} diff --git a/src/steps/writeReadme/generateTopContent.test.ts b/src/steps/writeReadme/generateTopContent.test.ts new file mode 100644 index 0000000..1e9c136 --- /dev/null +++ b/src/steps/writeReadme/generateTopContent.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from "vitest"; + +import { Options } from "../../shared/types.js"; +import { generateTopContent } from "./generateTopContent.js"; + +const optionsBase = { + access: "public", + description: "Test description", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + mode: "create", + owner: "test-owner", + repository: "test-repository", + title: "Test Title", +} satisfies Options; + +describe("findExistingBadges", () => { + it("generates full contents when there are no existing badges", () => { + expect(generateTopContent(optionsBase, [])).toMatchInlineSnapshot(` + "

Test Title

+ +

Test description

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`" + `); + }); + + it("replaces existing contents when there is an existing known badge", () => { + expect( + generateTopContent(optionsBase, [ + `TypeScript: Strict`, + ]), + ).toMatchInlineSnapshot(` + "

Test Title

+ +

Test description

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`" + `); + }); + + it("push existing badges to the end when there is an existing unknown badge", () => { + expect( + generateTopContent(optionsBase, [ + `Unknown Badge`, + ]), + ).toMatchInlineSnapshot(` + "

Test Title

+ +

Test description

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict + Unknown Badge +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`" + `); + }); + + it("does not include a greet section when the mode is migrate", () => { + expect(generateTopContent({ ...optionsBase, mode: "migrate" }, [])) + .toMatchInlineSnapshot(` + "

Test Title

+ +

Test description

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

" + `); + }); +}); diff --git a/src/steps/writeReadme/generateTopContent.ts b/src/steps/writeReadme/generateTopContent.ts new file mode 100644 index 0000000..04c04ea --- /dev/null +++ b/src/steps/writeReadme/generateTopContent.ts @@ -0,0 +1,84 @@ +import { Options } from "../../shared/types.js"; + +export function generateTopContent(options: Options, existingBadges: string[]) { + const remainingExistingBadges = new Set(existingBadges); + const badges: string[] = []; + + for (const [badgeLine, existingMatcher] of [ + [ + !options.excludeAllContributors && + ` + + 👪 All Contributors: 1 + + `, + /🤝 Code of Conduct: Kept`, + /CODE_OF_CONDUCT\.md/, + ], + [ + !options.excludeTests && + `🧪 Coverage`, + /https:\/\/codecov\.io\/gh/, + ], + [ + `📝 License: MIT`, + /LICENSE\.(md|txt)/, + ], + [ + `📦 npm version`, + /npm.*v/i, + ], + [ + `💪 TypeScript: Strict`, + /typescript.*strict/i, + ], + ] as const) { + const existingMatch = existingBadges.find((existingLine) => + existingMatcher.test(existingLine), + ); + + if (existingMatch) { + remainingExistingBadges.delete(existingMatch); + } + + if (badgeLine) { + badges.push(badgeLine); + } + } + + return `

${options.title}

+ +

${options.description}

+ +

+${[...badges, ...remainingExistingBadges] + .map((badge) => `\t${badge}`) + .join("\n")} +

${ + options.logo + ? ` + +${options.logo.alt} + +` + : "" + }${ + options.mode === "migrate" + ? "" + : ` + +## Usage + +\`\`\`shell +npm i ${options.repository} +\`\`\` +\`\`\`ts +import { greet } from "${options.repository}"; + +greet("Hello, world! 💖"); +\`\`\`` + }`; +} diff --git a/src/steps/writeReadme/index.test.ts b/src/steps/writeReadme/index.test.ts new file mode 100644 index 0000000..22c4558 --- /dev/null +++ b/src/steps/writeReadme/index.test.ts @@ -0,0 +1,319 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../../shared/types.js"; +import { writeReadme } from "./index.js"; + +const mockWriteFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get writeFile() { + return mockWriteFile; + }, +})); + +const mockReadFileSafe = vi.fn(); + +vi.mock("../../shared/readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); + +const options = { + access: "public", + author: "Test Author", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + funding: "TestFunding", + mode: "create", + owner: "TestOwner", + repository: "test-repository", + title: "Test Title", +} satisfies Options; + +describe("writeReadme", () => { + it("writes a new file when the README.md does not yet exist", async () => { + mockReadFileSafe.mockResolvedValueOnce(""); + + await writeReadme(options); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "README.md", + "

Test Title

+ +

Test description.

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\` + + ## Contributors + + + + + + +
+ + + + + + + + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). + ", + ], + ] + `); + }); + + it("adds sections when the README.md already exists and is sparse", async () => { + mockReadFileSafe.mockResolvedValueOnce(`# ${options.title}\n`); + + await writeReadme(options); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "README.md", + "

Test Title

+ +

Test description.

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\` + + ## Contributors + + + + + + +
+ + + + + + + + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). + ", + ], + ] + `); + }); + + it("adds all-contributors content when directed to and the indicator does not yet exist", async () => { + mockReadFileSafe.mockResolvedValueOnce(`# ${options.title}\n`); + + await writeReadme({ + ...options, + excludeAllContributors: false, + }); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "README.md", + "

Test Title

+ +

Test description.

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\` + + ## Contributors + + + + + + +
+ + + + + + + + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). + ", + ], + ] + `); + }); + + it("does not duplicate sections when the README.md already exists and has them", async () => { + mockReadFileSafe.mockResolvedValueOnce(`

Test Title

+ +

Test description.

+ +

+ + + All Contributors: 2 + + + + Contributor Covenant + License: MIT + Style: Prettier + TypeScript: Strict +

+ + +## Contributors + + + + + + + +
+ + + + + + + + + +> 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). +`); + + await writeReadme(options); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "README.md", + "

Test Title

+ +

Test description.

+ +

+ + + 👪 All Contributors: 1 + + + 🤝 Code of Conduct: Kept + 🧪 Coverage + 📝 License: MIT + 📦 npm version + 💪 TypeScript: Strict + Style: Prettier +

+ + ## Usage + + \`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\` + + ## Contributors + + + + + + + +
+ + + + + + + + + + > 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + ", + ], + ] + `); + }); +}); diff --git a/src/steps/writeReadme/index.ts b/src/steps/writeReadme/index.ts new file mode 100644 index 0000000..85478b5 --- /dev/null +++ b/src/steps/writeReadme/index.ts @@ -0,0 +1,73 @@ +import * as fs from "node:fs/promises"; + +import { readFileSafe } from "../../shared/readFileSafe.js"; +import { Options } from "../../shared/types.js"; +import { endOfReadmeMatcher, endOfReadmeNotice } from "../updateReadme.js"; +import { findExistingBadges } from "./findExistingBadges.js"; +import { findIntroSectionClose } from "./findIntroSectionClose.js"; +import { generateTopContent } from "./generateTopContent.js"; + +const contributorsIndicator = ``; + +function generateAllContributorsContent(options: Options) { + return [ + `## Contributors`, + ``, + ``, + contributorsIndicator, + ``, + !options.excludeLintMd && ``, + ``, + ``, + `
`, + ``, + !options.excludeLintMd && ``, + ``, + ``, + ``, + ``, + ] + .filter(Boolean) + .join("\n"); +} + +export async function writeReadme(options: Options) { + const allContributorsContent = + !options.excludeAllContributors && generateAllContributorsContent(options); + let contents = await readFileSafe("README.md", ""); + if (!contents) { + await fs.writeFile( + "README.md", + [ + generateTopContent(options, []), + allContributorsContent, + endOfReadmeNotice.slice(1), + ] + .filter(Boolean) + .join("\n\n"), + ); + return; + } + + const endOfIntroSection = findIntroSectionClose(contents); + + contents = [ + generateTopContent(options, findExistingBadges(contents)), + contents.slice(endOfIntroSection), + ] + .join("") + .replace(/\[!\[.+\]\(.+\)\]\(.+\)/g, "") + .replace(/!\[.+\]\(.+\)/g, "") + .replaceAll("\r", "") + .replaceAll("\n\n\n", "\n\n"); + + if (allContributorsContent && !contents.includes(contributorsIndicator)) { + contents = [contents, allContributorsContent].join("\n\n"); + } + + if (!endOfReadmeMatcher.test(contents)) { + contents = [contents, endOfReadmeNotice].join("\n"); + } + + await fs.writeFile("README.md", contents); +} diff --git a/src/steps/writing/creation/createDotGitignore.test.ts b/src/steps/writing/creation/createDotGitignore.test.ts new file mode 100644 index 0000000..c887e4b --- /dev/null +++ b/src/steps/writing/creation/createDotGitignore.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { createDotGitignore } from "./createDotGitignore.js"; + +describe("createDotGitignore", () => { + it("creates an ignore file with coverage when excludeTests is false", () => { + const actual = createDotGitignore({ excludeTests: false }); + + expect(actual).toMatchInlineSnapshot(` + "coverage/ + lib/ + node_modules/ + " + `); + }); + + it("creates an ignore file without coverage when excludeTests is true", () => { + const actual = createDotGitignore({ excludeTests: true }); + + expect(actual).toMatchInlineSnapshot(` + "lib/ + node_modules/ + " + `); + }); +}); diff --git a/src/steps/writing/creation/createDotGitignore.ts b/src/steps/writing/creation/createDotGitignore.ts new file mode 100644 index 0000000..3321ba8 --- /dev/null +++ b/src/steps/writing/creation/createDotGitignore.ts @@ -0,0 +1,10 @@ +import { Options } from "../../../shared/types.js"; +import { formatIgnoreFile } from "./formatters/formatIgnoreFile.js"; + +export function createDotGitignore(options: Pick) { + return formatIgnoreFile([ + ...(options.excludeTests ? [] : ["coverage/"]), + "lib/", + "node_modules/", + ]); +} diff --git a/src/steps/writing/creation/createESLintRC.test.ts b/src/steps/writing/creation/createESLintRC.test.ts new file mode 100644 index 0000000..014986a --- /dev/null +++ b/src/steps/writing/creation/createESLintRC.test.ts @@ -0,0 +1,276 @@ +import { describe, expect, it } from "vitest"; + +import { Options } from "../../../shared/types.js"; +import { createESLintRC } from "./createESLintRC.js"; + +function fakeOptions(getExcludeValue: (exclusionName: string) => boolean) { + return { + access: "public", + author: "TestAuthor", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + ...Object.fromEntries( + [ + "excludeCompliance", + "excludeAllContributors", + "excludeLintDeprecation", + "excludeLintESLint", + "excludeLintJSDoc", + "excludeLintJson", + "excludeLintKnip", + "excludeLintMd", + "excludeLintPackageJson", + "excludeLintPackages", + "excludeLintPerfectionist", + "excludeLintRegex", + "excludeLintSpelling", + "excludeLintStrict", + "excludeLintStylistic", + "excludeLintYml", + "excludeReleases", + "excludeRenovate", + "excludeTests", + ].map((key) => [key, getExcludeValue(key)]), + ), + mode: "create", + owner: "TestOwner", + repository: "test-repository", + skipGitHubApi: true, + skipInstall: true, + skipRemoval: true, + title: "Test Title", + } satisfies Options; +} + +describe("createESLintRC", () => { + it("creates a minimal config when all exclusions are enabled", async () => { + expect(await createESLintRC(fakeOptions(() => true))) + .toMatchInlineSnapshot(` + "/** @type {import("@types/eslint").Linter.Config} */ + module.exports = { + env: { + es2022: true, + node: true, + }, + extends: ["eslint:recommended", "plugin:n/recommended"], + ignorePatterns: ["!.*", "lib", "node_modules", "pnpm-lock.yaml"], + overrides: [ + { + extends: ["plugin:@typescript-eslint/recommended"], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + rules: { + // These off-by-default rules work well for this repo and we like them on. + "logical-assignment-operators": [ + "error", + "always", + { enforceForIfStatements: true }, + ], + "operator-assignment": "error", + }, + }, + { + files: "**/*.md/*.ts", + rules: { + "n/no-missing-import": ["error", { allowModules: ["test-repository"] }], + }, + }, + { + extends: ["plugin:@typescript-eslint/recommended-type-checked"], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.eslint.json", + }, + }, + ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + reportUnusedDisableDirectives: true, + root: true, + rules: { + // These off/less-strict-by-default rules work well for this repo and we like them on. + "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }], + + // These on-by-default rules don't work well for this repo and we like them off. + "no-case-declarations": "off", + "no-constant-condition": "off", + "no-inner-declarations": "off", + "no-mixed-spaces-and-tabs": "off", + }, + }; + " + `); + }); + + it("creates a full config when all exclusions are disabled", async () => { + expect(await createESLintRC(fakeOptions(() => false))) + .toMatchInlineSnapshot(` + "/** @type {import("@types/eslint").Linter.Config} */ + module.exports = { + env: { + es2022: true, + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:eslint-comments/recommended", + "plugin:n/recommended", + "plugin:perfectionist/recommended-natural", + "plugin:regexp/recommended", + "plugin:vitest/recommended", + ], + ignorePatterns: ["!.*", "coverage", "lib", "node_modules", "pnpm-lock.yaml"], + overrides: [ + { + extends: ["plugin:markdown/recommended"], + files: ["**/*.md"], + processor: "markdown/markdown", + }, + { + extends: [ + "plugin:jsdoc/recommended-typescript-error", + "plugin:@typescript-eslint/strict", + "plugin:@typescript-eslint/stylistic", + ], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + rules: { + // These off-by-default rules work well for this repo and we like them on. + "jsdoc/informative-docs": "error", + "logical-assignment-operators": [ + "error", + "always", + { enforceForIfStatements: true }, + ], + "operator-assignment": "error", + + // These on-by-default rules don't work well for this repo and we like them off. + "jsdoc/require-jsdoc": "off", + "jsdoc/require-param": "off", + "jsdoc/require-property": "off", + "jsdoc/require-returns": "off", + }, + }, + { + files: "**/*.md/*.ts", + rules: { + "n/no-missing-import": ["error", { allowModules: ["test-repository"] }], + }, + }, + { + excludedFiles: ["**/*.md/*.ts"], + extends: [ + "plugin:@typescript-eslint/strict-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + ], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.eslint.json", + }, + rules: { + // These off-by-default rules work well for this repo and we like them on. + "deprecation/deprecation": "error", + }, + }, + { + excludedFiles: ["package.json"], + extends: ["plugin:jsonc/recommended-with-json"], + files: ["*.json", "*.jsonc"], + parser: "jsonc-eslint-parser", + rules: { + "jsonc/comma-dangle": "off", + "jsonc/sort-keys": "error", + }, + }, + { + files: ["*.jsonc"], + rules: { + "jsonc/no-comments": "off", + }, + }, + { + extends: ["plugin:package-json/recommended"], + files: ["package.json"], + parser: "jsonc-eslint-parser", + plugins: ["package-json"], + }, + { + files: "**/*.test.ts", + rules: { + // These on-by-default rules aren't useful in test files. + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + }, + }, + { + extends: ["plugin:yml/standard", "plugin:yml/prettier"], + files: ["**/*.{yml,yaml}"], + parser: "yaml-eslint-parser", + rules: { + "yml/file-extension": ["error", { extension: "yml" }], + "yml/sort-keys": [ + "error", + { + order: { type: "asc" }, + pathPattern: "^.*$", + }, + ], + "yml/sort-sequence-values": [ + "error", + { + order: { type: "asc" }, + pathPattern: "^.*$", + }, + ], + }, + }, + ], + parser: "@typescript-eslint/parser", + plugins: [ + "@typescript-eslint", + "deprecation", + "jsdoc", + "perfectionist", + "regexp", + "vitest", + ], + reportUnusedDisableDirectives: true, + root: true, + rules: { + // These off/less-strict-by-default rules work well for this repo and we like them on. + "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }], + + // These on-by-default rules don't work well for this repo and we like them off. + "no-case-declarations": "off", + "no-constant-condition": "off", + "no-inner-declarations": "off", + "no-mixed-spaces-and-tabs": "off", + + // Stylistic concerns that don't interfere with Prettier + "@typescript-eslint/padding-line-between-statements": [ + "error", + { blankLine: "always", next: "*", prev: "block-like" }, + ], + "no-useless-rename": "error", + "object-shorthand": "error", + "perfectionist/sort-objects": [ + "error", + { + order: "asc", + "partition-by-comment": true, + type: "natural", + }, + ], + }, + }; + " + `); + }); +}); diff --git a/src/steps/writing/creation/createESLintRC.ts b/src/steps/writing/creation/createESLintRC.ts new file mode 100644 index 0000000..681ff40 --- /dev/null +++ b/src/steps/writing/creation/createESLintRC.ts @@ -0,0 +1,245 @@ +import { Options } from "../../../shared/types.js"; +import { formatTypeScript } from "./formatters/formatTypeScript.js"; + +export async function createESLintRC(options: Options) { + return await formatTypeScript(`/** @type {import("@types/eslint").Linter.Config} */ +module.exports = { + env: { + es2022: true, + node: true, + }, + extends: [ + "eslint:recommended", + ${ + options.excludeLintESLint + ? "" + : `"plugin:eslint-comments/recommended", + ` + }"plugin:n/recommended",${ + options.excludeLintPerfectionist + ? "" + : ` + "plugin:perfectionist/recommended-natural",` + }${options.excludeLintRegex ? "" : `"plugin:regexp/recommended",`}${ + options.excludeTests ? "" : `"plugin:vitest/recommended",` + } + ], + ignorePatterns: [ + "!.*",${options.excludeTests ? "" : `\n "coverage",`} + "lib", + "node_modules", + "pnpm-lock.yaml", + ], + overrides: [${ + options.excludeLintMd + ? "" + : ` + { + extends: ["plugin:markdown/recommended"], + files: ["**/*.md"], + processor: "markdown/markdown", + },` + } + { + extends: [ + ${ + options.excludeLintJSDoc + ? "" + : `"plugin:jsdoc/recommended-typescript-error", + ` + }"plugin:@typescript-eslint/${ + options.excludeLintStrict ? "recommended" : "strict" + }",${ + options.excludeLintStylistic + ? "" + : ` + "plugin:@typescript-eslint/stylistic",` + } + ], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + rules: { + // These off-by-default rules work well for this repo and we like them on. + ${ + options.excludeLintJSDoc + ? "" + : `"jsdoc/informative-docs": "error", + ` + }"logical-assignment-operators": [ + "error", + "always", + { enforceForIfStatements: true }, + ], + "operator-assignment": "error",${ + options.excludeLintJSDoc + ? "" + : ` + + // These on-by-default rules don't work well for this repo and we like them off. + "jsdoc/require-jsdoc": "off", + "jsdoc/require-param": "off", + "jsdoc/require-property": "off", + "jsdoc/require-returns": "off",` + } + }, + }, + { + files: "**/*.md/*.ts", + rules: { + "n/no-missing-import": [ + "error", + { allowModules: ["${options.repository}"] }, + ], + }, + }, + { + ${ + options.excludeLintMd + ? "" + : `excludedFiles: ["**/*.md/*.ts"], + ` + }extends: [ + "plugin:@typescript-eslint/${ + options.excludeLintStrict ? "recommended" : "strict" + }-type-checked",${ + options.excludeLintStylistic + ? "" + : ` + "plugin:@typescript-eslint/stylistic-type-checked",` + } + ], + files: ["**/*.ts"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.eslint.json", + },${ + options.excludeLintDeprecation + ? "" + : `rules: { + // These off-by-default rules work well for this repo and we like them on. + "deprecation/deprecation": "error", + },` + } + }, + ${ + options.excludeLintJson + ? "" + : `{ + excludedFiles: ["package.json"], + extends: ["plugin:jsonc/recommended-with-json"], + files: ["*.json", "*.jsonc"], + parser: "jsonc-eslint-parser", + rules: { + "jsonc/comma-dangle": "off", + "jsonc/sort-keys": "error", + }, + }, + { + files: ["*.jsonc"], + rules: { + "jsonc/no-comments": "off", + }, + },` + }${ + options.excludeLintPackageJson + ? "" + : `{ + extends: ["plugin:package-json/recommended"], + files: ["package.json"], + parser: "jsonc-eslint-parser", + plugins: ["package-json"], + },` + }${ + options.excludeTests + ? "" + : `\n{ + files: "**/*.test.ts", + rules: { + // These on-by-default rules aren't useful in test files. + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + }, + },` + }${ + options.excludeLintYml + ? "" + : `\n{ + extends: ["plugin:yml/standard", "plugin:yml/prettier"], + files: ["**/*.{yml,yaml}"], + parser: "yaml-eslint-parser", + rules: { + "yml/file-extension": ["error", { extension: "yml" }], + "yml/sort-keys": [ + "error", + { + order: { type: "asc" }, + pathPattern: "^.*$", + }, + ], + "yml/sort-sequence-values": [ + "error", + { + order: { type: "asc" }, + pathPattern: "^.*$", + }, + ], + }, + }, + ` + }], + parser: "@typescript-eslint/parser", + plugins: [ + "@typescript-eslint", + ${ + options.excludeLintDeprecation + ? "" + : `"deprecation", + ` + }${ + options.excludeLintJSDoc + ? "" + : `"jsdoc", + ` + }${options.excludeLintPerfectionist ? "" : `"perfectionist",`}${ + options.excludeLintRegex ? "" : `"regexp",` + }${options.excludeTests ? "" : `\n"vitest",`} + ], + reportUnusedDisableDirectives: true, + root: true, + rules: { + // These off/less-strict-by-default rules work well for this repo and we like them on. + "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }], + + // These on-by-default rules don't work well for this repo and we like them off. + "no-case-declarations": "off", + "no-constant-condition": "off", + "no-inner-declarations": "off", + "no-mixed-spaces-and-tabs": "off", + + ${ + options.excludeLintStylistic + ? "" + : `// Stylistic concerns that don't interfere with Prettier + "@typescript-eslint/padding-line-between-statements": [ + "error", + { blankLine: "always", next: "*", prev: "block-like" }, + ], + "no-useless-rename": "error", + "object-shorthand": "error", + ` + }${ + options.excludeLintPerfectionist + ? "" + : `"perfectionist/sort-objects": [ + "error", + { + order: "asc", + "partition-by-comment": true, + type: "natural", + }, + ],` + } + }, +}; +`); +} diff --git a/src/steps/writing/creation/createTsupConfig.test.ts b/src/steps/writing/creation/createTsupConfig.test.ts new file mode 100644 index 0000000..5ee0406 --- /dev/null +++ b/src/steps/writing/creation/createTsupConfig.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; + +import { createTsupConfig } from "./createTsupConfig.js"; + +describe("createTsupConfig", () => { + it("creates an ignore file with test entries when excludeTests is false", async () => { + const actual = await createTsupConfig({ excludeTests: false }); + + expect(actual).toMatchInlineSnapshot(` + "import { defineConfig } from "tsup"; + + export default defineConfig({ + bundle: false, + clean: true, + dts: true, + entry: ["src/**/*.ts", "!src/**/*.test.*"], + format: "esm", + outDir: "lib", + sourcemap: true, + }); + " + `); + }); + + it("creates an ignore file without test entries when excludeTests is true", async () => { + const actual = await createTsupConfig({ excludeTests: true }); + + expect(actual).toMatchInlineSnapshot(` + "import { defineConfig } from "tsup"; + + export default defineConfig({ + bundle: false, + clean: true, + dts: true, + entry: ["src/**/*.ts"], + format: "esm", + outDir: "lib", + sourcemap: true, + }); + " + `); + }); +}); diff --git a/src/steps/writing/creation/createTsupConfig.ts b/src/steps/writing/creation/createTsupConfig.ts new file mode 100644 index 0000000..fdb33d5 --- /dev/null +++ b/src/steps/writing/creation/createTsupConfig.ts @@ -0,0 +1,17 @@ +import { Options } from "../../../shared/types.js"; +import { formatTypeScript } from "./formatters/formatTypeScript.js"; + +export async function createTsupConfig(options: Pick) { + return await formatTypeScript(`import { defineConfig } from "tsup"; + + export default defineConfig({ + bundle: false, + clean: true, + dts: true, + entry: ["src/**/*.ts"${options.excludeTests ? "" : `, "!src/**/*.test.*"`}], + format: "esm", + outDir: "lib", + sourcemap: true, + }); + `); +} diff --git a/src/steps/writing/creation/dotGitHub/actions.test.ts b/src/steps/writing/creation/dotGitHub/actions.test.ts new file mode 100644 index 0000000..7990bb3 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/actions.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +import { createDotGitHubActions } from "./actions.js"; + +describe("createDotGitHubActions", () => { + it("creates a prepare/action.yml file", () => { + const actual = createDotGitHubActions(); + + expect(actual).toEqual({ + prepare: { + "action.yml": `description: Prepares the repo for a typical CI job + +name: Prepare + +runs: + steps: + - uses: pnpm/action-setup@v2 + - uses: actions/setup-node@v4 + with: + cache: pnpm + node-version: '20' + - run: pnpm install --frozen-lockfile + shell: bash + using: composite +`, + }, + }); + }); +}); diff --git a/src/steps/writing/creation/dotGitHub/actions.ts b/src/steps/writing/creation/dotGitHub/actions.ts new file mode 100644 index 0000000..3ed304d --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/actions.ts @@ -0,0 +1,28 @@ +import jsYaml from "js-yaml"; + +export function createDotGitHubActions() { + return { + prepare: { + "action.yml": jsYaml + .dump({ + description: "Prepares the repo for a typical CI job", + name: "Prepare", + runs: { + steps: [ + { uses: "pnpm/action-setup@v2" }, + { + uses: "actions/setup-node@v4", + with: { cache: "pnpm", "node-version": "20" }, + }, + { + run: "pnpm install --frozen-lockfile", + shell: "bash", + }, + ], + using: "composite", + }, + }) + .replaceAll(/\n(\S)/g, "\n\n$1"), + }, + }; +} diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts new file mode 100644 index 0000000..cddd40e --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts @@ -0,0 +1,393 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../../../../../shared/types.js"; +import { createDevelopment } from "./index.js"; + +const mockReadFileSafe = vi.fn(); + +vi.mock("../../../../../shared/readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); + +const options = { + access: "public", + author: "Test Author", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + mode: "create", + owner: "TestOwner", + repository: "test-repository", + skipInstall: true, + title: "Test Title", +} satisfies Options; + +describe("createDevelopment", () => { + it("creates a file with extra options turned on when options disable them", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const actual = await createDevelopment({ + ...options, + excludeLintKnip: false, + excludeLintMd: false, + excludeLintPackageJson: false, + excludeLintPackages: false, + excludeLintSpelling: false, + }); + + expect(actual).toMatchInlineSnapshot(` + "# Development + + After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + + \`\`\`shell + git clone https://github.com//test-repository + cd test-repository + pnpm install + \`\`\` + + > This repository includes a list of suggested VS Code extensions. + > It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + + ## Building + + Run [**tsup**](https://tsup.egoist.dev) locally to build source files from \`src/\` into output files in \`lib/\`: + + \`\`\`shell + pnpm build + \`\`\` + + Add \`--watch\` to run the builder in a watch mode that continuously cleans and recreates \`lib/\` as you save files: + + \`\`\`shell + pnpm build --watch + \`\`\` + + ## Formatting + + [Prettier](https://prettier.io) is used to format code. + It should be applied automatically when you save files in VS Code or make a Git commit. + + To manually reformat all files, you can run: + + \`\`\`shell + pnpm format --write + \`\`\` + + ## Linting + + This package includes several forms of linting to enforce consistent code quality and styling. + Each should be shown in VS Code, and can be run manually on the command-line: + + - \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files + - \`pnpm lint:knip\` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports + - \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint): Checks Markdown source files + - \`pnpm lint:packages\` ([pnpm dedupe --check](https://pnpm.io/cli/dedupe)): Checks for unnecessarily duplicated packages in the \`pnpm-lock.yml\` file + - \`pnpm lint:spelling\` ([cspell](https://cspell.org)): Spell checks across all source files + + Read the individual documentation for each linter to understand how it can be configured and used best. + + For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + + Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files. + + ## Testing + + [Vitest](https://vitest.dev) is used for tests. + You can run it locally on the command-line: + + \`\`\`shell + pnpm run test + \`\`\` + + Add the \`--coverage\` flag to compute test coverage and place reports in the \`coverage/\` directory: + + \`\`\`shell + pnpm run test --coverage + \`\`\` + + Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. + Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail. + + ### Debugging Tests + + This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. + To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). + + ## Type Checking + + You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + + However, it can be useful to run the TypeScript command-line (\`tsc\`) to type check all files in \`src/\`: + + \`\`\`shell + pnpm tsc + \`\`\` + + Add \`--watch\` to keep the type checker running in a watch mode that updates the display as you save files: + + \`\`\`shell + pnpm tsc --watch + \`\`\` + " + `); + }); + + it("creates a file with extra options turned off when options enable them", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const actual = await createDevelopment({ + ...options, + excludeLintKnip: true, + excludeLintMd: true, + excludeLintPackageJson: true, + excludeLintPackages: true, + excludeLintSpelling: true, + guide: { + href: "https://example.com", + title: "Example Guide", + }, + }); + + expect(actual).toMatchInlineSnapshot(` + "# Development + + > If you'd like a more guided walkthrough, see [Example Guide](https://example.com). + > It'll walk you through the common activities you'll need to contribute. + + After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + + \`\`\`shell + git clone https://github.com//test-repository + cd test-repository + pnpm install + \`\`\` + + > This repository includes a list of suggested VS Code extensions. + > It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + + ## Building + + Run [**tsup**](https://tsup.egoist.dev) locally to build source files from \`src/\` into output files in \`lib/\`: + + \`\`\`shell + pnpm build + \`\`\` + + Add \`--watch\` to run the builder in a watch mode that continuously cleans and recreates \`lib/\` as you save files: + + \`\`\`shell + pnpm build --watch + \`\`\` + + ## Formatting + + [Prettier](https://prettier.io) is used to format code. + It should be applied automatically when you save files in VS Code or make a Git commit. + + To manually reformat all files, you can run: + + \`\`\`shell + pnpm format --write + \`\`\` + + ## Linting + + [ESLint](https://eslint.org) is used with with [typescript-eslint](https://typescript-eslint.io)) to lint JavaScript and TypeScript source files. + You can run it locally on the command-line: + + \`\`\`shell + pnpm run lint + \`\`\` + + ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + + Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files. + + ## Testing + + [Vitest](https://vitest.dev) is used for tests. + You can run it locally on the command-line: + + \`\`\`shell + pnpm run test + \`\`\` + + Add the \`--coverage\` flag to compute test coverage and place reports in the \`coverage/\` directory: + + \`\`\`shell + pnpm run test --coverage + \`\`\` + + Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. + Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail. + + ### Debugging Tests + + This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. + To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). + + ## Type Checking + + You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + + However, it can be useful to run the TypeScript command-line (\`tsc\`) to type check all files in \`src/\`: + + \`\`\`shell + pnpm tsc + \`\`\` + + Add \`--watch\` to keep the type checker running in a watch mode that updates the display as you save files: + + \`\`\`shell + pnpm tsc --watch + \`\`\` + " + `); + }); + + it("preserves existing sections when they don't match the new sections", async () => { + mockReadFileSafe.mockResolvedValue(`## Existing One + +Abc 123. + +## Building + +Will be removed. + +## Tests + +Will be removed. + +## Existing Two + +Def 456. +`); + + const actual = await createDevelopment({ + ...options, + excludeLintKnip: false, + excludeLintMd: false, + excludeLintPackageJson: false, + excludeLintPackages: false, + excludeLintSpelling: false, + }); + + expect(actual).toMatchInlineSnapshot(` + "# Development + + After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + + \`\`\`shell + git clone https://github.com//test-repository + cd test-repository + pnpm install + \`\`\` + + > This repository includes a list of suggested VS Code extensions. + > It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + + ## Building + + Will be removed. + + ## Formatting + + [Prettier](https://prettier.io) is used to format code. + It should be applied automatically when you save files in VS Code or make a Git commit. + + To manually reformat all files, you can run: + + \`\`\`shell + pnpm format --write + \`\`\` + + ## Linting + + This package includes several forms of linting to enforce consistent code quality and styling. + Each should be shown in VS Code, and can be run manually on the command-line: + + - \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files + - \`pnpm lint:knip\` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports + - \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint): Checks Markdown source files + - \`pnpm lint:packages\` ([pnpm dedupe --check](https://pnpm.io/cli/dedupe)): Checks for unnecessarily duplicated packages in the \`pnpm-lock.yml\` file + - \`pnpm lint:spelling\` ([cspell](https://cspell.org)): Spell checks across all source files + + Read the individual documentation for each linter to understand how it can be configured and used best. + + For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + + Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files. + + ## Testing + + [Vitest](https://vitest.dev) is used for tests. + You can run it locally on the command-line: + + \`\`\`shell + pnpm run test + \`\`\` + + Add the \`--coverage\` flag to compute test coverage and place reports in the \`coverage/\` directory: + + \`\`\`shell + pnpm run test --coverage + \`\`\` + + Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. + Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail. + + ### Debugging Tests + + This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. + To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). + + ## Type Checking + + You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + + However, it can be useful to run the TypeScript command-line (\`tsc\`) to type check all files in \`src/\`: + + \`\`\`shell + pnpm tsc + \`\`\` + + Add \`--watch\` to keep the type checker running in a watch mode that updates the display as you save files: + + \`\`\`shell + pnpm tsc --watch + \`\`\` + + ## Existing One + + Abc 123. + + ## Tests + + Will be removed. + + ## Existing Two + + Def 456. + " + `); + }); +}); diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment/index.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/index.ts new file mode 100644 index 0000000..56bffab --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/index.ts @@ -0,0 +1,157 @@ +import { readFileSafe } from "../../../../../shared/readFileSafe.js"; +import { Options } from "../../../../../shared/types.js"; +import { splitIntoSections } from "./splitIntoSections.js"; + +const headingAliases = new Map([ + ["build", "Building"], + ["builds", "Building"], + ["format", "Formatting"], + ["lint", "Linting"], + ["test", "Testing"], + ["tests", "Testing"], +]); + +function createLintingSection(options: Options) { + const lintLines = [ + !options.excludeLintKnip && + `- \`pnpm lint:knip\` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports`, + !options.excludeLintMd && + `- \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint): Checks Markdown source files`, + !options.excludeLintPackages && + `- \`pnpm lint:packages\` ([pnpm dedupe --check](https://pnpm.io/cli/dedupe)): Checks for unnecessarily duplicated packages in the \`pnpm-lock.yml\` file`, + !options.excludeLintSpelling && + `- \`pnpm lint:spelling\` ([cspell](https://cspell.org)): Spell checks across all source files`, + ].filter(Boolean); + + return lintLines.length + ? [ + `This package includes several forms of linting to enforce consistent code quality and styling.`, + `Each should be shown in VS Code, and can be run manually on the command-line:`, + ``, + `- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files`, + ...lintLines, + ``, + `Read the individual documentation for each linter to understand how it can be configured and used best.`, + ``, + `For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:`, + ].join("\n") + : `[ESLint](https://eslint.org) is used with with [typescript-eslint](https://typescript-eslint.io)) to lint JavaScript and TypeScript source files. +You can run it locally on the command-line: + +\`\`\`shell +pnpm run lint +\`\`\` + +ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:`; +} + +export async function createDevelopment(options: Options) { + const existingContents = await readFileSafe(".github/DEVELOPMENT.md", ""); + + const newSections = { + "## Building": `Run [**tsup**](https://tsup.egoist.dev) locally to build source files from \`src/\` into output files in \`lib/\`: + +\`\`\`shell +pnpm build +\`\`\` + +Add \`--watch\` to run the builder in a watch mode that continuously cleans and recreates \`lib/\` as you save files: + +\`\`\`shell +pnpm build --watch +\`\`\``, + "## Formatting": `[Prettier](https://prettier.io) is used to format code. +It should be applied automatically when you save files in VS Code or make a Git commit. + +To manually reformat all files, you can run: + +\`\`\`shell +pnpm format --write +\`\`\``, + "## Linting": `${createLintingSection(options)} + +\`\`\`shell +pnpm run lint --fix +\`\`\` + +Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files.`, + ...(!options.excludeTests && { + "## Testing": `[Vitest](https://vitest.dev) is used for tests. +You can run it locally on the command-line: + +\`\`\`shell +pnpm run test +\`\`\` + +Add the \`--coverage\` flag to compute test coverage and place reports in the \`coverage/\` directory: + +\`\`\`shell +pnpm run test --coverage +\`\`\` + +Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. +Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail.`, + + "### Debugging Tests": `This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. +To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5).`, + }), + "## Type Checking": `You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + +However, it can be useful to run the TypeScript command-line (\`tsc\`) to type check all files in \`src/\`: + +\`\`\`shell +pnpm tsc +\`\`\` + +Add \`--watch\` to keep the type checker running in a watch mode that updates the display as you save files: + +\`\`\`shell +pnpm tsc --watch +\`\`\``, + }; + + const newSectionHeadings = new Set([ + "Development", + Object.keys(newSections).map((key) => key.replace(/^#* /, "")), + ]); + + const preservedSections = Object.fromEntries( + splitIntoSections(existingContents).filter(([key]) => { + const keyText = key.replace(/^#* /, ""); + return !newSectionHeadings.has( + headingAliases.get(keyText.toLowerCase()) ?? keyText, + ); + }), + ); + + const result = `# Development +${ + options.guide + ? ` +> If you'd like a more guided walkthrough, see [${options.guide.title}](${options.guide.href}). +> It'll walk you through the common activities you'll need to contribute. +` + : "" +} +After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + +\`\`\`shell +git clone https://github.com//${options.repository} +cd ${options.repository} +pnpm install +\`\`\` + +> This repository includes a list of suggested VS Code extensions. +> It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + +${Object.entries({ ...newSections, ...preservedSections }) + .map( + ([heading, content]) => `${heading} + +${content}`, + ) + .join("\n\n")} +`; + + return result; +} diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts new file mode 100644 index 0000000..bd00ebc --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "vitest"; + +import { splitIntoSections } from "./splitIntoSections.js"; + +describe("createDevelopment", () => { + test.each([ + ["", []], + ["# Development \nabc 123", [["# Development", "abc 123"]]], + ["# Development \n\nabc 123 ", [["# Development", "abc 123"]]], + ["# Development \n Indented. ", [["# Development", " Indented."]]], + ["# Development \n Indented. ", [["# Development", " Indented."]]], + ["# Development \n\tIndented. ", [["# Development", "\tIndented."]]], + [ + `# Development + +. + +## Abc + +abc 123 + +### Def + +def 456 + +## Ghi + +ghi 789 +`, + [ + ["# Development", "."], + ["## Abc", "abc 123"], + ["### Def", "def 456"], + ["## Ghi", "ghi 789"], + ], + ], + ])("%j", (text, expected) => { + expect(splitIntoSections(text)).toEqual(expected); + }); +}); diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts new file mode 100644 index 0000000..6ac430c --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts @@ -0,0 +1,27 @@ +export function splitIntoSections(text: string) { + const sections: [string, string][] = []; + if (!text) { + return sections; + } + + let remaining = `${text}\n`; + + while (remaining) { + const indexOfNewline = remaining.indexOf("\n", 1); + let nextStart = remaining.indexOf("\n#", 1); + if (nextStart === -1) { + nextStart = remaining.length; + } + + const heading = remaining.slice(0, indexOfNewline).trim(); + const contents = remaining + .slice(indexOfNewline, nextStart) + .trimEnd() + .replace(/^\n+/, ""); + + sections.push([heading, contents]); + remaining = remaining.slice(nextStart); + } + + return sections; +} diff --git a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts new file mode 100644 index 0000000..1e2788f --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts @@ -0,0 +1,290 @@ +/* spellchecker: disable */ +import { Options } from "../../../../shared/types.js"; +import { formatJson } from "../formatters/formatJson.js"; +import { formatYaml } from "../formatters/formatYaml.js"; +import { createDevelopment } from "./createDevelopment/index.js"; + +export async function createDotGitHubFiles(options: Options) { + return { + "CODE_OF_CONDUCT.md": `# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, +and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall +community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of +any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, +without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a +professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +${options.email.github}. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations +`, + "CONTRIBUTING.md": `# Contributing + +Thanks for your interest in contributing to \`${options.repository}\`! 💖 + +> After this page, see [DEVELOPMENT.md](./DEVELOPMENT.md) for local development instructions. + +## Code of Conduct + +This project contains a [Contributor Covenant code of conduct](./CODE_OF_CONDUCT.md) all contributors are expected to follow. + +## Reporting Issues + +Please do [report an issue on the issue tracker](https://github.com/${options.owner}/${options.repository}/issues/new/choose) if there's any bugfix, documentation improvement, or general enhancement you'd like to see in the repository! Please fully fill out all required fields in the most appropriate issue form. + +## Sending Contributions + +Sending your own changes as contribution is always appreciated! +There are two steps involved: + +1. [Finding an Issue](#finding-an-issue) +2. [Sending a Pull Request](#sending-a-pull-request) + +### Finding an Issue + +With the exception of very small typos, all changes to this repository generally need to correspond to an [unassigned open issue marked as \`status: accepting prs\` on the issue tracker](https://github.com/${options.owner}/${options.repository}/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). +If this is your first time contributing, consider searching for [unassigned issues that also have the \`good first issue\` label](https://github.com/${options.owner}/${options.repository}/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). +If the issue you'd like to fix isn't found on the issue, see [Reporting Issues](#reporting-issues) for filing your own (please do!). + +#### Issue Claiming + +We don't use any kind of issue claiming system. +We've found in the past that they result in accidental ["licked cookie"](https://devblogs.microsoft.com/oldnewthing/20091201-00/?p=15843) situations where contributors claim an issue but run out of time or energy trying before sending a PR. + +If an unassigned issue has been marked as \`status: accepting prs\` and an open PR does not exist, feel free to send a PR. +Please don't post comments asking for permission or stating you will work on an issue. + +### Sending a Pull Request + +Once you've identified an open issue accepting PRs that doesn't yet have a PR sent, you're free to send a pull request. +Be sure to fill out the pull request template's requested information -- otherwise your PR will likely be closed. + +PRs are also expected to have a title that adheres to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0). +Only PR titles need to be in that format, not individual commits. +Don't worry if you get this wrong: you can always change the PR title after sending it. +Check [previously merged PRs](https://github.com/${options.owner}/${options.repository}/pulls?q=is%3Apr+is%3Amerged+-label%3Adependencies+) for reference. + +#### Draft PRs + +If you don't think your PR is ready for review, [set it as a draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#converting-a-pull-request-to-a-draft). +Draft PRs won't be reviewed. + +#### Granular PRs + +Please keep pull requests single-purpose: in other words, don't attempt to solve multiple unrelated problems in one pull request. +Send one PR per area of concern. +Multi-purpose pull requests are harder and slower to review, block all changes from being merged until the whole pull request is reviewed, and are difficult to name well with semantic PR titles. + +#### Pull Request Reviews + +When a PR is not in draft, it's considered ready for review. +Please don't manually \`@\` tag anybody to request review. +A maintainer will look at it when they're next able to. + +PRs should have passing [GitHub status checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks) before review is requested (unless there are explicit questions asked in the PR about any failures). + +#### Asking Questions + +If you need help and/or have a question, posting a comment in the PR is a great way to do so. +There's no need to tag anybody individually. +One of us will drop by and help when we can. + +Please post comments as [line comments](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#adding-line-comments-to-a-pull-request) when possible, so that they can be threaded. +You can [resolve conversations](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#resolving-conversations) on your own when you feel they're resolved - no need to comment explicitly and/or wait for a maintainer. + +#### Requested Changes + +After a maintainer reviews your PR, they may request changes on it. +Once you've made those changes, [re-request review on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews#re-requesting-a-review). + +Please try not to force-push commits to PRs that have already been reviewed. +Doing so makes it harder to review the changes. +We squash merge all commits so there's no need to try to preserve Git history within a PR branch. + +Once you've addressed all our feedback by making code changes and/or started a followup discussion, [re-request review](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews#re-requesting-a-review) from each maintainer whose feedback you addressed. + +Once all feedback is addressed and the PR is approved, we'll ensure the branch is up to date with \`main\` and merge it for you. + +#### Post-Merge Recognition + +Once your PR is merged, if you haven't yet been added to the [_Contributors_ table in the README.md](../README.md#contributors) for its [type of contribution](https://allcontributors.org/docs/en/emoji-key "Allcontributors emoji key"), you should be soon. +Please do ping the maintainer who merged your PR if that doesn't happen within 24 hours - it was likely an oversight on our end! + +## Emojis & Appreciation + +If you made it all the way to the end, bravo dear user, we love you. +Please include your favorite emoji in the bottom of your issues and PRs to signal to us that you did in fact read this file and are trying to conform to it as best as possible. +💖 is a good starter if you're not sure which to use. +`, + "DEVELOPMENT.md": await createDevelopment(options), + ...(options.funding && { + "FUNDING.yml": formatYaml({ github: options.funding }), + }), + + "ISSUE_TEMPLATE.md": ` + + + + + +## Overview + +... +`, + "PULL_REQUEST_TEMPLATE.md": ` + +## PR Checklist + +- [ ] Addresses an existing open issue: fixes #000 +- [ ] That issue was marked as [\`status: accepting prs\`](https://github.com/${options.owner}/${options.repository}/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) +- [ ] Steps in [CONTRIBUTING.md](https://github.com/${options.owner}/${options.repository}/blob/main/.github/CONTRIBUTING.md) were taken + +## Overview + + +`, + "SECURITY.md": `# Security Policy + +We take all security vulnerabilities seriously. +If you have a vulnerability or other security issues to disclose: + +- Thank you very much, please do! +- Please send them to us by emailing \`${options.email.github}\` + +We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. +`, + ...(!options.excludeRenovate && { + "renovate.json": await formatJson({ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + automerge: true, + internalChecksFilter: "strict", + labels: ["dependencies"], + minimumReleaseAge: "3 days", + postUpdateOptions: ["pnpmDedupe"], + }), + }), + }; +} diff --git a/src/steps/writing/creation/dotGitHub/createWorkflowFile.test.ts b/src/steps/writing/creation/dotGitHub/createWorkflowFile.test.ts new file mode 100644 index 0000000..396a067 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createWorkflowFile.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { createWorkflowFile } from "./createWorkflowFile.js"; + +describe("createWorkflowFile", () => { + it("creates a workflow file when runs are provided", () => { + const actual = createWorkflowFile({ + name: "Test Name", + runs: ["pnpm build"], + }); + + expect(actual).toBe( + `jobs: + test_name: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build + +name: Test Name + +on: + pull_request: ~ + push: + branches: + - main +`, + ); + }); +}); diff --git a/src/steps/writing/creation/dotGitHub/createWorkflowFile.ts b/src/steps/writing/creation/dotGitHub/createWorkflowFile.ts new file mode 100644 index 0000000..534df93 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createWorkflowFile.ts @@ -0,0 +1,107 @@ +import { formatYaml } from "../formatters/formatYaml.js"; + +interface WorkflowFileConcurrency { + "cancel-in-progress"?: boolean; + group: string; +} + +interface WorkflowFileOn { + issue_comment?: { + types?: string[]; + }; + issues?: { + types?: string[]; + }; + pull_request?: + | { + branches?: string | string[]; + types?: string[]; + } + | null + | string; + pull_request_target?: { + types: string[]; + }; + push?: { + branches: string[]; + }; + release?: { + types: string[]; + }; + workflow_dispatch?: null | string; +} + +interface WorkflowFilePermissions { + contents?: string; + "id-token"?: string; + issues?: string; + "pull-requests"?: string; +} + +interface WorkflowFileStep { + env?: Record; + if?: string; + name?: string; + run?: string; + uses?: string; + with?: Record; +} + +interface WorkflowFileOptionsBase { + concurrency?: WorkflowFileConcurrency; + if?: string; + name: string; + on?: WorkflowFileOn; + permissions?: WorkflowFilePermissions; +} + +interface WorkflowFileOptionsRuns extends WorkflowFileOptionsBase { + runs: (WorkflowFileStep | string)[]; +} + +interface WorkflowFileOptionsSteps extends WorkflowFileOptionsBase { + steps: WorkflowFileStep[]; +} + +type WorkflowFileOptions = WorkflowFileOptionsRuns | WorkflowFileOptionsSteps; + +export function createWorkflowFile({ + concurrency, + name, + on = { + pull_request: null, + push: { + branches: ["main"], + }, + }, + permissions, + ...options +}: WorkflowFileOptions) { + return ( + formatYaml({ + concurrency, + jobs: { + [name.replaceAll(" ", "_").toLowerCase()]: { + ...(options.if && { if: options.if }), + "runs-on": "ubuntu-latest", + steps: + "runs" in options + ? [ + { uses: "actions/checkout@v4" }, + { uses: "./.github/actions/prepare" }, + ...options.runs.map((run) => ({ run })), + ] + : options.steps, + }, + }, + name, + on, + permissions, + }) + .replaceAll(/\n(\S)/g, "\n\n$1") + // https://github.com/nodeca/js-yaml/pull/515 + .replaceAll(/: "\\n(.+)"/g, ": |\n$1") + .replaceAll("\\n", "\n") + .replaceAll("\\t", " ") + ); +} diff --git a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts new file mode 100644 index 0000000..8e488e4 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts @@ -0,0 +1,470 @@ +import { describe, expect, it } from "vitest"; + +import { Options } from "../../../../shared/types.js"; +import { createWorkflows } from "./createWorkflows.js"; + +const createOptions = (exclude: boolean) => + ({ + access: "public", + base: "everything", + bin: exclude ? undefined : "./bin/index.js", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + excludeAllContributors: exclude, + excludeCompliance: exclude, + excludeLintJson: exclude, + excludeLintKnip: exclude, + excludeLintMd: exclude, + excludeLintPackageJson: exclude, + excludeLintPackages: exclude, + excludeLintPerfectionist: exclude, + excludeLintSpelling: exclude, + excludeLintYml: exclude, + excludeReleases: exclude, + excludeRenovate: exclude, + excludeTests: exclude, + mode: "create", + owner: "StubOwner", + repository: "stub-repository", + title: "Stub Title", + }) satisfies Options; + +describe("createWorkflows", () => { + it("creates a full set of workflows when all excludes are disabled", () => { + const workflows = createWorkflows(createOptions(false)); + + expect(workflows).toMatchInlineSnapshot(` + { + "accessibility-alt-text-bot.yml": "jobs: + accessibility_alt_text_bot: + if: \${{ !endsWith(github.actor, '[bot]') }} + runs-on: ubuntu-latest + steps: + - uses: github/accessibility-alt-text-bot@v1.4.0 + + name: Accessibility Alt Text Bot + + on: + issue_comment: + types: + - created + - edited + issues: + types: + - edited + - opened + pull_request: + types: + - edited + - opened + + permissions: + issues: write + pull-requests: write + ", + "build.yml": "jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build + - run: node ./lib/index.js + + name: Build + + on: + pull_request: ~ + push: + branches: + - main + ", + "compliance.yml": "jobs: + compliance: + runs-on: ubuntu-latest + steps: + - uses: mtfoley/pr-compliance-action@main + with: + body-auto-close: false + ignore-authors: |- + allcontributors + allcontributors[bot] + renovate + renovate[bot] + ignore-team-members: false + + name: Compliance + + on: + pull_request: + branches: + - main + types: + - edited + - opened + - reopened + - synchronize + + permissions: + pull-requests: write + ", + "contributors.yml": "jobs: + contributors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/prepare + - env: + GITHUB_TOKEN: \${{ secrets.ACCESS_TOKEN }} + uses: JoshuaKGoldberg/all-contributors-auto-action@v0.4.3 + + name: Contributors + + on: + push: + branches: + - main + ", + "lint-knip.yml": "jobs: + lint_knip: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:knip + + name: Lint Knip + + on: + pull_request: ~ + push: + branches: + - main + ", + "lint-markdown.yml": "jobs: + lint_markdown: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:md + + name: Lint Markdown + + on: + pull_request: ~ + push: + branches: + - main + ", + "lint-packages.yml": "jobs: + lint_packages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:packages + + name: Lint Packages + + on: + pull_request: ~ + push: + branches: + - main + ", + "lint-spelling.yml": "jobs: + lint_spelling: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint:spelling + + name: Lint spelling + + on: + pull_request: ~ + push: + branches: + - main + ", + "lint.yml": "jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build --no-dts + - run: pnpm lint + + name: Lint + + on: + pull_request: ~ + push: + branches: + - main + ", + "post-release.yml": "jobs: + post_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: echo "npm_version=$(npm pkg get version | tr -d '"')" >> "$GITHUB_ENV" + - uses: apexskier/github-release-commenter@v1 + with: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + comment-template: | + :tada: This is included in version {release_link} :tada: + + The release is available on: + + * [GitHub releases](https://github.com/StubOwner/stub-repository/releases/tag/{release_tag}) + * [npm package (@latest dist-tag)](https://www.npmjs.com/package/stub-repository/v/\${{ env.npm_version }}) + + Cheers! 📦🚀 + + name: Post Release + + on: + release: + types: + - published + ", + "pr-review-requested.yml": "jobs: + pr_review_requested: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: 'status: waiting for author' + - if: failure() + run: | + echo "Don't worry if the previous step failed." + echo "See https://github.com/actions-ecosystem/action-remove-labels/issues/221." + + name: PR Review Requested + + on: + pull_request_target: + types: + - review_requested + + permissions: + pull-requests: write + ", + "prettier.yml": "jobs: + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm format --list-different + + name: Prettier + + on: + pull_request: ~ + push: + branches: + - main + ", + "release.yml": "concurrency: + group: \${{ github.workflow }} + + jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + - uses: ./.github/actions/prepare + - run: pnpm build + - env: + GITHUB_TOKEN: \${{ secrets.ACCESS_TOKEN }} + NPM_TOKEN: \${{ secrets.NPM_TOKEN }} + uses: JoshuaKGoldberg/release-it-action@v0.2.2 + + name: Release + + on: + push: + branches: + - main + + permissions: + contents: write + id-token: write + ", + "test.yml": "jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm run test --coverage + - name: Codecov + uses: codecov/codecov-action@v3 + + name: Test + + on: + pull_request: ~ + push: + branches: + - main + ", + "tsc.yml": "jobs: + type_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm tsc + + name: Type Check + + on: + pull_request: ~ + push: + branches: + - main + ", + } + `); + }); + + it("creates a minimal set of workflows when all options are enabled", () => { + const workflows = createWorkflows(createOptions(true)); + + expect(workflows).toMatchInlineSnapshot(` + { + "accessibility-alt-text-bot.yml": "jobs: + accessibility_alt_text_bot: + if: \${{ !endsWith(github.actor, '[bot]') }} + runs-on: ubuntu-latest + steps: + - uses: github/accessibility-alt-text-bot@v1.4.0 + + name: Accessibility Alt Text Bot + + on: + issue_comment: + types: + - created + - edited + issues: + types: + - edited + - opened + pull_request: + types: + - edited + - opened + + permissions: + issues: write + pull-requests: write + ", + "build.yml": "jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm build + - run: node ./lib/index.js + + name: Build + + on: + pull_request: ~ + push: + branches: + - main + ", + "lint.yml": "jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm lint + + name: Lint + + on: + pull_request: ~ + push: + branches: + - main + ", + "pr-review-requested.yml": "jobs: + pr_review_requested: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: 'status: waiting for author' + - if: failure() + run: | + echo "Don't worry if the previous step failed." + echo "See https://github.com/actions-ecosystem/action-remove-labels/issues/221." + + name: PR Review Requested + + on: + pull_request_target: + types: + - review_requested + + permissions: + pull-requests: write + ", + "prettier.yml": "jobs: + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm format --list-different + + name: Prettier + + on: + pull_request: ~ + push: + branches: + - main + ", + "tsc.yml": "jobs: + type_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm tsc + + name: Type Check + + on: + pull_request: ~ + push: + branches: + - main + ", + } + `); + }); +}); diff --git a/src/steps/writing/creation/dotGitHub/createWorkflows.ts b/src/steps/writing/creation/dotGitHub/createWorkflows.ts new file mode 100644 index 0000000..f45e62c --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createWorkflows.ts @@ -0,0 +1,228 @@ +/* spellchecker: disable */ +import { Options } from "../../../../shared/types.js"; +import { createWorkflowFile } from "./createWorkflowFile.js"; + +export function createWorkflows(options: Options) { + return { + "build.yml": createWorkflowFile({ + name: "Build", + runs: ["pnpm build", "node ./lib/index.js"], + }), + "pr-review-requested.yml": createWorkflowFile({ + name: "PR Review Requested", + on: { + pull_request_target: { + types: ["review_requested"], + }, + }, + permissions: { + "pull-requests": "write", + }, + steps: [ + { + uses: "actions-ecosystem/action-remove-labels@v1", + with: { + labels: "status: waiting for author", + }, + }, + { + if: "failure()", + run: 'echo "Don\'t worry if the previous step failed."\necho "See https://github.com/actions-ecosystem/action-remove-labels/issues/221."\n', + }, + ], + }), + "prettier.yml": createWorkflowFile({ + name: "Prettier", + runs: ["pnpm format --list-different"], + }), + "tsc.yml": createWorkflowFile({ + name: "Type Check", + runs: ["pnpm tsc"], + }), + ...(!options.excludeCompliance && { + "compliance.yml": createWorkflowFile({ + name: "Compliance", + on: { + pull_request: { + branches: ["main"], + types: ["edited", "opened", "reopened", "synchronize"], + }, + }, + permissions: { + "pull-requests": "write", + }, + steps: [ + { + uses: "mtfoley/pr-compliance-action@main", + with: { + "body-auto-close": false, + "ignore-authors": + [ + ...(options.excludeAllContributors + ? [] + : ["allcontributors", "allcontributors[bot]"]), + ...(options.excludeRenovate + ? [] + : ["renovate", "renovate[bot]"]), + ].join("\n") || undefined, + "ignore-team-members": false, + }, + }, + ], + }), + }), + ...(!options.excludeAllContributors && { + "contributors.yml": createWorkflowFile({ + name: "Contributors", + on: { + push: { + branches: ["main"], + }, + }, + steps: [ + { uses: "actions/checkout@v4", with: { "fetch-depth": 0 } }, + { uses: "./.github/actions/prepare" }, + { + env: { GITHUB_TOKEN: "${{ secrets.ACCESS_TOKEN }}" }, + uses: `JoshuaKGoldberg/all-contributors-auto-action@v0.4.3`, + }, + ], + }), + }), + "accessibility-alt-text-bot.yml": createWorkflowFile({ + if: "${{ !endsWith(github.actor, '[bot]') }}", + name: "Accessibility Alt Text Bot", + on: { + issue_comment: { + types: ["created", "edited"], + }, + issues: { + types: ["edited", "opened"], + }, + pull_request: { + types: ["edited", "opened"], + }, + }, + permissions: { + issues: "write", + "pull-requests": "write", + }, + steps: [ + { + uses: "github/accessibility-alt-text-bot@v1.4.0", + }, + ], + }), + "lint.yml": createWorkflowFile({ + name: "Lint", + runs: [...(options.bin ? ["pnpm build --no-dts"] : []), "pnpm lint"], + }), + ...(!options.excludeLintKnip && { + "lint-knip.yml": createWorkflowFile({ + name: "Lint Knip", + runs: ["pnpm lint:knip"], + }), + }), + ...(!options.excludeLintMd && { + "lint-markdown.yml": createWorkflowFile({ + name: "Lint Markdown", + runs: ["pnpm lint:md"], + }), + }), + ...(!options.excludeLintPackages && { + "lint-packages.yml": createWorkflowFile({ + name: "Lint Packages", + runs: ["pnpm lint:packages"], + }), + }), + ...(!options.excludeLintSpelling && { + "lint-spelling.yml": createWorkflowFile({ + name: "Lint spelling", + runs: ["pnpm lint:spelling"], + }), + }), + ...(!options.excludeReleases && { + "post-release.yml": createWorkflowFile({ + name: "Post Release", + on: { + release: { + types: ["published"], + }, + }, + steps: [ + { uses: "actions/checkout@v4", with: { "fetch-depth": 0 } }, + { + run: `echo "npm_version=$(npm pkg get version | tr -d '"')" >> "$GITHUB_ENV"`, + }, + { + uses: "apexskier/github-release-commenter@v1", + with: { + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}", + "comment-template": ` + :tada: This is included in version {release_link} :tada: + + The release is available on: + + * [GitHub releases](https://github.com/${options.owner}/${options.repository}/releases/tag/{release_tag}) + * [npm package (@latest dist-tag)](https://www.npmjs.com/package/${options.repository}/v/\${{ env.npm_version }}) + + Cheers! 📦🚀 + `, + }, + }, + ], + }), + "release.yml": createWorkflowFile({ + concurrency: { + group: "${{ github.workflow }}", + }, + name: "Release", + on: { + push: { + branches: ["main"], + }, + }, + permissions: { + contents: "write", + "id-token": "write", + }, + steps: [ + { + uses: "actions/checkout@v4", + with: { + "fetch-depth": 0, + ref: "main", + }, + }, + { + uses: "./.github/actions/prepare", + }, + { + run: "pnpm build", + }, + { + env: { + GITHUB_TOKEN: "${{ secrets.ACCESS_TOKEN }}", + NPM_TOKEN: "${{ secrets.NPM_TOKEN }}", + }, + uses: "JoshuaKGoldberg/release-it-action@v0.2.2", + }, + ], + }), + }), + ...(!options.excludeTests && { + "test.yml": createWorkflowFile({ + name: "Test", + steps: [ + { uses: "actions/checkout@v4" }, + { uses: "./.github/actions/prepare" }, + { run: "pnpm run test --coverage" }, + { + name: "Codecov", + uses: "codecov/codecov-action@v3", + }, + ], + }), + }), + }; +} diff --git a/src/steps/writing/creation/dotGitHub/index.ts b/src/steps/writing/creation/dotGitHub/index.ts new file mode 100644 index 0000000..876e8f6 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/index.ts @@ -0,0 +1,14 @@ +import { Options } from "../../../../shared/types.js"; +import { createDotGitHubActions } from "./actions.js"; +import { createDotGitHubFiles } from "./createDotGitHubFiles.js"; +import { createWorkflows } from "./createWorkflows.js"; +import { createDotGitHubIssueTemplate } from "./issueTemplate.js"; + +export async function createDotGitHub(options: Options) { + return { + ISSUE_TEMPLATE: createDotGitHubIssueTemplate(options), + actions: createDotGitHubActions(), + workflows: createWorkflows(options), + ...(await createDotGitHubFiles(options)), + }; +} diff --git a/src/steps/writing/creation/dotGitHub/issueTemplate.ts b/src/steps/writing/creation/dotGitHub/issueTemplate.ts new file mode 100644 index 0000000..3142401 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/issueTemplate.ts @@ -0,0 +1,206 @@ +import { Options } from "../../../../shared/types.js"; +import { formatYaml } from "../formatters/formatYaml.js"; + +export function createDotGitHubIssueTemplate({ + owner, + repository, +}: Pick) { + return { + "01-bug.yml": formatYaml({ + body: [ + { + attributes: { + description: + "If any of these required steps are not taken, we may not be able to review your issue. Help us to help you!", + label: "Bug Report Checklist", + options: [ + { + label: "I have tried restarting my IDE and the issue persists.", + required: true, + }, + { + label: + "I have pulled the latest `main` branch of the repository.", + required: true, + }, + { + label: `I have [searched for related issues](https://github.com/${owner}/${repository}/issues?q=is%3Aissue) and found none that matched my issue.`, + required: true, + }, + ], + }, + type: "checkboxes", + }, + { + attributes: { + description: "What did you expect to happen?", + label: "Expected", + }, + type: "textarea", + validations: { + required: true, + }, + }, + { + attributes: { + description: "What happened instead?", + label: "Actual", + }, + type: "textarea", + validations: { + required: true, + }, + }, + { + attributes: { + description: "Any additional info you'd like to provide.", + label: "Additional Info", + }, + type: "textarea", + }, + ], + description: "Report a bug trying to run the code", + labels: ["type: bug"], + name: "🐛 Bug", + title: "🐛 Bug: ", + }), + "02-documentation.yml": formatYaml({ + body: [ + { + attributes: { + description: + "If any of these required steps are not taken, we may not be able to review your issue. Help us to help you!", + label: "Bug Report Checklist", + options: [ + { + label: + "I have pulled the latest `main` branch of the repository.", + required: true, + }, + { + label: `I have [searched for related issues](https://github.com/${owner}/${repository}/issues?q=is%3Aissue) and found none that matched my issue.`, + required: true, + }, + ], + }, + type: "checkboxes", + }, + { + attributes: { + description: "What would you like to report?", + label: "Overview", + }, + type: "textarea", + validations: { + required: true, + }, + }, + { + attributes: { + description: "Any additional info you'd like to provide.", + label: "Additional Info", + }, + type: "textarea", + }, + ], + description: "Report a typo or missing area of documentation", + labels: ["area: documentation"], + name: "📝 Documentation", + title: "📝 Documentation: ", + }), + "03-feature.yml": formatYaml({ + body: [ + { + attributes: { + description: + "If any of these required steps are not taken, we may not be able to review your issue. Help us to help you!", + label: "Bug Report Checklist", + options: [ + { + label: + "I have pulled the latest `main` branch of the repository.", + required: true, + }, + { + label: `I have [searched for related issues](https://github.com/${owner}/${repository}/issues?q=is%3Aissue) and found none that matched my issue.`, + required: true, + }, + ], + }, + type: "checkboxes", + }, + { + attributes: { + description: "What did you expect to be able to do?", + label: "Overview", + }, + type: "textarea", + validations: { + required: true, + }, + }, + { + attributes: { + description: "Any additional info you'd like to provide.", + label: "Additional Info", + }, + type: "textarea", + }, + ], + description: + "Request that a new feature be added or an existing feature improved", + labels: ["type: feature"], + name: "🚀 Feature", + title: "🚀 Feature: ", + }), + "04-tooling.yml": formatYaml({ + body: [ + { + attributes: { + description: + "If any of these required steps are not taken, we may not be able to review your issue. Help us to help you!", + label: "Bug Report Checklist", + options: [ + { + label: "I have tried restarting my IDE and the issue persists.", + required: true, + }, + { + label: + "I have pulled the latest `main` branch of the repository.", + required: true, + }, + { + label: `I have [searched for related issues](https://github.com/${owner}/${repository}/issues?q=is%3Aissue) and found none that matched my issue.`, + required: true, + }, + ], + }, + type: "checkboxes", + }, + { + attributes: { + description: "What did you expect to be able to do?", + label: "Overview", + }, + type: "textarea", + validations: { + required: true, + }, + }, + { + attributes: { + description: "Any additional info you'd like to provide.", + label: "Additional Info", + }, + type: "textarea", + }, + ], + description: + "Report a bug or request an enhancement in repository tooling", + labels: ["area: tooling"], + name: "🛠 Tooling", + title: "🛠 Tooling: ", + }), + }; +} diff --git a/src/steps/writing/creation/dotHusky.ts b/src/steps/writing/creation/dotHusky.ts new file mode 100644 index 0000000..b3484d7 --- /dev/null +++ b/src/steps/writing/creation/dotHusky.ts @@ -0,0 +1,12 @@ +import { formatIgnoreFile } from "./formatters/formatIgnoreFile.js"; + +export function createDotHusky() { + return { + ".gitignore": formatIgnoreFile(["_"]), + "pre-commit": formatIgnoreFile([ + `#!/bin/sh`, + `. "$(dirname "$0")/_/husky.sh"`, + "npx lint-staged", + ]), + }; +} diff --git a/src/steps/writing/creation/dotVSCode.test.ts b/src/steps/writing/creation/dotVSCode.test.ts new file mode 100644 index 0000000..d06f40a --- /dev/null +++ b/src/steps/writing/creation/dotVSCode.test.ts @@ -0,0 +1,301 @@ +import { describe, expect, it } from "vitest"; + +import { Options } from "../../../shared/types.js"; +import { createDotVSCode } from "./dotVSCode.js"; + +/* spellchecker: disable */ +function fakeOptions( + getExcludeValue: (exclusionName: string) => boolean, + bin?: string | undefined, +) { + return { + access: "public", + author: "TestAuthor", + base: "everything", + ...(bin ? { bin } : {}), + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + ...Object.fromEntries( + [ + "excludeCompliance", + "excludeAllContributors", + "excludeLintDeprecation", + "excludeLintESLint", + "excludeLintJSDoc", + "excludeLintJson", + "excludeLintKnip", + "excludeLintMd", + "excludeLintPackageJson", + "excludeLintPackages", + "excludeLintPerfectionist", + "excludeLintRegex", + "excludeLintSpelling", + "excludeLintStrict", + "excludeLintStylistic", + "excludeLintYml", + "excludeReleases", + "excludeRenovate", + "excludeTests", + ].map((key) => [key, getExcludeValue(key)]), + ), + mode: "create", + owner: "TestOwner", + repository: "test-repository", + skipGitHubApi: true, + skipInstall: true, + skipRemoval: true, + title: "Test Title", + } satisfies Options; +} + +describe("createDotVSCode", () => { + it("creates a minimal config when all exclusions are enabled", async () => { + expect(await createDotVSCode(fakeOptions(() => true))) + .toMatchInlineSnapshot(` + { + "extensions.json": "{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] + } + ", + "settings.json": "{ + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml" + ], + "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "typescript.tsdk": "node_modules/typescript/lib" + } + ", + "tasks.json": "{ + "tasks": [ + { + "detail": "Build the project", + "label": "build", + "script": "build", + "type": "npm" + } + ], + "version": "2.0.0" + } + ", + } + `); + }); + + it("creates a full config when all exclusions are disabled and bin is provided", async () => { + expect(await createDotVSCode(fakeOptions(() => false, "bin/index.js"))) + .toMatchInlineSnapshot(` + { + "extensions.json": "{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "streetsidesoftware.code-spell-checker" + ] + } + ", + "launch.json": "{ + "configurations": [ + { + "args": ["run", "\${relativeFile}"], + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "name": "Debug Current Test File", + "program": "\${workspaceRoot}/node_modules/vitest/vitest.mjs", + "request": "launch", + "skipFiles": ["/**", "**/node_modules/**"], + "smartStep": true, + "type": "node" + }, + { + "name": "Debug Program", + "preLaunchTask": "build", + "program": "bin/index.js", + "request": "launch", + "skipFiles": ["/**"], + "type": "node" + } + ], + "version": "0.2.0" + } + ", + "settings.json": "{ + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml" + ], + "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "typescript.tsdk": "node_modules/typescript/lib" + } + ", + "tasks.json": "{ + "tasks": [ + { + "detail": "Build the project", + "label": "build", + "script": "build", + "type": "npm" + } + ], + "version": "2.0.0" + } + ", + } + `); + }); + + it("creates a full config when all exclusions are disabled and bin is not provided", async () => { + expect(await createDotVSCode(fakeOptions(() => false))) + .toMatchInlineSnapshot(` + { + "extensions.json": "{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "streetsidesoftware.code-spell-checker" + ] + } + ", + "launch.json": "{ + "configurations": [ + { + "args": ["run", "\${relativeFile}"], + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "name": "Debug Current Test File", + "program": "\${workspaceRoot}/node_modules/vitest/vitest.mjs", + "request": "launch", + "skipFiles": ["/**", "**/node_modules/**"], + "smartStep": true, + "type": "node" + } + ], + "version": "0.2.0" + } + ", + "settings.json": "{ + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml" + ], + "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "typescript.tsdk": "node_modules/typescript/lib" + } + ", + "tasks.json": "{ + "tasks": [ + { + "detail": "Build the project", + "label": "build", + "script": "build", + "type": "npm" + } + ], + "version": "2.0.0" + } + ", + } + `); + }); + + it("creates a minimal config including launch.json when all exclusions are enabled and bin is provided", async () => { + expect(await createDotVSCode(fakeOptions(() => true, "bin/index.js"))) + .toMatchInlineSnapshot(` + { + "extensions.json": "{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] + } + ", + "launch.json": "{ + "configurations": [ + { + "name": "Debug Program", + "preLaunchTask": "build", + "program": "bin/index.js", + "request": "launch", + "skipFiles": ["/**"], + "type": "node" + } + ], + "version": "0.2.0" + } + ", + "settings.json": "{ + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml" + ], + "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "typescript.tsdk": "node_modules/typescript/lib" + } + ", + "tasks.json": "{ + "tasks": [ + { + "detail": "Build the project", + "label": "build", + "script": "build", + "type": "npm" + } + ], + "version": "2.0.0" + } + ", + } + `); + }); +}); diff --git a/src/steps/writing/creation/dotVSCode.ts b/src/steps/writing/creation/dotVSCode.ts new file mode 100644 index 0000000..22e9774 --- /dev/null +++ b/src/steps/writing/creation/dotVSCode.ts @@ -0,0 +1,84 @@ +import { Options } from "../../../shared/types.js"; +import { formatJson } from "./formatters/formatJson.js"; + +/* spellchecker: disable */ +export async function createDotVSCode(options: Options) { + return { + "extensions.json": await formatJson({ + recommendations: [ + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + !options.excludeLintSpelling && "streetsidesoftware.code-spell-checker", + ].filter(Boolean), + }), + ...(options.excludeTests && !options.bin + ? {} + : { + "launch.json": await formatJson({ + configurations: [ + ...(options.excludeTests + ? [] + : [ + { + args: ["run", "${relativeFile}"], + autoAttachChildProcesses: true, + console: "integratedTerminal", + name: "Debug Current Test File", + program: + "${workspaceRoot}/node_modules/vitest/vitest.mjs", + request: "launch", + skipFiles: ["/**", "**/node_modules/**"], + smartStep: true, + type: "node", + }, + ]), + ...(options.bin + ? [ + { + name: "Debug Program", + preLaunchTask: "build", + program: options.bin, + request: "launch", + skipFiles: ["/**"], + type: "node", + }, + ] + : []), + ], + version: "0.2.0", + }), + }), + "settings.json": await formatJson({ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml", + ], + "eslint.rules.customizations": [{ rule: "*", severity: "warn" }], + "typescript.tsdk": "node_modules/typescript/lib", + }), + "tasks.json": await formatJson({ + tasks: [ + { + detail: "Build the project", + label: "build", + script: "build", + type: "npm", + }, + ], + version: "2.0.0", + }), + }; +} diff --git a/src/steps/writing/creation/formatters/formatIgnoreFile.ts b/src/steps/writing/creation/formatters/formatIgnoreFile.ts new file mode 100644 index 0000000..f4a1835 --- /dev/null +++ b/src/steps/writing/creation/formatters/formatIgnoreFile.ts @@ -0,0 +1,3 @@ +export function formatIgnoreFile(lines: string[]) { + return [...lines, ""].join("\n"); +} diff --git a/src/steps/writing/creation/formatters/formatJson.test.ts b/src/steps/writing/creation/formatters/formatJson.test.ts new file mode 100644 index 0000000..719dd72 --- /dev/null +++ b/src/steps/writing/creation/formatters/formatJson.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { formatJson } from "./formatJson.js"; + +describe("formatJson", () => { + it("removes undefined values", async () => { + const actual = await formatJson({ empty: undefined, exists: true }); + + expect(actual).toMatchInlineSnapshot(` + "{ "exists": true } + " + `); + }); +}); diff --git a/src/steps/writing/creation/formatters/formatJson.ts b/src/steps/writing/creation/formatters/formatJson.ts new file mode 100644 index 0000000..e35f8eb --- /dev/null +++ b/src/steps/writing/creation/formatters/formatJson.ts @@ -0,0 +1,15 @@ +import prettier from "prettier"; + +export async function formatJson(value: object) { + return await prettier.format( + JSON.stringify( + Object.fromEntries( + Object.entries(value).filter((entry) => entry[1] !== undefined), + ), + ), + { + parser: "json", + useTabs: true, + }, + ); +} diff --git a/src/steps/writing/creation/formatters/formatTypeScript.ts b/src/steps/writing/creation/formatters/formatTypeScript.ts new file mode 100644 index 0000000..e18daea --- /dev/null +++ b/src/steps/writing/creation/formatters/formatTypeScript.ts @@ -0,0 +1,5 @@ +import prettier from "prettier"; + +export async function formatTypeScript(value: string) { + return await prettier.format(value, { parser: "typescript" }); +} diff --git a/src/steps/writing/creation/formatters/formatYaml.ts b/src/steps/writing/creation/formatters/formatYaml.ts new file mode 100644 index 0000000..cbfd753 --- /dev/null +++ b/src/steps/writing/creation/formatters/formatYaml.ts @@ -0,0 +1,26 @@ +import jsYaml from "js-yaml"; + +const options: jsYaml.DumpOptions = { + lineWidth: -1, + noCompatMode: true, + // https://github.com/nodeca/js-yaml/pull/515 + replacer(_, value: unknown) { + if (typeof value !== "string" || !value.includes("\n\t\t")) { + return value; + } + + return value + .replaceAll(": |-\n", ": |\n") + .replaceAll("\n\t \t\t\t", "") + + .replaceAll(/\n\t\t\t\t\t\t$/g, ""); + }, + sortKeys: true, + styles: { + "!!null": "canonical", + }, +}; + +export function formatYaml(value: unknown) { + return jsYaml.dump(value, options).replaceAll(`\\"`, `"`); +} diff --git a/src/steps/writing/creation/index.ts b/src/steps/writing/creation/index.ts new file mode 100644 index 0000000..897e6b3 --- /dev/null +++ b/src/steps/writing/creation/index.ts @@ -0,0 +1,17 @@ +import { Options } from "../../../shared/types.js"; +import { Structure } from "../types.js"; +import { createDotGitHub } from "./dotGitHub/index.js"; +import { createDotHusky } from "./dotHusky.js"; +import { createDotVSCode } from "./dotVSCode.js"; +import { createRootFiles } from "./rootFiles.js"; +import { createSrc } from "./src.js"; + +export async function createStructure(options: Options): Promise { + return { + ".github": await createDotGitHub(options), + ".husky": createDotHusky(), + ".vscode": await createDotVSCode(options), + ...(options.mode !== "migrate" && { src: await createSrc(options) }), + ...(await createRootFiles(options)), + }; +} diff --git a/src/steps/writing/creation/rootFiles.ts b/src/steps/writing/creation/rootFiles.ts new file mode 100644 index 0000000..68b860e --- /dev/null +++ b/src/steps/writing/creation/rootFiles.ts @@ -0,0 +1,155 @@ +import { Options } from "../../../shared/types.js"; +import { createDotGitignore } from "./createDotGitignore.js"; +import { createESLintRC } from "./createESLintRC.js"; +import { createTsupConfig } from "./createTsupConfig.js"; +import { formatIgnoreFile } from "./formatters/formatIgnoreFile.js"; +import { formatJson } from "./formatters/formatJson.js"; +import { writeAllContributorsRC } from "./writeAllContributorsRC.js"; +import { writePackageJson } from "./writePackageJson.js"; + +export async function createRootFiles(options: Options) { + return { + ".all-contributorsrc": await writeAllContributorsRC(options), + ".eslintrc.cjs": await createESLintRC(options), + ".gitignore": createDotGitignore(options), + ...(!options.excludeLintMd && { + ".markdownlint.json": await formatJson({ + extends: "markdownlint/style/prettier", + "first-line-h1": false, + "no-inline-html": false, + }), + ".markdownlintignore": formatIgnoreFile([ + ".github/CODE_OF_CONDUCT.md", + "CHANGELOG.md", + "lib/", + "node_modules/", + ]), + }), + ".nvmrc": `20.11.0\n`, + ".prettierignore": formatIgnoreFile([ + ...(options.excludeAllContributors ? [] : [".all-contributorsrc"]), + ...(options.excludeTests ? [] : ["coverage/"]), + "lib/", + "pnpm-lock.yaml", + ]), + ".prettierrc.json": await formatJson({ + $schema: "http://json.schemastore.org/prettierrc", + overrides: [ + { + files: ".nvmrc", + options: { parser: "yaml" }, + }, + ], + plugins: ["prettier-plugin-curly", "prettier-plugin-packagejson"], + useTabs: true, + }), + ...(!options.excludeReleases && { + ".release-it.json": await formatJson({ + git: { + commitMessage: "chore: release v${version}", + requireCommits: true, + }, + github: { + autoGenerate: true, + release: true, + releaseName: "v${version}", + }, + npm: { + publishArgs: [`--access ${options.access}`, "--provenance"], + }, + plugins: { + "@release-it/conventional-changelog": { + infile: "CHANGELOG.md", + preset: "angular", + }, + }, + }), + }), + "LICENSE.md": `# MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +`, + ...(!options.excludeLintSpelling && { + "cspell.json": await formatJson({ + dictionaries: ["typescript"], + ignorePaths: [ + ".github", + "CHANGELOG.md", + ...(options.excludeTests ? [] : ["coverage"]), + "lib", + "node_modules", + "pnpm-lock.yaml", + ], + }), + }), + ...(!options.excludeLintKnip && { + "knip.json": await formatJson({ + $schema: "https://unpkg.com/knip@latest/schema.json", + entry: ["src/index.ts!"], + ignoreExportsUsedInFile: { + interface: true, + type: true, + }, + project: ["src/**/*.ts!"], + }), + }), + "package.json": await writePackageJson(options), + "tsconfig.eslint.json": await formatJson({ + extends: "./tsconfig.json", + include: ["."], + }), + "tsconfig.json": await formatJson({ + compilerOptions: { + declaration: true, + declarationMap: true, + esModuleInterop: true, + module: "NodeNext", + moduleResolution: "NodeNext", + noEmit: true, + outDir: "lib", + resolveJsonModule: true, + skipLibCheck: true, + sourceMap: true, + strict: true, + target: "ES2022", + }, + include: ["src"], + }), + "tsup.config.ts": await createTsupConfig(options), + ...(!options.excludeTests && { + "vitest.config.ts": `import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + clearMocks: true, + coverage: { + all: true, + exclude: ["lib"], + include: ["src"], + reporter: ["html", "lcov"], + }, + exclude: ["lib", "node_modules"], + setupFiles: ["console-fail-test/setup"], + }, +}); +`, + }), + }; +} diff --git a/src/steps/writing/creation/src.ts b/src/steps/writing/creation/src.ts new file mode 100644 index 0000000..895d44d --- /dev/null +++ b/src/steps/writing/creation/src.ts @@ -0,0 +1,88 @@ +import { Options } from "../../../shared/types.js"; +import { formatTypeScript } from "./formatters/formatTypeScript.js"; + +export async function createSrc(options: Options) { + return { + ...(!options.excludeTests && { + "greet.test.ts": await formatTypeScript( + ` + import { describe, expect, it, vi } from "vitest"; + + import { greet } from "./greet.js"; + + const message = "Yay, testing!"; + + describe("greet", () => { + it("logs to the console once when message is provided as a string", () => { + const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); + + greet(message); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it("logs to the console once when message is provided as an object", () => { + const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); + + greet({ message }); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it("logs once when times is not provided in an object", () => { + const logger = vi.fn(); + + greet({ logger, message }); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it("logs a specified number of times when times is provided", () => { + const logger = vi.fn(); + const times = 7; + + greet({ logger, message, times }); + + expect(logger).toHaveBeenCalledWith(message); + expect(logger).toHaveBeenCalledTimes(7); + }); + }); + `, + ), + }), + "greet.ts": await formatTypeScript( + `import { GreetOptions } from "./types.js"; + + export function greet(options: GreetOptions | string) { + const { + logger = console.log.bind(console), + message, + times = 1, + } = typeof options === "string" ? { message: options } : options; + + for (let i = 0; i < times; i += 1) { + logger(message); + } + } + `, + ), + "index.ts": await formatTypeScript( + ` + export * from "./greet.js"; + export * from "./types.js"; + `, + ), + "types.ts": await formatTypeScript( + ` + export interface GreetOptions { + logger?: (message: string) => void; + message: string; + times?: number; + } + `, + ), + }; +} diff --git a/src/steps/writing/creation/writeAllContributorsRC.ts b/src/steps/writing/creation/writeAllContributorsRC.ts new file mode 100644 index 0000000..b47a382 --- /dev/null +++ b/src/steps/writing/creation/writeAllContributorsRC.ts @@ -0,0 +1,26 @@ +import { readFileSafeAsJson } from "../../../shared/readFileSafeAsJson.js"; +import { AllContributorsData, Options } from "../../../shared/types.js"; +import { formatJson } from "./formatters/formatJson.js"; + +export async function writeAllContributorsRC(options: Options) { + const existing = (await readFileSafeAsJson( + ".all-contributorsrc", + )) as AllContributorsData | null; + + return await formatJson({ + badgeTemplate: + ' 👪 All Contributors: <%= contributors.length %>', + commit: false, + commitConvention: "angular", + commitType: "docs", + contributors: existing?.contributors ?? [], + contributorsPerLine: 7, + contributorsSortAlphabetically: true, + files: ["README.md"], + imageSize: 100, + projectName: options.repository, + projectOwner: options.owner, + repoHost: "https://github.com", + repoType: "github", + }); +} diff --git a/src/steps/writing/creation/writePackageJson.test.ts b/src/steps/writing/creation/writePackageJson.test.ts new file mode 100644 index 0000000..0100336 --- /dev/null +++ b/src/steps/writing/creation/writePackageJson.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../../../shared/types.js"; +import { writePackageJson } from "./writePackageJson.js"; + +const mockReadFileSafeAsJson = vi.fn(); + +vi.mock("../../../shared/readFileSafeAsJson.js", () => ({ + get readFileSafeAsJson() { + return mockReadFileSafeAsJson; + }, +})); + +const options = { + access: "public", + author: "test-author", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + mode: "create", + owner: "test-owner", + repository: "test-repository", + title: "", +} satisfies Options; + +describe("writePackageJson", () => { + it("preserves existing dependencies when they exist", async () => { + const dependencies = { abc: "1.2.3" }; + mockReadFileSafeAsJson.mockResolvedValue({ dependencies }); + + const packageJson = await writePackageJson(options); + + expect(JSON.parse(packageJson)).toEqual( + expect.objectContaining({ dependencies }), + ); + }); + + it("preserves existing devDependencies that aren't known to be unnecessary when they exist", async () => { + const devDependencies = { abc: "1.2.3", jest: "4.5.6" }; + mockReadFileSafeAsJson.mockResolvedValue({ devDependencies }); + + const packageJson = await writePackageJson(options); + + expect(JSON.parse(packageJson)).toEqual( + expect.objectContaining({ devDependencies }), + ); + }); + + it("includes flattened keywords when they're specified", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + + const keywords = ["abc", "def ghi", "jkl mno pqr"]; + const packageJson = await writePackageJson({ ...options, keywords }); + + expect(JSON.parse(packageJson)).toEqual( + expect.objectContaining({ + keywords: ["abc", "def", "ghi", "jkl", "mno", "pqr"], + }), + ); + }); + + it("includes all optional portions when no exclusions are enabled", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + + const packageJson = await writePackageJson(options); + + expect(JSON.parse(packageJson)).toMatchInlineSnapshot(` + { + "author": { + "email": "npm@email.com", + "name": "test-author", + }, + "description": "Test description.", + "devDependencies": {}, + "engines": { + "node": ">=18", + }, + "files": [ + "lib/", + "package.json", + "LICENSE.md", + "README.md", + ], + "license": "MIT", + "lint-staged": { + "*": "prettier --ignore-unknown --write", + }, + "main": "./lib/index.js", + "name": "test-repository", + "packageManager": "pnpm@8.15.1", + "publishConfig": { + "provenance": true, + }, + "repository": { + "type": "git", + "url": "https://github.com/test-owner/test-repository", + }, + "scripts": { + "build": "tsup", + "format": "prettier .", + "lint": "eslint . .*js --max-warnings 0", + "lint:knip": "knip", + "lint:md": "markdownlint "**/*.md" ".github/**/*.md" --rules sentences-per-line", + "lint:packages": "pnpm dedupe --check", + "lint:spelling": "cspell "**" ".github/**/*"", + "prepare": "husky install", + "test": "vitest", + "tsc": "tsc", + }, + "type": "module", + "version": "0.0.0", + } + `); + }); + + it("excludes all optional portions when all exclusions are enabled", async () => { + mockReadFileSafeAsJson.mockResolvedValue({}); + + const packageJson = await writePackageJson({ + ...options, + bin: "./bin/index.js", + excludeAllContributors: true, + excludeCompliance: true, + excludeLintJson: true, + excludeLintKnip: true, + excludeLintMd: true, + excludeLintPackageJson: true, + excludeLintPackages: true, + excludeLintPerfectionist: true, + excludeLintSpelling: true, + excludeLintYml: true, + excludeReleases: true, + excludeRenovate: true, + }); + + expect(JSON.parse(packageJson)).toMatchInlineSnapshot(` + { + "author": { + "email": "npm@email.com", + "name": "test-author", + }, + "bin": "./bin/index.js", + "description": "Test description.", + "devDependencies": {}, + "engines": { + "node": ">=18", + }, + "files": [ + "bin/index.js", + "lib/", + "package.json", + "LICENSE.md", + "README.md", + ], + "license": "MIT", + "lint-staged": { + "*": "prettier --ignore-unknown --write", + }, + "main": "./lib/index.js", + "name": "test-repository", + "packageManager": "pnpm@8.15.1", + "publishConfig": { + "provenance": true, + }, + "repository": { + "type": "git", + "url": "https://github.com/test-owner/test-repository", + }, + "scripts": { + "build": "tsup", + "format": "prettier .", + "lint": "eslint . .*js --max-warnings 0", + "prepare": "husky install", + "tsc": "tsc", + }, + "type": "module", + "version": "0.0.0", + } + `); + }); +}); diff --git a/src/steps/writing/creation/writePackageJson.ts b/src/steps/writing/creation/writePackageJson.ts new file mode 100644 index 0000000..988af5f --- /dev/null +++ b/src/steps/writing/creation/writePackageJson.ts @@ -0,0 +1,124 @@ +import { readFileSafeAsJson } from "../../../shared/readFileSafeAsJson.js"; +import { Options, PartialPackageData } from "../../../shared/types.js"; +import { formatJson } from "./formatters/formatJson.js"; + +const devDependenciesToRemove = [ + "@babel/core", + "@babel/preset-env", + "@babel/preset-react", + "@babel/preset-typescript", + "@swc/jest", + "@vitest/coverage-istanbul", + "ava", + "babel-jest", + "commitlint", + "cson-parser", + "esbuild", + "eslint-config-prettier", + "eslint-plugin-import", + "eslint-plugin-jest", + "eslint-plugin-prettier", + "eslint-plugin-simple-import-sort", + "eslint-plugin-typescript-sort-keys", + "jasmine", + "jest", + "mocha", + "npm-run-all", + "pnpm-deduplicate", + "pretty-quick", + "ts-jest", +]; + +export async function writePackageJson(options: Options) { + const existingPackageJson = + ((await readFileSafeAsJson( + "./package.json", + )) as PartialPackageData | null) ?? {}; + + return await formatJson({ + // If we didn't already have a version, set it to 0.0.0 + version: "0.0.0", + + // To start, copy over all existing package fields (e.g. "dependencies") + ...existingPackageJson, + + author: { email: options.email.npm, name: options.author }, + bin: options.bin, + description: options.description, + keywords: options.keywords?.length + ? options.keywords.flatMap((keyword) => keyword.split(/ /)) + : undefined, + + // We copy all existing dev dependencies except those we know are not used anymore + devDependencies: copyDevDependencies(existingPackageJson), + + // Remove fields we know we don't want, such as old or redundant configs + eslintConfig: undefined, + husky: undefined, + prettierConfig: undefined, + types: undefined, + + // The rest of the fields are ones we know from our template + engines: { + node: ">=18", + }, + files: [ + options.bin?.replace(/^\.\//, ""), + "lib/", + "package.json", + "LICENSE.md", + "README.md", + ].filter(Boolean), + license: "MIT", + "lint-staged": { + "*": "prettier --ignore-unknown --write", + }, + main: "./lib/index.js", + name: options.repository, + packageManager: "pnpm@8.15.1", + publishConfig: { + provenance: true, + }, + repository: { + type: "git", + url: `https://github.com/${options.owner}/${options.repository}`, + }, + scripts: { + ...existingPackageJson.scripts, + build: "tsup", + format: "prettier .", + lint: "eslint . .*js --max-warnings 0", + ...(!options.excludeLintKnip && { + "lint:knip": "knip", + }), + ...(!options.excludeLintMd && { + "lint:md": + 'markdownlint "**/*.md" ".github/**/*.md" --rules sentences-per-line', + }), + ...(!options.excludeLintPackages && { + "lint:packages": "pnpm dedupe --check", + }), + ...(!options.excludeLintSpelling && { + "lint:spelling": 'cspell "**" ".github/**/*"', + }), + prepare: "husky install", + ...(!options.excludeReleases && { test: "vitest" }), + tsc: "tsc", + }, + type: "module", + }); +} + +function copyDevDependencies(existingPackageJson: object) { + const devDependencies = + "devDependencies" in existingPackageJson + ? (existingPackageJson.devDependencies as Record) + : {}; + + for (const devDependencyToRemove of devDependenciesToRemove) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete devDependencies[devDependencyToRemove]; + } + + return devDependencies; +} diff --git a/src/steps/writing/types.ts b/src/steps/writing/types.ts new file mode 100644 index 0000000..6b5449c --- /dev/null +++ b/src/steps/writing/types.ts @@ -0,0 +1,3 @@ +export interface Structure { + [i: string]: Structure | string; +} diff --git a/src/steps/writing/writeStructure.test.ts b/src/steps/writing/writeStructure.test.ts new file mode 100644 index 0000000..63dcf85 --- /dev/null +++ b/src/steps/writing/writeStructure.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from "vitest"; + +import { Options } from "../../shared/types.js"; +import { writeStructure } from "./writeStructure.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +vi.mock("./creation/index.js"); +vi.mock("./writeStructureWorker.js"); + +const options = { + access: "public", + author: "TestAuthor", + base: "everything", + description: "Test description.", + directory: ".", + email: { + github: "github@email.com", + npm: "npm@email.com", + }, + keywords: ["abc", "def ghi", "jkl mno pqr"], + mode: "create", + owner: "TestOwner", + repository: "test-repository", + title: "Test Title", +} satisfies Options; + +describe("writeStructure", () => { + it("resolves when chmod resolves", async () => { + mock$.mockResolvedValue(undefined); + + await expect(writeStructure(options)).resolves.toBeUndefined(); + }); + + it("resolves when chmod rejects", async () => { + mock$.mockRejectedValue(new Error("Oh no!")); + + await expect(writeStructure(options)).resolves.toBeUndefined(); + }); +}); diff --git a/src/steps/writing/writeStructure.ts b/src/steps/writing/writeStructure.ts new file mode 100644 index 0000000..1f5fd7e --- /dev/null +++ b/src/steps/writing/writeStructure.ts @@ -0,0 +1,16 @@ +import { $ } from "execa"; + +import { Options } from "../../shared/types.js"; +import { createStructure } from "./creation/index.js"; +import { writeStructureWorker } from "./writeStructureWorker.js"; + +export async function writeStructure(options: Options) { + await writeStructureWorker(await createStructure(options), "."); + + try { + // https://github.com/JoshuaKGoldberg/create-typescript-app/issues/718 + await $`chmod ug+x .husky/pre-commit`; + } catch { + // https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1195 + } +} diff --git a/src/steps/writing/writeStructureWorker.test.ts b/src/steps/writing/writeStructureWorker.test.ts new file mode 100644 index 0000000..92c03b1 --- /dev/null +++ b/src/steps/writing/writeStructureWorker.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from "vitest"; + +import { writeStructureWorker } from "./writeStructureWorker.js"; + +const mockMkdir = vi.fn(); +const mockWriteFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get mkdir() { + return mockMkdir; + }, + get writeFile() { + return mockWriteFile; + }, +})); + +describe("writeStructureWorker", () => { + it("writes an unformatted file when structure has a file", async () => { + await writeStructureWorker( + { + file: "content", + }, + ".", + ); + + expect(mockMkdir).toHaveBeenCalledWith(".", { recursive: true }); + expect(mockWriteFile).toHaveBeenCalledWith("file", "content"); + }); + + it.each([ + ["implicit json", ".rc", '{ "value": true }', '{ "value": true }\n'], + ["cjs", "file.cjs", " module.exports = { };", "module.exports = {};\n"], + ["js", "file.js", " export default { }", "export default {};\n"], + ["explicit json", "file.json", "{ }", "{}\n"], + ["md", "file.md", " # h1 ", "# h1\n"], + ["yml", "file.yml", " on: true ", "on: true\n"], + ])("writes a formatted %s file", async (_, file, input, output) => { + await writeStructureWorker({ [file]: input }, "."); + expect(mockWriteFile).toHaveBeenCalledWith(file, output); + }); + + it("writes a nested file when structure has a file inside a directory", async () => { + await writeStructureWorker( + { + directory: { + file: "content", + }, + }, + ".", + ); + + expect(mockMkdir).toHaveBeenCalledWith(".", { recursive: true }); + expect(mockMkdir).toHaveBeenCalledWith("directory", { recursive: true }); + }); +}); diff --git a/src/steps/writing/writeStructureWorker.ts b/src/steps/writing/writeStructureWorker.ts new file mode 100644 index 0000000..9a02547 --- /dev/null +++ b/src/steps/writing/writeStructureWorker.ts @@ -0,0 +1,55 @@ +import * as fs from "node:fs/promises"; +import * as path from "path"; +import prettier from "prettier"; + +import { Structure } from "./types.js"; + +export async function writeStructureWorker( + structure: Structure, + basePath: string, +) { + await fs.mkdir(basePath, { recursive: true }); + + for (const [fileName, contents] of Object.entries(structure)) { + if (typeof contents === "string") { + await fs.writeFile( + path.join(basePath, fileName), + await format(fileName, contents), + ); + } else { + await writeStructureWorker(contents, path.join(basePath, fileName)); + } + } +} + +async function format(fileName: string, text: string) { + const parser = inferParser(fileName, text); + if (!parser) { + return text; + } + + return await prettier.format(text, { + parser, + useTabs: true, + }); +} + +function inferParser(fileName: string, text: string) { + if (text.startsWith("{")) { + return "json"; + } + + switch (fileName.split(".").at(-1)) { + case "cjs": + case "js": + return "babel"; + case "json": + return "json"; + case "md": + return "markdown"; + case "yml": + return "yaml"; + } + + return undefined; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4f16ae3 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,5 @@ +export interface GreetOptions { + logger?: (message: string) => void; + message: string; + times?: number; +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..3e219c8 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1 @@ +{ "extends": "./tsconfig.json", "include": ["."] } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f0eaf1a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "outDir": "lib", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022" + }, + "include": ["src"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..43c6f24 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + bundle: false, + clean: true, + dts: true, + entry: ["src/**/*.ts", "!src/**/*.test.*"], + format: "esm", + outDir: "lib", + sourcemap: true, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..36fbb03 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + clearMocks: true, + coverage: { + all: true, + exclude: ["lib"], + include: ["src"], + reporter: ["html", "lcov"], + }, + exclude: ["lib", "node_modules"], + setupFiles: ["console-fail-test/setup"], + }, +});