Skip to content

codegen-patch: real .ts templates, AST splice anchors, watch-mode dev orchestration#9

Merged
dogmar merged 6 commits into
mainfrom
claude/flamboyant-kirch-d45bfc
May 4, 2026
Merged

codegen-patch: real .ts templates, AST splice anchors, watch-mode dev orchestration#9
dogmar merged 6 commits into
mainfrom
claude/flamboyant-kirch-d45bfc

Conversation

@dogmar
Copy link
Copy Markdown
Collaborator

@dogmar dogmar commented May 4, 2026

Summary

Two themes, one branch:

1. Refactor codegen-patch.ts to read real .ts template files instead of joined string arrays. The static body of every block spliced into Panda's css.d.ts / css.mjs now lives in real TypeScript files under packages/bearbones-vite/src/templates/. Editors highlight them as TS, reviewers see meaningful diffs, and a small renderer fills in the two dynamic bits (utility-name union members + condition-vocabulary map entries) via sentinel substitution. Splice points in Panda's emit are located by AST shape (@babel/parser + magic-string, both already runtime deps) instead of exact-string match, so benign whitespace / quote-style / comment drift no longer breaks the patch. Snapshot stability is enforced by piping the patched output through oxfmt before comparison; new fixtures cover the AST-locator robustness.

