Skip to content

Commit a478c37

Browse files
authored
feat: add pnpm run play playground picker (#118)
1 parent f8d8f48 commit a478c37

4 files changed

Lines changed: 140 additions & 0 deletions

File tree

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"scripts": {
1717
"build": "turbo run build --concurrency=3",
1818
"watch": "pnpm -r run watch",
19+
"play": "tsx scripts/play.ts",
1920
"docs": "pnpm -C docs run docs",
2021
"docs:build": "pnpm -C docs run docs:build",
2122
"docs:serve": "pnpm -C docs run docs:serve",
@@ -37,10 +38,12 @@
3738
"@antfu/utils": "catalog:inlined",
3839
"@playwright/test": "catalog:testing",
3940
"@types/node": "catalog:types",
41+
"@types/prompts": "catalog:types",
4042
"@types/ws": "catalog:types",
4143
"bumpp": "catalog:tooling",
4244
"eslint": "catalog:tooling",
4345
"nano-staged": "catalog:tooling",
46+
"prompts": "catalog:tooling",
4447
"simple-git-hooks": "catalog:tooling",
4548
"skills-npm": "catalog:tooling",
4649
"tsdown": "catalog:build",

pnpm-lock.yaml

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,13 @@ catalogs:
137137
bumpp: ^11.1.0
138138
eslint: ^10.7.0
139139
nano-staged: ^1.0.2
140+
prompts: ^2.4.2
140141
simple-git-hooks: ^2.13.1
141142
skills-npm: ^1.2.0
142143
typescript: ^6.0.3
143144
types:
144145
'@types/node': ^26.1.1
146+
'@types/prompts': ^2.4.9
145147
'@types/react': ^19.2.17
146148
'@types/react-dom': ^19.2.3
147149
'@types/ws': ^8.18.1

scripts/play.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { Choice } from 'prompts'
2+
import { spawnSync } from 'node:child_process'
3+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
4+
import { dirname, join, resolve } from 'node:path'
5+
import process from 'node:process'
6+
import { fileURLToPath } from 'node:url'
7+
import prompts from 'prompts'
8+
9+
/**
10+
* Workspace globs, mirrored from `pnpm-workspace.yaml`'s `packages:` list
11+
* (same mirroring rationale as `scripts/verify-typecheck-coverage.ts`): a
12+
* new example, plugin, or other playground shows up here for free, with no
13+
* change to this script.
14+
*/
15+
const WORKSPACE_PATTERNS = ['packages/*', 'plugins/*', 'examples/*', 'storybook', 'docs']
16+
17+
/**
18+
* Script names that make a workspace package runnable as a "play" — the
19+
* first one present in a package's `scripts` wins.
20+
*/
21+
const RUN_SCRIPTS = ['dev', 'storybook', 'docs', 'start']
22+
23+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
24+
25+
interface Play {
26+
dir: string
27+
pkgName: string
28+
script: string
29+
}
30+
31+
function expandPattern(pattern: string): string[] {
32+
if (!pattern.endsWith('/*'))
33+
return [pattern]
34+
const base = pattern.slice(0, -2)
35+
const baseDir = join(rootDir, base)
36+
if (!existsSync(baseDir))
37+
return []
38+
return readdirSync(baseDir, { withFileTypes: true })
39+
.filter(entry => entry.isDirectory())
40+
.map(entry => `${base}/${entry.name}`)
41+
}
42+
43+
function findPlay(dir: string): Play | undefined {
44+
const pkgPath = join(rootDir, dir, 'package.json')
45+
if (!existsSync(pkgPath))
46+
return undefined
47+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
48+
const script = RUN_SCRIPTS.find(name => pkg.scripts?.[name])
49+
if (!script)
50+
return undefined
51+
return { dir, pkgName: pkg.name, script }
52+
}
53+
54+
function suggest(input: string, choices: Choice[]): Promise<Choice[]> {
55+
const needle = input.toLowerCase()
56+
return Promise.resolve(choices.filter(choice => choice.title.toLowerCase().includes(needle)))
57+
}
58+
59+
async function main(): Promise<void> {
60+
const plays = WORKSPACE_PATTERNS
61+
.flatMap(expandPattern)
62+
.map(findPlay)
63+
.filter((play): play is Play => play !== undefined)
64+
.sort((a, b) => a.dir.localeCompare(b.dir))
65+
66+
if (plays.length === 0) {
67+
console.error(`No playgrounds found — none of ${WORKSPACE_PATTERNS.join(', ')} has a package.json with a ${RUN_SCRIPTS.join('/')} script.`)
68+
process.exitCode = 1
69+
return
70+
}
71+
72+
const { play } = await prompts({
73+
type: 'autocomplete',
74+
name: 'play',
75+
message: 'Select a playground to run',
76+
choices: plays.map(p => ({
77+
title: p.dir,
78+
description: `${p.pkgName} · pnpm run ${p.script}`,
79+
value: p,
80+
})),
81+
suggest,
82+
}) as { play?: Play }
83+
84+
if (!play) {
85+
console.log('No playground selected.')
86+
return
87+
}
88+
89+
console.log(`\n▶ pnpm run ${play.script} (${play.dir})\n`)
90+
91+
const result = spawnSync('pnpm', ['run', play.script], {
92+
cwd: join(rootDir, play.dir),
93+
stdio: 'inherit',
94+
shell: false,
95+
})
96+
97+
process.exitCode = result.status ?? 1
98+
}
99+
100+
main()

0 commit comments

Comments
 (0)