bank20baht-cli 0.0.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.
Files changed (48) hide show
  1. package/dist/application/create.d.ts +16 -0
  2. package/dist/application/create.js +35 -0
  3. package/dist/application/ports.d.ts +23 -0
  4. package/dist/application/ports.js +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +75 -0
  7. package/dist/domain/blueprint.d.ts +23 -0
  8. package/dist/domain/blueprint.js +6 -0
  9. package/dist/domain/create-plan.d.ts +24 -0
  10. package/dist/domain/create-plan.js +44 -0
  11. package/dist/domain/naming.d.ts +2 -0
  12. package/dist/domain/naming.js +18 -0
  13. package/dist/infrastructure/fs-writer.d.ts +6 -0
  14. package/dist/infrastructure/fs-writer.js +20 -0
  15. package/dist/infrastructure/hbs-renderer.d.ts +4 -0
  16. package/dist/infrastructure/hbs-renderer.js +7 -0
  17. package/dist/infrastructure/shell-runner.d.ts +4 -0
  18. package/dist/infrastructure/shell-runner.js +11 -0
  19. package/dist/infrastructure/template-source.d.ts +7 -0
  20. package/dist/infrastructure/template-source.js +25 -0
  21. package/package.json +50 -0
  22. package/templates/create/README.md.hbs +32 -0
  23. package/templates/create/_github/workflows/ci.yml +56 -0
  24. package/templates/create/_gitignore +5 -0
  25. package/templates/create/_husky/pre-commit +3 -0
  26. package/templates/create/_prettierignore +3 -0
  27. package/templates/create/_prettierrc.json +4 -0
  28. package/templates/create/apps/api/package.json.hbs +21 -0
  29. package/templates/create/apps/api/src/index.test.ts.hbs +10 -0
  30. package/templates/create/apps/api/src/index.ts.hbs +14 -0
  31. package/templates/create/apps/api/tsconfig.json +7 -0
  32. package/templates/create/apps/web/index.html.hbs +12 -0
  33. package/templates/create/apps/web/package.json.hbs +23 -0
  34. package/templates/create/apps/web/src/App.tsx.hbs +20 -0
  35. package/templates/create/apps/web/src/health.test.ts.hbs +14 -0
  36. package/templates/create/apps/web/src/health.ts.hbs +6 -0
  37. package/templates/create/apps/web/src/main.tsx.hbs +9 -0
  38. package/templates/create/apps/web/src/vite-env.d.ts.hbs +1 -0
  39. package/templates/create/apps/web/tsconfig.json +8 -0
  40. package/templates/create/apps/web/vite.config.ts.hbs +6 -0
  41. package/templates/create/bahtui.json.hbs +7 -0
  42. package/templates/create/eslint.config.js +30 -0
  43. package/templates/create/knip.json +9 -0
  44. package/templates/create/libs/shared/eden/index.ts.hbs +9 -0
  45. package/templates/create/libs/shared/eden/package.json.hbs +15 -0
  46. package/templates/create/libs/shared/eden/tsconfig.json +4 -0
  47. package/templates/create/package.json.hbs +37 -0
  48. package/templates/create/tsconfig.base.json +17 -0
