Repository navigation
Conversation
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>
There was a problem hiding this comment.
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 torun_worker_firstconfiguration 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.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hyfdev
force-pushed
the
fix/unocss-virtual-module-bypass
branch
from
February 27, 2026 13:30
70863b1 to
466f5c3
Compare
run_worker_first
Contributor
|
fixed by b7c4317 |
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`)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
UnoCSS resolves
virtual:uno.cssto/__uno.cssinstead of using Rollup's\0prefix convention (source). This means/__uno.cssdoesn'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.cssas/:slugbefore Vite's module pipeline can serve it, causing a 500 error.This adds
!/__uno*to therun_worker_firstexclusion 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'inuno.config.tsso the resolved path becomes/@uno.css, which is already bypassed by!/@*.Test plan
[slug].vue+ UnoCSSvirtual:uno.cssin a layout/__uno.cssserves CSS instead of hitting the catch-all route🤖 Generated with Claude Code