Debugging wrangler static asset serving

/home/jm/data/code/opencode · 16 messages · 54 parts · · 198,287 tokens
Created Jan 18, 01:47:29 AM · Updated Jan 18, 02:33:37 AM · 46 syncs
userJan 18, 01:47:29 AM

What are the configuration variables involved with determining how the static assets in the @packages/web project are served up when bundled alongside a worker script with wrangler? I do a build with vite write before bundling it, in order to generate an dist/src/index.html, dist/js/index-asdf.js, and ***/*.css.

Inn what precedence to the conditions override others? such as worker running before serving static, redirect at the server, handle routing in the worker etc.. etc..

How can I debug and find logs for what is happening?

assistantJan 18, 01:47:29 AM
glm-4.7 · zai-coding-plan
The user is asking about configuration variables for serving static assets in the @packages/web project when bundled with a Worker script using wrangler. They want to understand: 1. Configuration variables involved in static asset serving 2. Precedence of conditions (worker vs static vs redirects vs routing) 3. How to debug and find logs Let me explore the packages/web project to understand the configuration.
globcompleted
Output
/home/jm/data/code/opencode/packages/web/wrangler.jsonc
globcompleted
Output
/home/jm/data/code/opencode/packages/web/vite.config.ts
readcompleted
packages/web/package.json
Output
<file>
00001| {
00002|   "name": "@opencode/web",
00003|   "type": "module",
00004|   "version": "0.10.4",
00005|   "scripts": {
00006|     "dev": "vite dev",
00007|     "build": "vite build",
00008|     "typecheck": "tsc --noEmit",
00009|     "lint": "biome check src/",
00010|     "format": "biome format --write src/",
00011|     "preview": "wrangler dev --env dev",
00012|     "deploy": "bun run build && wrangler deploy --env production"
00013|   },
00014|   "dependencies": {
00015|     "@cloudflare/vite-plugin": "1.21.0",
00016|     "hono": "^4.0.0",
00017|     "solid-js": "^1.9.0"
00018|   },
00019|   "devDependencies": {
00020|     "@biomejs/biome": "^1.9.0",
00021|     "@cloudflare/workers-types": "4.20260117.0",
00022|     "typescript": "^5.7.0",
00023|     "vite": "^6.0.0",
00024|     "vite-plugin-solid": "^2.11.0",
00025|     "wrangler": "^4.0.0"
00026|   }
00027| }
00028| 

(End of file - total 28 lines)
</file>
17,900in / 137out / 79reasoning
assistantJan 18, 01:47:37 AM
glm-4.7 · zai-coding-plan
Let me read the wrangler.jsonc and vite.config.ts files to understand the configuration.
readcompleted
packages/web/wrangler.jsonc
Output
<file>
00001| {
00002|   "$schema": "../../node_modules/wrangler/config-schema.json",
00003|   "name": "opencode-web",
00004|   "main": "src/worker.ts",
00005|   "compatibility_date": "2024-10-30",
00006| 
00007|   "observability": {
00008|     "enabled": false,
00009|     "head_sampling_rate": 1,
00010|     "logs": {
00011|       "enabled": true,
00012|       "head_sampling_rate": 1,
00013|       "persist": true,
00014|       "invocation_logs": true,
00015|     },
00016|   },
00017| 
00018|   // Static assets configuration
00019|   // Note: assets.directory is automatically set by Cloudflare Vite plugin
00020|   "assets": {
00021|     "html_handling": "none",
00022|     "directory": "./dist/",
00023|     "not_found_handling": "single-page-application"
00024|   },
00025| 
00026|   // Custom domain (public)
00027|   // Route bindings can ONLY be set in the TOP-MOST configuration,
00028|   // and are only used during deployment.
00029|   // It is at deployment time that these checks for uniqueness of custom domain is checked.
00030|   //
00031|   // Custom domains are recommended to be used when a worker is acting as the Origin Server.
00032|   // What is an origin server?
00033|   //
00034|   "routes": [
00035|     { "pattern": "opencode.j9xym.com/api/*", "zone_name": "j9xym.com" },
00036|     { "pattern": "opencode.j9xym.com/*", "zone_name": "j9xym.com" },
00037|   ],
00038| 
00039|   // Migration for Durable Object
00040|   "migrations": [
00041|     {
00042|       "tag": "v1",
00043|       "new_classes": ["ShareRoom"],
00044|     },
00045|   ],
00046| 
00047|   // Production environment
00048|   "env": {
00049|     "production": {
00050|       // Service binding to sessions API
00051|       "services": [
00052|         {
00053|           "binding": "SESSIONS_API",
00054|           "service": "opencode-sessions-api",
00055|         },
00056|       ],
00057|     },
00058|   },
00059| }
00060| 
00061| //   #1
00062| //   A custom domain creates a default route simult...
readcompleted
packages/web/vite.config.ts
Output
<file>
00001| import { defineConfig } from "vite"
00002| import { cloudflare } from "@cloudflare/vite-plugin"
00003| import solidPlugin from "vite-plugin-solid"
00004| 
00005| export default defineConfig({
00006|   plugins: [
00007|     solidPlugin(),
00008|     cloudflare(),
00009|   ],
00010| 
00011|   environments: {
00012| 
00013|     client: {
00014|       build: {
00015|         target: "es2022",                    // Target for modern browsers
00016|         sourcemap: false,                    // Don't generate source maps for production
00017|         minify: "esbuild",                   // Minify JavaScript (esbuild is faster than terser)
00018|         emptyOutDir: true,                   // Empty output directory before build
00019|         copyPublicDir: true,                 // Copy files from public/ directory to outDir
00020|         reportCompressedSize: true,          // Report compressed size (useful for monitoring bundle size)
00021| 
00022|         rollupOptions: {  // Explicitly specify the entry point
00023|           input: "./src/index.html",         
00024| 
00025| 
00026|           output: {       // No code splitting needed for a single-page app
00027|             manualChunks: undefined,
00028|             entryFileNames: "js/[name]-[hash].js",
00029|             chunkFileNames: "js/[name]-[hash].js",
00030|             assetFileNames: "assets/[name]-[hash].[ext]",
00031|           },
00032|         },
00033|       },
00034|     },
00035|   },
00036| 
00037|   // Development server
00038|   server: {
00039|     port: 5173,
00040|     proxy: {
00041|       // Proxy API requests to the backend server
00042|       "/": {
00043|         target: "http://localhost:5173",
00044|         changeOrigin: true,
00045|       }
00046|     },
00047|   },
00048| })
00049| 

(End of file - total 49 lines)
</file>
18,425in / 63out / 18reasoning
assistantJan 18, 01:47:41 AM
glm-4.7 · zai-coding-plan
Now let me look at the worker.ts file to understand how the worker handles requests, and also check for any other configuration related to static assets.
readcompleted
packages/web/src/worker.ts
Output
<file>
00001| import { Hono } from "hono"
00002| import { cors } from "hono/cors"
00003| 
00004| interface Bindings {
00005|   SESSIONS_API?: {
00006|     fetch: (request: Request) => Promise<Response>
00007|   }
00008|   ASSETS?: {
00009|     fetch: (request: Request) => Promise<Response>
00010|   }
00011| }
00012| 
00013| const app = new Hono<{ Bindings: Bindings }>()
00014| 
00015| // CORS for API/WS
00016| app.use("/api/*", cors())
00017| app.use("/ws/*", cors())
00018| 
00019| // API proxy (internal only)
00020| app.all("/api/*", async (c) => {
00021|   const path = c.req.path
00022|   console.log("Handling request in the web worker from /api/* to SESSIONS_API:", path)
00023| 
00024|   try {
00025|     const response = await c.env.SESSIONS_API!.fetch(
00026|       new Request(`http://internal${path}`, {
00027|         method: c.req.method,
00028|         headers: c.req.raw.headers,
00029|         body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.blob() : null,
00030|       }),
00031|     )
00032| 
00033|     return new Response(response.body, {
00034|       status: response.status,
00035|       statusText: response.statusText,
00036|       headers: response.headers,
00037|     })
00038|   } catch (error) {
00039|     return c.json({ error: "API unavailable" }, 503)
00040|   }
00041| })
00042| 
00043| // WebSocket endpoint - proxy to sessions API
00044| app.get("/ws/:shareId", async (c) => {
00045|   const shareId = c.req.param("shareId")
00046| 
00047|   try {
00048|     const response = await c.env.SESSIONS_API!.fetch(
00049|       new Request(`http://internal/ws/${shareId}`, {
00050|         method: c.req.method,
00051|         headers: c.req.raw.headers,
00052|         body: c.req.method !== "GET" ? await c.req.text() : null,
00053|       }),
00054|     )
00055| 
00056|     return new Response(response.body, {
00057|       status: response.status,
00058|       statusText: response.statusText,
00059|       headers: response.headers,
00060|...
globcompleted
Output
/home/jm/data/code/opencode/packages/web/src/worker.ts
/home/jm/data/code/opencode/packages/web/src/pages/[...slug].md.ts
/home/jm/data/code/opencode/packages/web/src/share-room.ts
/home/jm/data/code/opencode/packages/web/src/types/lang-map.d.ts
/home/jm/data/code/opencode/packages/web/src/api.ts
20,174in / 65out / 29reasoning
assistantJan 18, 01:47:44 AM
glm-4.7 · zai-coding-plan
Now I have a good understanding of the setup. Let me also look at what Cloudflare documentation says about Workers and static assets to provide accurate information about precedence and debugging.
cloudflare-docs_search_cloudflare_documentationcompleted
Output
<result>
<url>https://developers.cloudflare.com/https://developers.cloudflare.com/workers/static-assets/binding/</url>
<title></title>
<text>
/binding/#directory) and, optionally, an [assets binding](/workers/static-assets/binding/), in your Worker's Wrangler file. The [assets binding](/workers/static-assets/binding/) allows you to dynamically fetch assets from within your Worker script (e.g. `env.ASSETS.fetch()`), similarly to how you might with a make a `fetch()` call with a [Service binding](/workers/runtime-apis/bindings/service-bindings/http/).

Only one collection of static assets can be configured in each Worker.

## `directory`

The fold

# Configuration and Bindings

Configuring a Worker with assets requires specifying a [directory](/workers/static-assets/binding/#directory) and, optionally, an [assets binding](/workers/static-assets/binding/), in your Worker's Wrangler file. The [assets binding](/workers/static-assets/binding/) allows you to dynamically fetch assets from within your Worker script (e.g. `env.ASSETS.fetch()`), similarly to how you might with a make a `fetch()` call with a [Service binding](/workers/runtime-apis/bindings/service-bindings/http/).

Only one collection of static assets can be configured in each Worker.

## `directory`

The folder of static assets to be served. For many frameworks, this is the `./public/`, `./dist/`, or `./build/` folder.

* [  wrangler.jsonc ](#tab-panel-3811)
* [  wrangler.toml ](#tab-panel-3812)

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "my-worker",

  "compatibility_date": "2024-09-19",

  "assets": {

    "directory": "./public/"

  }

}


```

```

name = "my-worker"

compatibility_date = "2024-09-19"

assets = { directory = "./public/" }


```

### Ignoring assets

Sometime there are files in the asset directory that should not be uploaded.

In this case, create a `.assetsignore` file in the root of the assets directory. This file takes the same format as `.gitignore`.
...
2,631in / 61out / 34reasoning
assistantJan 18, 01:47:50 AM
glm-4.7 · zai-coding-plan
Let me search for more information about the routing behavior and `run_worker_first` option.
cloudflare-docs_search_cloudflare_documentationcompleted
Output
<result>
<url>https://developers.cloudflare.com/https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/</url>
<title></title>
<text>

##### `_routes.json` and Pages Functions middleware

If you authored [a \_routes.json file](/pages/functions/routing/#create-a-%5Froutesjson-file) in your Pages project, or used [middleware](/pages/functions/middleware/) in Pages Functions, you must pay close attention to the configuration of your Worker script. Pages would default to serving your Pages Functions ahead of static assets and `_routes.json` and Pages Functions middleware allowed you to customize this behavior.

Workers, on the other hand, will default to serving static assets ahead of your Worker script, unless you have configured [assets.run\_worker\_first](/workers/static-assets/routing/worker-script/#run-your-worker-script-first). This option is required if you are, for example, performing any authentication checks or logging requests before serving static assets.

* [  wrangler.jsonc ](#tab-panel-6112)
* [  wrangler.toml ](#tab-panel-6113)

```

{

  "name": "my-worker",

  "compatibility_date": "2025-04-01",

  "main": "./dist/worker/index.js",

  "assets": {

    "directory": "./dist/client/",

    "run_worker_first": true

  }

}


```

```

name = "my-worker"

compatibility_date = "2025-04-01"

main = "./dist/worker/index.js"


[assets]

directory = "./dist/client/"

run_worker_first = true


```


</text>
</result>
<result>
<url>https://developers.cloudflare.com/https://developers.cloudflare.com/workers/static-assets/binding/</url>
<title></title>
<text>

## `run_worker_first`

Controls whether to invoke the Worker script regardless of a request which would have otherwise matched an asset. `run_worker_first = false` (default) will serve any static asset matching a request, while `run_worker_first = true` will unconditionally [invoke your Worker script](/workers/static-assets/routing/worker-script/#run-your-worker-script-first...
2,491in / 48out / 18reasoning
assistantJan 18, 01:47:55 AM
glm-4.7 · zai-coding-plan
Now let me search for information about logging and debugging in Workers.
cloudflare-docs_search_cloudflare_documentationcompleted
Output
<result>
<url>https://developers.cloudflare.com/https://developers.cloudflare.com/workers/observability/errors/</url>
<title></title>
<text>

## Debug exceptions with Workers Logs

[Workers Logs](/workers/observability/logs/workers-logs) is a powerful tool for debugging your Workers. It shows all the historic logs generated by your Worker, including any uncaught exceptions that occur during execution.

To find all your errors in Workers Logs, you can use the following filter: `$metadata.error EXISTS`. This will show all the logs that have an error associated with them. You can also filter by `$workers.outcome` to find the requests that resulted in an error. For example, you can filter by `$workers.outcome = "exception"` to find all the requests that resulted in an uncaught exception.

All the possible outcome values can be found in the [Workers Trace Event](/logs/logpush/logpush-job/datasets/account/workers%5Ftrace%5Fevents/#outcome) reference.

## Debug exceptions from `Wrangler`

To debug your worker via wrangler use `wrangler tail` to inspect and fix the exceptions.

Exceptions will show up under the `exceptions` field in the JSON returned by `wrangler tail`. After you have identified the exception that is causing errors, redeploy your code with a fix, and continue tailing the logs to confirm that it is fixed.


-------------------------------------------------------------------------------------------------------------------- |
| Response Stream Disconnected | Connection was terminated during the deferred proxying stage of a Worker request flow. It commonly appears for longer lived connections such as [WebSockets](/workers/runtime-apis/websockets/). |
| Cancelled                    | The Client disconnected before the Worker completed its response.                                                                                                                                |

## Debug exceptions with Workers Logs

[Workers Logs](/workers/observability/logs/worke...
26,797in / 40out / 13reasoning
assistantJan 18, 01:48:03 AM
glm-4.7 · zai-coding-plan
Now I have comprehensive information about the configuration. Let me provide a complete answer to the user's questions about: 1. Configuration variables involved in static asset serving 2. Precedence of conditions 3. How to debug and find logs Based on my analysis: ## Configuration Variables in wrangler.jsonc (packages/web/wrangler.jsonc): 1. `assets.directory` - "./dist/" - where static assets are located 2. `assets.html_handling` - "none" - how HTML requests are handled 3. `assets.not_found_handling` - "single-page-application" - what happens when no asset is found 4. `assets.binding` - "ASSETS" - the binding name for programmatic access (implied) 5. `assets.run_worker_first` - (not set, defaults to false) - whether worker runs before assets ## In the worker code (packages/web/src/worker.ts): - The ASSETS binding is used at packages/web/src/worker.ts:70-71 - Routes are handled by Hono framework ## Precedence: 1. Static assets (run_worker_first=false by default) 2. Worker script (if no asset match, and ASSETS binding is available) 3. Worker can delegate back to assets via `env.ASSETS.fetch()` ## Debugging: - `wrangler dev` - local development - `wrangler tail` - real-time logs - `wrangler dev --remote` - test on remote - Cloudflare Dashboard -> Workers & Pages -> Workers Logs - Console.log in worker code - observability.logs settings in wrangler config

Configuration Variables

wrangler.jsonc (lines 18-24):

  • assets.directory: "./dist/" - static asset location
  • assets.html_handling: "none" - HTML request handling
  • assets.not_found_handling: "single-page-application" - fallback behavior
  • assets.binding: "ASSETS" (implicit) - programmatic access via env.ASSETS.fetch()
  • assets.run_worker_first: not set (defaults to false)

Precedence (highest to lowest):

  1. Static assets - When run_worker_first: false (default), Cloudflare serves matching assets directly from assets.directory without invoking worker
  2. Worker script - Invoked only if no asset matches the path
  3. Worker delegate to assets - Worker can explicitly call env.ASSETS.fetch() (worker.ts:70-71)
  4. not_found_handling - Applied when asset not found and worker doesn't respond

Selective worker-first routing (if configured):

  • run_worker_first: true - worker always runs first
  • run_worker_first: ["/api/*"] - pattern-based, negative patterns (!) have precedence

Debugging

Local development:

bun run dev          # vite dev
wrangler dev --env dev     # worker dev
wrangler dev --remote      # test against remote CF infrastructure

Real-time logs:

wrangler tail              # live console logs from deployed worker

Production logs:

  • Dashboard → Workers & Pages → Your Worker → Logs
  • Filter: $metadata.error EXISTS for errors
  • Configure observability.logs in wrangler.jsonc (lines 7-15)

Console logging in worker.ts:

  • console.log() in worker.ts:22, 62 for API/WS requests
  • Logs appear in wrangler tail or dashboard
11,749in / 747out / 334reasoning
userJan 18, 01:49:38 AM

How can I rearrange or maniuplate the position of the assets in relation to the way they are refernced by the html and code?

assistantJan 18, 01:49:38 AM
glm-4.7 · zai-coding-plan
The user is asking about how to rearrange or manipulate the position of assets in relation to how they're referenced by HTML and code. This is about how Vite outputs files and how they're referenced in the built HTML/JS. Looking at the vite.config.ts file: - Entry point is `./src/index.html` - Output file names: - `entryFileNames: "js/[name]-[hash].js"` - `chunkFileNames: "js/[name]-[hash].js"` - `assetFileNames: "assets/[name]-[hash].[ext]"` So the user can control: 1. The directory structure of output files 2. The naming patterns (including hashes) 3. How assets are referenced in HTML/JS (Vite handles this automatically via its build system) The user might want to: - Change output directory structure - Remove hashes from filenames - Change asset file naming patterns - Control how assets are referenced (base paths, public paths, etc.) I should explain the Vite configuration options that control this.

Vite Output Configuration (vite.config.ts:22-31)

rollupOptions: {
  input: "./src/index.html",
  output: {
    entryFileNames: "js/[name]-[hash].js",      // Main JS files
    chunkFileNames: "js/[name]-[hash].js",       // Code split chunks
    assetFileNames: "assets/[name]-[hash].[ext]", // CSS, images, fonts
  },
}

Common Manipulations

Remove hashes (for debugging/CDNs):

entryFileNames: "js/[name].js"
chunkFileNames: "js/[name].js"
assetFileNames: "assets/[name].[ext]"

Change directory structure:

// Flatten everything
entryFileNames: "[name].js"
assetFileNames: "[name].[ext]"

// Organize by type
assetFileNames: "[ext]/[name]-[hash].[ext]"

Custom public path (asset base URL):

base: "/cdn/",
// or environment-specific
base: process.env.CDN_URL || "/"

Manual asset import control:

// In src/index.html or .tsx files
import logo from "./logo.png?url"  // Get URL instead of bundling
import styles from "./style.css?inline"  // Inline CSS

Important Notes

  • Vite automatically updates HTML references during build
  • Hashes in filenames enable cache busting (recommended for production)
  • copyPublicDir: true copies public/ to output root
  • Worker fetches assets via paths relative to dist/ (wrangler.jsonc:22)
30,029in / 588out / 219reasoning
userJan 18, 01:57:38 AM

What is causing it to obuild into ./dist/src/client/index.html ?

assistantJan 18, 01:57:38 AM
glm-4.7 · zai-coding-plan
The user is asking why the build is outputting to `./dist/src/client/index.html` instead of just `./dist/index.html`. Let me look at the Vite configuration more carefully to understand the output directory structure. Looking at the vite.config.ts I read earlier: - There's no explicit `build.outDir` set, so it defaults to `dist` - The rollupOptions specify `input: "./src/index.html"` - The output uses `entryFileNames: "js/[name]-[hash].js"` etc. But wait - the user mentioned they're building with `vite build` and getting output in `./dist/src/client/index.html`. This is unusual because the default Vite behavior would be to output to `./dist/index.html`. Let me think about what could cause this: 1. There might be an `outDir` configuration I missed 2. The Cloudflare Vite plugin might be affecting the output 3. There could be environment-specific configuration Looking at the wrangler.jsonc, it mentions that `assets.directory` is automatically set by the Cloudflare Vite plugin (line 19 comment). Let me search for more configuration or check if there's anything in the project that might be affecting the output directory. I should also check if there's a vite.config in other environments or if the build script is doing something custom. Let me search for any other vite configurations or build scripts that might be involved.
bashcompleted
Check build script in package.json
Output
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "typecheck": "tsc --noEmit",
    "lint": "biome check src/",
    "format": "biome format --write src/",
grepcompleted
outDir|build\.outDir|outputDir
Output
Found 2 matches
/home/jm/data/code/opencode/packages/web/vite.config.ts:
  Line 19:         // copyPublicDir: true,                 // Copy files from public/ directory to outDir

/home/jm/data/code/opencode/packages/web/tsconfig.json:
  Line 22:     "outDir": "./dist",
bashcompleted
Run build to see actual output
Output
$ vite build
vite v6.4.1 building SSR bundle for production...
transforming...
✓ 28 modules transformed.
rendering chunks...
Using vars defined in .env
dist/opencode_web/.dev.vars             0.15 kB
dist/opencode_web/.vite/manifest.json   0.16 kB
dist/opencode_web/wrangler.json         1.55 kB
dist/opencode_web/index.js             48.38 kB
✓ built in 82ms
vite v6.4.1 building for production...
transforming...
✓ 10 modules transformed.
rendering chunks...
computing gzip size...
dist/client/src/index.html   0.91 kB │ gzip: 0.51 kB
dist/client/index.js        17.70 kB │ gzip: 6.78 kB
✓ built in 120ms
30,414in / 414out / 295reasoning
assistantJan 18, 01:57:52 AM
glm-4.7 · zai-coding-plan
Interesting! The build output shows two separate builds happening: 1. First build (SSR bundle for production): - Output to `dist/opencode_web/` - Generates: `.dev.vars`, `.vite/manifest.json`, `wrangler.json`, `index.js` 2. Second build (client production): - Output to `dist/client/src/index.html` - this is the one the user is asking about! - Also generates `dist/client/index.js` So the output is `dist/client/src/index.html`, not `dist/src/client/index.html` as the user mentioned. But close enough - they're asking about the nested path structure. The key insight is that the Cloudflare Vite plugin is doing two builds: 1. A server/worker build (SSR) to `dist/opencode_web/` 2. A client build to `dist/client/` But the client build is outputting `index.html` to `dist/client/src/index.html` because the input is `./src/index.html` and there's no `outDir` specified. So it's preserving the directory structure from the input path. Let me check the @cloudflare/vite-plugin documentation to understand this better and also look at what the expected behavior is.
cloudflare-docs_search_cloudflare_documentationcompleted
Output
<result>
<url>https://developers.cloudflare.com/https://developers.cloudflare.com/workers/vite-plugin/tutorial/</url>
<title></title>
<text>

### Create your Worker config file

* [  wrangler.jsonc ](#tab-panel-3872)
* [  wrangler.toml ](#tab-panel-3873)

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "cloudflare-vite-tutorial",

  "compatibility_date": "2025-04-03",

  "assets": {

    "not_found_handling": "single-page-application"

  }

}


```

```

name = "cloudflare-vite-tutorial"

compatibility_date = "2025-04-03"

assets = { not_found_handling = "single-page-application" }


```

The [not\_found\_handling](/workers/static-assets/routing/single-page-application/) value has been set to `single-page-application`. This means that all not found requests will serve the `index.html` file. With the Cloudflare plugin, the `assets` routing configuration is used in place of Vite's default behavior. This ensures that your application's [routing configuration](/workers/static-assets/routing/) works the same way while developing as it does when deployed to production.

Note that the [directory](/workers/static-assets/binding/#directory) field is not used when configuring assets with Vite. The `directory` in the output configuration will automatically point to the client build output. See [Static Assets](/workers/vite-plugin/reference/static-assets/) for more information.

Note

When using the Cloudflare Vite plugin, the Worker config (for example, `wrangler.jsonc`) that you provide is the input configuration file. A separate output `wrangler.json` file is created when you run `vite build`. [CONTENT TRIMMED DUE TO SIZE]
`

The [not\_found\_handling](/workers/static-assets/routing/single-page-application/) value has been set to `single-page-application`. This means that all not found requests will serve the `index.html` file. With the Cloudflare plugin, the `assets` routing configuration is used in place of Vite's default behavior. This ensure...
31,148in / 285out / 256reasoning
assistantJan 18, 01:58:04 AM
glm-4.7 · zai-coding-plan
Now I understand the issue better. The build output shows two builds happening: 1. SSR build to `dist/opencode_web/` (the worker) 2. Client build to `dist/client/` The client build is outputting `dist/client/src/index.html` because: - The input is `./src/index.html` (vite.config.ts:23) - There's no `outDir` specified in the client build config - So Vite preserves the directory structure from input to output The Cloudflare Vite plugin creates two separate build outputs and uses the plugin's defaults. The client environment output needs to have an `outDir` configured to flatten the structure. Let me explain this to the user and show them how to fix it.

Root Cause

The Cloudflare Vite plugin runs two builds:

  1. Server/Worker builddist/opencode_web/ (SSR bundle)
  2. Client builddist/client/src/index.html (preserves input directory structure)

Your rollupOptions.input: "./src/index.html" preserves the src/ directory in the output because no outDir is specified for the client environment.

Fix: Add outDir to client environment

environments: {
  client: {
    build: {
      outDir: "./dist/client",           // Add this line
      emptyOutDir: true,
      // ... rest of config
    },
  },
}

This will output:

  • dist/client/index.html (not dist/client/src/index.html)
  • dist/client/js/...
  • dist/client/assets/...

Then update wrangler.jsonc (line 22):

"assets": {
  "directory": "./dist/client",  // Change from "./dist/"
}

Why This Happens

The Cloudflare plugin separates concerns:

  • Worker codedist/opencode_web/ (executable, includes bindings)
  • Static assetsdist/client/ (served directly, faster edge caching)
3,646in / 435out / 153reasoning
userJan 18, 02:33:35 AM

/exit

assistantJan 18, 02:33:35 AM
glm-4.7 · zai-coding-plan