bank20baht-cli 0.2.0 → 0.3.1
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 +156 -0
- package/dist/application/create.js +29 -20
- package/dist/args.js +48 -0
- package/dist/cli.js +86 -101
- package/dist/domain/form-dsl.js +45 -41
- package/package.json +4 -3
- package/templates/new/_dependency-cruiser.cjs +29 -0
- package/templates/new/_github/actions/setup/action.yml +15 -0
- package/templates/new/_github/workflows/ci.yml +122 -0
- package/templates/new/_gitleaks.toml +6 -0
- package/templates/new/_husky/pre-commit +7 -0
- package/templates/new/_husky/pre-push +1 -1
- package/templates/new/_jscpd.json +20 -0
- package/templates/new/_prettierignore +1 -0
- package/templates/new/eslint.config.ts.hbs +64 -0
- package/templates/new/knip.json +2 -1
- package/templates/new/package.json.hbs +24 -4
- package/templates/new/renovate.json +23 -0
- package/templates/new/scripts/dev.ts +2 -2
- package/templates/new/scripts/each.ts +6 -2
- package/templates/new/scripts/workspaces.ts +1 -1
- package/templates/new/tsconfig.json +1 -1
- package/templates/new/eslint.config.ts +0 -24
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
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 → gitleaks + lint-staged · commit-msg → commitlint · pre-push → bun run check:fast
|
|
49
|
+
.gitleaks.toml · .jscpd.json · .dependency-cruiser.cjs · renovate.json · .github/ (CI: lint, typecheck, knip/madge/deps, dup, test, build, gitleaks, audit, commitlint)
|
|
50
|
+
scripts/dev.ts runs every apps/*/ "dev" script in parallel with prefixed output
|
|
51
|
+
scripts/each.ts `bun run scripts/each.ts <script>` in every workspace that defines it
|
|
52
|
+
.mcp.json, CLAUDE.md zvec_grep code-search MCP wiring for Claude Code
|
|
53
|
+
apps/, packages/ empty (.gitkeep)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
By default the CLI runs `git init`, `bun install`, then makes the first commit — so the
|
|
57
|
+
husky hooks prove themselves before you touch anything. Flags:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
--skip-install (alias --no-install) skip bun install and the initial commit
|
|
61
|
+
--skip-git (alias --no-git) skip git init and the initial commit
|
|
62
|
+
--dry-run print the file list, write nothing
|
|
63
|
+
--force write into a non-empty directory
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Add a workspace by creating `apps/<name>/package.json` with `dev` / `build` / `typecheck`
|
|
67
|
+
scripts; `bun run dev`, `bun run build` and `bun run typecheck` pick it up automatically.
|
|
68
|
+
Add a matching entry to `knip.json` so dead-code checks cover it.
|
|
69
|
+
|
|
70
|
+
### `bahtui create <name>`
|
|
71
|
+
|
|
72
|
+
Same flags as `new`, but scaffolds a complete stack:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
apps/web React 19 + Vite
|
|
76
|
+
apps/api Elysia — exports `type App`, the Eden type anchor
|
|
77
|
+
libs/shared/eden treaty<App> — the one typed HTTP client
|
|
78
|
+
bahtui.json generator config (see below)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
bahtui create shop
|
|
83
|
+
cd shop
|
|
84
|
+
bun run dev # web on :5173, api on :3001
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `bahtui init`
|
|
88
|
+
|
|
89
|
+
Writes `bahtui.json` into the current directory so `bahtui g …` knows where things live.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
bahtui init # frontend react, backend elysia
|
|
93
|
+
bahtui init --frontend none # no React feature slices
|
|
94
|
+
bahtui init --backend none # no Elysia plugins
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"apps": { "web": "apps/web", "api": "apps/api" },
|
|
100
|
+
"libsDir": "libs",
|
|
101
|
+
"frontend": "react",
|
|
102
|
+
"backend": "elysia",
|
|
103
|
+
"edenClient": "libs/shared/eden"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `bahtui g model <domain>`
|
|
108
|
+
|
|
109
|
+
Creates `<libsDir>/domain/<domain>/model` — a framework-free package for entities and form
|
|
110
|
+
JSON. `<domain>` is kebab-case.
|
|
111
|
+
|
|
112
|
+
### `bahtui g form <domain>/<name> --fields "<dsl>"`
|
|
113
|
+
|
|
114
|
+
Builds a `FormDefinition`, validates it with
|
|
115
|
+
[`bank20baht-validator`](https://www.npmjs.com/package/bank20baht-validator) (the same
|
|
116
|
+
engine the browser components run), and writes
|
|
117
|
+
`<libsDir>/domain/<domain>/model/src/<name>.form.json`.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
bahtui g form ticket/create \
|
|
121
|
+
--fields "title:text:required,priority:dropdown:options=low|medium|high,qty:number:min=1"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Field DSL — comma-separated `key:type[:modifier…]`:
|
|
125
|
+
|
|
126
|
+
| Part | Values |
|
|
127
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------- |
|
|
128
|
+
| `key` | `[a-z][a-z0-9_]*`; the label is derived (`due_date` → "Due Date") |
|
|
129
|
+
| `type` | `text` `textarea` `number` `email` `password` `dropdown` `multiselect` `checkbox` `radio` `toggle` `date` `file` |
|
|
130
|
+
| modifiers | `required` · `options=a\|b\|c` · `min=N` · `max=N` · `flag=NAME` |
|
|
131
|
+
|
|
132
|
+
Invalid DSL (unknown type, empty options, non-numeric `min`) fails before anything is
|
|
133
|
+
written.
|
|
134
|
+
|
|
135
|
+
## Global flags
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
--dry-run print what would be generated, write nothing
|
|
139
|
+
--force overwrite existing files / write into non-empty dirs
|
|
140
|
+
-h, --help
|
|
141
|
+
-v, --version
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## How it works
|
|
145
|
+
|
|
146
|
+
Every command produces a **blueprint** — plain data listing files and post-commands — then
|
|
147
|
+
adapters write it to disk and run the commands. `--dry-run` prints the blueprint. Template
|
|
148
|
+
files ending in `.hbs` get `{{name}}` substitution; everything else is copied verbatim, and a
|
|
149
|
+
leading `_` in a template filename becomes `.` (so `.gitignore` survives `npm pack`).
|
|
150
|
+
|
|
151
|
+
Generated file paths are confined to the target directory; a hostile `bahtui.json` cannot
|
|
152
|
+
write outside the project root.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { planCreate, PRESET_TEMPLATE_SET } from '../domain/create-plan.js';
|
|
2
2
|
import { validateProjectName } from '../domain/naming.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (input.dryRun) {
|
|
12
|
-
deps.log.info(`dry-run — would create in ${blueprint.root}:`);
|
|
13
|
-
for (const f of blueprint.files)
|
|
14
|
-
deps.log.info(` ${f.path}`);
|
|
15
|
-
for (const c of blueprint.postCommands)
|
|
16
|
-
deps.log.info(` $ ${c.cmd} ${c.args.join(' ')}`);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
deps.writer.ensureUsableDir(blueprint.root, input.force);
|
|
20
|
-
deps.writer.writeFiles(blueprint);
|
|
21
|
-
deps.log.step(`${blueprint.files.length} files written to ${blueprint.root}`);
|
|
3
|
+
function logDryRun(blueprint, deps) {
|
|
4
|
+
deps.log.info(`dry-run — would create in ${blueprint.root}:`);
|
|
5
|
+
for (const f of blueprint.files)
|
|
6
|
+
deps.log.info(` ${f.path}`);
|
|
7
|
+
for (const c of blueprint.postCommands)
|
|
8
|
+
deps.log.info(` $ ${c.cmd} ${c.args.join(' ')}`);
|
|
9
|
+
}
|
|
10
|
+
function runPostCommands(blueprint, input, deps) {
|
|
22
11
|
for (const c of blueprint.postCommands) {
|
|
23
12
|
deps.log.step(c.label);
|
|
24
13
|
try {
|
|
@@ -29,6 +18,8 @@ export function runCreate(input, deps) {
|
|
|
29
18
|
throw new CreateError(`${err instanceof Error ? err.message : String(err)}\nproject files are written — cd ${input.name} and rerun: ${retry}`);
|
|
30
19
|
}
|
|
31
20
|
}
|
|
21
|
+
}
|
|
22
|
+
function logNextSteps(input, deps) {
|
|
32
23
|
deps.log.info('');
|
|
33
24
|
deps.log.info(`Done. Next:`);
|
|
34
25
|
deps.log.info(` cd ${input.name}`);
|
|
@@ -37,6 +28,24 @@ export function runCreate(input, deps) {
|
|
|
37
28
|
for (const line of NEXT_STEPS[input.preset])
|
|
38
29
|
deps.log.info(` ${line}`);
|
|
39
30
|
}
|
|
31
|
+
export function runCreate(input, deps) {
|
|
32
|
+
const errors = validateProjectName(input.name);
|
|
33
|
+
if (errors.length > 0) {
|
|
34
|
+
for (const e of errors)
|
|
35
|
+
deps.log.error(e);
|
|
36
|
+
throw new CreateError('Invalid project name.');
|
|
37
|
+
}
|
|
38
|
+
const blueprint = planCreate(input, deps.templates.load(PRESET_TEMPLATE_SET[input.preset]), (raw, data) => deps.renderer.render(raw, data));
|
|
39
|
+
if (input.dryRun) {
|
|
40
|
+
logDryRun(blueprint, deps);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
deps.writer.ensureUsableDir(blueprint.root, input.force);
|
|
44
|
+
deps.writer.writeFiles(blueprint);
|
|
45
|
+
deps.log.step(`${blueprint.files.length} files written to ${blueprint.root}`);
|
|
46
|
+
runPostCommands(blueprint, input, deps);
|
|
47
|
+
logNextSteps(input, deps);
|
|
48
|
+
}
|
|
40
49
|
const NEXT_STEPS = {
|
|
41
50
|
full: [
|
|
42
51
|
'bun run dev # web :5173 + api :3001',
|
|
@@ -45,7 +54,7 @@ const NEXT_STEPS = {
|
|
|
45
54
|
workspace: [
|
|
46
55
|
'mkdir -p apps/<name> && add a package.json with dev/build/typecheck scripts',
|
|
47
56
|
'bun run dev # runs every apps/*/ dev script in parallel',
|
|
48
|
-
'bun run check # lint + format:check + knip +
|
|
57
|
+
'bun run check # lint + format:check + typecheck + madge + deps + knip + dup + build + test',
|
|
49
58
|
],
|
|
50
59
|
};
|
|
51
60
|
export class CreateError extends Error {
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Boolean flags recognized anywhere on the command line (no value). */
|
|
2
|
+
export const KNOWN_FLAGS = new Set([
|
|
3
|
+
'-v',
|
|
4
|
+
'--version',
|
|
5
|
+
'-h',
|
|
6
|
+
'--help',
|
|
7
|
+
'--dry-run',
|
|
8
|
+
'--force',
|
|
9
|
+
'--no-install',
|
|
10
|
+
'--no-git',
|
|
11
|
+
'--skip-install',
|
|
12
|
+
'--skip-git',
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Value-carrying flags pulled out before positional parsing. Supports both
|
|
16
|
+
* `--flag value` and `--flag=value`. If the token after `--flag value` looks
|
|
17
|
+
* like another flag (starts with `-`), the option is treated as missing
|
|
18
|
+
* rather than swallowing that flag as the value.
|
|
19
|
+
*/
|
|
20
|
+
export function takeOption(argv, flag) {
|
|
21
|
+
const eqPrefix = `${flag}=`;
|
|
22
|
+
const eqIndex = argv.findIndex((a) => a.startsWith(eqPrefix));
|
|
23
|
+
if (eqIndex !== -1) {
|
|
24
|
+
const value = argv[eqIndex].slice(eqPrefix.length);
|
|
25
|
+
argv.splice(eqIndex, 1);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
const i = argv.indexOf(flag);
|
|
29
|
+
if (i === -1)
|
|
30
|
+
return undefined;
|
|
31
|
+
const value = argv[i + 1];
|
|
32
|
+
if (value === undefined || value.startsWith('-')) {
|
|
33
|
+
argv.splice(i, 1);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
argv.splice(i, 2);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
export function parseArgs(rawArgv) {
|
|
40
|
+
const argv = [...rawArgv];
|
|
41
|
+
const fields = takeOption(argv, '--fields');
|
|
42
|
+
const frontend = takeOption(argv, '--frontend');
|
|
43
|
+
const backend = takeOption(argv, '--backend');
|
|
44
|
+
const args = argv.filter((a) => !a.startsWith('-'));
|
|
45
|
+
const flags = new Set(argv.filter((a) => a.startsWith('-')));
|
|
46
|
+
const [command, name, sub] = args;
|
|
47
|
+
return { fields, frontend, backend, flags, command, name, sub };
|
|
48
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -9,11 +9,12 @@ import { TemplateRenderer } from './infrastructure/template-renderer.js';
|
|
|
9
9
|
import { DiskFileWriter } from './infrastructure/fs-writer.js';
|
|
10
10
|
import { DiskFileReader } from './infrastructure/fs-reader.js';
|
|
11
11
|
import { ShellRunner } from './infrastructure/shell-runner.js';
|
|
12
|
+
import { KNOWN_FLAGS, parseArgs } from './args.js';
|
|
12
13
|
const HELP = `bahtui — bun-workspace DDD scaffolding (React + Elysia + Eden)
|
|
13
14
|
|
|
14
15
|
Usage:
|
|
15
16
|
bahtui new <name> [options] bare bun workspace in ./<name> — toolchain only
|
|
16
|
-
(eslint, prettier, knip, husky, commitlint), no apps
|
|
17
|
+
(eslint, prettier, knip, husky, commitlint, gitleaks, jscpd, depcruise, CI), no apps
|
|
17
18
|
bahtui create <name> [options] full monorepo in ./<name> (React + Elysia + Eden)
|
|
18
19
|
bahtui init [options] write bahtui.json into an existing repo
|
|
19
20
|
bahtui g model <domain> libs/domain/<d>/model package
|
|
@@ -39,56 +40,18 @@ function version() {
|
|
|
39
40
|
return JSON.parse(readFileSync(pkg, 'utf8')).version;
|
|
40
41
|
}
|
|
41
42
|
const log = {
|
|
42
|
-
info: (m) =>
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
info: (m) => {
|
|
44
|
+
console.log(m);
|
|
45
|
+
},
|
|
46
|
+
step: (m) => {
|
|
47
|
+
console.log(`• ${m}`);
|
|
48
|
+
},
|
|
49
|
+
error: (m) => {
|
|
50
|
+
console.error(`✖ ${m}`);
|
|
51
|
+
},
|
|
45
52
|
};
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
'-v',
|
|
49
|
-
'--version',
|
|
50
|
-
'-h',
|
|
51
|
-
'--help',
|
|
52
|
-
'--dry-run',
|
|
53
|
-
'--force',
|
|
54
|
-
'--no-install',
|
|
55
|
-
'--no-git',
|
|
56
|
-
'--skip-install',
|
|
57
|
-
'--skip-git',
|
|
58
|
-
]);
|
|
59
|
-
/**
|
|
60
|
-
* Value-carrying flags pulled out before positional parsing. Supports both
|
|
61
|
-
* `--flag value` and `--flag=value`. If the token after `--flag value` looks
|
|
62
|
-
* like another flag (starts with `-`), the option is treated as missing
|
|
63
|
-
* rather than swallowing that flag as the value.
|
|
64
|
-
*/
|
|
65
|
-
function takeOption(argv, flag) {
|
|
66
|
-
const eqPrefix = `${flag}=`;
|
|
67
|
-
const eqIndex = argv.findIndex((a) => a.startsWith(eqPrefix));
|
|
68
|
-
if (eqIndex !== -1) {
|
|
69
|
-
const value = argv[eqIndex].slice(eqPrefix.length);
|
|
70
|
-
argv.splice(eqIndex, 1);
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
const i = argv.indexOf(flag);
|
|
74
|
-
if (i === -1)
|
|
75
|
-
return undefined;
|
|
76
|
-
const value = argv[i + 1];
|
|
77
|
-
if (value === undefined || value.startsWith('-')) {
|
|
78
|
-
argv.splice(i, 1);
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
argv.splice(i, 2);
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
function main(rawArgv) {
|
|
85
|
-
const argv = [...rawArgv];
|
|
86
|
-
const fields = takeOption(argv, '--fields');
|
|
87
|
-
const frontend = takeOption(argv, '--frontend');
|
|
88
|
-
const backend = takeOption(argv, '--backend');
|
|
89
|
-
const args = argv.filter((a) => !a.startsWith('-'));
|
|
90
|
-
const flags = new Set(argv.filter((a) => a.startsWith('-')));
|
|
91
|
-
const [command, name, sub] = args;
|
|
53
|
+
/** Handles unknown flags, --version and --help; null = carry on. */
|
|
54
|
+
function preflight({ flags }) {
|
|
92
55
|
const unknownFlags = [...flags].filter((f) => !KNOWN_FLAGS.has(f));
|
|
93
56
|
if (unknownFlags.length > 0) {
|
|
94
57
|
log.error(`Unknown flag${unknownFlags.length > 1 ? 's' : ''}: ${unknownFlags.join(', ')}`);
|
|
@@ -108,67 +71,89 @@ function main(rawArgv) {
|
|
|
108
71
|
log.info(HELP);
|
|
109
72
|
return 0;
|
|
110
73
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function commandCreate(a, command) {
|
|
77
|
+
if (!a.name) {
|
|
78
|
+
log.error(`${command} needs a project name: bahtui ${command} <name>`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
runCreate({
|
|
82
|
+
name: a.name,
|
|
83
|
+
preset: command === 'new' ? 'workspace' : 'full',
|
|
84
|
+
targetDir: resolve(process.cwd(), a.name),
|
|
85
|
+
install: !a.flags.has('--no-install') && !a.flags.has('--skip-install'),
|
|
86
|
+
git: !a.flags.has('--no-git') && !a.flags.has('--skip-git'),
|
|
87
|
+
dryRun: a.flags.has('--dry-run'),
|
|
88
|
+
force: a.flags.has('--force'),
|
|
89
|
+
}, {
|
|
90
|
+
templates: new DiskTemplateSource(),
|
|
91
|
+
renderer: new TemplateRenderer(),
|
|
92
|
+
writer: new DiskFileWriter(),
|
|
93
|
+
runner: new ShellRunner(),
|
|
94
|
+
log,
|
|
95
|
+
});
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
function commandInit(a, generateDeps) {
|
|
99
|
+
runInit({
|
|
100
|
+
root: process.cwd(),
|
|
101
|
+
frontend: a.frontend === 'none' ? null : 'react',
|
|
102
|
+
backend: a.backend === 'none' ? null : 'elysia',
|
|
103
|
+
dryRun: a.flags.has('--dry-run'),
|
|
104
|
+
force: a.flags.has('--force'),
|
|
105
|
+
}, generateDeps);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
function commandGenerate(a, generateDeps) {
|
|
109
|
+
const { name, sub } = a;
|
|
110
|
+
if (name !== 'model' && name !== 'form') {
|
|
111
|
+
log.error('Unknown generator — bahtui g model <domain> | g form <domain>/<name>.');
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
if (!sub) {
|
|
115
|
+
log.error(`g ${name} needs a target: bahtui g ${name} <${name === 'model' ? 'domain' : 'domain/name'}>`);
|
|
114
116
|
return 1;
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
runGenerate({
|
|
119
|
+
root: process.cwd(),
|
|
120
|
+
kind: name,
|
|
121
|
+
target: sub,
|
|
122
|
+
fields: a.fields,
|
|
123
|
+
dryRun: a.flags.has('--dry-run'),
|
|
124
|
+
force: a.flags.has('--force'),
|
|
125
|
+
}, generateDeps);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
function dispatch(a, command) {
|
|
118
129
|
const generateDeps = {
|
|
119
130
|
reader: new DiskFileReader(),
|
|
120
131
|
writer: new DiskFileWriter(),
|
|
121
132
|
log,
|
|
122
133
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
writer: new DiskFileWriter(),
|
|
141
|
-
runner: new ShellRunner(),
|
|
142
|
-
log,
|
|
143
|
-
});
|
|
144
|
-
return 0;
|
|
145
|
-
}
|
|
146
|
-
if (command === 'init') {
|
|
147
|
-
runInit({
|
|
148
|
-
root: process.cwd(),
|
|
149
|
-
frontend: frontend === 'none' ? null : 'react',
|
|
150
|
-
backend: backend === 'none' ? null : 'elysia',
|
|
151
|
-
dryRun,
|
|
152
|
-
force,
|
|
153
|
-
}, generateDeps);
|
|
154
|
-
return 0;
|
|
155
|
-
}
|
|
156
|
-
if (command === 'g') {
|
|
157
|
-
if (name !== 'model' && name !== 'form') {
|
|
158
|
-
log.error('Unknown generator — bahtui g model <domain> | g form <domain>/<name>.');
|
|
159
|
-
return 1;
|
|
160
|
-
}
|
|
161
|
-
if (!sub) {
|
|
162
|
-
log.error(`g ${name} needs a target: bahtui g ${name} <${name === 'model' ? 'domain' : 'domain/name'}>`);
|
|
163
|
-
return 1;
|
|
164
|
-
}
|
|
165
|
-
runGenerate({ root: process.cwd(), kind: name, target: sub, fields, dryRun, force }, generateDeps);
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
log.error(`Unknown command "${command}".`);
|
|
134
|
+
if (command === 'create' || command === 'new')
|
|
135
|
+
return commandCreate(a, command);
|
|
136
|
+
if (command === 'init')
|
|
137
|
+
return commandInit(a, generateDeps);
|
|
138
|
+
if (command === 'g')
|
|
139
|
+
return commandGenerate(a, generateDeps);
|
|
140
|
+
log.error(`Unknown command "${command}".`);
|
|
141
|
+
log.error(HELP);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
function main(rawArgv) {
|
|
145
|
+
const parsed = parseArgs(rawArgv);
|
|
146
|
+
const early = preflight(parsed);
|
|
147
|
+
if (early !== null)
|
|
148
|
+
return early;
|
|
149
|
+
if (parsed.command === undefined) {
|
|
150
|
+
log.error('No command given — see usage below.');
|
|
169
151
|
log.error(HELP);
|
|
170
152
|
return 1;
|
|
171
153
|
}
|
|
154
|
+
try {
|
|
155
|
+
return dispatch(parsed, parsed.command);
|
|
156
|
+
}
|
|
172
157
|
catch (err) {
|
|
173
158
|
log.error(err instanceof CreateError || err instanceof GenerateError ? err.message : String(err));
|
|
174
159
|
return 1;
|
package/dist/domain/form-dsl.js
CHANGED
|
@@ -37,51 +37,55 @@ function parseNumberMod(name, raw, part) {
|
|
|
37
37
|
}
|
|
38
38
|
return n;
|
|
39
39
|
}
|
|
40
|
+
function parseOptionsMod(mod, part) {
|
|
41
|
+
const opts = mod
|
|
42
|
+
.slice('options='.length)
|
|
43
|
+
.split('|')
|
|
44
|
+
.map((o) => o.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
if (opts.length === 0) {
|
|
47
|
+
throw new FormDslError(`options modifier needs at least one value in "${part}".`);
|
|
48
|
+
}
|
|
49
|
+
if (new Set(opts).size !== opts.length) {
|
|
50
|
+
throw new FormDslError(`Duplicate option keys in "${part}".`);
|
|
51
|
+
}
|
|
52
|
+
return opts.map((o) => ({ key: o, label: titleCase(o) }));
|
|
53
|
+
}
|
|
54
|
+
function applyModifier(field, mod, part) {
|
|
55
|
+
if (mod === 'required')
|
|
56
|
+
field.required = true;
|
|
57
|
+
else if (mod.startsWith('options='))
|
|
58
|
+
field.options = parseOptionsMod(mod, part);
|
|
59
|
+
else if (mod.startsWith('min='))
|
|
60
|
+
field.min = parseNumberMod('min', mod.slice(4), part);
|
|
61
|
+
else if (mod.startsWith('max='))
|
|
62
|
+
field.max = parseNumberMod('max', mod.slice(4), part);
|
|
63
|
+
else if (mod.startsWith('flag='))
|
|
64
|
+
field.flag = mod.slice(5);
|
|
65
|
+
else
|
|
66
|
+
throw new FormDslError(`Unknown modifier ${JSON.stringify(mod)} in "${part}".`);
|
|
67
|
+
}
|
|
68
|
+
function parseFieldPart(part, order) {
|
|
69
|
+
const [key, type, ...mods] = part.split(':').map((s) => s.trim());
|
|
70
|
+
if (!key || !KEY_PATTERN.test(key)) {
|
|
71
|
+
throw new FormDslError(`Bad field key ${JSON.stringify(key ?? '')} in "${part}".`);
|
|
72
|
+
}
|
|
73
|
+
if (!type || !FIELD_TYPES.has(type)) {
|
|
74
|
+
throw new FormDslError(`Unknown field type ${JSON.stringify(type ?? '')} in "${part}" — one of: ${[...FIELD_TYPES].join(', ')}.`);
|
|
75
|
+
}
|
|
76
|
+
const field = { order, type, key, label: titleCase(key) };
|
|
77
|
+
for (const mod of mods)
|
|
78
|
+
applyModifier(field, mod, part);
|
|
79
|
+
return field;
|
|
80
|
+
}
|
|
40
81
|
export function parseFieldsDsl(dsl) {
|
|
41
|
-
const fields = [];
|
|
42
82
|
const parts = dsl
|
|
43
83
|
.split(',')
|
|
44
84
|
.map((p) => p.trim())
|
|
45
85
|
.filter(Boolean);
|
|
46
86
|
if (parts.length === 0)
|
|
47
87
|
throw new FormDslError('No fields given — pass --fields "key:type,…".');
|
|
48
|
-
|
|
49
|
-
const [key, type, ...mods] = part.split(':').map((s) => s.trim());
|
|
50
|
-
if (!key || !KEY_PATTERN.test(key)) {
|
|
51
|
-
throw new FormDslError(`Bad field key ${JSON.stringify(key ?? '')} in "${part}".`);
|
|
52
|
-
}
|
|
53
|
-
if (!type || !FIELD_TYPES.has(type)) {
|
|
54
|
-
throw new FormDslError(`Unknown field type ${JSON.stringify(type ?? '')} in "${part}" — one of: ${[...FIELD_TYPES].join(', ')}.`);
|
|
55
|
-
}
|
|
56
|
-
const field = { order: i + 1, type, key, label: titleCase(key) };
|
|
57
|
-
for (const mod of mods) {
|
|
58
|
-
if (mod === 'required')
|
|
59
|
-
field.required = true;
|
|
60
|
-
else if (mod.startsWith('options=')) {
|
|
61
|
-
const opts = mod
|
|
62
|
-
.slice('options='.length)
|
|
63
|
-
.split('|')
|
|
64
|
-
.map((o) => o.trim())
|
|
65
|
-
.filter(Boolean);
|
|
66
|
-
if (opts.length === 0) {
|
|
67
|
-
throw new FormDslError(`options modifier needs at least one value in "${part}".`);
|
|
68
|
-
}
|
|
69
|
-
if (new Set(opts).size !== opts.length) {
|
|
70
|
-
throw new FormDslError(`Duplicate option keys in "${part}".`);
|
|
71
|
-
}
|
|
72
|
-
field.options = opts.map((o) => ({ key: o, label: titleCase(o) }));
|
|
73
|
-
}
|
|
74
|
-
else if (mod.startsWith('min='))
|
|
75
|
-
field.min = parseNumberMod('min', mod.slice(4), part);
|
|
76
|
-
else if (mod.startsWith('max='))
|
|
77
|
-
field.max = parseNumberMod('max', mod.slice(4), part);
|
|
78
|
-
else if (mod.startsWith('flag='))
|
|
79
|
-
field.flag = mod.slice(5);
|
|
80
|
-
else
|
|
81
|
-
throw new FormDslError(`Unknown modifier ${JSON.stringify(mod)} in "${part}".`);
|
|
82
|
-
}
|
|
83
|
-
fields.push(field);
|
|
84
|
-
}
|
|
88
|
+
const fields = parts.map((part, i) => parseFieldPart(part, i + 1));
|
|
85
89
|
const keys = fields.map((f) => f.key);
|
|
86
90
|
if (new Set(keys).size !== keys.length) {
|
|
87
91
|
throw new FormDslError('Field keys must be unique.');
|
|
@@ -91,9 +95,9 @@ export function parseFieldsDsl(dsl) {
|
|
|
91
95
|
/** DSL -> FormDefinition, engine-validated. Throws FormDslError on any problem. */
|
|
92
96
|
export function buildFormDefinition(domain, name, dsl) {
|
|
93
97
|
const form = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
form_id: `${domain}-${name}`,
|
|
99
|
+
form_name: titleCase(name.replace(/-/g, '_')).replace(/ /g, '_'),
|
|
100
|
+
fields: parseFieldsDsl(dsl),
|
|
97
101
|
};
|
|
98
102
|
try {
|
|
99
103
|
return parseSchema(JSON.stringify(form));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bank20baht-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Scaffold bun workspaces (bahtui new) and DDD monorepos (React + Elysia + Eden, bahtui create) plus bahtui generators.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|
|
42
42
|
"test": "vitest run",
|
|
43
|
-
"test:watch": "vitest"
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"test:coverage": "vitest run --coverage"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"bank20baht-validator": "^0.
|
|
47
|
+
"bank20baht-validator": "^0.3.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/node": "^22",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture boundaries, checked with `bun run deps`. Add a rule per layer
|
|
3
|
+
* rule you want enforced (features may not import each other, shared/ knows no
|
|
4
|
+
* feature, ...). Every rule is `error`: a violation is a decision, not a warning.
|
|
5
|
+
*/
|
|
6
|
+
const TESTS = '\\.(test|spec)\\.tsx?$'
|
|
7
|
+
|
|
8
|
+
/** @type {import('dependency-cruiser').IConfiguration} */
|
|
9
|
+
module.exports = {
|
|
10
|
+
forbidden: [
|
|
11
|
+
{
|
|
12
|
+
name: 'no-circular',
|
|
13
|
+
severity: 'error',
|
|
14
|
+
from: {},
|
|
15
|
+
to: { circular: true },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'production-not-tests',
|
|
19
|
+
severity: 'error',
|
|
20
|
+
from: { path: '^(apps|packages)/[^/]+/src/', pathNot: TESTS },
|
|
21
|
+
to: { path: TESTS },
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
options: {
|
|
25
|
+
doNotFollow: { path: 'node_modules' },
|
|
26
|
+
tsPreCompilationDeps: true,
|
|
27
|
+
exclude: { path: ['/dist/', '\\.gen\\.ts$'] },
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Setup
|
|
2
|
+
description: Bun at the version pinned in package.json, with the install cache restored, and dependencies installed.
|
|
3
|
+
runs:
|
|
4
|
+
using: composite
|
|
5
|
+
steps:
|
|
6
|
+
- uses: oven-sh/setup-bun@v2
|
|
7
|
+
with:
|
|
8
|
+
bun-version-file: package.json
|
|
9
|
+
- uses: actions/cache@v4
|
|
10
|
+
with:
|
|
11
|
+
path: ~/.bun/install/cache
|
|
12
|
+
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
|
13
|
+
restore-keys: bun-${{ runner.os }}-
|
|
14
|
+
- run: bun install --frozen-lockfile
|
|
15
|
+
shell: bash
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: lint · prettier
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 10
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: ./.github/actions/setup
|
|
24
|
+
- run: bun run lint
|
|
25
|
+
- run: bun run format:check
|
|
26
|
+
|
|
27
|
+
typecheck:
|
|
28
|
+
name: typecheck
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
timeout-minutes: 10
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: ./.github/actions/setup
|
|
34
|
+
- run: bun run typecheck
|
|
35
|
+
|
|
36
|
+
structure:
|
|
37
|
+
name: knip · madge · boundaries
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
timeout-minutes: 10
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: ./.github/actions/setup
|
|
43
|
+
- run: bun run knip
|
|
44
|
+
- run: bun run madge
|
|
45
|
+
- run: bun run deps
|
|
46
|
+
|
|
47
|
+
dup:
|
|
48
|
+
name: duplication
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
timeout-minutes: 10
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- uses: ./.github/actions/setup
|
|
54
|
+
- run: bun run dup -- --reporters console,json --output jscpd-report
|
|
55
|
+
- uses: actions/upload-artifact@v4
|
|
56
|
+
if: failure()
|
|
57
|
+
with:
|
|
58
|
+
name: jscpd-report
|
|
59
|
+
path: jscpd-report
|
|
60
|
+
retention-days: 7
|
|
61
|
+
|
|
62
|
+
test:
|
|
63
|
+
name: test
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
timeout-minutes: 10
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: ./.github/actions/setup
|
|
69
|
+
- run: bun run test
|
|
70
|
+
- uses: actions/upload-artifact@v4
|
|
71
|
+
if: failure()
|
|
72
|
+
with:
|
|
73
|
+
name: coverage
|
|
74
|
+
path: '**/coverage'
|
|
75
|
+
retention-days: 7
|
|
76
|
+
|
|
77
|
+
build:
|
|
78
|
+
name: build
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
timeout-minutes: 10
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/checkout@v4
|
|
83
|
+
- uses: ./.github/actions/setup
|
|
84
|
+
- run: bun run build
|
|
85
|
+
|
|
86
|
+
gitleaks:
|
|
87
|
+
name: gitleaks
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
timeout-minutes: 5
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
with:
|
|
93
|
+
fetch-depth: 0
|
|
94
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
95
|
+
env:
|
|
96
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
97
|
+
|
|
98
|
+
audit:
|
|
99
|
+
name: dependency audit
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
timeout-minutes: 5
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
- uses: ./.github/actions/setup
|
|
105
|
+
# The npm advisory endpoint answers 503 now and then; retry before giving
|
|
106
|
+
# up, and never block a merge on it - a red audit is a warning to look at.
|
|
107
|
+
- run: |
|
|
108
|
+
for i in 1 2 3; do bun run audit && exit 0; echo "audit attempt $i failed"; sleep 15; done
|
|
109
|
+
exit 1
|
|
110
|
+
continue-on-error: true
|
|
111
|
+
|
|
112
|
+
commitlint:
|
|
113
|
+
name: commitlint
|
|
114
|
+
if: github.event_name == 'pull_request'
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
timeout-minutes: 5
|
|
117
|
+
steps:
|
|
118
|
+
- uses: actions/checkout@v4
|
|
119
|
+
with:
|
|
120
|
+
fetch-depth: 0
|
|
121
|
+
- uses: ./.github/actions/setup
|
|
122
|
+
- run: bunx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
# Secret scan of what is about to be committed. CI runs the same scan over the
|
|
2
|
+
# whole history, so a machine without gitleaks only loses the early warning.
|
|
3
|
+
if command -v gitleaks >/dev/null 2>&1; then
|
|
4
|
+
gitleaks git --pre-commit --staged --redact --no-banner || exit 1
|
|
5
|
+
else
|
|
6
|
+
echo "gitleaks is not installed - skipping the local secret scan (CI still runs it). brew install gitleaks" >&2
|
|
7
|
+
fi
|
|
1
8
|
bunx lint-staged
|
|
@@ -1 +1 @@
|
|
|
1
|
-
bun run check
|
|
1
|
+
bun run check:fast
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"path": ["apps", "packages", "scripts"],
|
|
3
|
+
"format": ["typescript", "markup", "css"],
|
|
4
|
+
"ignore": [
|
|
5
|
+
"**/node_modules/**",
|
|
6
|
+
"**/dist/**",
|
|
7
|
+
"**/coverage/**",
|
|
8
|
+
"**/test-results/**",
|
|
9
|
+
"**/playwright-report/**",
|
|
10
|
+
"**/*.spec.ts",
|
|
11
|
+
"**/*.test.ts",
|
|
12
|
+
"**/*.d.ts",
|
|
13
|
+
"**/*.gen.ts"
|
|
14
|
+
],
|
|
15
|
+
"minTokens": 50,
|
|
16
|
+
"minLines": 5,
|
|
17
|
+
"gitignore": true,
|
|
18
|
+
"reporters": ["console"],
|
|
19
|
+
"threshold": 5
|
|
20
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import prettier from 'eslint-config-prettier'
|
|
3
|
+
import sonarjs from 'eslint-plugin-sonarjs'
|
|
4
|
+
import { defineConfig } from 'eslint/config'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
|
|
7
|
+
export default defineConfig([
|
|
8
|
+
{
|
|
9
|
+
ignores: [
|
|
10
|
+
'**/dist/**',
|
|
11
|
+
'**/node_modules/**',
|
|
12
|
+
'**/*.gen.ts',
|
|
13
|
+
'**/.angular/**',
|
|
14
|
+
'**/coverage/**',
|
|
15
|
+
'**/playwright-report/**',
|
|
16
|
+
'**/test-results/**',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
// Plain JS config files: no type information available.
|
|
20
|
+
{
|
|
21
|
+
files: ['**/*.{js,mjs,cjs}'],
|
|
22
|
+
extends: [js.configs.recommended],
|
|
23
|
+
},
|
|
24
|
+
// TypeScript, type-aware. Every .ts file must be covered by a tsconfig.
|
|
25
|
+
{
|
|
26
|
+
files: ['**/*.{ts,tsx}'],
|
|
27
|
+
extends: [
|
|
28
|
+
js.configs.recommended,
|
|
29
|
+
...tseslint.configs.strictTypeChecked,
|
|
30
|
+
...tseslint.configs.stylisticTypeChecked,
|
|
31
|
+
],
|
|
32
|
+
languageOptions: {
|
|
33
|
+
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
'@typescript-eslint/no-unused-vars': [
|
|
37
|
+
'error',
|
|
38
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
|
39
|
+
],
|
|
40
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
41
|
+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
42
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
43
|
+
'@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
// Duplication and complexity (jscpd covers whole-block clones, see .jscpd.json).
|
|
47
|
+
{
|
|
48
|
+
files: ['**/*.{ts,tsx}'],
|
|
49
|
+
plugins: { sonarjs },
|
|
50
|
+
rules: {
|
|
51
|
+
'sonarjs/no-identical-functions': 'error',
|
|
52
|
+
'sonarjs/no-identical-expressions': 'error',
|
|
53
|
+
'sonarjs/cognitive-complexity': ['error', 15],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// Repeated literals are ordinary in tests, so the rule only guards production code.
|
|
57
|
+
{
|
|
58
|
+
files: ['**/*.{ts,tsx}'],
|
|
59
|
+
ignores: ['**/*.test.ts', '**/*.spec.ts'],
|
|
60
|
+
plugins: { sonarjs },
|
|
61
|
+
rules: { 'sonarjs/no-duplicate-string': ['error', { threshold: 4 }] },
|
|
62
|
+
},
|
|
63
|
+
prettier,
|
|
64
|
+
])
|
package/templates/new/knip.json
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
"name": "{{name}}",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"workspaces": [
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"apps/*",
|
|
7
|
+
"packages/*"
|
|
8
|
+
],
|
|
6
9
|
"scripts": {
|
|
7
10
|
"dev": "bun run scripts/dev.ts",
|
|
8
11
|
"build": "bun run scripts/each.ts build",
|
|
@@ -12,24 +15,41 @@
|
|
|
12
15
|
"format": "prettier --write .",
|
|
13
16
|
"format:check": "prettier --check .",
|
|
14
17
|
"knip": "knip",
|
|
15
|
-
"
|
|
18
|
+
"madge": "madge --circular --extensions ts,tsx apps packages scripts",
|
|
19
|
+
"deps": "depcruise apps packages scripts --config .dependency-cruiser.cjs",
|
|
20
|
+
"dup": "jscpd",
|
|
21
|
+
"audit": "bun audit --audit-level=high",
|
|
22
|
+
"secrets": "gitleaks git --redact --no-banner",
|
|
23
|
+
"test": "bun run scripts/each.ts test:coverage",
|
|
24
|
+
"check:fast": "bun run lint && bun run format:check && bun run typecheck && bun run madge && bun run deps",
|
|
25
|
+
"check": "bun run check:fast && bun run knip && bun run dup && bun run build && bun run test",
|
|
16
26
|
"prepare": "husky"
|
|
17
27
|
},
|
|
18
28
|
"lint-staged": {
|
|
19
|
-
"*.{ts,tsx}": [
|
|
20
|
-
|
|
29
|
+
"*.{ts,tsx}": [
|
|
30
|
+
"eslint --fix",
|
|
31
|
+
"prettier --write"
|
|
32
|
+
],
|
|
33
|
+
"*.{json,md,html,css}": [
|
|
34
|
+
"prettier --write"
|
|
35
|
+
]
|
|
21
36
|
},
|
|
37
|
+
"packageManager": "bun@1.3.14",
|
|
22
38
|
"devDependencies": {
|
|
23
39
|
"@commitlint/cli": "^21.2.2",
|
|
24
40
|
"@commitlint/config-conventional": "^21.2.2",
|
|
25
41
|
"@eslint/js": "^10.0.1",
|
|
26
42
|
"@types/bun": "^1.3.0",
|
|
43
|
+
"dependency-cruiser": "^18.3.1",
|
|
27
44
|
"eslint": "^10.10.0",
|
|
28
45
|
"eslint-config-prettier": "^10.1.8",
|
|
46
|
+
"eslint-plugin-sonarjs": "^4.2.1",
|
|
29
47
|
"husky": "^9.1.7",
|
|
30
48
|
"jiti": "^2.6.1",
|
|
49
|
+
"jscpd": "^5.3.0",
|
|
31
50
|
"knip": "^6.35.1",
|
|
32
51
|
"lint-staged": "^17.5.1",
|
|
52
|
+
"madge": "^8.0.0",
|
|
33
53
|
"prettier": "^3.9.6",
|
|
34
54
|
"typescript": "^5.9.3",
|
|
35
55
|
"typescript-eslint": "^8.70.0"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended", ":semanticCommitTypeAll(chore)"],
|
|
4
|
+
"schedule": ["before 6am on monday"],
|
|
5
|
+
"prConcurrentLimit": 4,
|
|
6
|
+
"packageRules": [
|
|
7
|
+
{
|
|
8
|
+
"description": "Everything not matched below: one weekly PR.",
|
|
9
|
+
"matchPackageNames": ["*"],
|
|
10
|
+
"groupName": "others"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"description": "Dev tooling: linters, formatters, test runners, type packages.",
|
|
14
|
+
"matchDepTypes": ["devDependencies"],
|
|
15
|
+
"groupName": "dev-deps"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"description": "GitHub Actions move together.",
|
|
19
|
+
"matchManagers": ["github-actions"],
|
|
20
|
+
"groupName": "github-actions"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -20,6 +20,10 @@ if (targets.length === 0) {
|
|
|
20
20
|
|
|
21
21
|
for (const { name, cwd } of targets) {
|
|
22
22
|
console.log(`\x1b[36m[${name}]\x1b[0m bun run ${script}`)
|
|
23
|
-
const proc = Bun.spawnSync(['bun', 'run', script], {
|
|
24
|
-
|
|
23
|
+
const proc = Bun.spawnSync(['bun', 'run', script], {
|
|
24
|
+
cwd,
|
|
25
|
+
env: process.env,
|
|
26
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
27
|
+
})
|
|
28
|
+
if (proc.exitCode !== 0) process.exit(proc.exitCode)
|
|
25
29
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
)
|