bank20baht-cli 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/application/create.js +15 -4
- package/dist/cli.js +13 -7
- package/dist/domain/create-plan.js +4 -0
- package/dist/domain/form-dsl.js +3 -3
- package/package.json +3 -3
- package/templates/new/CLAUDE.md +3 -0
- package/templates/new/README.md.hbs +36 -0
- package/templates/new/_gitignore +9 -0
- package/templates/new/_husky/commit-msg +1 -0
- package/templates/new/_husky/pre-commit +1 -0
- package/templates/new/_husky/pre-push +1 -0
- package/templates/new/_mcp.json +9 -0
- package/templates/new/_prettierignore +3 -0
- package/templates/new/_prettierrc +6 -0
- package/templates/new/apps/_gitkeep +0 -0
- package/templates/new/commitlint.config.js +3 -0
- package/templates/new/eslint.config.ts +24 -0
- package/templates/new/knip.json +9 -0
- package/templates/new/package.json.hbs +37 -0
- package/templates/new/packages/_gitkeep +0 -0
- package/templates/new/scripts/dev.ts +54 -0
- package/templates/new/scripts/each.ts +25 -0
- package/templates/new/scripts/workspaces.ts +24 -0
- package/templates/new/tsconfig.base.json +16 -0
- package/templates/new/tsconfig.json +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# bank20baht-cli
|
|
2
|
+
|
|
3
|
+
Scaffolding CLI for bun workspaces and [bahtui](https://github.com/bank20baht/bahtui) form
|
|
4
|
+
projects. Ships the `bahtui` binary.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
bun add -g bank20baht-cli # or: npm i -g bank20baht-cli
|
|
8
|
+
bahtui --help
|
|
9
|
+
|
|
10
|
+
# one-off, no install
|
|
11
|
+
bunx bank20baht-cli new my-workspace
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Node ≥ 20.12 (or bun). `bun` and `git` must be on `PATH` for the post-scaffold
|
|
15
|
+
steps.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | What it does |
|
|
20
|
+
| ---------------------------------------- | ----------------------------------------------------------------- |
|
|
21
|
+
| `bahtui new <name>` | Bare bun workspace in `./<name>` — toolchain only, no apps |
|
|
22
|
+
| `bahtui create <name>` | Full DDD monorepo in `./<name>` — React + Elysia + Eden |
|
|
23
|
+
| `bahtui init` | Write `bahtui.json` into an existing repo (enables the generators) |
|
|
24
|
+
| `bahtui g model <domain>` | `libs/domain/<domain>/model` package |
|
|
25
|
+
| `bahtui g form <domain>/<name> --fields` | Engine-validated `FormDefinition` JSON inside that model package |
|
|
26
|
+
|
|
27
|
+
### `bahtui new <name>`
|
|
28
|
+
|
|
29
|
+
Angular-style project bootstrap. Generates a bun workspace with the shared toolchain and
|
|
30
|
+
nothing else — you add `apps/*` and `packages/*` yourself.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bahtui new acme
|
|
34
|
+
cd acme
|
|
35
|
+
bun run check # lint + format:check + knip + typecheck — green on the empty repo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
What you get:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
package.json workspaces apps/* + packages/*, lint-staged, check script
|
|
42
|
+
tsconfig.base.json strict, moduleResolution bundler, verbatimModuleSyntax
|
|
43
|
+
tsconfig.json typechecks scripts/ (extend the base from each workspace)
|
|
44
|
+
eslint.config.ts ESLint 10 + typescript-eslint + eslint-config-prettier
|
|
45
|
+
.prettierrc no semicolons, single quotes, printWidth 100
|
|
46
|
+
knip.json dead-code / unused-dependency checks
|
|
47
|
+
commitlint.config.js Conventional Commits
|
|
48
|
+
.husky/ pre-commit → lint-staged · commit-msg → commitlint · pre-push → bun run check
|
|
49
|
+
scripts/dev.ts runs every apps/*/ "dev" script in parallel with prefixed output
|
|
50
|
+
scripts/each.ts `bun run scripts/each.ts <script>` in every workspace that defines it
|
|
51
|
+
.mcp.json, CLAUDE.md zvec_grep code-search MCP wiring for Claude Code
|
|
52
|
+
apps/, packages/ empty (.gitkeep)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
By default the CLI runs `git init`, `bun install`, then makes the first commit — so the
|
|
56
|
+
husky hooks prove themselves before you touch anything. Flags:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
--skip-install (alias --no-install) skip bun install and the initial commit
|
|
60
|
+
--skip-git (alias --no-git) skip git init and the initial commit
|
|
61
|
+
--dry-run print the file list, write nothing
|
|
62
|
+
--force write into a non-empty directory
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Add a workspace by creating `apps/<name>/package.json` with `dev` / `build` / `typecheck`
|
|
66
|
+
scripts; `bun run dev`, `bun run build` and `bun run typecheck` pick it up automatically.
|
|
67
|
+
Add a matching entry to `knip.json` so dead-code checks cover it.
|
|
68
|
+
|
|
69
|
+
### `bahtui create <name>`
|
|
70
|
+
|
|
71
|
+
Same flags as `new`, but scaffolds a complete stack:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
apps/web React 19 + Vite
|
|
75
|
+
apps/api Elysia — exports `type App`, the Eden type anchor
|
|
76
|
+
libs/shared/eden treaty<App> — the one typed HTTP client
|
|
77
|
+
bahtui.json generator config (see below)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
bahtui create shop
|
|
82
|
+
cd shop
|
|
83
|
+
bun run dev # web on :5173, api on :3001
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `bahtui init`
|
|
87
|
+
|
|
88
|
+
Writes `bahtui.json` into the current directory so `bahtui g …` knows where things live.
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
bahtui init # frontend react, backend elysia
|
|
92
|
+
bahtui init --frontend none # no React feature slices
|
|
93
|
+
bahtui init --backend none # no Elysia plugins
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"apps": { "web": "apps/web", "api": "apps/api" },
|
|
99
|
+
"libsDir": "libs",
|
|
100
|
+
"frontend": "react",
|
|
101
|
+
"backend": "elysia",
|
|
102
|
+
"edenClient": "libs/shared/eden"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `bahtui g model <domain>`
|
|
107
|
+
|
|
108
|
+
Creates `<libsDir>/domain/<domain>/model` — a framework-free package for entities and form
|
|
109
|
+
JSON. `<domain>` is kebab-case.
|
|
110
|
+
|
|
111
|
+
### `bahtui g form <domain>/<name> --fields "<dsl>"`
|
|
112
|
+
|
|
113
|
+
Builds a `FormDefinition`, validates it with
|
|
114
|
+
[`bank20baht-validator`](https://www.npmjs.com/package/bank20baht-validator) (the same
|
|
115
|
+
engine the browser components run), and writes
|
|
116
|
+
`<libsDir>/domain/<domain>/model/src/<name>.form.json`.
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
bahtui g form ticket/create \
|
|
120
|
+
--fields "title:text:required,priority:dropdown:options=low|medium|high,qty:number:min=1"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Field DSL — comma-separated `key:type[:modifier…]`:
|
|
124
|
+
|
|
125
|
+
| Part | Values |
|
|
126
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `key` | `[a-z][a-z0-9_]*`; the label is derived (`due_date` → "Due Date") |
|
|
128
|
+
| `type` | `text` `textarea` `number` `email` `password` `dropdown` `multiselect` `checkbox` `radio` `toggle` `date` `file` |
|
|
129
|
+
| modifiers | `required` · `options=a\|b\|c` · `min=N` · `max=N` · `flag=NAME` |
|
|
130
|
+
|
|
131
|
+
Invalid DSL (unknown type, empty options, non-numeric `min`) fails before anything is
|
|
132
|
+
written.
|
|
133
|
+
|
|
134
|
+
## Global flags
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
--dry-run print what would be generated, write nothing
|
|
138
|
+
--force overwrite existing files / write into non-empty dirs
|
|
139
|
+
-h, --help
|
|
140
|
+
-v, --version
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## How it works
|
|
144
|
+
|
|
145
|
+
Every command produces a **blueprint** — plain data listing files and post-commands — then
|
|
146
|
+
adapters write it to disk and run the commands. `--dry-run` prints the blueprint. Template
|
|
147
|
+
files ending in `.hbs` get `{{name}}` substitution; everything else is copied verbatim, and a
|
|
148
|
+
leading `_` in a template filename becomes `.` (so `.gitignore` survives `npm pack`).
|
|
149
|
+
|
|
150
|
+
Generated file paths are confined to the target directory; a hostile `bahtui.json` cannot
|
|
151
|
+
write outside the project root.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { planCreate } from '../domain/create-plan.js';
|
|
1
|
+
import { planCreate, PRESET_TEMPLATE_SET } from '../domain/create-plan.js';
|
|
2
2
|
import { validateProjectName } from '../domain/naming.js';
|
|
3
3
|
export function runCreate(input, deps) {
|
|
4
4
|
const errors = validateProjectName(input.name);
|
|
@@ -7,7 +7,7 @@ export function runCreate(input, deps) {
|
|
|
7
7
|
deps.log.error(e);
|
|
8
8
|
throw new CreateError('Invalid project name.');
|
|
9
9
|
}
|
|
10
|
-
const blueprint = planCreate(input, deps.templates.load(
|
|
10
|
+
const blueprint = planCreate(input, deps.templates.load(PRESET_TEMPLATE_SET[input.preset]), (raw, data) => deps.renderer.render(raw, data));
|
|
11
11
|
if (input.dryRun) {
|
|
12
12
|
deps.log.info(`dry-run — would create in ${blueprint.root}:`);
|
|
13
13
|
for (const f of blueprint.files)
|
|
@@ -34,8 +34,19 @@ export function runCreate(input, deps) {
|
|
|
34
34
|
deps.log.info(` cd ${input.name}`);
|
|
35
35
|
if (!input.install)
|
|
36
36
|
deps.log.info(' bun install');
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
for (const line of NEXT_STEPS[input.preset])
|
|
38
|
+
deps.log.info(` ${line}`);
|
|
39
39
|
}
|
|
40
|
+
const NEXT_STEPS = {
|
|
41
|
+
full: [
|
|
42
|
+
'bun run dev # web :5173 + api :3001',
|
|
43
|
+
'bun run lint && bun run test && bun run build',
|
|
44
|
+
],
|
|
45
|
+
workspace: [
|
|
46
|
+
'mkdir -p apps/<name> && add a package.json with dev/build/typecheck scripts',
|
|
47
|
+
'bun run dev # runs every apps/*/ dev script in parallel',
|
|
48
|
+
'bun run check # lint + format:check + knip + typecheck',
|
|
49
|
+
],
|
|
50
|
+
};
|
|
40
51
|
export class CreateError extends Error {
|
|
41
52
|
}
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,9 @@ import { ShellRunner } from './infrastructure/shell-runner.js';
|
|
|
12
12
|
const HELP = `bahtui — bun-workspace DDD scaffolding (React + Elysia + Eden)
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
|
-
bahtui
|
|
15
|
+
bahtui new <name> [options] bare bun workspace in ./<name> — toolchain only
|
|
16
|
+
(eslint, prettier, knip, husky, commitlint), no apps
|
|
17
|
+
bahtui create <name> [options] full monorepo in ./<name> (React + Elysia + Eden)
|
|
16
18
|
bahtui init [options] write bahtui.json into an existing repo
|
|
17
19
|
bahtui g model <domain> libs/domain/<d>/model package
|
|
18
20
|
bahtui g form <domain>/<name> --fields "key:type:mod,…"
|
|
@@ -25,8 +27,9 @@ Options:
|
|
|
25
27
|
--frontend <react|none> init: default react
|
|
26
28
|
--backend <elysia|none> init: default elysia
|
|
27
29
|
--dry-run print what would be generated, write nothing
|
|
28
|
-
--no-install create: skip "bun install" (also skips the initial commit)
|
|
29
|
-
--no-git create: skip git init + initial commit
|
|
30
|
+
--no-install new/create: skip "bun install" (also skips the initial commit)
|
|
31
|
+
--no-git new/create: skip git init + initial commit
|
|
32
|
+
--skip-install / --skip-git Angular-style aliases of the two above
|
|
30
33
|
--force overwrite existing files/dirs
|
|
31
34
|
-h, --help this help
|
|
32
35
|
-v, --version print version
|
|
@@ -50,6 +53,8 @@ const KNOWN_FLAGS = new Set([
|
|
|
50
53
|
'--force',
|
|
51
54
|
'--no-install',
|
|
52
55
|
'--no-git',
|
|
56
|
+
'--skip-install',
|
|
57
|
+
'--skip-git',
|
|
53
58
|
]);
|
|
54
59
|
/**
|
|
55
60
|
* Value-carrying flags pulled out before positional parsing. Supports both
|
|
@@ -116,16 +121,17 @@ function main(rawArgv) {
|
|
|
116
121
|
log,
|
|
117
122
|
};
|
|
118
123
|
try {
|
|
119
|
-
if (command === 'create') {
|
|
124
|
+
if (command === 'create' || command === 'new') {
|
|
120
125
|
if (!name) {
|
|
121
|
-
log.error(
|
|
126
|
+
log.error(`${command} needs a project name: bahtui ${command} <name>`);
|
|
122
127
|
return 1;
|
|
123
128
|
}
|
|
124
129
|
runCreate({
|
|
125
130
|
name,
|
|
131
|
+
preset: command === 'new' ? 'workspace' : 'full',
|
|
126
132
|
targetDir: resolve(process.cwd(), name),
|
|
127
|
-
install: !flags.has('--no-install'),
|
|
128
|
-
git: !flags.has('--no-git'),
|
|
133
|
+
install: !flags.has('--no-install') && !flags.has('--skip-install'),
|
|
134
|
+
git: !flags.has('--no-git') && !flags.has('--skip-git'),
|
|
129
135
|
dryRun,
|
|
130
136
|
force,
|
|
131
137
|
}, {
|
package/dist/domain/form-dsl.js
CHANGED
|
@@ -91,9 +91,9 @@ export function parseFieldsDsl(dsl) {
|
|
|
91
91
|
/** DSL -> FormDefinition, engine-validated. Throws FormDslError on any problem. */
|
|
92
92
|
export function buildFormDefinition(domain, name, dsl) {
|
|
93
93
|
const form = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
form_id: `${domain}-${name}`,
|
|
95
|
+
form_name: titleCase(name.replace(/-/g, '_')).replace(/ /g, '_'),
|
|
96
|
+
fields: parseFieldsDsl(dsl),
|
|
97
97
|
};
|
|
98
98
|
try {
|
|
99
99
|
return parseSchema(JSON.stringify(form));
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bank20baht-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Scaffold bun
|
|
5
|
+
"description": "Scaffold bun workspaces (bahtui new) and DDD monorepos (React + Elysia + Eden, bahtui create) plus bahtui generators.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Nattapong Promthong",
|
|
8
8
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"test:watch": "vitest"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"bank20baht-validator": "^0.
|
|
46
|
+
"bank20baht-validator": "^0.2.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^22",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
Bun-workspace monorepo scaffolded by [`bank20baht-cli`](https://github.com/bank20baht/bahtui)
|
|
4
|
+
(`bahtui new`). Toolchain only — add your own apps under `apps/*` and shared
|
|
5
|
+
libraries under `packages/*`.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun install
|
|
11
|
+
bun run dev # runs every apps/*/package.json "dev" script in parallel
|
|
12
|
+
bun run check # lint + format:check + knip + typecheck (also the pre-push hook)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
apps/<name> runnable applications (each with its own package.json)
|
|
19
|
+
packages/<name> shared libraries consumed by apps via workspace:*
|
|
20
|
+
scripts/dev.ts parallel dev runner with prefixed output (apps/* only)
|
|
21
|
+
scripts/each.ts `bun run scripts/each.ts <script>` in every workspace that has it
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Add a workspace by creating `apps/<name>/package.json` (or `packages/<name>`)
|
|
25
|
+
with `dev`, `build` and `typecheck` scripts, then extend `knip.json` with a
|
|
26
|
+
matching workspace entry so dead-code checks cover it.
|
|
27
|
+
|
|
28
|
+
## Toolchain
|
|
29
|
+
|
|
30
|
+
- **ESLint 10 + typescript-eslint** (`eslint.config.ts`), **Prettier** (no semi,
|
|
31
|
+
single quotes, width 100), **knip** — all run by `bun run check`.
|
|
32
|
+
- **husky** hooks: `pre-commit` → lint-staged, `commit-msg` → commitlint
|
|
33
|
+
(Conventional Commits), `pre-push` → `bun run check`.
|
|
34
|
+
- `tsconfig.base.json` — strict, `moduleResolution: bundler`,
|
|
35
|
+
`verbatimModuleSyntax`, `noUncheckedIndexedAccess`. Extend it from each
|
|
36
|
+
workspace's `tsconfig.json`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bunx commitlint --edit "$1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bunx lint-staged
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bun run check
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import prettier from 'eslint-config-prettier'
|
|
3
|
+
import tseslint from 'typescript-eslint'
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
{
|
|
7
|
+
ignores: ['**/dist/**', '**/node_modules/**', '**/*.gen.ts', '**/.angular/**'],
|
|
8
|
+
},
|
|
9
|
+
js.configs.recommended,
|
|
10
|
+
...tseslint.configs.recommended,
|
|
11
|
+
{
|
|
12
|
+
languageOptions: {
|
|
13
|
+
parserOptions: { tsconfigRootDir: import.meta.dirname },
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
'@typescript-eslint/no-unused-vars': [
|
|
17
|
+
'error',
|
|
18
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
|
19
|
+
],
|
|
20
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
prettier,
|
|
24
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"workspaces": ["apps/*", "packages/*"],
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun run scripts/dev.ts",
|
|
8
|
+
"build": "bun run scripts/each.ts build",
|
|
9
|
+
"typecheck": "tsc -p tsconfig.json && bun run scripts/each.ts typecheck",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"lint:fix": "eslint . --fix",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check .",
|
|
14
|
+
"knip": "knip",
|
|
15
|
+
"check": "bun run lint && bun run format:check && bun run knip && bun run typecheck",
|
|
16
|
+
"prepare": "husky"
|
|
17
|
+
},
|
|
18
|
+
"lint-staged": {
|
|
19
|
+
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
20
|
+
"*.{json,md,html,css}": ["prettier --write"]
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@commitlint/cli": "^21.2.2",
|
|
24
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
25
|
+
"@eslint/js": "^10.0.1",
|
|
26
|
+
"@types/bun": "^1.3.0",
|
|
27
|
+
"eslint": "^10.10.0",
|
|
28
|
+
"eslint-config-prettier": "^10.1.8",
|
|
29
|
+
"husky": "^9.1.7",
|
|
30
|
+
"jiti": "^2.6.1",
|
|
31
|
+
"knip": "^6.35.1",
|
|
32
|
+
"lint-staged": "^17.5.1",
|
|
33
|
+
"prettier": "^3.9.6",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"typescript-eslint": "^8.70.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run every `apps/*` workspace `dev` script in parallel with prefixed,
|
|
3
|
+
* live-streamed output. Replaces `bun run --filter '*' dev`, which serializes
|
|
4
|
+
* long-running scripts and never starts the second one.
|
|
5
|
+
*/
|
|
6
|
+
import { workspacesWithScript } from './workspaces.ts'
|
|
7
|
+
|
|
8
|
+
const colors = ['\x1b[36m', '\x1b[35m', '\x1b[33m', '\x1b[32m', '\x1b[34m']
|
|
9
|
+
const reset = '\x1b[0m'
|
|
10
|
+
|
|
11
|
+
const targets = workspacesWithScript('dev', ['apps']).map((ws, i) => ({
|
|
12
|
+
...ws,
|
|
13
|
+
color: colors[i % colors.length],
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
if (targets.length === 0) {
|
|
17
|
+
console.log('No apps/*/package.json with a "dev" script yet — nothing to run.')
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const procs = targets.map(({ name, cwd, color }) => {
|
|
22
|
+
const proc = Bun.spawn(['bun', 'run', 'dev'], {
|
|
23
|
+
cwd,
|
|
24
|
+
env: process.env,
|
|
25
|
+
stdout: 'pipe',
|
|
26
|
+
stderr: 'pipe',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const prefix = (line: string) => `${color}[${name}]${reset} ${line}`
|
|
30
|
+
const pump = async (stream: ReadableStream<Uint8Array>) => {
|
|
31
|
+
const decoder = new TextDecoder()
|
|
32
|
+
let buf = ''
|
|
33
|
+
for await (const chunk of stream) {
|
|
34
|
+
buf += decoder.decode(chunk, { stream: true })
|
|
35
|
+
const lines = buf.split('\n')
|
|
36
|
+
buf = lines.pop() ?? ''
|
|
37
|
+
for (const line of lines) console.log(prefix(line))
|
|
38
|
+
}
|
|
39
|
+
if (buf) console.log(prefix(buf))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pump(proc.stdout)
|
|
43
|
+
pump(proc.stderr)
|
|
44
|
+
return proc
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const shutdown = () => {
|
|
48
|
+
for (const p of procs) p.kill()
|
|
49
|
+
process.exit(0)
|
|
50
|
+
}
|
|
51
|
+
process.on('SIGINT', shutdown)
|
|
52
|
+
process.on('SIGTERM', shutdown)
|
|
53
|
+
|
|
54
|
+
await Promise.all(procs.map((p) => p.exited))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun run scripts/each.ts <script>` — run `<script>` in every apps/* and
|
|
3
|
+
* packages/* workspace that defines it, one after another, failing on the
|
|
4
|
+
* first non-zero exit. Unlike `bun run --filter '*' <script>` it exits 0 when
|
|
5
|
+
* no workspace defines the script yet, so a fresh repo passes `bun run check`.
|
|
6
|
+
*/
|
|
7
|
+
import { workspacesWithScript } from './workspaces.ts'
|
|
8
|
+
|
|
9
|
+
const script = process.argv[2]
|
|
10
|
+
if (!script) {
|
|
11
|
+
console.error('usage: bun run scripts/each.ts <script>')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const targets = workspacesWithScript(script)
|
|
16
|
+
if (targets.length === 0) {
|
|
17
|
+
console.log(`No workspace defines "${script}" yet — nothing to run.`)
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const { name, cwd } of targets) {
|
|
22
|
+
console.log(`\x1b[36m[${name}]\x1b[0m bun run ${script}`)
|
|
23
|
+
const proc = Bun.spawnSync(['bun', 'run', script], { cwd, env: process.env, stdio: ['inherit', 'inherit', 'inherit'] })
|
|
24
|
+
if (proc.exitCode !== 0) process.exit(proc.exitCode ?? 1)
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface Workspace {
|
|
5
|
+
name: string
|
|
6
|
+
cwd: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Every `apps/*` and `packages/*` directory whose package.json defines `script`. */
|
|
10
|
+
export function workspacesWithScript(script: string, groups = ['apps', 'packages']): Workspace[] {
|
|
11
|
+
const found: Workspace[] = []
|
|
12
|
+
for (const group of groups) {
|
|
13
|
+
if (!existsSync(group)) continue
|
|
14
|
+
for (const entry of readdirSync(group, { withFileTypes: true })) {
|
|
15
|
+
if (!entry.isDirectory()) continue
|
|
16
|
+
const cwd = join(group, entry.name)
|
|
17
|
+
const pkgPath = join(cwd, 'package.json')
|
|
18
|
+
if (!existsSync(pkgPath)) continue
|
|
19
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { scripts?: Record<string, string> }
|
|
20
|
+
if (typeof pkg.scripts?.[script] === 'string') found.push({ name: entry.name, cwd })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return found
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"noEmit": true
|
|
15
|
+
}
|
|
16
|
+
}
|