diff --git a/docs/.astro/types.d.ts b/docs/.astro/types.d.ts index 7f6d8050..9381c7df 100644 --- a/docs/.astro/types.d.ts +++ b/docs/.astro/types.d.ts @@ -409,6 +409,13 @@ declare module 'astro:content' { collection: "docs"; data: InferEntrySchema<"docs"> } & { render(): Render[".mdx"] }; +"core/workspace.mdx": { + id: "core/workspace.mdx"; + slug: "core/workspace"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".mdx"] }; "docs/commands.mdx": { id: "docs/commands.mdx"; slug: "docs/commands"; diff --git a/docs/astro.config.ts b/docs/astro.config.ts index 8ebecc71..7a8e3ea9 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -65,7 +65,7 @@ export default defineConfig({ sidebar: [ { label: 'Core concepts', autogenerate: { directory: 'concepts' } }, { label: 'Documentation', autogenerate: { directory: 'docs' } }, - { label: 'Core utilities', autogenerate: { directory: 'core' } }, + { label: 'Core commands', autogenerate: { directory: 'core' } }, { label: 'Plugins', autogenerate: { directory: 'plugins' } }, { label: 'Project', autogenerate: { directory: 'project' } }, { diff --git a/docs/commands/astro.ts b/docs/commands/astro.ts deleted file mode 100644 index 60e752db..00000000 --- a/docs/commands/astro.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { run } from 'onerepo'; -import type { Builder, Handler } from 'onerepo'; - -export const command = 'astro'; - -export const description = 'Run astro commands.'; - -export const builder: Builder = (yargs) => yargs.usage('$0 astro'); - -export const handler: Handler = async (argv, { graph }) => { - const { '--': rest = [] } = argv; - - if (rest.includes('dev')) { - throw new Error('To run the dev server, please run `docs start`'); - } - - const ws = graph.getByLocation(__dirname); - - const [bin] = await run({ - name: 'Get Astro', - cmd: 'yarn', - args: ['bin', 'astro'], - runDry: true, - opts: { - cwd: ws.location, - }, - }); - - await run({ - name: 'Run astro', - cmd: bin, - args: rest, - opts: { - cwd: ws.location, - }, - }); -}; diff --git a/docs/commands/start.ts b/docs/commands/start.ts deleted file mode 100644 index f2052545..00000000 --- a/docs/commands/start.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Builder, Handler } from 'onerepo'; - -export const command = 'start'; - -export const description = 'Start the docs development server'; - -type Argv = { - netlify: boolean; -}; - -export const builder: Builder = (yargs) => - yargs.usage('$0 start').option('netlify', { - alias: 'n', - type: 'boolean', - default: false, - description: 'Run with the Netlify CLI', - }); - -export const handler: Handler = async (argv, { graph }) => { - const { netlify } = argv; - - const ws = graph.getByLocation(__dirname); - - await graph.packageManager.run({ - name: 'Run astro', - cmd: netlify ? 'netlify' : 'astro', - args: ['dev'], - opts: { - cwd: ws.location, - stdio: 'inherit', - }, - }); -}; diff --git a/docs/onerepo.config.ts b/docs/onerepo.config.ts index b29a5de4..c507a80f 100644 --- a/docs/onerepo.config.ts +++ b/docs/onerepo.config.ts @@ -1,20 +1,28 @@ import type { Config } from 'onerepo'; export default { + commands: { + passthrough: { + start: { description: 'Start the Astro dev server.', command: 'astro dev' }, + build: { description: 'Build the documentation site for production.', command: 'astro build' }, + check: { description: 'Check Astro pages for errors.', command: 'astro check' }, + astro: { description: 'Run Astro directly.' }, + }, + }, tasks: { 'pre-commit': { serial: [ - { match: '**/*.{astro}', cmd: '$0 ws docs astro -- check' }, + { match: '**/*.{astro}', cmd: '$0 ws docs check' }, { match: '../modules/**/*.ts', cmd: '$0 ws docs typedoc --add' }, { match: '../**/*', cmd: '$0 ws docs collect-content -w ${workspaces} --add' }, { match: '../**/CHANGELOG.md', cmd: '$0 ws docs pull-changelogs --add' }, ], }, 'pre-merge': { - serial: [{ match: '**/*.{astro}', cmd: '$0 ws docs astro -- check' }], + serial: [{ match: '**/*.{astro}', cmd: '$0 ws docs check' }], }, build: { - serial: ['$0 ws docs astro -- build'], + serial: ['$0 ws docs build'], }, }, } satisfies Config; diff --git a/docs/src/content/docs/api/index.md b/docs/src/content/docs/api/index.md index 726964d9..ab8b8469 100644 --- a/docs/src/content/docs/api/index.md +++ b/docs/src/content/docs/api/index.md @@ -8,7 +8,7 @@ oneRepo is in currently in public beta. Some APIs may not be specifically necess ::: - + ## Namespaces @@ -926,6 +926,12 @@ serial?: Task[]; ```ts type WorkspaceConfig: { codeowners: Record; + commands: { + passthrough: Record; + }; meta: Record; tasks: TaskConfig; }; @@ -959,6 +965,43 @@ export default { }; ``` +##### commands? + +```ts +commands?: { + passthrough: Record; +}; +``` + +Configuration for custom commands. To configure the commands directory, see [`RootConfig` `commands.directory`](#commandsdirectory). + +##### commands.passthrough + +```ts +commands.passthrough: Record; +``` + +**Default:** `{}` + +Enable commands from installed dependencies. Similar to running `npx `, but pulled into the oneRepo CLI and able to be limited by workspace. Passthrough commands _must_ have helpful descriptions. + +```ts title="onerepo.config.ts" +export default { + commands: { + passthrough: { + astro: { description: 'Run Astro commands directly.' }, + start: { description: 'Run the Astro dev server.', command: 'astro dev --port=8000' }, + }, + }, +}; +``` + ##### meta? ```ts @@ -1312,12 +1355,12 @@ get codeowners(): Required> ##### config ```ts -get config(): WorkspaceConfig +get config(): Required ``` Get the workspace's configuration -**Returns:** [`WorkspaceConfig`](#workspaceconfigcustomlifecycles) +**Returns:** `Required`\<[`WorkspaceConfig`](#workspaceconfigcustomlifecycles) \| [`RootConfig`](#rootconfigcustomlifecycles)\> **Source:** [modules/graph/src/Workspace.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/graph/src/Workspace.ts) ##### dependencies @@ -2641,7 +2684,7 @@ versions: string[]; ### Plugin ```ts -type Plugin: PluginObject | (config) => PluginObject; +type Plugin: PluginObject | (config, graph) => PluginObject; ``` **Source:** [modules/onerepo/src/types/plugin.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/onerepo/src/types/plugin.ts) diff --git a/docs/src/content/docs/core/workspace.mdx b/docs/src/content/docs/core/workspace.mdx new file mode 100644 index 00000000..3b9d3010 --- /dev/null +++ b/docs/src/content/docs/core/workspace.mdx @@ -0,0 +1,127 @@ +--- +title: Workspace commands +description: Run commands in Workspaces. +meta: + stability: stable +--- + +import FileTree from '../../../components/FileTree.astro'; + +Your monorepo's Workspaces may end up having unique needs that differ from other Workspaces. When this occurs, you may want to have commands that are specific to an individual Workspace. There are two ways to configure oneRepo to enable Workspace-specific commands. + +## Custom commands + +Workspaces can have custom commands that work identically to commands at the root of your repository. The [`RootConfig.commands.directory`](/docs/config/#commandsdirectory) configuration value enables a directory within _each_ Workspace of your application, if present, to contain command files that will automatically be included in the `one` CLI when invoked. + +By default, the `commands.directory` is set to `'commands'`. + +```ts title="onerepo.config.ts" +import type { Config } from 'onerepo'; + +export default { + commands: { + directory: 'commands', + }, +} satisfies Config; +``` + +This setting will automatically enable each Workspace to add command files: + + + +- apps + - public-app + - commands/ + - start.ts # one ws public-app start + - build.ts + - private-app + - commands/ + - start.ts # one ws private-app start + - build.ts + + + +And in turn, our custom commands will be exposed on the `one workspace` (or `one ws` alias, for short) command: + +```sh title="Run public-app commands." +# Run the `start` command +one workspace public-app start + +# Run the `build` command +one workspace public-app build +``` + +```sh title="Build private-app commands." +one workspace private-app build +# ↑ Equivalent to ↓ +one ws private-app build +``` + +Read the [writing commands documentation](/docs/commands/) for a tutorial and in-depth information on the structure, shape, and strategies for commands. + +## Passthrough commands + +Alternatively, for quick access to third-party commands that don't require extra configuration, we can configure _passthrough_ commands in Workspace configuration files: + +```ts title="apps/public-app/onerepo.config.ts" +export default { + commands: { + passthrough: { + start: { command: 'astro dev', description: 'Start the Astro dev server.' }, + build: { command: 'astro build', description: 'Build the app using Astro.' }, + }, + }, +} satisfies WorkspaceConfig; +``` + +Some third-party spackages expose executables for running servers and quick helpers that use configuration files instead of command-line flags, like [Astro](https://astro.build) and [Vite](https://vitejs.dev). These types of commands are great candidates for passthroughs. + +:::danger +This configuration is a shortcut for quick one-offs with little to no extra options necessary. Avoid over-using this configuration to avoid [script overload](/concepts/why-onerepo/#script-overload) pitfalls. +::: + +By enabling the passthrough using the above configuration for `public-app`, we add two commands to the Workspace: `start` and `build` and can be called with either form of the `workspace` command using `one workspace …` or the alias `one ws …`: + +```sh title="Run the Astro dev server." +one workspace public-app start +# ↑ Equivalent to ↓ +one ws public-app start +``` + +```sh title="Build the app using Astro." +one workspace public-app build +# ↑ Equivalent to ↓ +one ws public-app build +``` + +If you need a little bit more and want to pass arguments through to the underlying command (in this case `astro …`), include any extra argument flags after the CLI parser stop point (`--`): + +```sh title="Pass arguments to Astro" +one workspace public app start -- --port=8000 --open +``` + +## Command usage + +{/* start-auto-generated-from-cli-workspace */} +{/* @generated SignedSource<<88b5edb9556a0953b6fc2f856d4b6c5d>> */} + +### `one workspace` + +Aliases: `one ws` + +Run commands within individual Workspaces. + +```sh +one workspace [options...] -- [passthrough...] +``` + +This enables running both custom commands as defined via the `commands.directory` configuration option within each Workspace as well as `commands.passthrough` aliases. + +Arguments for passthrough commands meant for the underlying command must be sent after `--`. + +| Positional | Type | Description | +| ---------------- | -------- | --------------------------------- | +| `command` | `string` | Command to run. | +| `workspace-name` | `string` | The name or alias of a Workspace. | + +{/* end-auto-generated-from-cli-workspace */} diff --git a/docs/src/content/docs/docs/config.mdx b/docs/src/content/docs/docs/config.mdx index f00f1581..29a1b25c 100644 --- a/docs/src/content/docs/docs/config.mdx +++ b/docs/src/content/docs/docs/config.mdx @@ -367,7 +367,7 @@ module.exports = { {/* start-usage-typedoc-workspace */} -{/* @generated SignedSource<<2f550887309eb9605eec80f301fb8d29>> */} +{/* @generated SignedSource<> */} ### WorkspaceConfig\ @@ -395,6 +395,30 @@ export default { }; ``` +#### commands? + +- **Type:** `Object` + +Configuration for custom commands. To configure the commands directory, see [`RootConfig` `commands.directory`](#commandsdirectory). + +#### commands.passthrough + +- **Type:** `Record`\<`string`, `Object`\> +- **Default:** `{}` + +Enable commands from installed dependencies. Similar to running `npx `, but pulled into the oneRepo CLI and able to be limited by workspace. Passthrough commands _must_ have helpful descriptions. + +```ts title="onerepo.config.ts" +export default { + commands: { + passthrough: { + astro: { description: 'Run Astro commands directly.' }, + start: { description: 'Run the Astro dev server.', command: 'astro dev --port=8000' }, + }, + }, +}; +``` + #### meta? - **Type:** `Record`\<`string`, `unknown`\> diff --git a/docs/src/content/docs/plugins/docgen/example.mdx b/docs/src/content/docs/plugins/docgen/example.mdx index ab1c0c5b..b9c97a27 100644 --- a/docs/src/content/docs/plugins/docgen/example.mdx +++ b/docs/src/content/docs/plugins/docgen/example.mdx @@ -3,6 +3,8 @@ title: Docgen Example sidebar: hidden: true pagefind: false +tableOfContents: + maxHeadingLevel: 5 --- :::note @@ -10,7 +12,7 @@ The following content is auto-generated using the [official documentation plugin ::: {/* start-auto-generated-from-cli */} -{/* @generated SignedSource<<46fffee68c0c3f21e75e50830aa3200b>> */} +{/* @generated SignedSource<<20d792693f551fd42c691bd5a0edecf4>> */} ## `one` @@ -917,15 +919,17 @@ Checks for the existence of `tsconfig.json` file and batches running `tsc --noEm ### `one workspace` -Aliases: `one`, `one ws` +Aliases: `one ws` -Run Workspace-specific commands +Run commands within individual Workspaces. ```sh -one workspace [options] +one workspace [options...] -- [passthrough...] ``` -Add commands to the `commands` directory within a Workspace to create Workspace-specific commands. +This enables running both custom commands as defined via the `commands.directory` configuration option within each Workspace as well as `commands.passthrough` aliases. + +Arguments for passthrough commands meant for the underlying command must be sent after `--`. | Positional | Type | Description | | ---------------- | -------- | --------------------------------- | @@ -934,27 +938,37 @@ Add commands to the `commands` directory within a Workspace to create Workspace- --- -#### `one workspace @onerepo/docs` +#### `one workspace docs` -Aliases: `one workspace docs` +Aliases: `one workspace @onerepo/docs` -Runs commands in the `@onerepo/docs` Workspace +Runs commands in the "@onerepo/docs" Workspace. --- -##### `one workspace @onerepo/docs astro` +##### `one workspace docs astro` -Run astro commands. +Run Astro directly. -```sh -one workspace @onerepo/docs astro -``` +| Option | Type | Description | +| ----------------- | --------- | -------------------------------------------- | +| `--show-advanced` | `boolean` | Pair with `--help` to show advanced options. | + +--- + +##### `one workspace docs build` + +Build the documentation site for production. + +| Option | Type | Description | +| ----------------- | --------- | -------------------------------------------- | +| `--show-advanced` | `boolean` | Pair with `--help` to show advanced options. | --- -##### `one workspace @onerepo/docs changelogs` +##### `one workspace docs changelogs` -Aliases: `one workspace @onerepo/docs pull-changelogs` +Aliases: `one workspace docs pull-changelogs` Update changelogs from source files @@ -980,7 +994,17 @@ Update changelogs from source files --- -##### `one workspace @onerepo/docs collect-content` +##### `one workspace docs check` + +Check Astro pages for errors. + +| Option | Type | Description | +| ----------------- | --------- | -------------------------------------------- | +| `--show-advanced` | `boolean` | Pair with `--help` to show advanced options. | + +--- + +##### `one workspace docs collect-content` Generate docs for the oneRepo monorepo @@ -1006,26 +1030,22 @@ Generate docs for the oneRepo monorepo --- -##### `one workspace @onerepo/docs start` - -Start the docs development server +##### `one workspace docs start` -```sh -one workspace @onerepo/docs start -``` +Start the Astro dev server. -| Option | Type | Description | -| --------------- | --------- | ------------------------ | -| `--netlify, -n` | `boolean` | Run with the Netlify CLI | +| Option | Type | Description | +| ----------------- | --------- | -------------------------------------------- | +| `--show-advanced` | `boolean` | Pair with `--help` to show advanced options. | --- -##### `one workspace @onerepo/docs typedoc` +##### `one workspace docs typedoc` Generate typedoc markdown files for the toolchain. ```sh -one workspace @onerepo/docs typedoc +one workspace docs typedoc ``` | Option | Type | Description | @@ -1034,20 +1054,20 @@ one workspace @onerepo/docs typedoc --- -#### `one workspace @onerepo/github-action` +#### `one workspace github-action` -Aliases: `one workspace github-action` +Aliases: `one workspace @onerepo/github-action` -Runs commands in the `@onerepo/github-action` Workspace +Runs commands in the "@onerepo/github-action" Workspace. --- -##### `one workspace @onerepo/github-action build` +##### `one workspace github-action build` Build public Workspaces using esbuild. ```sh -one workspace @onerepo/github-action build [options] +one workspace github-action build [options] ``` | Option | Type | Description | @@ -1071,19 +1091,19 @@ one workspace @onerepo/github-action build [options] Build all Workspaces. ```sh -one workspace @onerepo/github-action build +one workspace github-action build ``` Build the `graph` Workspace only. ```sh -one workspace @onerepo/github-action build -w graph +one workspace github-action build -w graph ``` Build the `graph`, `cli`, and `logger` workspaces. ```sh -one workspace @onerepo/github-action build -w graph cli logger +one workspace github-action build -w graph cli logger ``` {/* end-auto-generated-from-cli */} diff --git a/modules/graph/package.json b/modules/graph/package.json index 10323bba..04a1487c 100644 --- a/modules/graph/package.json +++ b/modules/graph/package.json @@ -22,6 +22,7 @@ ], "dependencies": { "@onerepo/package-manager": "0.5.0", + "defaults": "^3.0.0", "glob": "^10.1.0", "graph-data-structure": "^3.2.0", "jiti": "^1.21.0", diff --git a/modules/graph/src/Workspace.ts b/modules/graph/src/Workspace.ts index 9cdcedf5..a06f53e6 100644 --- a/modules/graph/src/Workspace.ts +++ b/modules/graph/src/Workspace.ts @@ -1,6 +1,16 @@ import path from 'node:path'; +import defaults from 'defaults'; import { minimatch } from 'minimatch'; -import type { Tasks, TaskConfig, WorkspaceConfig } from 'onerepo'; +import type { Tasks, TaskConfig, WorkspaceConfig, RootConfig } from 'onerepo'; + +const defaultConfig: Required = { + codeowners: {}, + commands: { + passthrough: {}, + }, + meta: {}, + tasks: {}, +}; /** * @group Graph @@ -11,7 +21,7 @@ export class Workspace { #rootLocation: string; #tasks: TaskConfig | null = null; #require: typeof require; - #config?: WorkspaceConfig; + #config?: Required; /** * @internal @@ -123,16 +133,21 @@ export class Workspace { /** * Get the workspace's configuration */ - get config(): WorkspaceConfig { + get config(): Required { if (!this.#config) { try { const config = this.#require(this.resolve('onerepo.config')); - this.#config = (config.default ?? config) as WorkspaceConfig; + if (this.isRoot) { + this.#config = config as Required; + } else { + this.#config = defaults(config.default ?? config, defaultConfig) as Required; + } } catch (e) { if (e && (e as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { - return {}; + this.#config = { ...defaultConfig } as Required; + } else { + throw e; } - throw e; } } return this.#config; diff --git a/modules/onerepo/package.json b/modules/onerepo/package.json index f13f56df..21efa9f2 100644 --- a/modules/onerepo/package.json +++ b/modules/onerepo/package.json @@ -52,7 +52,9 @@ "minimatch": "^9.0.3", "picocolors": "^1.0.0", "semver": "^7.5.4", - "yargs": "^17.6.2" + "yargs": "^17.6.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "devDependencies": { "@internal/tsconfig": "workspace:^", @@ -66,6 +68,7 @@ "@types/semver": "^7.5.6", "@types/unist": "^3.0.2", "@types/yargs": "^17.0.32", + "@types/yargs-unparser": "^2.0.3", "typescript": "^5.3.3" }, "engines": { diff --git a/modules/onerepo/src/core/tasks/tasks.ts b/modules/onerepo/src/core/tasks/tasks.ts index f5be1aa4..2a378dd7 100644 --- a/modules/onerepo/src/core/tasks/tasks.ts +++ b/modules/onerepo/src/core/tasks/tasks.ts @@ -16,8 +16,11 @@ import type { Config, CorePlugins, Lifecycle, Task, TaskDef } from '../../types' import { generate } from '../generate'; import { graph } from '../graph'; import { codeowners } from '../codeowners'; +import { dependencies } from '../dependencies'; +import { hooks } from '../hooks'; +import { workspace } from '../workspace'; -const corePlugins: CorePlugins = [generate, graph, codeowners]; +const corePlugins: CorePlugins = [codeowners, dependencies, generate, graph, hooks, workspace]; export const command = 'tasks'; diff --git a/modules/onerepo/src/core/workspace/index.ts b/modules/onerepo/src/core/workspace/index.ts new file mode 100644 index 00000000..cbcdfd72 --- /dev/null +++ b/modules/onerepo/src/core/workspace/index.ts @@ -0,0 +1,104 @@ +import { existsSync, lstatSync } from 'node:fs'; +import type { Workspace } from '@onerepo/graph'; +import type { Builder, Yargs } from '@onerepo/yargs'; +import type { RequireDirectoryOptions } from 'yargs'; +import type { Plugin, WorkspaceConfig } from '../../types'; +import { getHandler } from './passthrough'; + +export const workspace: Plugin = function workspaces(config, graph) { + const commandDirectory = config.commands.directory; + + return { + yargs(yargs, visitor) { + return yargs.command( + ['workspace', 'ws'], + 'Run commands within individual Workspaces.', + (yargs) => { + yargs + .usage(`$0 workspace [options...] -- [passthrough...]`) + .positional('workspace-name', { + description: 'The name or alias of a Workspace.', + type: 'string', + }) + .positional('command', { + description: 'Command to run.', + demandCommand: true, + type: 'string', + }) + .epilogue( + `This enables running both custom commands as defined via the \`commands.directory\` configuration option within each Workspace as well as \`commands.passthrough\` aliases. + +Arguments for passthrough commands meant for the underlying command must be sent after \` -- \`.`, + ); + + if (commandDirectory && !process.env.ONEREPO_DOCGEN) { + const inputWorkspaceName = process.argv[3]; + if (inputWorkspaceName && !inputWorkspaceName.startsWith('-')) { + try { + const ws = graph.getByName(inputWorkspaceName)!; + if (existsSync(ws.resolve(commandDirectory))) { + addWorkspace(yargs, ws, commandDirectory, visitor); + } + return yargs.demandCommand(1, `Please enter a command to run in the ${ws.name} Workspace.`); + } catch (e) { + //pass + } + } + + for (const ws of graph.workspaces) { + if (ws.isRoot) { + continue; + } + const exists = existsSync(ws.resolve(commandDirectory)); + if (exists && lstatSync(ws.resolve(commandDirectory)).isDirectory()) { + addWorkspace(yargs, ws, commandDirectory, visitor); + } + } + } + + return yargs.demandCommand(1, 'Please enter a Workspace name from the list available.'); + }, + () => {}, + ); + }, + }; +}; + +function addWorkspace( + yargs: Yargs, + ws: Workspace, + commandDirectory: string, + visitor: NonNullable, +): Yargs { + const names = ws.scope ? [ws.name.replace(`${ws.scope}/`, ''), ws.name] : [ws.name]; + const wsNames = [...names, ...ws.aliases].filter((value, index, self) => self.indexOf(value) === index); + + return yargs.command(wsNames, `Runs commands in the "${ws.name}" Workspace.`, (yargs: Yargs) => { + if (commandDirectory) { + addCommandDir(yargs, ws, commandDirectory); + } + + if (ws.isRoot) { + return yargs; + } + + for (const [cmd, { description, command }] of Object.entries( + (ws.config as Required).commands.passthrough, + )) { + const mod = visitor({ + command: cmd, + description, + builder: ((yargs) => yargs) as Builder, + handler: getHandler(command ?? cmd, ws), + }); + yargs.command(mod.command, mod.description, mod.builder, mod.handler); + } + return yargs; + }); +} + +function addCommandDir(yargs: Yargs, ws: Workspace, dirname: string): Yargs { + return yargs + .commandDir(ws.resolve(dirname)) + .demandCommand(1, `Please enter a valid command for the ${ws.name} Workspace.`); +} diff --git a/modules/onerepo/src/core/workspace/passthrough.ts b/modules/onerepo/src/core/workspace/passthrough.ts new file mode 100644 index 00000000..8fa6468a --- /dev/null +++ b/modules/onerepo/src/core/workspace/passthrough.ts @@ -0,0 +1,29 @@ +import type { Builder, Handler } from '@onerepo/yargs'; +import type { Workspace } from '@onerepo/graph'; +import parser from 'yargs-parser'; +import unparser from 'yargs-unparser'; + +export const builder: Builder = (yargs) => yargs; + +export function getHandler(cmd: string, workspace: Workspace): Handler { + return async function (argv, { graph }) { + const { '--': passthrough = [] } = argv; + + const defaults = parser(cmd); + const { + _: [command, ...rest], + ...args + } = defaults; + const restArgs = unparser({ _: [], args }).map(String); + + await graph.packageManager.run({ + name: `Run ${cmd}`, + cmd: `${command}`, + args: [...rest.map(String), ...restArgs, ...passthrough], + opts: { + cwd: workspace.location, + stdio: 'inherit', + }, + }); + }; +} diff --git a/modules/onerepo/src/setup/index.ts b/modules/onerepo/src/setup/index.ts index 4192fb8c..e95981b3 100644 --- a/modules/onerepo/src/setup/index.ts +++ b/modules/onerepo/src/setup/index.ts @@ -9,6 +9,7 @@ import { graph } from '../core/graph'; import { hooks } from '../core/hooks'; import { install } from '../core/install'; import { tasks } from '../core/tasks'; +import { workspace } from '../core/workspace'; import { setup as internalSetup } from './setup'; export type { GraphSchemaValidators } from '../core/graph'; @@ -37,7 +38,7 @@ export { internalSetup }; /** * @internal */ -export const corePlugins: CorePlugins = [codeowners, dependencies, generate, graph, hooks, install, tasks]; +export const corePlugins: CorePlugins = [codeowners, dependencies, generate, graph, hooks, install, tasks, workspace]; /** * @internal diff --git a/modules/onerepo/src/setup/setup.ts b/modules/onerepo/src/setup/setup.ts index 837e246b..70c3b7e4 100644 --- a/modules/onerepo/src/setup/setup.ts +++ b/modules/onerepo/src/setup/setup.ts @@ -14,7 +14,6 @@ import type { RequireDirectoryOptions, Argv as Yargv } from 'yargs'; import type { Argv, DefaultArgv, Yargs } from '@onerepo/yargs'; import type { Config, RootConfig, CorePlugins, PluginObject, Plugin } from '../types'; import pkg from '../../package.json'; -import { workspaceBuilder } from './workspaces'; const defaultConfig: Required = { root: true, @@ -153,7 +152,7 @@ export async function setup({ yargs: pluginYargs, startup: startupHandler, shutdown: shutdownHandler, - } = typeof plugin === 'function' ? plugin({ ...resolvedConfig, plugins: userPlugins }) : plugin; + } = typeof plugin === 'function' ? plugin({ ...resolvedConfig, plugins: userPlugins }, graph) : plugin; if (typeof pluginYargs === 'function') { pluginYargs(yargs, options.visit); } @@ -174,16 +173,6 @@ export async function setup({ yargs.commandDir(rootCommandPath); } } - - // Workspace commands using resolvedConfig.commands.directory - yargs.command({ - describe: 'Run Workspace-specific commands', - command: '$0', - aliases: ['workspace', 'ws'], - builder: workspaceBuilder(graph, resolvedConfig.commands.directory || 'commands'), - // This handler is a no-op because the builder demands N+1 command(s) be input - handler: () => {}, - }); } return { diff --git a/modules/onerepo/src/setup/workspaces.ts b/modules/onerepo/src/setup/workspaces.ts deleted file mode 100644 index a5d2d144..00000000 --- a/modules/onerepo/src/setup/workspaces.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { existsSync, lstatSync } from 'node:fs'; -import type { Graph, Workspace } from '@onerepo/graph'; -import type { Yargs } from '@onerepo/yargs'; - -export function workspaceBuilder(graph: Graph, dirname: string) { - return (yargs: Yargs) => { - yargs - .usage('$0 workspace [options]') - .positional('workspace-name', { - description: 'The name or alias of a Workspace.', - type: 'string', - }) - .positional('command', { - description: 'Command to run.', - demandCommand: true, - type: 'string', - }) - .epilogue( - `Add commands to the \`${dirname}\` directory within a Workspace to create Workspace-specific commands.`, - ); - - const workspaceName = process.argv[3]; - try { - const ws = graph.getByName(workspaceName)!; - if (existsSync(ws.resolve(dirname))) { - addWorkspace(yargs, ws, dirname); - return yargs.demandCommand(1, `Please enter a command to run in ${ws.name}.`); - } - } catch (e) { - // pass - } - - // Allow omitting the Workspace name if the process working directory is already in a workspace - const workingWorkspace = graph.getByLocation(process.cwd()); - if (workingWorkspace !== graph.root && existsSync(workingWorkspace.resolve(dirname))) { - yargs - .usage('$0 workspace [options]') - .usage('$0 ws [options]') - .epilogue( - `You are currently working in the ${workingWorkspace.name} Workspace, so Workspace-specific commands will be run by default when a suitable name or alias for this Workspace is omitted.`, - ); - return addCommandDir(yargs, workingWorkspace, dirname).demandCommand( - 2, - `Please enter a valid command for the ${workingWorkspace.name} Workspace or enter the name of another Workspace for more choices.`, - ); - } - - graph.dependencies().forEach((ws: Workspace) => { - if (ws.isRoot) { - return; - } - const exists = existsSync(ws.resolve(dirname)); - if (exists && lstatSync(ws.resolve(dirname)).isDirectory()) { - addWorkspace(yargs, ws, dirname); - } - }); - return yargs.demandCommand(1, 'Please enter a Workspace name from the list above.'); - }; -} - -function addWorkspace(yargs: Yargs, ws: Workspace, dirname: string): Yargs { - const wsNames = [ws.name, ...ws.aliases]; - - return yargs.command(wsNames, `Runs commands in the \`${ws.name}\` Workspace`, (yargs: Yargs) => { - return addCommandDir(yargs, ws, dirname); - }); -} - -function addCommandDir(yargs: Yargs, ws: Workspace, dirname: string): Yargs { - return yargs - .commandDir(ws.resolve(dirname)) - .demandCommand(1, `Please enter a valid command for the ${ws.name} Workspace.`); -} diff --git a/modules/onerepo/src/types/config-workspace.ts b/modules/onerepo/src/types/config-workspace.ts index ec5a58f6..82b30262 100644 --- a/modules/onerepo/src/types/config-workspace.ts +++ b/modules/onerepo/src/types/config-workspace.ts @@ -20,6 +20,28 @@ export type WorkspaceConfig = { * ``` */ codeowners?: Record>; + /** + * Configuration for custom commands. To configure the commands directory, see [`RootConfig` `commands.directory`](#commandsdirectory). + */ + commands?: { + /** + * @default `{}` + * + * Enable commands from installed dependencies. Similar to running `npx `, but pulled into the oneRepo CLI and able to be limited by workspace. Passthrough commands _must_ have helpful descriptions. + * + * ```ts title="onerepo.config.ts" + * export default { + * commands: { + * passthrough: { + * astro: { description: 'Run Astro commands directly.' }, + * start: { description: 'Run the Astro dev server.', command: 'astro dev --port=8000' }, + * }, + * }, + * }; + * ``` + */ + passthrough: Record; + }; /** * @default `{}` * A place to put any custom information or configuration. A helpful space for you to extend Workspace configurations for your own custom commands. diff --git a/modules/onerepo/src/types/plugin.ts b/modules/onerepo/src/types/plugin.ts index c4905dd1..6376ca51 100644 --- a/modules/onerepo/src/types/plugin.ts +++ b/modules/onerepo/src/types/plugin.ts @@ -1,5 +1,6 @@ import type { RequireDirectoryOptions } from 'yargs'; import type { Argv, DefaultArgv, Yargs } from '@onerepo/yargs'; +import type { Graph } from '@onerepo/graph'; import type { RootConfig } from './config-root'; /** @@ -27,7 +28,7 @@ export type PluginObject = { /** * @group Plugins */ -export type Plugin = PluginObject | ((config: Required) => PluginObject); +export type Plugin = PluginObject | ((config: Required, graph: Graph) => PluginObject); /** * @internal diff --git a/modules/yargs/src/yargs.ts b/modules/yargs/src/yargs.ts index 53c3e8ca..4a44a30f 100644 --- a/modules/yargs/src/yargs.ts +++ b/modules/yargs/src/yargs.ts @@ -72,6 +72,7 @@ export const parserConfiguration = { 'camel-case-expansion': false, 'greedy-arrays': true, 'populate--': true, + // 'sort-commands': true, }; function fallbackHandler(argv: Arguments) { diff --git a/plugins/docgen/src/index.ts b/plugins/docgen/src/index.ts index 0cddcb97..7789cb89 100644 --- a/plugins/docgen/src/index.ts +++ b/plugins/docgen/src/index.ts @@ -99,6 +99,10 @@ export const docgen = (opts: Options = {}): Plugin => { outPath = workspace.resolve(outFile); } + if (command) { + process.env.ONEREPO_DOCGEN = command; + } + const parseStep = logger.createStep('Parse commands'); const docsYargs = new Yargs(parseStep); await internalSetup({ diff --git a/yarn.lock b/yarn.lock index a71a638f..847593b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -65,14 +65,7 @@ __metadata: languageName: node linkType: hard -"@astrojs/compiler@npm:^2.0.0, @astrojs/compiler@npm:^2.4.0": - version: 2.4.1 - resolution: "@astrojs/compiler@npm:2.4.1" - checksum: ab20f6197d22d6fd4ce96d8ac81e5052207e02f445bff1f7113676ded2da9b193b156f13e98557382b49718e96fa00fb4e749423c61d3d4e47f21c28fd4aad5e - languageName: node - linkType: hard - -"@astrojs/compiler@npm:^2.5.0": +"@astrojs/compiler@npm:^2.0.0, @astrojs/compiler@npm:^2.4.0, @astrojs/compiler@npm:^2.5.0": version: 2.5.1 resolution: "@astrojs/compiler@npm:2.5.1" checksum: 1e6ae8c19e106e06e9f9b7678c2f9f0a7b53459b903c17611b72154a365def78ea4e46f2677c8f5f80c48bca17f10225d3f5a6c0d4cb1b70d086652318142257 @@ -178,17 +171,7 @@ __metadata: languageName: node linkType: hard -"@astrojs/sitemap@npm:^3.0.4": - version: 3.0.4 - resolution: "@astrojs/sitemap@npm:3.0.4" - dependencies: - sitemap: ^7.1.1 - zod: ^3.22.4 - checksum: 047c387ca501de933b9edbbc2fce7718987333716ddadcaaa52c24cb37b269a691985956cc4d6b44b768a846b1c9ba724db62e87d76b00b4f79936fc172ac512 - languageName: node - linkType: hard - -"@astrojs/sitemap@npm:^3.0.5": +"@astrojs/sitemap@npm:^3.0.4, @astrojs/sitemap@npm:^3.0.5": version: 3.0.5 resolution: "@astrojs/sitemap@npm:3.0.5" dependencies: @@ -3172,6 +3155,7 @@ __metadata: "@internal/tsconfig": "workspace:^" "@internal/vitest-config": "workspace:^" "@onerepo/package-manager": 0.5.0 + defaults: ^3.0.0 glob: ^10.1.0 graph-data-structure: ^3.2.0 jiti: ^1.21.0 @@ -4574,6 +4558,13 @@ __metadata: languageName: node linkType: hard +"@types/yargs-unparser@npm:^2.0.3": + version: 2.0.3 + resolution: "@types/yargs-unparser@npm:2.0.3" + checksum: 97e1a62be17b1307e86a18d422331f3155c3dd3a2d2adb84903722e68db9426374a4cdf5b331030c64d3ca5ed6d5675180451c7bb9e53b6cb99dc5f5bed178c5 + languageName: node + linkType: hard + "@types/yargs@npm:^15.0.0": version: 15.0.19 resolution: "@types/yargs@npm:15.0.19" @@ -6585,7 +6576,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.2.0": +"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -8017,6 +8008,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 + languageName: node + linkType: hard + "decimal.js@npm:^10.2.1": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" @@ -10086,6 +10084,15 @@ __metadata: languageName: node linkType: hard +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" + bin: + flat: cli.js + checksum: 12a1536ac746db74881316a181499a78ef953632ddd28050b7a3a43c62ef5462e3357c8c29d76072bb635f147f7a9a1f0c02efef6b4be28f8db62ceb3d5c7f5d + languageName: node + linkType: hard + "flatted@npm:^3.1.0": version: 3.2.7 resolution: "flatted@npm:3.2.7" @@ -16246,6 +16253,7 @@ __metadata: "@types/semver": ^7.5.6 "@types/unist": ^3.0.2 "@types/yargs": ^17.0.32 + "@types/yargs-unparser": ^2.0.3 ajv: ^8.12.0 ajv-errors: ^3.0.0 cjson: ^0.5.0 @@ -16262,6 +16270,8 @@ __metadata: semver: ^7.5.4 typescript: ^5.3.3 yargs: ^17.6.2 + yargs-parser: ^21.1.1 + yargs-unparser: ^2.0.0 bin: one: ./src/bin/one.ts onerepo: ./src/bin/one.ts @@ -19640,18 +19650,7 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.4": - version: 3.1.6 - resolution: "tar-stream@npm:3.1.6" - dependencies: - b4a: ^1.6.4 - fast-fifo: ^1.2.0 - streamx: ^2.15.0 - checksum: f3627f918581976e954ff03cb8d370551053796b82564f8c7ca8fac84c48e4d042026d0854fc222171a34ff9c682b72fae91be9c9b0a112d4c54f9e4f443e9c5 - languageName: node - linkType: hard - -"tar-stream@npm:^3.1.5": +"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.4, tar-stream@npm:^3.1.5": version: 3.1.7 resolution: "tar-stream@npm:3.1.7" dependencies: @@ -21693,6 +21692,18 @@ __metadata: languageName: node linkType: hard +"yargs-unparser@npm:^2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" + dependencies: + camelcase: ^6.0.0 + decamelize: ^4.0.0 + flat: ^5.0.2 + is-plain-obj: ^2.1.0 + checksum: 68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 + languageName: node + linkType: hard + "yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.0, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2"