Type-safe raw SQL for Bun without an ORM (codegen against your real schema)
▲ 4 r/bun

Type-safe raw SQL for Bun without an ORM (codegen against your real schema)

I write Bun.sql with raw SQL and didn't want an ORM, but kept losing types — queries come back as any[] and you end up hand-writing row types that drift from the actual columns.

So I made a codegen step. You name each query:

const [user] = await sql.GetUser`
  SELECT id, email, display_name FROM users WHERE id = ${id}
`

and it generates a .d.ts mapping each name to its real result type. The way it gets the types: it runs your migration .sql files into an in-process Postgres (PGlite, no Docker) or SQLite, prepares every query against that, and reads the column types back. So it's checking your actual schema, not parsing the SQL itself.

Nullability was the annoying bit — Postgres's describe gives you types but not whether a column can be null, so I pull that from the query plan plus the catalog, with an override file for cases it can't infer.

Runtime stays plain Bun.sql, the generated file is the only artifact, and it's fast enough to run on save.

v0.1, Postgres + SQLite. Curious whether the nullability inference holds up on uglier queries than mine. Repo: https://github.com/ilbertt/bun-sqlgen

u/ilbert_luca — 13 days ago
▲ 7 r/bun

parsh is a type-safe CLI router for TypeScript. 90s demo of the inference attached: file-based commands, ctx typed end-to-end, typo a positional and the compiler tells you which one.

Sharing here because I'm proud of the monorepo setup and want feedback from people who actually live in Bun. Workspaces handle the layout (4 published packages, examples, generated artifacts) on a single bun install. bun test runs runtime tests and type-only tests (using the native expectTypeOf from the bun:test module). Bun.build does per-package bundling in a 15-line build.ts. No vitest, no ts-jest, no tsup, no extra runner config.

One design choice worth mentioning here: parsh's core sticks to node:fs, node:path, node:url rather than Bun.file etc. Bun reimplements those modules natively, so the same code runs on Bun's optimized path and on Node. Cross-runtime compatibility for free.

Repo: https://github.com/ilbertt/parsh. Happy to hear what I got wrong or what I could simplify further.

u/ilbert_luca — 2 months ago

I guess someone else already posted this, but I'm switching from VS Code and I wanted to share some Zed settings I edited that made me feel at home again:

  1. Move the files view to the left and show the git status on the files:
"project_panel": {
  "dock": "left",
  "git_status": true,
  "git_status_indicator": true,
  "diagnostic_badges": true
}
  1. Move the Git panel to the left:
"git_panel": {
  "dock": "left"
}
  1. Install the VSCode Dark Modern theme: https://zed.dev/extensions/vscode-dark-modern
  2. Make the gitignored files appear more dimmed:
"theme_overrides": {
  "VSCode Dark Modern": {
    "ignored": "#666",
    "version_control.ignored": "#666"
  }
}

Anything else I should tweak?

u/ilbert_luca — 2 months ago
▲ 1 r/bun

I just shipped parsh, a TanStack Router-inspired, file-based CLI router for TypeScript. End-to-end type inference, schema-agnostic via Standard Schema, headless core. The library is the library, but the thing I keep recommending people try is the dev setup underneath it.

It's a Bun monorepo with Turbo on top. A few packages: @parshjs/core for the router, @parshjs/codegen for the file-walking codegen, @parshjs/env and @parshjs/files as add-ons. Five examples in examples/* that double as integration tests.

bun run --bun turbo build across the full repo finishes in under a second when nothing changed, a couple of seconds when everything did. I never use --filter because rebuilding everything is cheap enough that I stopped caring. Watch mode on the codegen package regenerates the routing tree in under 50ms on save, which is what makes file-based routing actually feel instant.

Bun's catalog: is doing more for me than I expected. TypeScript and Zod versions live in the root package.json, every package just says "typescript": "catalog:", and that's it. No version drift, no bumpall scripts, no Renovate config to babysit.

The whole toolchain is one binary per job. Bun runs the scripts, Turbo orchestrates, Biome lints and formats. No ESLint + Prettier + ts-node + tsx soup. bun test covers everything, including the codegen tests, which write fixtures to a temp dir and snapshot the emitted .gen.ts.

The one real pain: I still need tsc for declaration emit. Bun's transpiler is fast but it doesn't generate .d.ts files, and a published library needs them. So tsc -b still runs in every package's build step, and it's by far the slowest thing in the pipeline.

reddit.com
u/ilbert_luca — 2 months ago
▲ 12 r/CLI+1 crossposts

Every time I build a CLI in TypeScript I end up with Commander or yargs option configs in one place and a parallel set of types I maintain by hand. They drift. Stricli gets a lot closer, but full type safety still doesn't carry across commands the way I wanted, and extending the context class got annoying fast. I got tired of all of it and built parsh.

It's a file-based CLI router inspired by TanStack Router. Same idea, but for CLIs: the path string is the source of truth for params, schemas define the types, and the framework wires inference end to end.

// commands/projects/[name]/deploy.ts
defineCommand('projects [name] deploy', {
  params: { name: { schema: z.string() } },
  options: { env: { schema: z.enum(['staging', 'prod']) } },
  handler: ({ params, options }) => {
    params.name;    // string
    options.env;    // 'staging' | 'prod'
    options.foo;    // ❌ compile error
  },
});

Commands are files. The path above is generated from commands/projects/[name]/deploy.ts and invoked as mycli projects <name> deploy. A codegen step walks the directory and emits a commandTree.gen.ts with a declare module block, which is what makes ctx fully inferred via registry lookup. No generics at the call site.

Options declared on a parent flow into every descendant, also fully typed, without redeclaring them:

// commands/_root.ts
defineRootCommand({
  options: { region: { schema: z.string().default('eu-west-2'), forwardToChildren: true } },
});

// commands/projects/list.ts
defineCommand('projects list', {
  handler: ({ rootOptions }) => {
    rootOptions.region;  // string
    rootOptions.bar;     // ❌ compile error
  },
});

A few things I committed to early:

  • Schema-agnostic via Standard Schema v1, so Zod, Valibot and ArkType all work. Core doesn't depend on Zod.
  • Headless core. No Ink, chalk, ora. Bring whatever TUI you want.
  • Codegen instead of recursive conditional types. The generated declare module precomputes intersections, so compile times stay flat at depth.

There are also a couple of optional add-on packages that hook into ctx via the same registry: @parshjs/env for typed lazy process.env access and @parshjs/files for typed JSON config on disk.

Repo: https://github.com/ilbertt/parsh

Would love feedback, especially on the inference setup. The declare module registry is the part that makes everything else work, and I'm not sure if there's a cleaner way to do end-to-end inference across a routing tree without a generator step.

reddit.com
u/ilbert_luca — 2 months ago