diff --git a/core/cli/tsconfig.json b/core/cli/tsconfig.json index 63a6a88c2..9045b8311 100644 --- a/core/cli/tsconfig.json +++ b/core/cli/tsconfig.json @@ -24,11 +24,16 @@ }, { "path": "../../lib/state" + }, + { + "path": "../../lib/base" } ], "compilerOptions": { "outDir": "lib", "rootDir": "src" }, - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/lib/schemas/src/tasks.ts b/lib/schemas/src/tasks.ts index d037042c7..70b7a37dc 100644 --- a/lib/schemas/src/tasks.ts +++ b/lib/schemas/src/tasks.ts @@ -13,6 +13,7 @@ import { SmokeTestSchema } from './tasks/n-test' import { CypressSchema } from './tasks/cypress' import { HerokuProductionSchema } from './tasks/heroku-production' import { ServerlessRunSchema } from './tasks/serverless-run' +import { WorkspaceCommandSchema } from './tasks/workspace-command' import { z } from 'zod' export const TaskSchemas = { @@ -39,7 +40,8 @@ export const TaskSchemas = { ServerlessTeardown: z.object({}).describe('Tear down existing serverless functions'), TypeScript: TypeScriptSchema, UploadAssetsToS3: UploadAssetsToS3Schema, - Webpack: WebpackSchema + Webpack: WebpackSchema, + WorkspaceCommand: WorkspaceCommandSchema } export type TaskOptions = InferSchemaOptions diff --git a/lib/schemas/src/tasks/workspace-command.ts b/lib/schemas/src/tasks/workspace-command.ts new file mode 100644 index 000000000..62ab1f37b --- /dev/null +++ b/lib/schemas/src/tasks/workspace-command.ts @@ -0,0 +1,64 @@ +import { z } from 'zod' + +export const WorkspaceCommandSchema = z.object({ + command: z.string().optional().describe('A specific command to run instead of the command that ran this task.') +}).describe(`Runs a Tool Kit command in all workspace packages that have that command. By default, runs the command that was used to run this task. + +For example, imagine a monorepo with these \`.toolkitrc.yml\` files: + +
.toolkitrc.yml + +~~~yml +commands: + run:local: WorkspaceCommand + build:local: WorkspaceCommand +~~~ + +
+ +
packages/api/.toolkitrc.yml + +~~~yml +commands: + run:local: Node +~~~ + +
+ +
packages/client/.toolkitrc.yml + +~~~yml +commands: + build:local: TypeScript +~~~ + +
+ +
packages/components/.toolkitrc.yml + +~~~yml +commands: + build:local: Webpack + run:local: + Webpack: + watch: true +~~~ + +
+ +Running \`dotcom-tool-kit run:local\` at the root level will run the \`Node\` task in \`packages/api\` and the \`Webpack\` task in watch mode in \`packages/components\`; running \`dotcom-tool-kit build:local\` will run \`TypeScript\` in \`packages/client\` and \`Webpack\` in \`packages/components\`. + +To run a particular command in the workspace instead of dynamically inferring the command from which was run at root level, set the \`command\` option for the task: + +~~~yml +commands: + build:ci: + WorkspaceCommand: + command: build:local +~~~ + +`) + +export type WorkspaceCommandOptions = z.infer + +export const Schema = WorkspaceCommandSchema diff --git a/package-lock.json b/package-lock.json index 1bb28f43c..c15ce70ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ }, "core/cli": { "name": "dotcom-tool-kit", - "version": "4.0.5", + "version": "4.0.6", "license": "MIT", "dependencies": { "@dotcom-tool-kit/base": "^1.0.0", @@ -141,7 +141,7 @@ }, "core/create": { "name": "@dotcom-tool-kit/create", - "version": "4.0.5", + "version": "4.0.6", "license": "ISC", "dependencies": { "@aws-sdk/client-iam": "^3.282.0", @@ -177,7 +177,7 @@ "@types/node-fetch": "^2.6.2", "@types/pacote": "^11.1.3", "@types/prompts": "^2.0.14", - "dotcom-tool-kit": "^4.0.5", + "dotcom-tool-kit": "^4.0.6", "type-fest": "^3.13.1" }, "engines": { @@ -6514,6 +6514,10 @@ "resolved": "plugins/mocha", "link": true }, + "node_modules/@dotcom-tool-kit/monorepo": { + "resolved": "plugins/monorepo", + "link": true + }, "node_modules/@dotcom-tool-kit/n-test": { "resolved": "plugins/n-test", "link": true @@ -7469,6 +7473,95 @@ "dev": true, "license": "ISC" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "license": "ISC", @@ -9160,6 +9253,99 @@ "node": ">= 10" } }, + "node_modules/@npmcli/map-workspaces": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", + "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@npmcli/move-file": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", @@ -9173,6 +9359,14 @@ "node": ">=10" } }, + "node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", + "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@npmcli/node-gyp": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz", @@ -9479,6 +9673,15 @@ "@octokit/openapi-types": "^11.2.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@quarterto/parse-makefile-rules": { "version": "1.1.0" }, @@ -10189,6 +10392,15 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/cacache": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@types/cacache/-/cacache-17.0.2.tgz", + "integrity": "sha512-IrqHzVX2VRMDQQKa7CtKRnuoCLdRJiLW6hWU+w7i7+AaQ0Ii5bKwJxd5uRK4zBCyrHd3tG6G8zOm2LplxbSfQg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -10320,9 +10532,10 @@ "license": "MIT" }, "node_modules/@types/minimatch": { - "version": "3.0.5", - "dev": true, - "license": "MIT" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true }, "node_modules/@types/minimist": { "version": "1.2.2", @@ -10379,6 +10592,46 @@ "@types/ssri": "*" } }, + "node_modules/@types/npmcli__arborist": { + "version": "5.6.6", + "resolved": "https://registry.npmjs.org/@types/npmcli__arborist/-/npmcli__arborist-5.6.6.tgz", + "integrity": "sha512-uJyeAINDxppb8uxDe/r9EqvHrA3huz8RcdOX2IVbiU+cb+NaNATSQ8oyCF1HQ3EMferzrQJ8wmP7/b9Z4BlvNw==", + "dev": true, + "dependencies": { + "@npm/types": "*", + "@types/cacache": "*", + "@types/node": "*", + "@types/npmcli__package-json": "*", + "@types/pacote": "*" + } + }, + "node_modules/@types/npmcli__map-workspaces": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/npmcli__map-workspaces/-/npmcli__map-workspaces-3.0.4.tgz", + "integrity": "sha512-NKs2WY8V24b21xBSmofuQEJX/XHIGaQT5UmiOYzdrmIR7dZh9ED9TDXeV8GcAAH0t12J3kMufbxtsli50Zjm9w==", + "dev": true, + "dependencies": { + "@types/glob": "~8.1.0", + "@types/npmcli__arborist": "*", + "@types/npmcli__package-json": "*" + } + }, + "node_modules/@types/npmcli__map-workspaces/node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/npmcli__package-json": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/npmcli__package-json/-/npmcli__package-json-4.0.4.tgz", + "integrity": "sha512-6QjlFUSHBmZJWuC08bz1ZCx6tm4t+7+OJXAdvM6tL2pI7n6Bh5SIp/YxQvnOLFf8MzCXs2ijyFgrzaiu1UFBGA==", + "dev": true + }, "node_modules/@types/npmlog": { "version": "4.1.4", "dev": true, @@ -14665,8 +14918,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "peer": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecc-jsbn": { "version": "0.1.2", @@ -16361,6 +16613,78 @@ "node": ">=0.10.0" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "license": "Apache-2.0", @@ -18423,6 +18747,23 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/java-invoke-local": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/java-invoke-local/-/java-invoke-local-0.0.6.tgz", @@ -24097,6 +24438,37 @@ "version": "1.0.7", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "license": "MIT", @@ -27579,6 +27951,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -27654,6 +28040,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "license": "MIT", @@ -30122,6 +30520,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "license": "ISC" @@ -31967,7 +32382,23 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "plugins/monorepo": { - "extraneous": true + "name": "@dotcom-tool-kit/monorepo", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/schemas": "^1.1.1", + "@npmcli/map-workspaces": "^3.0.6" + }, + "devDependencies": { + "@types/npmcli__map-workspaces": "^3.0.4" + }, + "engines": { + "node": "18.x || 20.x", + "npm": "7.x || 8.x || 9.x" + }, + "peerDependencies": { + "dotcom-tool-kit": "4.x" + } }, "plugins/n-test": { "name": "@dotcom-tool-kit/n-test", diff --git a/plugins/monorepo/.toolkitrc.yml b/plugins/monorepo/.toolkitrc.yml new file mode 100644 index 000000000..18ce7c7de --- /dev/null +++ b/plugins/monorepo/.toolkitrc.yml @@ -0,0 +1,4 @@ +version: 2 + +tasks: + WorkspaceCommand: ./lib/tasks/workspace-command.js diff --git a/plugins/monorepo/package.json b/plugins/monorepo/package.json new file mode 100644 index 000000000..04db19f4f --- /dev/null +++ b/plugins/monorepo/package.json @@ -0,0 +1,40 @@ +{ + "name": "@dotcom-tool-kit/monorepo", + "version": "0.1.0", + "description": "", + "main": "lib", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "FT.com Platforms Team ", + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/financial-times/dotcom-tool-kit.git", + "directory": "plugins/monorepo" + }, + "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", + "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/monorepo", + "files": [ + "/lib", + ".toolkitrc.yml" + ], + "engines": { + "node": "18.x || 20.x", + "npm": "7.x || 8.x || 9.x" + }, + "volta": { + "extends": "../../package.json" + }, + "peerDependencies": { + "dotcom-tool-kit": "4.x" + }, + "dependencies": { + "@dotcom-tool-kit/schemas": "^1.1.1", + "@npmcli/map-workspaces": "^3.0.6" + }, + "devDependencies": { + "@types/npmcli__map-workspaces": "^3.0.4" + } +} diff --git a/plugins/monorepo/readme.md b/plugins/monorepo/readme.md new file mode 100644 index 000000000..6585f757e --- /dev/null +++ b/plugins/monorepo/readme.md @@ -0,0 +1,118 @@ +# @dotcom-tool-kit/monorepo + +Enables Tool Kit to run commands across monorepo workspace packages. + +## Installation + +Install `@dotcom-tool-kit/monorepo` as a `devDependency` in your app: + +```sh +npm install --save-dev @dotcom-tool-kit/monorepo +``` + +Add the plugin to your [Tool Kit configuration](https://github.com/financial-times/dotcom-tool-kit/blob/main/readme.md#configuration): + +```yaml +plugins: + - '@dotcom-tool-kit/monorepo' +``` + +## Tool Kit monorepo workflows + +Most monorepos won't actually need to use this plugin. + +### When you _don't_ need the plugin + +Some common tooling, such as TypeScript and Jest, are already workspace-aware using configuration such as TypeScript's [project references](https://www.typescriptlang.org/docs/handbook/project-references.html). You can run these at the root level of your monorepo across all packages instead of running them package-by-package via Tool Kit and this plugin. + +In almost all cases that will work better; for example, running Jest separately in workspace packages will produce separate test summaries for each package, instead of one summary for all test suites across your monorepo. + +### When you _do_ need the plugin + +Where tooling isn't monorepo-aware itself, or you have tooling that you only want to run in a subset of your workspace packages, the `@dotcom-tool-kit/monorepo` plugin allows the root-level `.toolkitrc.yml` to run commands from workspace-package-level `.toolkitrc.yml` files. + +With this plugin, each workspace package is an independent Tool Kit root, which installs separate plugins for its own use cases. For example, a monorepo with an `api` package which is deployed to Heroku would install the `@dotcom-tool-kit/heroku` plugin as a dependency of the package, not at the root level, and would then include that plugin in the package `.toolkitrc.yml`. + +The root-level `.toolkitrc.yml` can then use the [`WorkspaceCommand`](#workspacecommand) task to forward commands such as `deploy:production` to just the workspace packages that have that command defined (in this case, the `api` package). + +### `@dotcom-tool-kit/monorepo` vs `npm run --workspaces` + +It's also possible to run `package.json` scripts in workspace packages using `npm run --workspaces`, and you can acheive a similar workflow by adding these commands as `package.json` scripts at the root level. Depending on your use case that may be preferable for your team or project, but be aware of these tradeoffs: + +- If we find there are common monorepo use cases between multiple projects, we can extract the Tool Kit configuration for that into a plugin that can be shared between projects. We can't do that if you're using `package.json` scripts. +- Mixing between `package.json` scripts and Tool Kit tasks would require chaining them with shell syntax in the `package.json` script, which is harder to read and less maintainable than having all the configuration in `.toolkitrc.yml`, and has worse error-handling behaviour. +- Due to a [limitation](#plugin-limitations) in this plugin, `package.json` scripts in workspace packages can't be managed by Tool Kit. Any Tool Kit tasks in workspace packages you'd want to run via `npm run --workspaces` would have to be manually maintained as `package.json` scripts, risking your scripts becoming out of sync with the Tool Kit tasks you expect to be running. + +### Plugin limitations + +- Hooks in workspace packages are not installed, so configuration files like `package.json` in workspace packages can't be managed by Tool Kit. If you have a use case for this, please contact the Platforms team. + + +## Tasks + +### `WorkspaceCommand` + +Runs a Tool Kit command in all workspace packages that have that command. By default, runs the command that was used to run this task. + +For example, imagine a monorepo with these `.toolkitrc.yml` files: + +
.toolkitrc.yml + +~~~yml +commands: + run:local: WorkspaceCommand + build:local: WorkspaceCommand +~~~ + +
+ +
packages/api/.toolkitrc.yml + +~~~yml +commands: + run:local: Node +~~~ + +
+ +
packages/client/.toolkitrc.yml + +~~~yml +commands: + build:local: TypeScript +~~~ + +
+ +
packages/components/.toolkitrc.yml + +~~~yml +commands: + build:local: Webpack + run:local: + Webpack: + watch: true +~~~ + +
+ +Running `dotcom-tool-kit run:local` at the root level will run the `Node` task in `packages/api` and the `Webpack` task in watch mode in `packages/components`; running `dotcom-tool-kit build:local` will run `TypeScript` in `packages/client` and `Webpack` in `packages/components`. + +To run a particular command in the workspace instead of dynamically inferring the command from which was run at root level, set the `command` option for the task: + +~~~yml +commands: + build:ci: + WorkspaceCommand: + command: build:local +~~~ + + +#### Task options + +| Property | Description | Type | +| :-------- | :------------------------------------------------------------------- | :------- | +| `command` | A specific command to run instead of the command that ran this task. | `string` | + +_All properties are optional._ + diff --git a/plugins/monorepo/src/tasks/workspace-command.ts b/plugins/monorepo/src/tasks/workspace-command.ts new file mode 100644 index 000000000..38d8ebc4c --- /dev/null +++ b/plugins/monorepo/src/tasks/workspace-command.ts @@ -0,0 +1,39 @@ +import { Task, TaskRunContext } from '@dotcom-tool-kit/base' +import mapWorkspaces from '@npmcli/map-workspaces' +import fs from 'fs/promises' +import path from 'path' +import { loadConfig } from 'dotcom-tool-kit/lib/config' +import { runTasksFromConfig } from 'dotcom-tool-kit/lib/tasks' +import { ToolKitError } from '@dotcom-tool-kit/error' +import { WorkspaceCommandSchema } from '@dotcom-tool-kit/schemas/lib/tasks/workspace-command' + +export default class WorkspaceCommand extends Task<{ task: typeof WorkspaceCommandSchema }> { + async runPackageCommand(packageId: string, packagePath: string, command: string, files?: string[]) { + const config = await loadConfig(this.logger, { root: packagePath }) + + return runTasksFromConfig(this.logger.child({ packageId }), config, [command], files) + } + + async run({ command, files, cwd }: TaskRunContext) { + const pkg = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf8')) + + const workspaces = await mapWorkspaces({ cwd, pkg }) + + const results = await Promise.allSettled( + Array.from(workspaces, ([id, packagePath]) => + this.runPackageCommand(id, packagePath, this.options.command ?? command, files) + ) + ) + + const erroredCommands = results.filter( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ) + + if (erroredCommands.length) { + // TODO improve error messages + const error = new ToolKitError(`error running workspace command ${this.options.command ?? command}`) + error.details = erroredCommands.map((result) => result.reason.toString()).join('\n\n') + throw error + } + } +} diff --git a/plugins/monorepo/tsconfig.json b/plugins/monorepo/tsconfig.json new file mode 100644 index 000000000..65a97dec3 --- /dev/null +++ b/plugins/monorepo/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "references": [ + { + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" + } + ], + "include": ["src/**/*"] +} diff --git a/scripts/create-plugin.js b/scripts/create-plugin.js index f33e804ed..d0339f1a3 100755 --- a/scripts/create-plugin.js +++ b/scripts/create-plugin.js @@ -38,6 +38,9 @@ pkg.engines = { node: '18.x || 20.x', npm: '7.x || 8.x || 9.x' } +pkg.volta = { + extends: '../../package.json' +} pkg.peerDependencies = { 'dotcom-tool-kit': '3.x' } diff --git a/scripts/generate-docs.js b/scripts/generate-docs.js index 356727a38..51be65c26 100644 --- a/scripts/generate-docs.js +++ b/scripts/generate-docs.js @@ -114,9 +114,9 @@ async function main() { const readmePath = path.join('plugins', plugin, 'readme.md') const generatedOptionsMarkdown = await formatPluginSchemas(plugin) - const originalReadme = await fs.readFile(readmePath, 'utf-8') - try { + const originalReadme = await fs.readFile(readmePath, 'utf-8') + const replacedReadme = replaceBetween( originalReadme, generatedOptionsMarkdown, @@ -127,7 +127,11 @@ async function main() { await fs.writeFile(readmePath, replacedReadme, 'utf-8') console.log(`written ${readmePath}`) } catch (e) { - console.error(`no replacement markers in ${readmePath}`) + if (e.code === 'ENOENT') { + console.error(`${plugin} has no readme`) + } else { + console.error(`no replacement markers in ${readmePath}`) + } } }) ) diff --git a/tsconfig.json b/tsconfig.json index 9c507516b..8acba2379 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -123,6 +123,9 @@ }, { "path": "lib/schemas" + }, + { + "path": "plugins/monorepo" } ] }