Skip to content

fix: bypass UnoCSS's default virtual module paths in run_worker_first - #5

Closed
hyfdev wants to merge 2 commits into
mainfrom
fix/unocss-virtual-module-bypass
Closed

hyfdev wants to merge 2 commits into
mainfrom
fix/unocss-virtual-module-bypass

Conversation

@hyfdev

@hyfdev hyfdev commented Feb 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

UnoCSS resolves virtual:uno.css to /__uno.css instead of using Rollup's \0 prefix convention (source). This means /__uno.css doesn't get served through Vite's /@id/ path like standard virtual modules do.

When a Pages app has a catch-all route like [slug], Hono matches /__uno.css as /:slug before Vite's module pipeline can serve it, causing a 500 error.

This adds !/__uno* to the run_worker_first exclusion list so UnoCSS's virtual modules bypass the worker and are handled by Vite directly. The glob covers layered imports like /__uno_components.css.

Workaround (without this fix): set virtualModulePrefix: '@uno' in uno.config.ts so the resolved path becomes /@uno.css, which is already bypassed by !/@*.

Test plan

  • Create a Pages app with [slug].vue + UnoCSS
  • Import virtual:uno.css in a layout
  • Verify /__uno.css serves CSS instead of hitting the catch-all route

🤖 Generated with Claude Code

UnoCSS resolves `virtual:uno.css` to `/__uno.css` instead of using
Rollup's `\0` prefix convention. Without this exclusion, catch-all
page routes (e.g. `[slug]`) intercept `/__uno.css` requests before
Vite's module pipeline can serve them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 27, 2026 13:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a routing issue that occurs when using UnoCSS with Void Pages apps that have catch-all routes. UnoCSS resolves its virtual module virtual:uno.css to /__uno.css instead of following Vite's standard \0 prefix convention for virtual modules. This causes the path to be matched by catch-all routes (like [slug]) before Vite can serve it, resulting in 500 errors.

Changes:

  • Adds !/__uno* exclusion pattern to run_worker_first configuration to bypass worker routing for UnoCSS virtual modules
  • Includes explanatory comment documenting UnoCSS's non-standard virtual module resolution

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/void/src/index.ts Outdated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hyfdev
hyfdev force-pushed the fix/unocss-virtual-module-bypass branch from 70863b1 to 466f5c3 Compare February 27, 2026 13:30
@hyfdev hyfdev changed the title fix: bypass UnoCSS virtual module paths in run_worker_first fix: bypass UnoCSS's default virtual module paths in run_worker_first Feb 27, 2026
@yyx990803

Copy link
Copy Markdown
Contributor

fixed by b7c4317

