Conversation
) ## Problem The **Greetings** workflow fails on every first-time issue/PR: ``` Error: Input required and not supplied: issue_message ##[warning]Unexpected input(s) 'repo-token', 'issue-message', 'pr-message', valid inputs are ['issue_message', 'pr_message', 'repo_token'] ``` `actions/first-interaction@v3` renamed its inputs to snake_case, but `.github/workflows/greetings.yml` still passes the old kebab-case names (`repo-token`, `issue-message`, `pr-message`). The action reads them as missing and aborts before posting the greeting. ## Fix Rename the three inputs to the v3 names: `repo_token`, `issue_message`, `pr_message`. Message content is unchanged. Surfaced by the Greetings run on #151. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…hydration, resumable boundaries (#127–#130) (#151) Collected implementation of all four open **`ssr`** tickets, on one branch, targeting `dev`. Closes #128 · Closes #129 · Closes #130 · Advances #127 (tracking) ## What & why `ssr` is the last foundational module marked **Experimental**. This PR resolves the three substantive prerequisites for promotion and publishes the stability contract. ### #128 — Interactive directive parity (`bq-model` / `bq-on`) - `RenderOptions.directives: 'full' | 'static'` (default **`'static'`** — byte-for-byte backwards compatible). - `'full'` renders `bq-model` initial state (`value` / `checked` / selected `<option>` / `<textarea>` body) and emits a `data-bq-on` hydration marker for `bq-on:*`. **Handlers are never executed on the server**; inline `on*` attrs / `javascript:` URLs are still stripped. - `RenderOptions.onUnsupportedDirective: 'warn' | 'throw' | 'ignore'` (default **`'ignore'`**) — formalizes the "static-render subset" into an explicit, enforced, documented boundary (option **b** of the ticket) while parity (option **a**) is delivered via `'full'`. - New shared `src/ssr/directive-support.ts` keeps the **pure (DOM-free)** and **DOM** backends in lock-step; options also flow through `renderToStringAsync` / `renderToStream` / `renderToStreamSuspense` / `renderToResponse`. ### #130 — Guaranteed hydration correctness - `hydrate(selector, context, { onMismatch, onError })` — boundary-scoped recovery: `warn` (dev default) · `repair` (rewrite the boundary from client state) · `error` (route to `onError`, else throw before mounting). - `detectHydrationMismatches(root, context)` — content-level diff over `bq-text` / `bq-show` / `bq-bind:*` / `bq-model` plus the structural `data-bq-h` signature, using the CSP-safe evaluator. Skips expressions whose root identifier is absent from the context, so `bq-for` loop variables never false-positive. ### #129 — True resumability model (resume, not replay) - `createResumableBoundary` / `createResumableGraph` (server) serialize **signals** (values), **handlers** (ids only — no code), and **store** slices. - `resume()` (client) seeds existing signals, wires handlers by id from a caller-supplied registry (**no `eval`**), and rehydrates store slices in place — pairs with island hydration. Opt-in, tree-shakeable, prototype-pollution-filtered, `<script>`-escaped. ### #127 — Stability (tracking) - SSR guide gains a **Stability** section: exit-criteria checklist (3/3 prerequisites resolved here; the freeze-for-one-minor item remains open by definition), **frozen surface** list, and a **per-runtime support matrix**. `introduction.md` notes the **1.15.0** target. Public exports are now frozen for the cycle. ## New public API Runtime: `hydrate`, `detectHydrationMismatches`, `createResumableBoundary`, `createResumableGraph`, `resume`, `SSR_ON_MARKER_ATTR`, `RESUMABLE_BOUNDARY_ATTR`, `RESUMABLE_HANDLER_ATTR`, `RESUMABLE_EVENT_ATTR` — all re-exported from `src/full.ts` and documented in `docs/guide/ssr.md`. ## Verification - `bun test` — **2743 pass / 0 fail** (incl. new `tests/ssr-stable.test.ts`, both backends). - `bun run test:types` + `bun run lint:types` (tsc src + tests) — clean. - `bunx eslint .` — clean. - `bun run build` — succeeds; `check:full-bundle` in sync; `ssr` doc-export coverage 54/54 (100%). - Cross-runtime smoke (`tests/cross-runtime/run.mjs`) extended for `'full'` mode + resumable boundaries — **13/13 on Node 24 & Bun**. ## Compatibility Defaults preserve prior output exactly (no `value`/`checked`/`data-bq-on`, no warnings). All additions are optional. No version bump (release handled separately). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…able (#131, #132) (#153) Collected work for the two open `server` tickets, landed in one branch and targeting `dev`. Closes #131. Closes #132. ## #132 — session, auth, and middleware primitives - **`session()` + `memoryStore()`** — HMAC-signed session-id cookie (Web Crypto, cross-runtime), payload in a pluggable `SessionStore` (in-memory default; bring-your-own Redis/DB **without bundling a client**). `ctx.session` is a Proxy: payload via plain props, lifecycle via `$id` / `$isNew` / `$data` / `$regenerate` / `$destroy` / `$clear`. Includes secret rotation, rolling sessions, prototype-pollution filtering, and session-fixation defense. - **`csrf()` + `csrfToken()`** — OWASP double-submit cookie (signed when a `secret` is supplied); token via `x-csrf-token` header or `_csrf` body field. Composes with the `security` module (integrity vs. output). - **`guard()`** — predicate route guard mirroring the router's guard ergonomics. - **`basicAuth()` / `bearerAuth()`** — `Authorization` parsing with a `verify()` hook; resolved user on `ctx.state`. - **Signing utilities** — `signValue` / `unsignValue` / `timingSafeEqual` / `randomToken` / `randomId` / `base64Url*`, all on `globalThis.crypto.subtle` (no `node:crypto`). - Shared cookie helpers extracted to `src/server/cookies.ts` (+ `appendSetCookie`); no behavioral change to existing `ctx.setCookie`. ## #131 — promote `server` toward Stable - Guide **Stability** section: exit-criteria checklist, frozen `ctx`/`app` surface, per-runtime support matrix; intro + README notes; version history. - `app.listen()` now supports **Deno** via `Deno.serve` (Node/Bun/Deno covered). - `ctx.session` is **additive/optional** — no breaking changes to the 1.14 surface. - New exports wired through `server/index.ts` and the `/full` bundle. ## Zero-dependency & secure-by-default No runtime dependencies added; each primitive is independently importable/tree-shakeable. Sessions default to `httpOnly` + `SameSite=Lax`; signing is HMAC-SHA-256; comparisons are constant-time. ## Verification - Full suite **2785 pass / 0 fail** (42 new in `tests/server-stable.test.ts`: sessions, CSRF, guards, auth, crypto — incl. secret rotation, rolling, `$destroy`-revive, immutable-response cookies). - Cross-runtime smoke extended for server (sign/verify, session, CSRF) — passes on **Node + Bun** locally; **Deno** in CI. - `tsc`, `eslint`, `bun run build`, `check:full-bundle` (in sync), `check:doc-exports` (server 23/23) all green. ## Review Ran an adversarial multi-agent review of the diff; applied the confirmed hardening: don't cache a rejected HMAC key-import, guard `timingSafeEqual` against empty input, revive a session written-to after `$destroy()`, simplify the Deno `listen()` address resolution, and strengthened tests/docs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…+ graduate toward Stable (#133, #134, #135) (#154) Collected work for the three open `concurrency` tickets, landed in one branch and targeting `dev`. Closes #133. Closes #134. Closes #135. ## #134 — CSP-safe module workers (remove the mandatory `'unsafe-eval'`) - **`defineWorker()` / `defineRpcWorker()`** describe a pre-bundled worker module by URL on the main thread; **`exposeTask()` / `exposeRpc()`** wire that module up to the bQuery worker protocol inside the worker. Because the body is a real module loaded by URL — never a function revived with `new Function(...)` — module mode runs under a strict CSP with **no `'unsafe-eval'`** and no `blob:` worker source. - Every factory now accepts **either** an inline handler (dynamic mode, opt-in, needs `'unsafe-eval'`) **or** a `WorkerModule` (module mode, default/recommended): `runTask`, `createTaskWorker`, `createTaskPool`, `createRpcWorker`, `createRpcPool`, `callWorkerMethod`, and the reactive wrappers — additive and non-breaking via the `WorkerTaskSource` / `WorkerRpcSource` unions. - `isWorkerModule()`, `isModuleWorkerSupported()`, and `support.moduleWorker` (module mode requires only the `Worker` constructor, not `Blob`/`URL.createObjectURL`). ## #135 — Client async-concurrency primitives (UI scheduling) - **`suspense()`** — declarative async boundary aggregating promises and reactive async states (`useAsyncData`/`useResource`) into reactive `pending` / `settled` / `error` signals; `retrigger` option; `dispose()`. - **`startTransition()`** — `[isPending, start]`; `start(scope)` flips pending immediately then runs the scope on a low-priority schedule inside a `batch`, decoupling expensive updates from urgent input. A throwing scope is contained and reported (mirrors `effect()`), never escapes as an uncaught timer error. - **`deferred()`** — readonly signal that lags its source and coalesces rapid changes to throttle expensive derived UI. - Built on signals, zero-dependency, tree-shakeable; a distinct concern from worker concurrency. Pairs with SSR suspense streaming (`renderToStreamSuspense`/`defer`). ## #133 — promote `concurrency` toward Stable - Guide **Stability** section: exit-criteria checklist, frozen surface, runtime boundary (browser-focused per non-goals), and a per-environment support matrix; intro + README notes; version history. - Serializable-handler constraint documented + enforced (dynamic mode) with a clear `TaskWorkerSerializationError`; module mode is exempt by design. ## Zero-dependency & secure-by-default No runtime dependencies added; each primitive is independently importable/tree-shakeable. Module mode is the documented CSP-clean default, resolving the `'unsafe-eval'` requirement most at odds with bQuery's security posture. ## Verification - Full suite **2811 pass / 0 fail** (26 new in `tests/concurrency-stable.test.ts`: module task/RPC/pool execution with no blob/eval available, `exposeTask`/`exposeRpc` protocol, support detection, and the client primitives incl. throwing-scope containment, deferred coalescing, suspense promise/state aggregation). - `tsc`, `eslint`, `bun run build`, `check:full-bundle` (in sync), `check:doc-exports` (concurrency **37/37**), and `test:types` all green. ## Review Ran an adversarial multi-agent review of the diff; applied the confirmed hardening: contain a throwing `startTransition` scope and report it via `console.error` instead of letting it escape as an uncaught timer/idle exception (mirrors `effect()`), with a regression test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…ptional compiler (#136, #137, #138) (#155) Collected PR for all three open `view` tickets, implemented on one branch. Closes #136 Closes #137 Closes #138 ## #136 — Promote `view` to Stable (freeze the directive contract) - **`bq-for` duplicate-key edge case resolved.** Colliding `:key` values fall back to a deterministic, referentially-stable composite key, so duplicate rows reuse their DOM across re-renders instead of being recreated. The duplicate-key warning is now **dev-only** and emitted **once per offending key** instead of on every reactive update. - **Object-expression shorthand resolved.** `bq-class="{ active }"` (and `bq-style` / `bq-aria` object syntax) now behaves like JS object shorthand (`{ active: active }`) instead of silently dropping the property. - **Directive contract frozen** and a **per-directive SSR support matrix** published in the View guide. README, introduction, the module header, and the directive reference are updated to "targeting Stable in 1.15.0". ## #137 — Declarative enter/leave/move transitions A thin declarative layer over the existing `motion` engine — no second animation engine. | Attribute | Applies to | Purpose | |---|---|---| | `bq-transition` / `bq-in` / `bq-out` | `bq-if` / `bq-show` / `bq-for` | Named-preset enter/leave | | `bq-transition-duration` / `bq-transition-easing` | same | Timing | | `bq-animate="flip"` | `bq-for` | FLIP move on reorder | Presets: `fade`, `scale`, `slide`, `slide-up/-down/-left/-right`. Behaviour: no animation on first paint; `bq-if`/`bq-for` defer removal until the leave finishes; rapid toggles are race-safe; and **`prefers-reduced-motion` is honoured everywhere, including the FLIP path**. ## #138 — Optional compiled-template path New opt-in entry **`@bquery/bquery/view/compiler`** that pre-parses `bq-*` expressions at build time into optimized, **`with`-free** update functions (CSP-safe, no `new Function()` on the hot path). - `compileViews()`, `compileToModule()`, `compileExpression()`, `emitModule()` — small transforms usable from any bundler. - Dependency-free CLI: `bquery-view-compile [options] <file...>` (`runCompileCli` / `compileFiles`). - Runtime hooks `registerCompiledExpressions()` / `clearCompiledExpressions()` (from `@bquery/bquery/view`). The **runtime evaluator stays the default**; expressions the compiler can't statically handle fall back transparently, so both paths are behaviourally identical. Skipped expressions are reported in `stats.skipped` — nothing is silently dropped. ## Quality - **42 new tests** across `tests/view-stable.test.ts`, `tests/view-transitions.test.ts`, `tests/view-compiler.test.ts` (compiler tests eval the emitted source to assert semantic identity). - Full suite **2854 pass / 0 fail**; `tsc --noEmit`, `eslint`, and `build:lib` / `build:types` / `build:umd` all green; `check-full-bundle` in sync. - An adversarial multi-agent review of the diff was run; three confirmed findings were fixed before this PR: FLIP now honours reduced motion, `bq-if`/`bq-show` cancel an in-flight leave before restarting, and the SSR matrix's `bq-html-safe` row was corrected (it is client-only). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…ctions + optimistic updates (#139, #140) (#156) Collected PR for both open `forms` tickets, implemented on one branch. Closes #139 Closes #140 ## #139 — Promote `forms` to Stable (freeze the 1.13 surface) The `forms` surface expanded materially in 1.13.0 and has documented sharp edges. This freezes the surface for one minor cycle and settles those edges into **guaranteed, tested contracts**: - **`validationStrategy` default reviewed + documented.** The default stays `'manual'` (the least-surprising choice for "validate on submit"). The contract is now explicit: **`handleSubmit()` always runs the full validation pass**, regardless of strategy — `validationStrategy` only gates *automatic* per-change/per-blur validation. Behaviour is unchanged; the surprise is removed by documentation. - **SSR serialization boundary is now guaranteed.** `serializeFormState()` deterministically drops functions, `File` / `Blob` / `FileList`, `bigint`, and `symbol` via an explicit replacer, instead of relying on incidental `JSON.stringify` behaviour. Re-attach blobs on the client after hydration. - **`createFieldArray()` stable-key contract validated with clear errors.** New optional `getKey` enforces present, unique keys on every structural mutation and throws a descriptive error naming the offending key (e.g. *"requires stable, unique item keys, but getKey returned \"a\" for both index 0 and index 1"*). Adds `keys()` / `keyAt(index)`. Without `getKey` the array stays positional — **no behaviour change**. - **Surface frozen + documented.** New Stability section in the [Forms guide](docs/guide/forms.md) with the exit-criteria checklist and frozen-surface reference; module header and stability matrices updated to "targeting Stable in 1.15.0". ## #140 — Progressive-enhancement form actions + optimistic updates The headline React-19-parity feature: forms that work before/without JS and reconcile optimistically. ```ts import { formAction, useFormStatus, optimistic } from '@bquery/bquery/forms'; const submit = formAction('/todos', { method: 'POST', csrf: () => csrfToken }); const { pending } = useFormStatus(submit); const list = optimistic(todos, (cur, draft) => [...cur, draft]); submit.enhance(document.querySelector('form')!); // native POST without JS; fetch-enhanced with JS ``` - **`formAction(target, options)`** → reactive `pending` / `error` / `result` / `submitCount` / `submittedAt`; `enhance(form)` sets the native `action`/`method` (+ optional hidden CSRF field) for the no-JS path, then intercepts `submit` for a fetch-based, optimistic-aware submit; programmatic `submit(formData)`; `reset()`. `target` is an endpoint URL **or** a function. Non-OK responses throw **`FormActionError`** (carrying `status` / `response`). Composes with the validation pipeline and the `server` module's `csrf()` (#132). Native forms only support GET/POST, so PUT/PATCH/DELETE degrade to a native POST (the enhanced fetch keeps the real verb). - **`useFormStatus(action)`** → read-only `readonly()` views of the action's signals, mirroring React 19's `useFormStatus`. - **`optimistic(base, reducer)`** → an optimistic-update primitive whose reactive `value` folds pending drafts over the base and reverts automatically. `add(draft)` → handle with `remove()`; `run(draft, task)` applies the overlay around an async task; `pending` / `drafts` reactive; `clear()`. ## Quality - **28 new tests** across `tests/forms-stable.test.ts` (field-array key contract, SSR boundary, validationStrategy timing) and `tests/forms-actions.test.ts` (formAction function + string targets, CSRF, enhance/PE, optimistic composition, useFormStatus). - Full suite **2882 pass / 0 fail**; `tsc --noEmit`, `eslint`, `check-full-bundle` (in sync), `check-doc-exports` (forms **46/46**), and `build:lib` / `build:types` / `build:umd` all green. - New public exports (`formAction`, `useFormStatus`, `optimistic`, `FormActionError`, `FieldArrayKeyFn` + action/optimistic types) are wired into `src/full.ts` and documented in the guide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…ork (#159) ## Problem Vite's library build bundled `node:*` imports into its **browser-external stub** (an empty default export). That left `await import('node:fs/promises')` in the optional CLI entry resolving to `undefined`, so the published **`bquery-view-compile`** CLI crashed at runtime with `e is not a function` when run against the `dist` build. ## Fix Mark `node:*` as external in `rollupOptions` so those imports are emitted verbatim and resolved by Node at runtime: ```ts rollupOptions: { external: (id) => id.startsWith('node:'), output: { ... }, } ``` Browser entries never import `node:*`, so this is a no-op for them. ## Verification - Rebuilt `dist`; the `vite-browser-external` stub is **gone** from `dist/view-compiler.es.mjs` (0 references, was 1). - `bquery-view-compile --out-dir … file.html` runs end-to-end against the built bundle (`1/1 compiled`). ## Note This is the standalone hotfix for `dev`. The same one-line change is also present in the stable-graduation PR #157 (which introduces the `bquery-i18n` CLI that surfaced the bug); since the change is byte-identical, the branches reconcile cleanly when both land — or the line can be dropped from #157 once this merges. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
…144) (#157) Collected PR for the next four open stabilization tickets, implemented on one branch. Closes #141 Closes #142 Closes #143 Closes #144 ## #141 — i18n toward Stable: ICU MessageFormat + message extraction - **ICU MessageFormat** routed through a locale-aware formatter backed by `Intl.PluralRules`: `plural`, `selectordinal`, `select`, nested arguments, `offset:`, exact `=N` selectors, the `#` token, and apostrophe escaping. ICU is **detected automatically**; plain `{name}` interpolation and the legacy `singular | plural` pipe form keep the untouched fast path. - New authoring helpers **`defineMessages()`** (identity + extraction anchor) and **`formatMessage()`** (standalone single-message formatter). - Optional, dependency-free **message-extraction tooling** at the new `@bquery/bquery/i18n/extract` entry + **`bquery-i18n` CLI**: scans `defineMessages` catalogs and `t()`/`tc()` calls, merges into nested JSON catalogs **without overwriting translations** (`--prune` opt-in). Zero-build runtime path preserved. - i18n guide gains a Stability section: frozen surface, **ICU coverage table** (with documented non-coverage — inline `number`/`date` skeletons, rich-text), extraction, and lazy-loading. Tests cover ICU + extraction + CLI. ## #142 — a11y toward Stable: documented audit scope - Every `AuditFinding` now carries a **`wcag`** criterion; the full rule catalog is exported as **`auditRules`** (rule → WCAG → severity → *what it cannot detect*). - a11y guide gains the audit-scope table with **known limitations** (contrast, focus order, reading order, motion, meaningfulness, cross-root refs) and a frozen surface reference. Tests cover audit scope, focus trapping, live regions, `inert`/`scrollLock`, and preference signals. ## #143 — dnd toward Stable: keyboard accessibility statement - Frozen surface + **accessibility statement**: the keyboard model (pick up / move / drop / cancel, `aria-grabbed`) and confirmation that drag announcements route through the **shared a11y live-region announcer** (no second channel). - Tests cover keyboard pick/move/drop/cancel, `aria-grabbed` transitions, and the `grid`/`delay`/`viewport` option surface. ## #144 — media toward Stable: SSR-safe defaults + cleanup (bake-and-verify) - **Per-composable SSR fallback** + cleanup contract documented; frozen surface. No new features. - Tests cover SSR-safe defaults, idempotent `destroy()`, **listener detachment (no leaks)**, and `AbortSignal` teardown. ## Shared / build - Externalize `node:*` in the Vite lib build so the CLI entries emit real `import('node:fs/promises')` instead of Vite's browser-external stub — this also **fixes the previously-shipped `bquery-view-compile` CLI** in `dist`. - README stability matrix, `introduction.md` stability paragraphs, CHANGELOG, `src/full.ts` exports, and `package.json` (`./i18n/extract` export + `bquery-i18n` bin) updated. ## Verification - `bun test` — **2929 pass / 0 fail** (47 new tests across 5 files). - `tsc --noEmit` clean; `check:doc-exports` 100% for i18n/a11y/dnd/media; `check:full-bundle` and `check:ai-guidance` pass. - `bquery-i18n extract "src/**/*.ts" --out en.json` smoke-tested end-to-end against the built `dist`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Jonas Pfalzgraf <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
…y matrix (#149, #150) (#160) Works through all open GitHub issues (#149, #150) in one branch. Closes #149 Closes #150 ## #149 — File-based routing with typed loaders and actions An **opt-in, bundler-agnostic** file-route convention that bridges `router`, `ssr`, and `server`. Programmatic routing (`createRouter`) stays fully supported and unchanged; **no bundler is shipped** — the input is a manifest (a bundler glob such as `import.meta.glob`, or a hand-written map for zero-build). **`router`** - `createFileRoutes(manifest, options?)` → `{ routes, entries }` — plain `RouteDefinition`s `createRouter()` already consumes. - `parseFilePath` / `filePathToRoutePattern` — `routes/users/[id]/+page.ts` → `/users/:id`, `[...rest]` → `*`, `(group)` dropped; flat Next/Nuxt-style files supported. - `sortEntriesBySpecificity` — static beats dynamic, catch-all sorts last (correct first-match resolution). - Typed `Load` / `Action` contracts; `createRouteData(router)` / `useRouteData()` run `load` on client navigation with stale-response guarding. **`ssr`** — the router bridge now recognises `meta.load` (adapting `SSRContext`) alongside the legacy `meta.loader`, so loaders run on the server before render via `createSSRRouterContext`. **`server`** — `mountFileRoutes(app, entries, options?)` / `createFileRouteServerRoutes(entries, options?)` expose each route's `action` over HTTP (eager no-action routes are skipped; lazy ones reply `405`), with an optional JSON loader endpoint under `dataPath`. Composes with `csrf()`. Docs: new [File-based Routing guide](docs/guide/file-routing.md) + sections in `router.md` / `server.md` + sidebar entry. ## #150 — Reconcile version history + per-module stability changelog The 1.11.0 date conflict and the version drift were already resolved in earlier PRs; this finishes the issue: - **Single source of truth:** `scripts/stability-matrix.mjs` (canonical data) + `STABILITY.md` (human-facing matrix **with per-module status history**). - The README "Modules at a glance" table and the docs `introduction.md` matrix now reference it and are **validated against it** by `scripts/check-stability-matrix.mjs` (`bun run check:stability`) — so the three surfaces can no longer silently drift. - CHANGELOG `[Unreleased]` gains a **"Module status"** section (per the issue's proposed shape); `release-notes/index.md` reconciled to list **1.14.2**; release-process docs document the new gate. - Enforced in CI via `tests/check-stability-matrix.test.ts` (runs in `bun test`, mirroring how `check:full-bundle` is enforced). ## Validation All green locally: - `bun run lint` · `bun run lint:types` · `bun run test:types` - `bun run build` - `bun test` — **2984 pass / 0 fail** - `bun run check:full-bundle` · `bun run check:ai-guidance` · `bun run check:stability` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <[email protected]>
Bump 1.14.2 → 1.15.0 and graduate view, forms, i18n, a11y, dnd, media, plugin, devtools, testing, storybook, concurrency, ssr, and server to Stable. Every bQuery module is now Stable; all changes are additive with no breaking changes. - package.json: 1.14.2 → 1.15.0 - scripts/stability-matrix.mjs: 13 modules → Stable (canonical source) - STABILITY.md / README.md / docs/introduction.md: matrix surfaces reconciled - CHANGELOG.md: Unreleased → [1.15.0] - 2026-06-29 (+ fresh Unreleased) - docs/release-notes/1.15.md (new) + index row + VitePress sidebar entry - 13 module guides: "targeting Stable" → "graduated to Stable" (+ anchors) - AI guidance (AGENT.md, llms.txt, .clinerules, .cursorrules, .github/copilot-instructions.md): version + 1.15.0 highlights - tests/check-stability-matrix.test.ts: assert the graduation outcome Verified: check:stability, check:ai-guidance, check:doc-exports, check:full-bundle, bun test (2984 pass, 0 fail), build (lib + umd + types). Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (22)
📝 WalkthroughWalkthroughbQuery v1.15.0 graduates all 13 modules to Stable and adds new APIs for view transitions/compiler, forms actions and optimistic updates, i18n ICU/extraction, concurrency module workers/scheduling, server sessions/CSRF/auth/file routes, SSR hydration/resumability, and a versioned DevTools bridge. Documentation, tests, and stability checks are updated accordingly. Changesv1.15.0 Feature Release
CI workflow input rename
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Address every alert flagged by github-advanced-security on this PR: - router/server file-routes: replace the `\/+$` slash-trimming regexes with linear index scans. The unanchored trailing-slash quantifier backtracks super-linearly on repeated `/` (js/polynomial-redos, high). - i18n unflatten(): drop `__proto__` / `constructor` / `prototype` key segments before any nested assignment so a malicious scanned source file can no longer reach Object.prototype (js/prototype-polluting-assignment, medium). - codeql: scope analysis to shipped source via a config-file that ignores the test tree. The two js/xss-through-dom hits are test helpers feeding trusted renderToString() output into innerHTML to build fixtures — a false positive for code that never sees attacker input. - tests: add a regression test for the unflatten prototype-pollution guard. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…lears it The previous `parts.some(...)` pre-check left js/prototype-polluting-assignment open: CodeQL's barrier needs the `__proto__`/`constructor`/`prototype` guard to dominate the exact computed-property assignment. Check each segment inline, right before it is written, and bail on the key otherwise. Co-Authored-By: Claude Opus 4.8 <[email protected]>
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/i18n/translate.ts (1)
140-152: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse the fallback locale when formatting fallback ICU messages.
When
templatecomes fromfallbackMessages,formatICU()still receives the activelocale. That can select plural categories for the wrong language against the fallback catalog.Suggested fix
export const translate = ( messages: LocaleMessages | undefined, key: string, params: TranslateParams, locale: string, - fallbackMessages?: LocaleMessages + fallbackMessages?: LocaleMessages, + fallbackLocale = locale ): string => { let template: string | undefined; + let templateLocale = locale; // Try current locale if (messages) { template = resolveKey(messages, key); } // Fallback locale if (template === undefined && fallbackMessages) { template = resolveKey(fallbackMessages, key); + if (template !== undefined) { + templateLocale = fallbackLocale; + } } // Key not found — return key as-is if (template === undefined) { return key; } // ICU MessageFormat (plural / selectordinal / select) — locale-aware path. if (isICUMessage(template)) { - return formatICU(template, params, locale); + return formatICU(template, params, templateLocale); }Also update the
createI18n()call sites to passfallbackLocaleas the new final argument.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/translate.ts` around lines 140 - 152, When `translate()` resolves `template` from `fallbackMessages`, it still passes the active `locale` into `formatICU()`, which can apply the wrong plural rules for fallback ICU strings. Update `translate()` to track when the template comes from the fallback catalog and pass `fallbackLocale` to `formatICU()` in that case, while keeping the active locale for primary messages. Also update the `createI18n()` call sites so they provide `fallbackLocale` as the new final argument.src/forms/field-array.ts (1)
157-162: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBuild the replacement array before tearing down the current one.
buildInitial()now throws on duplicate/invalid stable keys. Becausereset()destroys the current items first, that throw leaves the field array holding already-destroyed fields. Build and validate the replacement array first, then swap and dispose the previous items.Suggested fix
const reset = (): void => { + const next = buildInitial(); + const current = items.peek(); + items.value = next; + for (const item of current) { + destroyItem(item); + } - for (const item of items.peek()) { - destroyItem(item); - } - items.value = buildInitial(); error.value = ''; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/forms/field-array.ts` around lines 157 - 162, The reset flow in field-array.ts currently destroys existing items before calling buildInitial(), so if buildInitial() throws on duplicate or invalid stable keys the array is left in a partially torn-down state. Update reset() to call buildInitial() and validate the replacement array first, then swap items.value to the new array and only after that iterate over the previous items and destroyItem() them; keep the error.value cleanup as part of the successful reset path.
🟠 Major comments (28)
scripts/stability-matrix.mjs-3-13 (1)
3-13: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winStop maintaining a second canonical stability source.
This file now hard-codes the matrix and calls itself canonical, but
STABILITY.mdand the repo rules say the markdown record is the canonical stability source. That leaves two hand-edited authorities for the same release metadata, so the advertised “updateSTABILITY.mdand run the check” workflow is no longer true. Please derive this module fromSTABILITY.md, or demote it to a generated mirror instead of another source of truth.As per coding guidelines,
STABILITY.md: “Maintain the canonical module stability record so the stable/beta/experimental status stays correct.”Also applies to: 25-48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/stability-matrix.mjs` around lines 3 - 13, The stability matrix is currently being maintained as a second source of truth, which conflicts with the canonical record in STABILITY.md. Update the module data in stability-matrix.mjs so it is derived from or generated from STABILITY.md, or clearly demote this file to a mirror used only for validation; keep scripts/check-stability-matrix.mjs and the module history workflow aligned so STABILITY.md remains the only hand-edited authority.Source: Coding guidelines
src/plugin/define.ts-39-41 (1)
39-41: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReturn the concrete plugin type from
definePlugin().
BQueryPlugin<TOptions>collapses plugin-specific fields and narrow literals at the call site, so this helper no longer preserves the object shape it advertises. Use a generic likeTPlugin extends BQueryPlugin<unknown>and returnTPlugininstead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugin/define.ts` around lines 39 - 41, `definePlugin()` is currently returning the widened `BQueryPlugin<TOptions>` type, which loses plugin-specific fields and literal precision at the call site. Update the `definePlugin` generic so it accepts a concrete plugin type like `TPlugin extends BQueryPlugin<unknown>` and returns `TPlugin` instead, while keeping the function body as a simple identity return.Source: Coding guidelines
src/server/session.ts-185-260 (1)
185-260: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftTrack nested session mutations before persisting.
The proxy only marks the session dirty for top-level assignment/deletion. Mutations like
ctx.session.cart.items.push(item)update the in-memory object but never callmarkDirty(), so the change is skipped atfinalize()and not written to the store.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/session.ts` around lines 185 - 260, The ServerSession proxy in session.ts only marks dirty on top-level set/delete, so nested object/array mutations can be missed before finalize(). Update the Proxy behavior around the session object to detect or wrap nested mutations (for example in the get path for data-backed values) so operations like ctx.session.cart.items.push(...) trigger markDirty() and persist correctly. Use the existing session proxy, markDirty(), and finalize() flow as the main touchpoints.src/router/file-routes/data.ts-121-124 (1)
121-124: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRun loaders outside the reactive tracking frame.
run(route)starts synchronously insideeffect(). If a loader reads signals before its firstawait, those signals can become dependencies and retrigger loading without navigation.Proposed fix
const dispose = effect(() => { const route = router.currentRoute.value; - void run(route); + queueMicrotask(() => { + void run(route); + }); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/router/file-routes/data.ts` around lines 121 - 124, The loader kickoff inside the effect in the route watcher is running within the reactive tracking frame, so any signals touched before the first await can become unintended dependencies. Update the effect callback that reads router.currentRoute and calls run(route) so the loader invocation happens outside tracking (for example by deferring it or wrapping the call in a non-tracking boundary) while still using the same route value. Keep the fix localized to the effect/run path in data.ts so loaders are triggered only by navigation changes, not by reactive reads inside loaders.src/router/file-routes/routes.ts-80-132 (1)
80-132: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject duplicate file-route patterns.
routes/index.tsandroutes/+page.tsboth normalize to/; pathless groups can also collide. Returning both makes route resolution depend on manifest insertion order.Proposed fix
const entries: FileRoute[] = []; + const seenPatterns = new Map<string, string>(); // Second pass: build a FileRoute per page. for (const [source, entry] of Object.entries(manifest)) { const parsed = parseFilePath(source, options); if (!parsed || parsed.kind !== 'page') continue; + const previousSource = seenPatterns.get(parsed.pattern); + if (previousSource) { + throw new Error( + `bQuery router: duplicate file route pattern "${parsed.pattern}" from "${previousSource}" and "${source}".` + ); + } + seenPatterns.set(parsed.pattern, source);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/router/file-routes/routes.ts` around lines 80 - 132, The route builder in routes.ts currently pushes every parsed page into entries without checking for duplicate normalized patterns, so colliding files like routes/index.ts and routes/+page.ts can both produce "/". Add a deduplication check in the second pass around parseFilePath and entries.push, keyed by parsed.pattern, and reject duplicates with a clear error instead of relying on manifest order. Use the existing symbols parseFilePath, entries, and the page-building loop to locate the change.src/ssr/resumable-boundary.ts-304-327 (1)
304-327: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDeduplicate resumed handler bindings across nested boundaries.
wireBoundaryHandlers()scans every descendant for each boundary. If resumable boundaries are nested, a child handler can be wired once while scanning the parent and again while scanning the child, causing duplicate user actions.Suggested fix
const wireBoundaryHandlers = ( scope: Element, - handlers: Record<string, ResumableHandler> + handlers: Record<string, ResumableHandler>, + wiredBindings: WeakMap<Element, Set<string>> ): number => { @@ const events = (el.getAttribute(RESUMABLE_EVENT_ATTR) ?? 'click') .split(/\s+/) .filter(Boolean); for (const event of events) { + const bindingKey = `${event}\u0000${handlerId}`; + let elementBindings = wiredBindings.get(el); + if (!elementBindings) { + elementBindings = new Set<string>(); + wiredBindings.set(el, elementBindings); + } + if (elementBindings.has(bindingKey)) continue; el.addEventListener(event, fn as EventListener); + elementBindings.add(bindingKey); wired += 1; } } @@ const boundaryIds: string[] = []; const seededSignals: string[] = []; + const wiredBindings = new WeakMap<Element, Set<string>>(); let wiredHandlers = 0; @@ if (root) { const scope = findBoundaryScope(root, boundary.id); - if (scope) wiredHandlers += wireBoundaryHandlers(scope, handlerRegistry); + if (scope) wiredHandlers += wireBoundaryHandlers(scope, handlerRegistry, wiredBindings); }Also applies to: 404-407
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ssr/resumable-boundary.ts` around lines 304 - 327, wireBoundaryHandlers() is wiring the same resumable handler multiple times when nested boundaries are scanned by both parent and child boundaries. Update the handler discovery in wireBoundaryHandlers to skip elements that belong to a nested resumable boundary so each handler is bound only once, and preserve the existing RESUMABLE_HANDLER_ATTR/RESUMABLE_EVENT_ATTR behavior while preventing duplicate addEventListener calls across nested scopes.src/ssr/hydration.ts-133-188 (1)
133-188: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGuard hydration detection against evaluator failures.
hasResolvableRoot()only checks the leading identifier. Expressions likeuser.namewhenuserisnullcan still throw and aborthydrate()beforewarn,repair, orerrorhandling runs.Suggested fix
+type EvaluationResult<T> = { ok: true; value: T } | { ok: false }; + +const tryEvaluateExpression = <T>( + expression: string, + context: BindingContext +): EvaluationResult<T> => { + try { + return { ok: true, value: evaluateExpression<T>(expression, context) }; + } catch { + return { ok: false }; + } +}; + const textExpr = el.getAttribute(`${prefix}-text`); if (textExpr !== null && hasResolvableRoot(textExpr, context)) { - const clientValue = String(evaluateExpression<unknown>(textExpr, context) ?? ''); - const domValue = el.textContent ?? ''; - if (clientValue !== domValue) { - out.push({ element: el, kind: 'text', directive: `${prefix}-text`, domValue, clientValue }); + const evaluated = tryEvaluateExpression<unknown>(textExpr, context); + if (evaluated.ok) { + const clientValue = String(evaluated.value ?? ''); + const domValue = el.textContent ?? ''; + if (clientValue !== domValue) { + out.push({ element: el, kind: 'text', directive: `${prefix}-text`, domValue, clientValue }); + } } }Apply the same wrapper to
bq-show,bq-bind:*, andbq-model.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ssr/hydration.ts` around lines 133 - 188, Guard hydration detection in hydrate() against expression evaluator failures by wrapping the evaluateExpression() calls used for bq-text, bq-show, bq-bind:*, and bq-model so a bad nested access like user.name does not throw and abort the scan. Keep hasResolvableRoot() as the precheck, but catch evaluator errors per directive, skip reporting that candidate when evaluation fails, and continue collecting mismatches so warn/repair/error handling still runs. Use the existing hydration flow in src/ssr/hydration.ts and the directive-specific blocks around textExpr, showExpr, attr.value, and modelExpr as the fix points.src/concurrency/scheduling.ts-141-151 (1)
141-151: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAlign
deferred()timeout with the maximum-lag contract.The cleanup cancels and reschedules the timeout on every source change, so continuous updates faster than
timeoutcan keep the deferred signal stale indefinitely. That makestimeouta debounce delay, not the documented maximum lag. Either implement max-wait flushing of the latest value or rename/reword the option before this API is stabilized.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/concurrency/scheduling.ts` around lines 141 - 151, The `deferred()` scheduling in `effect` currently behaves like a debounce because `scheduleDeferred` is canceled and rescheduled on every source change, so `mirror` may never update under constant churn. Update the logic around `readDeferredSource`, `scheduleDeferred`, and the `mirror.value` assignment to enforce a true maximum-lag contract by flushing the latest value after the timeout even during continuous updates, or otherwise rename/reword `options.timeout` to match the actual debounce behavior before stabilization.src/concurrency/task.ts-125-139 (1)
125-139: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winThread
options.runtimethrough the worker support checks insrc/concurrency/task.tsandsrc/concurrency/rpc.ts. The module branch still falls back to plaintypeof Worker, and the dynamic branch ignores the override entirely, soruntime: 'bun' | 'deno' | 'browser' | 'node' | 'unknown'cannot influence the guard and support can be reported incorrectly in tests and host-compat runtimes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/concurrency/task.ts` around lines 125 - 139, The worker support checks in task/concurrency setup ignore the runtime override, so support detection can be wrong for bun/deno/browser/node/unknown. Update the branching around isWorkerModuleDescriptor in task.ts and the related guard logic in rpc.ts to pass options.runtime through the support checks instead of relying only on typeof Worker or the default environment. Use the existing symbols like isConcurrencySupported, TaskWorkerUnsupportedError, createModuleWorkerInstance, and createWorkerInstance to keep the runtime-specific path selection consistent.src/concurrency/module-worker.ts-193-206 (1)
193-206: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCatch
postMessage()serialization failures.If a handler returns a non-cloneable result,
scope.postMessage()throws from the success callback and nobq:errorresponse is sent, leaving the main-thread call pending until timeout.Proposed fix
- void Promise.resolve() - .then(() => handler(message.payload as TInput)) - .then( - (result) => { - scope.postMessage({ id: message.id, result, type: WORKER_RESULT_MESSAGE }); - }, - (error: unknown) => { - scope.postMessage({ - error: serializeHostError(error), - id: message.id, - type: WORKER_ERROR_MESSAGE, - }); - } - ); + void (async () => { + try { + const result = await handler(message.payload as TInput); + scope.postMessage({ id: message.id, result, type: WORKER_RESULT_MESSAGE }); + } catch (error) { + scope.postMessage({ + error: serializeHostError(error), + id: message.id, + type: WORKER_ERROR_MESSAGE, + }); + } + })();Apply the same
try/catchshape around theexposeRpc()handler result post.Also applies to: 275-288
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/concurrency/module-worker.ts` around lines 193 - 206, The success path in `module-worker.ts` is missing error handling around `scope.postMessage()` in the `Promise.resolve().then(...).then(...)` flow, so non-cloneable handler results can throw before a `WORKER_ERROR_MESSAGE` is sent. Wrap the `scope.postMessage({ id, result, type: WORKER_RESULT_MESSAGE })` call in the same `try/catch` shape used elsewhere and, on failure, send a serialized error response with `serializeHostError(error)` so the main thread doesn’t hang. Apply the same fix to the matching `exposeRpc()` result posting path referenced by the other affected block.src/concurrency/module-worker.ts-43-58 (1)
43-58: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winSeparate task and RPC module descriptors.
defineWorker()anddefineRpcWorker()currently produce identical branded descriptors, whilecreateRpcWorker()treats any branded descriptor as RPC. Add an internalkind/separate brand and validate it in task/RPC factories so wrong descriptors fail fast instead of timing out. As per coding guidelines,src/**/*.ts: Do not weaken strict TypeScript type safety.Also applies to: 74-99
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/concurrency/module-worker.ts` around lines 43 - 58, The branded descriptor created by createModuleDescriptor is being reused for both task and RPC workers, so defineWorker() and defineRpcWorker() can produce indistinguishable objects. Add an internal kind field or separate brand in createModuleDescriptor and the RPC descriptor creator, then make createWorker/createRpcWorker validate that the descriptor kind matches the factory before proceeding. Keep the TypeScript types strict by narrowing the descriptor unions rather than weakening them, so invalid cross-use fails fast instead of reaching createRpcWorker and timing out.Source: Coding guidelines
src/i18n/extract/extract.ts-111-158 (1)
111-158: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winHarden catalog reconstruction against prototype-polluting keys.
Both
parseObjectLiteral()andunflatten()write source-derived path segments into plain{}objects. Keys like__proto__,constructor, orprototypecan mutate the prototype chain and corrupt later merges/output.Suggested fix
+const createCatalog = (): ExtractedCatalog => Object.create(null) as ExtractedCatalog; +const isUnsafeKey = (key: string): boolean => + key === '__proto__' || key === 'constructor' || key === 'prototype'; + const parseObjectLiteral = ( src: string, start: number ): { value: ExtractedCatalog; end: number } => { - const obj: ExtractedCatalog = {}; + const obj = createCatalog(); let i = start + 1; @@ if (src[i] === '{') { const r = parseObjectLiteral(src, i); - obj[key] = r.value; + if (!isUnsafeKey(key)) obj[key] = r.value; i = r.end; } else if (src[i] === '"' || src[i] === "'" || src[i] === '`') { const r = readString(src, i); - obj[key] = r.value; + if (!isUnsafeKey(key)) obj[key] = r.value; i = r.end; } else { i = skipValue(src, i); } @@ export const unflatten = (messages: ExtractedMessage[]): ExtractedCatalog => { - const root: ExtractedCatalog = {}; + const root = createCatalog(); for (const { key, value } of [...messages].sort((a, b) => a.key.localeCompare(b.key))) { const parts = key.split('.'); let node = root; for (let p = 0; p < parts.length - 1; p += 1) { const part = parts[p]; - if (typeof node[part] !== 'object') node[part] = {}; + if (isUnsafeKey(part)) throw new Error(`Unsafe catalog key: ${key}`); + if (typeof node[part] !== 'object') node[part] = createCatalog(); node = node[part] as ExtractedCatalog; } - node[parts[parts.length - 1]] = value; + const leaf = parts[parts.length - 1]; + if (isUnsafeKey(leaf)) throw new Error(`Unsafe catalog key: ${key}`); + node[leaf] = value; } return root; };Also applies to: 173-185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/extract/extract.ts` around lines 111 - 158, Harden the catalog-building paths in parseObjectLiteral and unflatten so source-derived keys cannot pollute object prototypes. Use a null-prototype container or explicitly reject unsafe segments like __proto__, constructor, and prototype before assigning into the plain object. Make the fix in the reconstruction logic where obj is populated so both nested object parsing and flattened path expansion are protected.Source: Linters/SAST tools
src/i18n/extract/cli.ts-111-118 (1)
111-118: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not treat every catalog read/parse failure as “missing file.”
If
options.outalready exists but contains malformed JSON or is unreadable,readJsonCatalog()returns{}andextractFiles()then overwrites that file with a fresh catalog. That silently destroys existing translations instead of surfacing the real error.Suggested fix
const readJsonCatalog = async (file: string): Promise<ExtractedCatalog> => { const { readFile } = await import('node:fs/promises'); try { const text = await readFile(file, 'utf8'); return JSON.parse(text) as ExtractedCatalog; - } catch { - return {}; + } catch (error) { + if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') { + return {}; + } + throw error; } };Also applies to: 156-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/extract/cli.ts` around lines 111 - 118, The readJsonCatalog helper currently swallows every read/parse error and returns an empty catalog, which makes extractFiles overwrite an existing out file even when it is malformed or unreadable. Update readJsonCatalog to distinguish a missing file from other failures: keep returning an empty object only for the “file not found” case, and rethrow or surface JSON parse/read errors so extractFiles does not replace existing translations with a fresh catalog. Use the readJsonCatalog and extractFiles flow to locate the fix.src/i18n/extract/extract.ts-173-183 (1)
173-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject parent/child key collisions instead of silently dropping one translation.
unflatten()overwrites data when bothaanda.bare present: the later nested write coercesafrom a string into an object and loses the original value. Since the extractor accepts arbitrary dot-delimited keys, this can silently discard messages during merge/write.Suggested fix
for (const { key, value } of [...messages].sort((a, b) => a.key.localeCompare(b.key))) { const parts = key.split('.'); let node = root; for (let p = 0; p < parts.length - 1; p += 1) { const part = parts[p]; - if (typeof node[part] !== 'object') node[part] = createCatalog(); - node = node[part] as ExtractedCatalog; + const existing = node[part]; + if (typeof existing === 'string') { + throw new Error(`Catalog key collision: "${parts.slice(0, p + 1).join('.')}" conflicts with "${key}"`); + } + if (existing === undefined) node[part] = createCatalog(); + node = node[part] as ExtractedCatalog; } - node[parts[parts.length - 1]] = value; + const leaf = parts[parts.length - 1]; + if (typeof node[leaf] === 'object') { + throw new Error(`Catalog key collision: "${key}" conflicts with nested keys`); + } + node[leaf] = value; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/extract/extract.ts` around lines 173 - 183, unflatten currently allows parent/child key collisions, so a key like a can be overwritten when a.b is processed and one translation is lost. Update unflatten in extract.ts to detect conflicts while walking parts, especially in the loop that builds node from ExtractedMessage.key, and reject any case where an intermediate segment already holds a non-object value or the final key already exists as an object/value conflict. Surface this as an explicit error instead of coercing node[part] into {} so the caller can handle the invalid catalog shape.src/forms/types.ts-303-313 (1)
303-313: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winThe documented
getKeyguarantee is broader than the current implementation.This says keyed arrays enforce the contract on every structural mutation, but
createFieldArray.move()still just reordersitemswithout re-runningassertStableKeys(). AgetKey(value, index)can be valid on insert/add and become invalid after a reorder, so either validate on move as well or narrow the public contract here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/forms/types.ts` around lines 303 - 313, The documented getKey contract in FormFieldArray is broader than what move currently enforces, since createFieldArray.move() only reorders items and does not re-run assertStableKeys(). Update the move path to validate keys after reordering (using assertStableKeys and the existing getKey contract) so invalid or duplicate keys are caught on every structural mutation, or else narrow the getKey documentation/comments in src/forms/types.ts to match the actual behavior.src/forms/action.ts-201-223 (1)
201-223: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
pendingflips false too early when submits overlap.Each
submit()call sets the shared boolean back tofalseinfinally, so the first completion clearspendingeven if a later submit is still in flight. Track an in-flight count/token and derivependingfrom that, otherwiseuseFormStatus()misreports active work.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/forms/action.ts` around lines 201 - 223, The shared pending state in submit() is being cleared by whichever request finishes first, so overlapping submissions can incorrectly report no active work. Update the useFormAction flow around submit(), pending, and submitCount to track in-flight submits with a count or token instead of a single boolean, and only set pending false when the last active submit completes. Keep the existing optimistic overlay cleanup and onSuccess/onError handling inside submit() unchanged aside from tying pending to the active submit tracking.src/forms/action.ts-142-148 (1)
142-148: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPreserve the clicked submitter in enhanced submits.
new FormData(form)omits the clicked submit button, so enhanced submits lose buttonname/valuedata and can diverge from native behavior when a form has multiple submit actions. Capture theSubmitEvent.submitterand build the payload from that submit context instead of the bare form.Also applies to: 253-258
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/forms/action.ts` around lines 142 - 148, The enhanced form payload builder in buildFormData currently uses only the form element, so the clicked submit button’s name/value is lost and behavior can diverge from native submits. Update the submit handling path to capture SubmitEvent.submitter and pass that submit context into buildFormData (and any other enhanced submit helpers that construct FormData) so the final payload preserves the clicked button data.src/forms/action.ts-175-191 (1)
175-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't send
FormDatain the enhancedGETpath.
formAction({ method: 'GET' })still callsfetch()withbody: formData. That breaks the enhanced flow, becauseGETsubmissions need to be encoded into the query string and sent without a body. Right now the native fallback and JS-enhanced behavior diverge for the same API surface.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/forms/action.ts` around lines 175 - 191, The enhanced GET path in runTarget is still sending formData as a request body, which must be removed to match native GET behavior. Update formAction’s runTarget logic to branch on the normalized method before calling fetchImpl: for GET, serialize the FormData into the target URL query string and call fetch without a body; for non-GET, keep the existing body submission. Use the existing method, target, and runTarget symbols to locate and adjust the request construction.src/view/directives/show.ts-54-62 (1)
54-62: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winStop async hide commits after cleanup.
Like
bq-if, a pending leave transition can still commitdisplay: noneafter the directive cleanup runs. Wrap the cleanup to invalidate the token and cancel active animations.🐛 Proposed fix
- cleanups.push(cleanup); + cleanups.push(() => { + token++; + cancelTransitions(el); + cleanup(); + });Also applies to: 69-69
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/directives/show.ts` around lines 54 - 62, The pending leave transition in show.ts can still commit a hide after directive cleanup, so update the cleanup path to invalidate the current transition token and cancel any active animations. Use the existing token check around runTransition, cancelTransitions, and the show directive’s cleanup registration so that cleanup prevents the then() callback from setting htmlEl.style.display = 'none' once the directive has been torn down.src/view/process.ts-15-15 (1)
15-15: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon’t globally shadow custom directive names.
This guard runs before plugin custom-directive lookup, so a previously valid custom directive like
bq-transitionorbq-inis now silently ignored even when nobq-if/bq-show/bq-forowns it. Only skip companion attributes when an owning directive is present.🐛 Proposed fix
-const PASSIVE_DIRECTIVES = new Set<string>(['key', ...TRANSITION_ATTRS]); +const TRANSITION_DIRECTIVES = new Set<string>(TRANSITION_ATTRS);- // Skip companion attributes (bq-key, bq-transition, bq-in, bq-out, …) that - // are read by their owning directive rather than processed here. - if (PASSIVE_DIRECTIVES.has(directive)) continue; + const hasStructuralTransitionOwner = + TRANSITION_DIRECTIVES.has(directive) && + (el.hasAttribute(`${prefix}-if`) || + el.hasAttribute(`${prefix}-show`) || + el.hasAttribute(`${prefix}-for`)); + const hasForKeyOwner = directive === 'key' && el.hasAttribute(`${prefix}-for`); + + // Skip companion attributes only when their owning directive will consume them. + if (hasStructuralTransitionOwner || hasForKeyOwner) continue;Also applies to: 72-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/process.ts` at line 15, The PASSIVE_DIRECTIVES guard in process.ts is too broad and can shadow valid custom directives before the custom-directive lookup runs. Update the logic around PASSIVE_DIRECTIVES and the directive handling in the process flow so companion attributes like bq-transition or bq-in are skipped only when an owning directive such as bq-if, bq-show, or bq-for is actually present. Make sure the affected directive-processing path preserves custom directives otherwise, using the relevant checks in the process function and any related companion-attribute filtering.src/view/directives/if.ts-57-63 (1)
57-63: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCancel pending transitions during directive cleanup.
A pending leave transition can still run its
.then(...)afterView.destroy()because cleanup only stops the effect. Invalidate the token and cancel animations in the registered cleanup.🐛 Proposed fix
- cleanups.push(cleanup); + cleanups.push(() => { + token++; + cancelTransitions(el); + cleanup(); + });Also applies to: 74-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/directives/if.ts` around lines 57 - 63, The pending leave transition in the if directive can still complete after cleanup because the registered effect teardown only stops reactivity. Update the cleanup path around runTransition, myToken/token, and cancelTransitions so that directive destruction invalidates the current token and explicitly cancels any in-flight animations before the promise callback can call replaceWith or change isInserted. Make sure the cleanup used by the if directive covers both the leave transition and any related enter/leave state so View.destroy cannot leave a stale .then(...) running.src/view/directives/transitions.ts-197-201 (1)
197-201: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCatch
animate()setup errors and fall back to the no-transition path.Element.animate()throws synchronously for invalid easing/timing values, sorunTransition()can reject before any finish handler is wired. Thebq-if/bq-showleave paths only use.then(...), so the element can stay mounted or visible instead of taking the synchronous commit path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/directives/transitions.ts` around lines 197 - 201, `runTransition()` is calling `animatable.animate()` in a way that can throw before the transition promise is created, which leaves the `bq-if`/`bq-show` leave flow without a fallback. Update the `animate()` setup in `transitions.ts` to catch synchronous setup errors around the `animate()` call and treat them as a no-transition case, so the existing commit path is taken when timing/easing values are invalid. Use the `runTransition` and `animatable.animate` logic as the anchor points for the fix.src/view/compiler/expression.ts-24-30 (1)
24-30: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPreserve runtime global fallback for non-whitelisted identifiers.
The runtime evaluator falls through to ambient scope for any name missing from
ctx, but the compiled path only preserves that behavior for names hard-coded inDEFAULT_GLOBALS. Expressions likeURL.createObjectURL(file)currently compile to__bq_ctx.URL.createObjectURL(__bq_ctx.file), which changes behavior instead of matching runtime semantics. Emit a fallback lookup such as("URL" in ctx ? ctx.URL : URL)for unknown names, or bail out unless the identifier is known to come from the binding context.Possible fix
- out += `${param}.${name}`; + out += `(${JSON.stringify(name)} in ${param} ? ${param}.${name} : ${name})`;Also applies to: 245-275
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/compiler/expression.ts` around lines 24 - 30, The compiled expression path in expression.ts is incorrectly limiting global fallback to only DEFAULT_GLOBALS, so unknown identifiers no longer match the runtime with(ctx) behavior. Update the identifier compilation logic used by the expression compiler to emit a context-or-global fallback for any name not known to be a binding-context value, or explicitly reject/bail out when the symbol cannot be proven local. Make sure the fix is applied in the identifier handling around DEFAULT_GLOBALS and the expression compilation flow that produces references like __bq_ctx.<name>, so cases such as URL.createObjectURL(file) continue to resolve against ambient globals when ctx does not provide the name.src/view/compiler/cli.ts-61-66 (1)
61-66: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
--out-dircan overwrite outputs from different source folders.When
outDiris set, every file is written to${outDir}/${basename}${suffix}${ext}. Compilingpages/home/index.htmlandemails/home/index.htmlwill silently target the same output path and drop one result. Preserve the relative input path underoutDir, or detect collisions and fail fast.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/compiler/cli.ts` around lines 61 - 66, The output path logic in cli.ts currently uses only path.basename in the write step, so different source folders can collide when --out-dir is set. Update the compile flow around the output construction and writeFile call to preserve each input file’s relative path under options.outDir (or, if that’s not desired, detect duplicate targets and throw before writing). Use the existing input, base, dir, suffix, and ext handling in the cli compiler path to locate the fix.src/view/compiler/expression.ts-143-152 (1)
143-152: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDon't mark syntactically invalid rewrites as compiled.
rewrite()tokenizes conservatively, but it never proves that the final body is still valid JavaScript. Inputs likecount +oritems]can return{ ok: true }and then generate an invalid module, turning one bad directive into an import-time failure instead of a normal runtime fallback. Add a final syntax/balance validation step before returningok: true, and convert failures took: false.Also applies to: 217-223, 336-338
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/compiler/expression.ts` around lines 143 - 152, The rewrite() flow is currently returning ok: true even when the rewritten expression is still syntactically invalid, which can produce an invalid module; add a final syntax/balance validation step before the success return and downgrade any invalid result to ok: false. Use the existing rewrite() path in expression.ts, including the string/token handling and any final assembly logic, to verify parentheses/brackets/quotes and overall JavaScript validity before marking the body as compiled.src/view/compiler/cli.ts-95-112 (1)
95-112: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't consume the next flag as an option value.
Each option blindly uses
argv[++i]. If the user forgets a value, the parser will happily treat the next flag as data (--prefix --out-dirsetsprefix="--out-dir"), which produces confusing behavior instead of the documented usage error. Validate that a non-flag value exists before assigning each option.Possible fix
+ const takeValue = (flag: string): string | null => { + const value = argv[i + 1]; + if (!value || value.startsWith('-')) { + io.error(`Missing value for ${flag}`); + return null; + } + i++; + return value; + }; + for (let i = 0; i < argv.length; i++) { const arg = argv[i]; switch (arg) { - case '--prefix': - options.prefix = argv[++i]; + case '--prefix': { + const value = takeValue('--prefix'); + if (value === null) return 1; + options.prefix = value; break; - case '--out-dir': - options.outDir = argv[++i]; + } + case '--out-dir': { + const value = takeValue('--out-dir'); + if (value === null) return 1; + options.outDir = value; break; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/compiler/cli.ts` around lines 95 - 112, The CLI parser in the argv loop is consuming the next token with argv[++i] for option values, which can incorrectly treat another flag as data when a value is missing. Update the parsing logic in cli.ts around the switch handling for --prefix, --out-dir, --suffix, --ext, and --import to first verify that argv[i + 1] exists and is not a flag before assigning it to options. If the next token is missing or starts with --, fail with the same usage/error path used by the parser instead of advancing i.extension/panel.js-68-70 (1)
68-70: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not render inspected-page data through
innerHTML.
fmt(item)ande.labelcome from the inspected page and are injected straight into the extension DOM. In the panel this crosses a privilege boundary and lets page-controlled markup rewrite or spoof the extension UI. Build these rows withtextContent/DOM nodes (or sanitize first) instead of HTML strings.Also applies to: 83-85, 92-94
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@extension/panel.js` around lines 68 - 70, The tree rendering in renderTree, along with the related row builders at the other noted locations, is injecting inspected-page values into the extension UI via innerHTML. Replace these HTML string assignments with DOM node creation and textContent (or sanitize before insertion) so node.tag, fmt(item), and e.label cannot inject markup into the panel.Source: Linters/SAST tools
src/devtools/bridge.ts-198-239 (1)
198-239: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject mismatched bridge protocol versions before dispatch.
handleMessage()currently accepts anyv, so a v0/v2 panel can still drive the v1 server. That makesBRIDGE_PROTOCOL_VERSIONinformational instead of contractual and will fail open once the protocol evolves. Gatehello/requestondata.v === BRIDGE_PROTOCOL_VERSIONand fail fast on mismatches.Patch sketch
const handleMessage = (data: unknown): void => { if (!isInbound(data)) return; + if (data.v !== BRIDGE_PROTOCOL_VERSION) { + if (data.kind === 'request') { + options.post({ + source: BRIDGE_SOURCE, + channel: 'page', + v: BRIDGE_PROTOCOL_VERSION, + kind: 'response', + id: data.id, + error: `Protocol mismatch: expected v${BRIDGE_PROTOCOL_VERSION}, received v${data.v}`, + }); + } + return; + } if (data.kind === 'hello') {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/devtools/bridge.ts` around lines 198 - 239, handleMessage currently dispatches every inbound message without enforcing the bridge version, so add a strict check that data.v matches BRIDGE_PROTOCOL_VERSION before processing both hello and request messages. If the version is missing or mismatched, reject the message early by returning a response/error instead of calling announce() or method(data.params), and keep the existing unknown-method and exception handling paths unchanged. Use the handleMessage function and BRIDGE_PROTOCOL_VERSION as the key entry points for the fix.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7f35bc8e-8e1b-4e4c-be6f-c50c2bbd6ccc
📒 Files selected for processing (143)
.clinerules.cursorrules.github/copilot-instructions.md.github/workflows/greetings.ymlAGENT.mdCHANGELOG.mdREADME.mdSTABILITY.mdbin/bquery-i18n.mjsbin/bquery-view-compile.mjsdocs/.vitepress/config.tsdocs/contributing/release-process.mddocs/guide/a11y.mddocs/guide/concurrency.mddocs/guide/devtools.mddocs/guide/dnd.mddocs/guide/file-routing.mddocs/guide/forms.mddocs/guide/i18n.mddocs/guide/media.mddocs/guide/plugin.mddocs/guide/router.mddocs/guide/server.mddocs/guide/ssr.mddocs/guide/storybook.mddocs/guide/testing.mddocs/guide/view.mddocs/introduction.mddocs/release-notes/1.15.mddocs/release-notes/index.mdextension/README.mdextension/background.jsextension/content.jsextension/devtools.htmlextension/devtools.jsextension/manifest.jsonextension/panel.htmlextension/panel.jsllms.txtpackage.jsonscripts/check-stability-matrix.mjsscripts/stability-matrix.mjssrc/a11y/audit.tssrc/a11y/index.tssrc/a11y/types.tssrc/concurrency/index.tssrc/concurrency/internal.tssrc/concurrency/module-worker.tssrc/concurrency/pool.tssrc/concurrency/reactive.tssrc/concurrency/rpc.tssrc/concurrency/scheduling.tssrc/concurrency/support.tssrc/concurrency/task.tssrc/concurrency/types.tssrc/devtools/bridge.tssrc/devtools/extensions.tssrc/devtools/index.tssrc/dnd/index.tssrc/forms/action.tssrc/forms/field-array.tssrc/forms/index.tssrc/forms/optimistic.tssrc/forms/ssr.tssrc/forms/types.tssrc/full.tssrc/i18n/define.tssrc/i18n/extract/cli.tssrc/i18n/extract/extract.tssrc/i18n/extract/index.tssrc/i18n/extract/merge.tssrc/i18n/i18n.tssrc/i18n/icu.tssrc/i18n/index.tssrc/i18n/translate.tssrc/media/index.tssrc/plugin/define.tssrc/plugin/index.tssrc/router/file-routes/data.tssrc/router/file-routes/path.tssrc/router/file-routes/routes.tssrc/router/file-routes/types.tssrc/router/index.tssrc/server/auth.tssrc/server/cookies.tssrc/server/create-server.tssrc/server/crypto.tssrc/server/csrf.tssrc/server/file-routes.tssrc/server/guard.tssrc/server/index.tssrc/server/session.tssrc/server/types.tssrc/ssr/directive-support.tssrc/ssr/hydration.tssrc/ssr/index.tssrc/ssr/render-async.tssrc/ssr/render.tssrc/ssr/renderer.tssrc/ssr/resumable-boundary.tssrc/ssr/router-bridge.tssrc/ssr/suspense.tssrc/ssr/types.tssrc/storybook/index.tssrc/testing/index.tssrc/view/compiler/cli.tssrc/view/compiler/compile.tssrc/view/compiler/emit.tssrc/view/compiler/expression.tssrc/view/compiler/index.tssrc/view/compiler/types.tssrc/view/directives/for.tssrc/view/directives/if.tssrc/view/directives/index.tssrc/view/directives/show.tssrc/view/directives/transitions.tssrc/view/evaluate.tssrc/view/index.tssrc/view/mount.tssrc/view/process.tstests/a11y-stable.test.tstests/check-stability-matrix.test.tstests/concurrency-stable.test.tstests/cross-runtime/run.mjstests/devtools-bridge.test.tstests/dnd-stable.test.tstests/forms-actions.test.tstests/forms-stable.test.tstests/i18n-extract.test.tstests/i18n-stable.test.tstests/media-stable.test.tstests/plugin-stable.test.tstests/router-file-routes.test.tstests/server-file-routes.test.tstests/server-stable.test.tstests/ssr-stable.test.tstests/storybook-stable.test.tstests/testing-stable.test.tstests/view-compiler.test.tstests/view-stable.test.tstests/view-transitions.test.tstests/view.test.tsvite.config.ts
Replaces the `node: … | null` walk sentinel with an `unsafe` flag so there is no `node === null` comparison — clears the github-code-quality "comparison between inconvertible types" note while keeping the inline prototype-pollution guards CodeQL relies on. Behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
…evtools Multi-agent review of the 1.15.0 PR surfaced these issues; each is fixed and covered by the existing or an added test. Security - server: ctx.body() buffered any unrecognized content type into memory with no size cap. Add a `raw` ServerLimits field and route the fall-through through the bounded body reader (unauthenticated memory-exhaustion DoS). - ssr: a bq-model value reflected onto <textarea> (a raw-text element) was emitted unescaped by the DOM-free renderer; escape it (stored XSS). - devtools extension: the panel rendered untrusted inspected-page data via innerHTML; HTML-escape every interpolated value. - server: SameSite=None cookies now force Secure, which browsers require or they silently drop the cookie. Correctness - view: bq-if and bq-show re-ran their enter/leave transitions on every reactive update, not only on a real flip — a falsy condition with ticking dependencies could keep an element visible forever. Guard on a normalized boolean flip (also handles a non-boolean/undefined condition). - view: bq-for duplicated a row when a key was re-added while its leave animation was still in flight. Track leaving elements and drop the stale node on re-add (+ regression test). - concurrency: scheduleDeferred's requestIdleCallback path had no timeout, so a saturated main thread could starve startTransition/deferred; bound it to 100ms. - devtools bridge: validate the inbound request shape so a malformed message no longer dispatches methods[undefined] and replies with an undefined id. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ss dot The numeric-literal scanner copied any run of `[0-9a-fA-FxXoObBeE._]`, so it swallowed the dot in `1.5.toFixed(2)`. That set `prev` to a number rather than `.`, so the following property was rewritten as a free identifier (`1.5.__bq_ctx.toFixed(...)`) and produced broken compiled code. Consume at most one decimal point, and only when a digit follows, so member-access dots end the literal. Valid numbers (hex, exponent, separators, leading-dot floats) are unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…, server, a11y
Second review pass over the modules the first (rate-limited) run left
unverified, plus the two areas it never reached (a11y, build tooling).
Security / robustness
- server/csrf: throw on a provided-but-empty `secret` instead of silently
downgrading to unsigned double-submit (e.g. an unset `CSRF_SECRET` env that
resolves to ''). Omitting `secret` is still the supported unsigned opt-in.
- i18n/translate: a malformed ICU string in the catalog no longer throws out
of translate()/t(); it falls back to plain `{name}` interpolation.
- i18n/extract CLI: guard the directory walk against symlink cycles
(canonical-path visited set) so a self-referential link can't hang it.
- forms/ssr: stripNonSerializable filtered object keys but turned
non-serializable ARRAY elements into `null`, breaking the documented
"dropped, never null" contract — filter them out of arrays instead.
- a11y/audit: the input-label check interpolated an element id into a
`label[for="…"]` selector, so an id with a quote/metacharacter threw a
SyntaxError and crashed the audit. Compare `for` attributes directly.
Correctness
- view/compiler: bail on unbalanced/mismatched brackets instead of emitting
unparsable code that breaks the whole generated module.
- view/process: a plugin directive named like a passive companion attribute
(key/in/out/animate/transition/…) was silently shadowed; an explicit
custom-directive registration now wins.
- i18n/icu: honour a doubled '' inside an apostrophe-quoted literal per the
ICU spec the module documents.
- forms/action: refresh the native-fallback CSRF hidden field before each
submit so a rotating token also reaches the form body, not just the header.
Docs
- i18n/extract: `total` documents extracted-key count (added+kept), matching
the value and CLI output.
- view/evaluate: state that the AoT and runtime paths agree for well-formed
expressions and how they differ for an unresolved free identifier.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
…e() complexity CodeFactor flagged rewrite() as a "Very Complex Method" after the bracket-balance and numeric-scanner fixes added branches. Extract the self-contained string- and numeric-literal scanners into helpers, which removes two nested loops from the method and brings it back under the complexity threshold. Behaviour is unchanged (verified: member-access numbers, strings containing brackets, and unbalanced expressions all compile/bail as before). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Summary
bQuery 1.15.0 graduates the final thirteen modules to Stable —
view,forms,i18n,a11y,dnd,media,plugin,devtools,testing,storybook,concurrency,ssr, andserver. With them, every bQuery module is now Stable and bound by the no-breaking-changes-between-minor-releases contract.Motivation
This is the release PR (
dev → main) for 1.15.0, the final step of the stability roadmap: every remaining module reaches the Stable bar — frozen public surface, met exit criteria, and the no-breaking-change guarantee. It bundles the per-module graduation work tracked in #127–#150.Changes included
Module graduations (
dev → maindelta)view/compiler(runs under strict CSP, no'unsafe-eval') ([Feature]: Promoteviewto Stable — freeze the directive contract after the 1.14 additions #136–[Feature]: Optional compiled-template path forview#138)formAction/useFormStatus+optimistic();createFieldArraykeyed reconciliation ([Feature]: Promoteformsto Stable — settle the 1.13 expanded surface #139, [Feature]: Progressive-enhancement form actions + optimistic updates #140)i18n/extracttoolkit &bquery-i18nCLI ([Feature]: Promotei18nto Stable + message-extraction tooling and documented ICU coverage #141)auditRules/wcag), hardened keyboard DnD + a11y statement, verified SSR-safe media ([Feature]: Promotea11yto Stable — settle the 1.14 surface and document audit scope #142–[Feature]: Promotemediato Stable — prove the 25+ new composables across a cycle #144)definePlugin, versioned devtools bridge + reference Manifest V3 extension, runner integration, pinnedunsafeHtmlcontract ([Feature]: Promotepluginto Stable — freeze the hook-bus / DI / directive APIs #145–[Feature]: Promotestorybookto Stable — settle the helper surface #148)concurrencyto Stable — define exit criteria from Experimental #133–[Feature]: Client async-concurrency primitives — Suspense / transition / deferred-value #135)ssrto Stable — freeze public API and define exit criteria #127–[Feature]: Guaranteed hydration correctness — graduate mismatch handling past dev-only warnings #130)listen()on Node/Bun/Deno ([Feature]: Promoteserverto Stable — freeze thectx/appAPI and define exit criteria #131, [Feature]: First-party session, auth, and middleware primitives forserver#132)load/action([Feature]: File-based routing with typed loaders and actions #149); single-source stability matrix ([Feature]: Reconcile version history and add a per-module stability changelog #150)Release-prep changes
scripts/stability-matrix.mjs) +STABILITY.md+ README +docs/introduction.md(single source of truth)CHANGELOG.md:[1.15.0] - 2026-06-29(+ freshUnreleased); newdocs/release-notes/1.15.md+ index row + VitePress sidebar entryAGENT.md,llms.txt,.clinerules,.cursorrules,.github/copilot-instructions.mdSecurity hardening (CodeQL fixes on this branch)
\/+$slash-trimming regexes insrc/router/file-routes/path.tsandsrc/server/file-routes.tswith linear index scans (js/polynomial-redos, high).src/i18n/extractunflatten()now rejects__proto__/constructor/prototypesegments inline, right before each computed-property assignment (js/prototype-polluting-assignment, medium); added a regression test..github/codeql/codeql-config.ymlto exclude the never-shipped test tree, clearing twojs/xss-through-domfalse positives (test helpers feeding trustedrenderToString()output intoinnerHTML).Validation
bun run lint— ESLint clean (CI + changed files);tsc --noEmitcleanbun run build— lib + umd + typesbun test— 2984 pass, 0 failbun run check:stability(21 modules in sync across all surfaces),bun run check:ai-guidance,bun run check:doc-exports,bun run check:full-bundle; CodeQL, CodeFactor, and all CI matrix jobs (node-24 / deno-2 / bun-1.3) greenChecklist
Notes for reviewers
A focused review of the new security- and parser-critical code (server
crypto/csrf/session/auth, SSR hydration, view compiler, i18n ICU, file-routing) found no critical bugs and a strong security posture: prototype-pollution guards, constant-time comparison, HMAC-signed double-submit CSRF, request-body memoization, JSON XSS escaping, and session-fixation prevention. CodeRabbit's full review surfaced no inline issues.Known low / informational follow-ups (none release-blocking):
createBodyReader's unknown-content-type path (arrayBuffer()) bypasses the configuredlimits(potential unbounded buffering).1.5.toFixed(2)).[...rest]file routes don't bind a named catch-all param server-side.🤖 Generated with Claude Code
Summary by CodeRabbit