@@ -0,0 +1,16 @@
1
+ import { type CreateOptions } from '../domain/create-plan.js';
2
+ import type { CommandRunner, FileWriter, Logger, Renderer, TemplateSource } from './ports.js';
3
+ export interface CreateDeps {
4
+ templates: TemplateSource;
5
+ renderer: Renderer;
6
+ writer: FileWriter;
7
+ runner: CommandRunner;
8
+ log: Logger;
9
+ }
10
+ export interface CreateInput extends CreateOptions {
11
+ dryRun: boolean;
12
+ force: boolean;
13
+ }
14
+ export declare function runCreate(input: CreateInput, deps: CreateDeps): void;
15
+ export declare class CreateError extends Error {
16
+ }
@@ -0,0 +1,35 @@
1
+ import { planCreate } from '../domain/create-plan.js';
2
+ import { validateProjectName } from '../domain/naming.js';
3
+ export function runCreate(input, deps) {
4
+ const errors = validateProjectName(input.name);
5
+ if (errors.length > 0) {
6
+ for (const e of errors)
7
+ deps.log.error(e);
8
+ throw new CreateError('Invalid project name.');
9
+ }
10
+ const blueprint = planCreate(input, deps.templates.load('create'), (raw, data) => deps.renderer.render(raw, data));
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}`);
22
+ for (const c of blueprint.postCommands) {
23
+ deps.log.step(c.label);
24
+ deps.runner.run(c.cmd, c.args, blueprint.root);
25
+ }
26
+ deps.log.info('');
27
+ deps.log.info(`Done. Next:`);
28
+ deps.log.info(` cd ${input.name}`);
29
+ if (!input.install)
30
+ deps.log.info(' bun install');
31
+ deps.log.info(' bun run dev # web :5173 + api :3001');
32
+ deps.log.info(' bun run lint && bun run test && bun run build');
33
+ }
34
+ export class CreateError extends Error {
35
+ }
@@ -0,0 +1,23 @@
1
+ import type { Blueprint } from '../domain/blueprint.js';
2
+ import type { TemplateFile } from '../domain/create-plan.js';
3
+ /** Loads a template set (e.g. "create") shipped inside the CLI package. */
4
+ export interface TemplateSource {
5
+ load(set: string): TemplateFile[];
6
+ }
7
+ export interface Renderer {
8
+ render(raw: string, data: Record<string, unknown>): string;
9
+ }
10
+ export interface FileWriter {
11
+ /** Throws unless `dir` is missing/empty (or `force` is set). */
12
+ ensureUsableDir(dir: string, force: boolean): void;
13
+ writeFiles(blueprint: Blueprint): void;
14
+ }
15
+ export interface CommandRunner {
16
+ /** Runs to completion with inherited stdio; throws on non-zero exit. */
17
+ run(cmd: string, args: string[], cwd: string): void;
18
+ }
19
+ export interface Logger {
20
+ info(message: string): void;
21
+ step(message: string): void;
22
+ error(message: string): void;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runCreate, CreateError } from './application/create.js';
6
+ import { DiskTemplateSource } from './infrastructure/template-source.js';
7
+ import { HbsRenderer } from './infrastructure/hbs-renderer.js';
8
+ import { DiskFileWriter } from './infrastructure/fs-writer.js';
9
+ import { ShellRunner } from './infrastructure/shell-runner.js';
10
+ const HELP = `bahtui — bun-workspace DDD scaffolding (React + Elysia + Eden)
11
+
12
+ Usage:
13
+ bahtui create <name> [options] scaffold a new monorepo in ./<name>
14
+
15
+ Options:
16
+ --dry-run print what would be generated, write nothing
17
+ --no-install skip "bun install" (also skips the initial commit)
18
+ --no-git skip git init + initial commit
19
+ --force write into an existing non-empty directory
20
+ -h, --help this help
21
+ -v, --version print version
22
+ `;
23
+ function version() {
24
+ const pkg = fileURLToPath(new URL('../package.json', import.meta.url));
25
+ return JSON.parse(readFileSync(pkg, 'utf8')).version;
26
+ }
27
+ const log = {
28
+ info: (m) => console.log(m),
29
+ step: (m) => console.log(`• ${m}`),
30
+ error: (m) => console.error(`✖ ${m}`),
31
+ };
32
+ function main(argv) {
33
+ const args = argv.filter((a) => !a.startsWith('-'));
34
+ const flags = new Set(argv.filter((a) => a.startsWith('-')));
35
+ const [command, name] = args;
36
+ if (flags.has('-v') || flags.has('--version')) {
37
+ log.info(version());
38
+ return 0;
39
+ }
40
+ if (command === undefined || flags.has('-h') || flags.has('--help')) {
41
+ log.info(HELP);
42
+ return command === undefined && !flags.has('-h') && !flags.has('--help') ? 1 : 0;
43
+ }
44
+ if (command !== 'create') {
45
+ log.error(`Unknown command "${command}".`);
46
+ log.info(HELP);
47
+ return 1;
48
+ }
49
+ if (!name) {
50
+ log.error('create needs a project name: bahtui create <name>');
51
+ return 1;
52
+ }
53
+ try {
54
+ runCreate({
55
+ name,
56
+ targetDir: resolve(process.cwd(), name),
57
+ install: !flags.has('--no-install'),
58
+ git: !flags.has('--no-git'),
59
+ dryRun: flags.has('--dry-run'),
60
+ force: flags.has('--force'),
61
+ }, {
62
+ templates: new DiskTemplateSource(),
63
+ renderer: new HbsRenderer(),
64
+ writer: new DiskFileWriter(),
65
+ runner: new ShellRunner(),
66
+ log,
67
+ });
68
+ return 0;
69
+ }
70
+ catch (err) {
71
+ log.error(err instanceof CreateError ? err.message : String(err));
72
+ return 1;
73
+ }
74
+ }
75
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A Blueprint is WHAT will be generated, as plain data — no fs, no side
3
+ * effects. Adapters (fs-writer, shell-runner) make it real; --dry-run just
4
+ * prints it. This split is the testing seam for every generator.
5
+ */
6
+ export interface FileSpec {
7
+ /** Path relative to the blueprint root. */
8
+ path: string;
9
+ contents: string;
10
+ }
11
+ export interface PostCommand {
12
+ cmd: string;
13
+ args: string[];
14
+ /** Human-readable step label for progress output. */
15
+ label: string;
16
+ }
17
+ export interface Blueprint {
18
+ /** Absolute directory every FileSpec.path is relative to. */
19
+ root: string;
20
+ files: FileSpec[];
21
+ /** Commands to run inside `root`, in order, after files are written. */
22
+ postCommands: PostCommand[];
23
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A Blueprint is WHAT will be generated, as plain data — no fs, no side
3
+ * effects. Adapters (fs-writer, shell-runner) make it real; --dry-run just
4
+ * prints it. This split is the testing seam for every generator.
5
+ */
6
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { Blueprint } from './blueprint.js';
2
+ export interface CreateOptions {
3
+ name: string;
4
+ /** Absolute path of the directory to create the project in. */
5
+ targetDir: string;
6
+ install: boolean;
7
+ git: boolean;
8
+ }
9
+ /** A template file as loaded from disk by the infrastructure layer. */
10
+ export interface TemplateFile {
11
+ /** Path relative to the template set root, e.g. "apps/api/src/index.ts.hbs". */
12
+ relPath: string;
13
+ raw: string;
14
+ }
15
+ export type RenderFn = (raw: string, data: Record<string, unknown>) => string;
16
+ /**
17
+ * Template naming conventions (the whole contract between templates/ and here):
18
+ * - `*.hbs` → rendered with the data below, suffix stripped.
19
+ * anything else → copied verbatim (so files with `${{ }}` like ci.yml
20
+ * never meet handlebars).
21
+ * - leading `_` on a basename → `.` (npm pack silently drops/renames real
22
+ * .gitignore files inside packages, so templates ship `_gitignore`).
23
+ */
24
+ export declare function planCreate(options: CreateOptions, templates: TemplateFile[], render: RenderFn): Blueprint;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Template naming conventions (the whole contract between templates/ and here):
3
+ * - `*.hbs` → rendered with the data below, suffix stripped.
4
+ * anything else → copied verbatim (so files with `${{ }}` like ci.yml
5
+ * never meet handlebars).
6
+ * - leading `_` on a basename → `.` (npm pack silently drops/renames real
7
+ * .gitignore files inside packages, so templates ship `_gitignore`).
8
+ */
9
+ export function planCreate(options, templates, render) {
10
+ const data = { name: options.name };
11
+ const files = templates
12
+ .map((t) => {
13
+ const isTemplate = t.relPath.endsWith('.hbs');
14
+ const path = undot(isTemplate ? t.relPath.slice(0, -'.hbs'.length) : t.relPath);
15
+ const contents = isTemplate ? render(t.raw, data) : t.raw;
16
+ return { path, contents };
17
+ })
18
+ .sort((a, b) => (a.path < b.path ? -1 : 1)); // codepoint order — stable across locales
19
+ const postCommands = [];
20
+ if (options.git) {
21
+ postCommands.push({ cmd: 'git', args: ['init', '-b', 'main'], label: 'git init' });
22
+ }
23
+ if (options.install) {
24
+ // after git init so husky's prepare hook finds .git
25
+ postCommands.push({ cmd: 'bun', args: ['install'], label: 'bun install' });
26
+ }
27
+ if (options.git && options.install) {
28
+ // the first commit runs the freshly installed pre-commit hook — the
29
+ // generated repo proves its own toolchain before the user touches it
30
+ postCommands.push({ cmd: 'git', args: ['add', '-A'], label: 'git add' });
31
+ postCommands.push({
32
+ cmd: 'git',
33
+ args: ['commit', '-m', 'chore: scaffold with bank20baht-cli'],
34
+ label: 'git commit (pre-commit hook runs here)',
35
+ });
36
+ }
37
+ return { root: options.targetDir, files, postCommands };
38
+ }
39
+ function undot(relPath) {
40
+ return relPath
41
+ .split('/')
42
+ .map((part) => (part.startsWith('_') ? `.${part.slice(1)}` : part))
43
+ .join('/');
44
+ }
@@ -0,0 +1,2 @@
1
+ /** Project-name rules: must be usable as npm scope, directory, and package name. */
2
+ export declare function validateProjectName(name: string): string[];
@@ -0,0 +1,18 @@
1
+ /** Project-name rules: must be usable as npm scope, directory, and package name. */
2
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
3
+ const RESERVED = new Set(['node_modules', 'apps', 'libs', 'src', 'dist', 'test', 'bun']);
4
+ export function validateProjectName(name) {
5
+ const errors = [];
6
+ if (name.length === 0)
7
+ errors.push('Project name is required.');
8
+ else if (name.length > 64)
9
+ errors.push('Project name must be 64 characters or fewer.');
10
+ if (name.length > 0 && !NAME_PATTERN.test(name)) {
11
+ errors.push(`"${name}" is not a valid project name — use lowercase letters, digits and dashes, starting with a letter (it becomes the @${name}/* package scope).`);
12
+ }
13
+ if (RESERVED.has(name))
14
+ errors.push(`"${name}" is a reserved name.`);
15
+ if (name.endsWith('-'))
16
+ errors.push('Project name must not end with a dash.');
17
+ return errors;
18
+ }
@@ -0,0 +1,6 @@
1
+ import type { Blueprint } from '../domain/blueprint.js';
2
+ import type { FileWriter } from '../application/ports.js';
3
+ export declare class DiskFileWriter implements FileWriter {
4
+ ensureUsableDir(dir: string, force: boolean): void;
5
+ writeFiles(blueprint: Blueprint): void;
6
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ export class DiskFileWriter {
4
+ ensureUsableDir(dir, force) {
5
+ if (!existsSync(dir))
6
+ return;
7
+ if (readdirSync(dir).length === 0)
8
+ return;
9
+ if (force)
10
+ return;
11
+ throw new Error(`${dir} already exists and is not empty (use --force to write anyway).`);
12
+ }
13
+ writeFiles(blueprint) {
14
+ for (const file of blueprint.files) {
15
+ const abs = join(blueprint.root, file.path);
16
+ mkdirSync(dirname(abs), { recursive: true });
17
+ writeFileSync(abs, file.contents);
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ import type { Renderer } from '../application/ports.js';
2
+ export declare class HbsRenderer implements Renderer {
3
+ render(raw: string, data: Record<string, unknown>): string;
4
+ }
@@ -0,0 +1,7 @@
1
+ import Handlebars from 'handlebars';
2
+ export class HbsRenderer {
3
+ render(raw, data) {
4
+ // noEscape: these are code/config files, not HTML — quotes must survive.
5
+ return Handlebars.compile(raw, { noEscape: true, strict: true })(data);
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ import type { CommandRunner } from '../application/ports.js';
2
+ export declare class ShellRunner implements CommandRunner {
3
+ run(cmd: string, args: string[], cwd: string): void;
4
+ }
@@ -0,0 +1,11 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export class ShellRunner {
3
+ run(cmd, args, cwd) {
4
+ const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
5
+ if (result.error)
6
+ throw result.error;
7
+ if (result.status !== 0) {
8
+ throw new Error(`${cmd} ${args.join(' ')} exited with code ${result.status}`);
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { TemplateFile } from '../domain/create-plan.js';
2
+ import type { TemplateSource } from '../application/ports.js';
3
+ export declare class DiskTemplateSource implements TemplateSource {
4
+ private readonly root;
5
+ constructor(root?: string);
6
+ load(set: string): TemplateFile[];
7
+ }
@@ -0,0 +1,25 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** templates/ sits at the package root, next to dist/ — hence ../../ from here. */
5
+ const TEMPLATES_ROOT = fileURLToPath(new URL('../../templates', import.meta.url));
6
+ export class DiskTemplateSource {
7
+ constructor(root = TEMPLATES_ROOT) {
8
+ this.root = root;
9
+ }
10
+ load(set) {
11
+ const setRoot = join(this.root, set);
12
+ const entries = readdirSync(setRoot, { recursive: true, withFileTypes: true });
13
+ const files = [];
14
+ for (const entry of entries) {
15
+ if (!entry.isFile())
16
+ continue;
17
+ const abs = join(entry.parentPath, entry.name);
18
+ files.push({
19
+ relPath: relative(setRoot, abs).split(sep).join('/'),
20
+ raw: readFileSync(abs, 'utf8'),
21
+ });
22
+ }
23
+ return files;
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "bank20baht-cli",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Scaffold bun-workspace DDD monorepos (React + Elysia + Eden) and bahtui generators.",
6
+ "license": "MIT",
7
+ "author": "Nattapong Promthong",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/bank20baht/bahtui.git",
11
+ "directory": "packages/cli"
12
+ },
13
+ "homepage": "https://github.com/bank20baht/bahtui#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/bank20baht/bahtui/issues"
16
+ },
17
+ "keywords": [
18
+ "cli",
19
+ "scaffold",
20
+ "generator",
21
+ "ddd",
22
+ "bun",
23
+ "elysia",
24
+ "eden",
25
+ "react"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "bin": {
31
+ "bahtui": "./dist/cli.js"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "templates"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest"
41
+ },
42
+ "dependencies": {
43
+ "handlebars": "^4.7.8"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22",
47
+ "typescript": "^5.8.3",
48
+ "vitest": "^3.2.4"
49
+ }
50
+ }
@@ -0,0 +1,32 @@
1
+ # {{name}}
2
+
3
+ Bun-workspace monorepo scaffolded by [`bank20baht-cli`](https://github.com/bank20baht/bahtui) —
4
+ React + Elysia wired end-to-end with Eden (type-safe client, zero codegen).
5
+
6
+ ## Quickstart
7
+
8
+ ```sh
9
+ bun install
10
+ bun run dev # web on :5173, api on :3001
11
+ ```
12
+
13
+ ## Layout (DDD taxonomy)
14
+
15
+ ```
16
+ apps/web React 19 + Vite
17
+ apps/api Elysia — exports `type App`, the Eden type anchor
18
+ libs/shared/eden treaty<App> — the ONE typed http client
19
+ libs/domain/<d>/model (later) entities + form JSON, framework-free
20
+ libs/domain/<d>/api (later) Elysia plugin for the domain
21
+ libs/domain/<d>/feature-*(later) React feature slices
22
+ ```
23
+
24
+ Dependency direction: features and api libs depend on `model`; `model` depends
25
+ on nothing; the frontend never imports server code — types travel through Eden.
26
+
27
+ ## Toolchain
28
+
29
+ - `bun run lint` / `format:check` / `knip` / `deps:circular` — also run by CI
30
+ and by the pre-commit hook (lint-staged → knip → madge).
31
+ - `bun run test` — bun test in every app.
32
+ - `bun run build` — typecheck + vite build.
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ lint:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+ - uses: oven-sh/setup-bun@v2
21
+ with:
22
+ bun-version: latest
23
+ - name: Install deps
24
+ run: bun install --frozen-lockfile
25
+ - name: ESLint
26
+ run: bun run lint
27
+ - name: Prettier
28
+ run: bun run format:check
29
+ - name: Knip (dead code)
30
+ run: bun run knip
31
+ - name: Circular dependencies (madge)
32
+ run: bun run deps:circular
33
+
34
+ test:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v5
38
+ - uses: oven-sh/setup-bun@v2
39
+ with:
40
+ bun-version: latest
41
+ - name: Install deps
42
+ run: bun install --frozen-lockfile
43
+ - name: Test
44
+ run: bun run test
45
+
46
+ build:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v5
50
+ - uses: oven-sh/setup-bun@v2
51
+ with:
52
+ bun-version: latest
53
+ - name: Install deps
54
+ run: bun install --frozen-lockfile
55
+ - name: Build
56
+ run: bun run build
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ *.local
4
+ *.tsbuildinfo
5
+ .DS_Store
@@ -0,0 +1,3 @@
1
+ bunx lint-staged
2
+ bun run knip
3
+ bun run deps:circular
@@ -0,0 +1,3 @@
1
+ dist/
2
+ *.tsbuildinfo
3
+ bun.lock
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "printWidth": 100
4
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@{{name}}/api",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "scripts": {
9
+ "dev": "bun run --watch src/index.ts",
10
+ "test": "bun test",
11
+ "build": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@elysiajs/cors": "^1.3.0",
15
+ "elysia": "^1.3.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "^1.2.0",
19
+ "typescript": "^5.8.3"
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { app } from './index';
3
+
4
+ describe('GET /health', () => {
5
+ it('reports the service as ok', async () => {
6
+ const res = await app.handle(new Request('http://localhost/health'));
7
+ expect(res.status).toBe(200);
8
+ expect(await res.json()).toEqual({ status: 'ok', service: '{{name}}-api' });
9
+ });
10
+ });
@@ -0,0 +1,14 @@
1
+ import { Elysia } from 'elysia';
2
+ import { cors } from '@elysiajs/cors';
3
+
4
+ export const app = new Elysia()
5
+ .use(cors())
6
+ .get('/health', () => ({ status: 'ok', service: '{{name}}-api' }));
7
+
8
+ /** Eden anchor — the whole API's type surface, consumed by libs/shared/eden. */
9
+ export type App = typeof app;
10
+
11
+ if (import.meta.main) {
12
+ app.listen(3001);
13
+ console.log(`{{name}}-api listening on http://localhost:${app.server?.port}`);
14
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["bun"]
5
+ },
6
+ "include": ["src"]
7
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@{{name}}/web",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "test": "bun test",
8
+ "build": "tsc --noEmit && vite build"
9
+ },
10
+ "dependencies": {
11
+ "@{{name}}/eden": "workspace:*",
12
+ "react": "^19.1.0",
13
+ "react-dom": "^19.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/bun": "^1.2.0",
17
+ "@types/react": "^19.1.0",
18
+ "@types/react-dom": "^19.1.0",
19
+ "@vitejs/plugin-react": "^5.0.0",
20
+ "typescript": "^5.8.3",
21
+ "vite": "^7.0.0"
22
+ }
23
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { api } from '@{{name}}/eden';
3
+ import { describeHealth } from './health';
4
+
5
+ export default function App() {
6
+ const [status, setStatus] = useState('checking api…');
7
+
8
+ useEffect(() => {
9
+ api.health.get().then(({ data, error }) => {
10
+ setStatus(error ? `api unreachable (start it: bun run dev)` : describeHealth(data));
11
+ });
12
+ }, []);
13
+
14
+ return (
15
+ <main style={ { fontFamily: 'system-ui', padding: '2rem' } }>
16
+ <h1>{{name}}</h1>
17
+ <p>{status}</p>
18
+ </main>
19
+ );
20
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { describeHealth } from './health';
3
+
4
+ describe('describeHealth', () => {
5
+ it('formats a healthy response', () => {
6
+ expect(describeHealth({ status: 'ok', service: '{{name}}-api' })).toBe(
7
+ '{{name}}-api is ok',
8
+ );
9
+ });
10
+
11
+ it('handles a missing response', () => {
12
+ expect(describeHealth(null)).toBe('no response from api');
13
+ });
14
+ });
@@ -0,0 +1,6 @@
1
+ export type Health = { status: string; service: string } | null;
2
+
3
+ export function describeHealth(health: Health): string {
4
+ if (!health) return 'no response from api';
5
+ return `${health.service} is ${health.status}`;
6
+ }
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ createRoot(document.getElementById('root')!).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ );
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "types": ["vite/client", "bun"]
6
+ },
7
+ "include": ["src", "vite.config.ts"]
8
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "apps": { "web": "apps/web", "api": "apps/api" },
3
+ "libsDir": "libs",
4
+ "frontend": "react",
5
+ "backend": "elysia",
6
+ "edenClient": "libs/shared/eden"
7
+ }
@@ -0,0 +1,30 @@
1
+ import js from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import reactHooks from 'eslint-plugin-react-hooks';
4
+ import prettier from 'eslint-config-prettier';
5
+ import globals from 'globals';
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['**/dist/**', '**/node_modules/**'] },
9
+ js.configs.recommended,
10
+ ...tseslint.configs.recommended,
11
+ {
12
+ languageOptions: {
13
+ globals: { ...globals.browser, ...globals.node },
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
+ {
24
+ files: ['apps/web/**/*.{ts,tsx}'],
25
+ plugins: { 'react-hooks': reactHooks },
26
+ rules: reactHooks.configs.recommended.rules,
27
+ },
28
+ // must come last — disables rules that fight Prettier
29
+ prettier,
30
+ );
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@6/schema.json",
3
+ "workspaces": {
4
+ ".": {},
5
+ "apps/web": {},
6
+ "apps/api": {},
7
+ "libs/shared/eden": {}
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ import { treaty } from '@elysiajs/eden';
2
+ import type { App } from '@{{name}}/api';
3
+
4
+ /**
5
+ * The ONE http client of the monorepo. Fully typed end-to-end: every route,
6
+ * body and response comes from apps/api's `App` type — no codegen, no drift.
7
+ * Point it elsewhere per environment by editing this single spot.
8
+ */
9
+ export const api = treaty<App>('http://localhost:3001');
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@{{name}}/eden",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./index.ts"
7
+ },
8
+ "dependencies": {
9
+ "@elysiajs/eden": "^1.3.0"
10
+ },
11
+ "devDependencies": {
12
+ "@{{name}}/api": "workspace:*",
13
+ "typescript": "^5.8.3"
14
+ }
15
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["index.ts"]
4
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "{{name}}-monorepo",
3
+ "private": true,
4
+ "type": "module",
5
+ "workspaces": ["apps/*", "libs/**"],
6
+ "scripts": {
7
+ "dev": "bun run --filter './apps/*' dev",
8
+ "build": "bun run --filter './apps/*' build",
9
+ "test": "bun run --filter './apps/*' test",
10
+ "lint": "eslint .",
11
+ "lint:fix": "eslint . --fix",
12
+ "format": "prettier --write .",
13
+ "format:check": "prettier --check .",
14
+ "knip": "knip",
15
+ "deps:circular": "madge --circular --extensions ts,tsx apps libs",
16
+ "prepare": "husky"
17
+ },
18
+ "lint-staged": {
19
+ "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
20
+ "*.{css,json,js,mjs}": ["prettier --write"]
21
+ },
22
+ "trustedDependencies": ["esbuild"],
23
+ "devDependencies": {
24
+ "@eslint/js": "^10.0.1",
25
+ "eslint": "^10.6.0",
26
+ "eslint-config-prettier": "^10.1.8",
27
+ "eslint-plugin-react-hooks": "^7.1.1",
28
+ "globals": "^17.7.0",
29
+ "husky": "^9.1.7",
30
+ "knip": "^6.24.0",
31
+ "lint-staged": "^17.0.8",
32
+ "madge": "^8.0.0",
33
+ "prettier": "^3.9.4",
34
+ "typescript": "^5.8.3",
35
+ "typescript-eslint": "^8.62.1"
36
+ }
37
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noImplicitOverride": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true,
12
+ "isolatedModules": true,
13
+ "verbatimModuleSyntax": true,
14
+ "resolveJsonModule": true,
15
+ "noEmit": true
16
+ }
17
+ }