@yyx990803 yyx990803 closed this Feb 28, 2026
@cpojer
cpojer deleted the fix/unocss-virtual-module-bypass branch May 7, 2026 07:54
cpojer pushed a commit that referenced this pull request Sep 11, 2026
…deploy) (#131)

## Summary

Fixes the empirically-confirmed bug where **every sandbox project fails on its second deploy**, plus the latent silent misroute in deploys 1+ before PR #124 surfaced it.

The bug:

- Per-deploy WfP script names (`user-${slug}-${deployment.id}`) cause Cloudflare to mint a fresh DO `namespace_id` for the `Sandbox` class on every deploy.
- Container apps' `durable_objects.namespace_id` is **immutable on PATCH**.
- Deploy 1 binds the container to namespace NS-1; deploy 2 mints NS-2 and `assertNamespaceMatch` (PR #124) fails the deploy.
- Pre-#124, the container kept routing traffic to the deploy-1 namespace silently; deploys "succeeded" but sandbox calls landed on stale instances.

The fix: a stable per-project `do-host-${slug}` script owns the `Sandbox` class. User workers cross-script-bind to it. Container apps bind to the host's namespace once and live for the project's lifetime. Sandbox state survives redeploys.

Design doc: [`meta/design-docs/0067-cross-script-do-host-architecture.md`](meta/design-docs/0067-cross-script-do-host-architecture.md). Two adversarial-review rounds before implementation; six review/fix rounds during implementation.

## What changes

**Schema (additive, backward-compatible):**
- `resources.metadata jsonb` — sandbox host info (`namespaceId`, `hostScriptName`)
- `projects.status text` — gates new deploys against in-flight teardowns

**New host script artifact:**
- `packages/api/src/provisioning/sandbox-host-source/index.ts` — re-exports `Sandbox` from `@cloudflare/sandbox` (pinned `0.9.1`)
- `packages/api/scripts/build-sandbox-host.ts` — tsdown bundler (`cloudflare:*` and `node:*` external; everything else bundled)
- `packages/api/src/provisioning/sandbox-host-bundle.generated.ts` — committed deterministic artifact (BUNDLE_BYTES + SHA256 + COMPATIBILITY_DATE)
- CI gate (`pnpm -C packages/api build:sandbox-host && git diff --exit-code`) catches drift on `@cloudflare/sandbox` bumps

**`provisionSandboxHost` (the new core helper):**
- Idempotent. Holds project-row `SELECT … FOR UPDATE` through CF reconciliation.
- Honors `projects.status='deleting'` to abort safely (`ProjectDeletingError`).
- Cached fast path verifies the live host's binding namespace; reconciles container config drift (image / instance_type / max_instances) via PATCH+rollout.
- Create-or-recover handles fresh provisioning and partial-failure replay (read-tag-first PUT preserves namespace). When a same-name container app exists on a mismatched namespace (the pre-PR cutover state), throws a clear runbook-pointing error so operators can run a one-time `wrangler containers delete` per the cutover runbook — silently destroying a tenant's container state is exactly the failure mode this architecture exists to prevent.
- Compensating DELETE if the resources UPSERT fails after the container POST — only deletes containers this attempt actually created.

**Deploy pipeline integration:**
- `provisionSandboxHost` runs early (after row insert, before metadata assembly), gated on `manifest.sandbox && hasWorkerFiles`.
- Metadata-assembly helpers (`buildSandboxUploadBindings` / `buildSandboxUploadContainers` / `buildDurableObjectUploadMigrations`) accept a `crossScriptHost` arg: emit `script_name: do-host-${slug}` on the SANDBOX binding, force `class_name: 'Sandbox'` (the host's literal class), strip the user worker's `containers` block, drop the `Sandbox` migration.
- Framework remains unchanged — local `wrangler dev` still uses same-script form. Platform rewrites at upload time.
- Concurrent-deploy precheck restructured into an atomic `FOR UPDATE` with placeholder-row insert; second concurrent request observes the placeholder and 409s.

**Project teardown (consolidated `teardownProject`):**
- Used from public delete, admin project delete, admin user delete (one shared path; previously three divergent ones).
- Takes `FOR UPDATE`, flips `status='deleting'`, marks all non-terminal deployments `'canceling'`, commits — then runs cleanup. Existing `assertDeploymentNotCanceled` calls in the deploy pipeline observe the cancellation.
- Cleanup is split into **critical** (host script + container app — fatal on non-404) and **best-effort** (D1/KV/R2/queues/etc. — warnings). DB cascade only runs after critical succeeds; transient CF failures preserve DB pointers for retry.
- Deterministic-name orphan recovery for containers without resource rows.

**Route-flip race hardening:**
- `assertProjectNotDeleting` helper called alongside every `assertDeploymentNotCanceled` (~15 sites); fails closed on missing project.
- Final commit (deployment status='live' + project type) wrapped in a `FOR UPDATE` tx with post-lock 'deleting' check.
- Catch path tracks every successful KV write (`writtenRoutingKeys`) — queue, slug, domain — and rolls back the exact set on teardown-race or pre-routing failure. Tracker is pushed AFTER each successful PUT (not before) so transient KV failures can't delete the previous deployment's live route.
- Teardown-race branch also DELETEs the uploaded user worker to recover orphans.

**Tests:**
- 16 unit tests (`packages/api/test/unit/sandbox-host.test.ts`) — `provisionSandboxHost`, `putSandboxHostScript`, metadata helpers (test #5 asserts the cutover throw path, not auto-migration)
- 11 integration tests + 1 intentional skip (`packages/api/test/integration/api-sandbox.test.ts`) — first deploy, redeploy, drift reconciliation, stale-host recovery, partial-failure replay, pre-PR cutover error event, concurrent-deploy 409, rollback preserves state, project teardown
- Mock CF API extended with stateful host scripts + container apps + cross-script binding validation + call-order recording (455 existing integration tests still pass unchanged)

**Documentation:**
- `meta/lessons.md` — architectural lesson appended (notes the cutover is intentionally operator-driven)
- `meta/operations.md` — "Pre-PR-#67 sandbox cutover" runbook + existing orphan-host runbook
- Framework submodule bumped: `framework/docs/guide/sandboxes.md` rewritten to distinguish stable DO identity from ephemeral container state (cf. `@cloudflare/containers`'s `sleepAfter` default)

## Pre-PR cutover

The 4 stuck sandbox tenants from before this PR (recappi, `dtnyzh0qckg3`, `2ves8mgqb4po`, `60c3lfqptd2i`) have stale CF container apps bound to per-deploy DO namespaces. Their next deploy after this PR ships will fail loudly with a runbook-pointing error. Operator runs the one-time cutover per [`meta/operations.md`](meta/operations.md) "Pre-PR-#67 sandbox cutover": notify each tenant 24h in advance, then `wrangler containers delete proj-<projectId>-void-sandbox` once per affected tenant before their next deploy. One-time sandbox state loss at cutover is unavoidable; documented.

## Out of scope (deferred follow-ups)

- Silent DO storage rotation footgun for **non-sandbox** DO users (would need extending the host pattern to all user-declared DO classes)
- Per-deploy churn for secrets, queue consumers, tail consumers, cron triggers
- Pre-existing partial-routing-failure imperfection at `deploy.ts:3282-3297`
- Slug rename for sandbox projects
- `deploy.ts:85` has its own `DEFAULT_DISPATCH_NAMESPACE` literal (pre-existing duplicate of `constants.ts`)
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.

3 participants