2. Make vp run dev from the workspace root pick up template / source edits in @bearbones/vite automatically. A single dev task in the root vite.config.ts declares dependsOn: [\"@bearbones/vite#build\", \"@bearbones/preset#build\"] to pre-build every workspace dist the website's Vite/Panda config loaders import at config-load time, then launches @bearbones/vite#dev and website#dev in parallel via explicit vp run pkg#task invocations. @bearbones/vite's pack uses clean: !process.argv.includes(\"--watch\") so real one-shot builds still wipe stale output but watch mode atomically overwrites the bundle the dependsOn pre-build just produced — no consumer race. Pure vite-plus task orchestration; no shell tricks.

Key design choices (why this is a few commits, not one)

  • Templates as data, not code. The .ts files in src/templates/ are excluded from this package's tsconfig + lint config — they reference types that only exist in Panda's emitted directory layout. Each carries @ts-nocheck and eslint-disable directives above a // ---bearbones-template-emit-below--- fence; the renderer strips everything up to the fence so those template-author directives never reach Panda's emitted output.
  • No runtime fs reads. Originally the loader used fs.readFileSync(new URL('./templates/...', import.meta.url)), but Panda's config loader bundles consumers via esbuild and may emit CJS, which strips import.meta.url to an empty string and throws Invalid URL. Templates are now inlined into src/templates.generated.ts (gitignored) by scripts/generate-templates.mjs and the loader just imports the constants. The bundle has zero runtime import.meta.url references.
  • Plugin gates regen on real template changes. The inlineTemplatesPlugin in packages/bearbones-vite/vite.config.ts runs gen only when watchChange fires for a file in src/templates/ (tracked via a pendingGen flag), not on every buildStart. Without this, the plugin wrote templates.generated.ts on every build — and since that file is imported by codegen-templates.ts, rolldown's import-graph watcher saw the write as a change → infinite rebuild loop.
  • AST splice locator uses already-installed deps. @babel/parser and magic-string are both already runtime deps of this package (used by transform.ts), so the AST swap added no new dep weight.

What changed

Area Files
Templates (new) packages/bearbones-vite/src/templates/{css-d-ts-injected,css-d-ts-marker,css-mjs-marker-stub}.ts
Loader / renderer / AST locator (new) src/codegen-templates.ts, src/codegen-patch-render.ts, src/codegen-patch-ast.ts
Build-time gen script (new) scripts/generate-templates.mjs
Refactored src/codegen-patch.ts (270 → 176 lines, orchestration only)
Tests tests/codegen-patch.test.ts (oxfmt normalization, AST-drift fixtures, loadTemplate contract); snapshot re-baselined
Build / dev wiring packages/bearbones-vite/vite.config.ts, root vite.config.ts, root package.json, tsconfig.json

Test plan

  • vp test in packages/bearbones-vite — 65/65 pass (3 test files)
  • vp check — formatter, linter, type-check all clean
  • vp pack — bundle produced; dist/index.mjs has zero runtime import.meta.url refs (only doc-comment mentions)
  • Smoke-test bundled module in Node — patchCssArtifact runs; marker block + condition map present
  • vp run dev from worktree root — dev server up at localhost:5173, zero Failed to resolve errors, zero rebuild loops; bearbones-vite watch and panda steady state
  • Live template edit during vp pack --watch — exactly 2 rebuilds, then steady (one for the source change, one as rolldown's import-graph picks up the regen-written file)
  • AST-drift fixtures (whitespace / alt quote style / comment in Panda's emit) — splice still locates correctly
  • Reviewer pass on the split between the two themes (codegen-patch refactor + dev orchestration); reasonable to land together since the dev wiring exists because the templates need a build/watch pipeline

dogmar and others added 6 commits May 3, 2026 17:17
… splice points by AST

The static body of every block spliced into Panda's `css.d.ts` / `css.mjs`
now lives in a real `.ts` template under `src/templates/`, read at runtime
by `codegen-templates.ts` and rendered by `codegen-patch-render.ts` via
sentinel substitution. Editors highlight the templates as TypeScript and
diffs to the emitted shape look like TS edits, not array-of-strings prose
edits. Splice points are located by AST shape (`@babel/parser` →
`magic-string`, both already runtime deps) instead of exact string match,
so benign whitespace / quote-style / comment drift in Panda's emit no
longer breaks the patch.

Snapshot stability is enforced by piping the patched output through oxfmt
before comparison; the AST-drift fixtures cover the new robustness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ader works

Panda's config loader bundles consumers via esbuild and may emit CJS,
which strips `import.meta.url` to an empty string and breaks the
`fs.readFileSync(new URL("./templates/...", import.meta.url))` scheme.
Symptom in `vp run dev`:

    [WARNING] "import.meta" is not available with the "cjs" output format
    🐼 error [hooks] Error in plugin "__panda.config__": Invalid URL

Inline `src/templates/*.ts` into `src/templates.generated.ts` (gitignored)
via `scripts/generate-templates.mjs`. The loader imports the constants
directly — no runtime fs, no `import.meta.url`. Generation runs as a
vitest globalSetup before tests and chains in front of `vp pack` in the
build task. The post-pack `dist/templates/` copy is gone.

Editor TypeScript highlighting on the templates is preserved — they remain
real `.ts` files in `src/templates/`. Only the runtime path changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…the website

Three coordinated changes so a single `vp run dev` from the workspace
root picks up template / source edits in `@bearbones/vite` without a
manual restart:

1. `@bearbones/vite`'s pack config gains a `buildStart` tsdown plugin
   (`inlineTemplatesPlugin`) that regenerates `src/templates.generated.ts`
   and registers each `src/templates/*.ts` file via `this.addWatchFile`.
   In `vp pack --watch`, editing a template now triggers a rebuild.
   Generation moves out of the `dev` / `build` shell prefix and into
   the bundler's lifecycle. The script exposes `generateTemplates()`
   for both vitest globalSetup and the plugin.

2. `@bearbones/vite`'s pack sets `clean: false`. tsdown's default
   cleans `dist/` before each (re)build — including the first build of
   a watch session, even if `dist/` already holds a fresh bundle. That
   open window broke the website's Vite/Panda config loader at startup
   with `Failed to resolve entry for package \"@bearbones/vite\"`. New
   builds atomically overwrite, so skipping the wipe is safe.

3. Root `dev` script now does
   `vp run --filter '@bearbones/*' build && vp run --filter website --filter @bearbones/vite --parallel dev`.
   The pre-step builds every workspace bearbones package in dep order
   (so the website's config loader has dist on first attempt); the
   parallel step then runs the website's dev server alongside
   bearbones-vite's watch. Pure vite-plus task orchestration, no shell
   tricks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…watch mode

Replaces the shell-chain orchestration (`vp run --filter '@bearbones/*' build && vp run --filter ... --parallel dev`) with a single `dev` task in the workspace-root `vite.config.ts`:

- `dependsOn: ["@bearbones/vite#build", "@bearbones/preset#build"]` pre-builds every workspace dist the website's `vite.config.ts` and `panda.config.ts` import at config-load time. vp respects task ordering (and dedups via the same dependsOn declared on `@bearbones/vite#build`).
- The command then launches both long-running watchers as explicit `vp run @bearbones/vite#dev` and `vp run website#dev` invocations in parallel, with `trap 'kill 0' INT TERM; ... & wait` so Ctrl-C cleanly stops the whole group.

`@bearbones/vite`'s pack now sets `clean: !process.argv.includes("--watch")`. Real one-shot builds still wipe stale output. `vp pack --watch` skips the wipe so its incremental builds atomically overwrite the bundle the dependsOn pre-build just produced — no race against the website's Vite/Panda config loader, no wait-for-deps wrapper.

Removes the prior `dev` script from the root `package.json` (the new task supersedes it; same name on both sides is rejected by vp's task-graph validator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified empirically that vp + the shell propagate Ctrl-C (process-group
SIGINT) to both backgrounded children without an explicit `trap 'kill 0'`.
Also confirmed the foreground `vp run website#dev` keeps the shell alive
on its own; no `& wait` needed. Edge case the simpler form gives up: if
the foreground watcher exits unexpectedly mid-session, the backgrounded
one orphans — acceptable for two long-running dev watchers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…infinite watch loop)

The previous plugin called \`generateTemplates()\` on every \`buildStart\` and wrote \`templates.generated.ts\` unconditionally. Since that file is imported by \`codegen-templates.ts\`, rolldown's import-graph watcher saw the write as a change → fired another \`buildStart\` → wrote again → infinite rebuild loop. Symptom in \`vp run dev\`: panda re-extracting CSS dozens of times per second forever.

Fix the root cause instead of papering over with a write-content-equality check: gate regen behind a \`pendingGen\` flag that's flipped to true ONLY when \`watchChange\` fires for a real template source file under \`src/templates/\`. \`buildStart\` consults the flag and skips the regen otherwise. Edits to \`templates.generated.ts\` that the plugin itself just wrote are correctly ignored — they're not in the template-files list — so the loop is broken.

Verified: \`vp pack --watch\` settles to 1 Rebuilt and stays there; touching a template triggers exactly 2 rebuilds (one for the source change, one as rolldown's import-graph picks up the regen-written file) then settles again. Full \`vp run dev\` orchestration: 1 Rebuilt total over 35s, zero panda \`ctx:change\` events in the steady state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dogmar dogmar merged commit 63395dc into main May 4, 2026
2 checks passed
@dogmar dogmar deleted the claude/flamboyant-kirch-d45bfc branch May 4, 2026 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant