Skip to content

Commit f92ce53

Browse files
authored
Revamp static assets (#26)
* move to proper static asset build time emission * Rebuild demo JS after rebase
1 parent d18b9b3 commit f92ce53

34 files changed

Lines changed: 1556 additions & 754 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# master
22

33
- Expose `RequestController` and `Handlers` as record-of-functions APIs so user code can migrate from `requestController->RequestController.setStatus(404)` to `requestController.setStatus(404)` and from `handler->ResX.Handlers.handleRequest({...})` to `handler.handleRequest({...})`; the old free-function surface is still available but deprecated.
4+
- BREAKING: Remove `ResX.BunUtils.serveStaticFile`; static assets now go through generated `ResXAssets.staticAssetRoutes`.
45
- Add `__rawProps?: Dict.t<JSON.t>` on JSX DOM props as a low-level escape hatch for arbitrary attributes (escaped values, invalid/non-serializable entries ignored, may emit duplicates after typed props), with accompanying tests and README docs.
56

67
# 1.2.1

README.md

Lines changed: 131 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -135,44 +135,43 @@ let port = 4444
135135
let server = Bun.serve({
136136
port,
137137
development: ResX.BunUtils.isDev,
138-
fetch: async (request, server) => {
139-
open Bun
140-
141-
// Serve static files first
142-
switch await ResX.BunUtils.serveStaticFile(request) {
143-
| Some(staticResponse) => staticResponse
144-
| None =>
145-
// Handle the request using the ResX handler if this wasn't a static file request.
146-
// Note: By default, all HTMX handler routes are prefixed with "_api", and all form action routes are prefixed with "_form".
147-
await Handler.handler.handleRequest({
148-
request,
149-
setupHeaders: () => {
150-
// You can do any basic headers setup here that you want. These can be overwritten easily by your main application regardless of what you set here.
151-
Headers.makeWithInit(FromArray([("Content-Type", "text/html")]))
152-
},
153-
render: async ({path, requestController, headers}) => {
154-
// This handles the actual request.
155-
switch path {
156-
| list{"sitemap.xml"} => <SiteMap />
157-
| appRoutes =>
158-
requestController.appendTitleSegment("Test App")
159-
<Html>
160-
<div>
161-
{switch appRoutes {
162-
| list{} =>
163-
<div> {Hjsx.string("Start page!")} </div>
164-
| list{"moved"} =>
165-
requestController.redirect("/start", ~status=302)
166-
| _ =>
167-
requestController.setStatus(404)
168-
<div>{Hjsx.string("404")}</div>
169-
}}
170-
</div>
171-
</Html>
172-
}
173-
},
174-
})
175-
}
138+
routes: Dict.assign(
139+
dict{
140+
"/health": {get: Bun.Static(Response.make("ok"))},
141+
},
142+
ResXAssets.staticAssetRoutes,
143+
),
144+
fetch: async (request, _server) => {
145+
// Handle the request using the ResX handler if this wasn't a static route.
146+
// Note: By default, all HTMX handler routes are prefixed with "_api", and all form action routes are prefixed with "_form".
147+
await Handler.handler.handleRequest({
148+
request,
149+
setupHeaders: () => {
150+
// You can do any basic headers setup here that you want. These can be overwritten easily by your main application regardless of what you set here.
151+
Headers.make(~init=FromArray([("Content-Type", "text/html")]))
152+
},
153+
render: async ({path, requestController, headers}) => {
154+
// This handles the actual request.
155+
switch path {
156+
| list{"sitemap.xml"} => <SiteMap />
157+
| appRoutes =>
158+
requestController.appendTitleSegment("Test App")
159+
<Html>
160+
<div>
161+
{switch appRoutes {
162+
| list{} =>
163+
<div> {Hjsx.string("Start page!")} </div>
164+
| list{"moved"} =>
165+
requestController.redirect("/start", ~status=302)
166+
| _ =>
167+
requestController.setStatus(404)
168+
<div>{Hjsx.string("404")}</div>
169+
}}
170+
</div>
171+
</Html>
172+
}
173+
},
174+
})
176175
},
177176
})
178177
@@ -215,20 +214,97 @@ switch path {
215214

216215
## Static assets
217216

218-
ResX comes with full static asset (fonts, images, etc) handling via Vite, that you can use if you want. In order to actually serve the static assets, make sure you use `ResX.BunUtils.serveStaticFile` before trying to handle your request in another way:
217+
ResX comes with full static asset (fonts, images, etc) handling via Vite, that you can use if you want. The asset pipeline generates Bun-ready static routes for you under `ResXAssets.staticAssetRoutes`:
219218

220219
```rescript
221-
fetch: async (request, server) => {
222-
open Bun
223-
224-
switch await ResX.BunUtils.serveStaticFile(request) {
225-
| Some(staticResponse) => staticResponse
226-
| None =>
227-
await Handler.handler.handleRequest({
228-
...
220+
let server = Bun.serve({
221+
port,
222+
routes: ResXAssets.staticAssetRoutes,
223+
fetch: async (request, _server) =>
224+
await Handler.handler.handleRequest({
225+
request,
226+
...
227+
}),
228+
})
229229
```
230230

231-
`ResX.BunUtils.serveStaticFile` check if the request is for a static file, and if it is return a response serving that static file via `Bun`. If it's not a static file request, you continue as usual with serving the response.
231+
If you want to add your own Bun static routes, `staticAssetRoutes` is a regular `Dict.t`, so you can merge it the same way you would merge any other ReScript dict:
232+
233+
```rescript
234+
Bun.serve({
235+
port,
236+
routes: Dict.assign(
237+
dict{
238+
"/health": {get: Bun.Static(Response.make("ok"))},
239+
},
240+
ResXAssets.staticAssetRoutes,
241+
),
242+
fetch: async (request, _server) =>
243+
await Handler.handler.handleRequest({
244+
request,
245+
...
246+
}),
247+
})
248+
```
249+
250+
If you want to configure how these generated static asset routes behave, pass `staticAssetRoutes` to the Vite plugin:
251+
252+
> These settings only apply to generated static asset routes, not your normal app routes.
253+
254+
```js
255+
// vite.config.js
256+
import { defineConfig } from "vite";
257+
import resXVitePlugin from "rescript-x/res-x-vite-plugin.mjs";
258+
259+
export default defineConfig({
260+
plugins: [
261+
resXVitePlugin({
262+
staticAssetRoutes: {
263+
headers: {
264+
"/assets/**": {
265+
"Cache-Control": "public, max-age=31536000, immutable",
266+
},
267+
"/robots.txt": {
268+
"Cache-Control": "public, max-age=300",
269+
},
270+
},
271+
},
272+
}),
273+
],
274+
});
275+
```
276+
277+
`staticAssetRoutes.headers` is an object where:
278+
279+
- Each key is a route pattern for generated static asset routes.
280+
- Each value is a map of response headers to apply when that pattern matches.
281+
- Exact paths like `"/robots.txt"` match only that route.
282+
- `*` matches a single path segment, for example `"/assets/*"`.
283+
- `**` matches any remaining path depth, for example `"/assets/**"`.
284+
- If multiple patterns match the same route, the last matching rule wins.
285+
286+
So this:
287+
288+
```js
289+
headers: {
290+
"/assets/**": {
291+
"Cache-Control": "public, max-age=31536000, immutable",
292+
},
293+
"/robots.txt": {
294+
"Cache-Control": "public, max-age=300",
295+
},
296+
}
297+
```
298+
299+
means:
300+
301+
- all generated `/assets/...` routes get long-lived immutable caching
302+
- `/robots.txt` gets a shorter cache policy
303+
- nothing outside the generated static asset routes is affected
304+
305+
ResX always generates exact Bun routes for the static assets it knows about at build time. That keeps the runtime simple: Bun just loads a generated file that already contains the route table and any configured headers.
306+
307+
This built-in pipeline is intended for standard webapp asset sets. If you have so many generated static asset routes that Bun startup is becoming slow, that is a sign that you should implement your own asset loading pipeline instead of pushing the built-in one further.
232308

233309
As for the assets themselves, there are two ways of handling them in ResX:
234310

@@ -241,6 +317,13 @@ Putting assets in the `public` directory. Any assets you put in the top level `p
241317
GET /robots.txt
242318
```
243319

320+
Nested paths are preserved as well:
321+
322+
```
323+
// public/assets/logo.svg exists
324+
GET /assets/logo.svg
325+
```
326+
244327
### `assets` for assets that do need transformation
245328

246329
If you have assets you'd like transformed by Vite before using, put them in the top level `assets` folder. This could be CSS, images, additional JavaScript, and so on. Anything you might want Vite to transform.

demo/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ node_modules
22
lib
33
dist
44
out
5-
src/__generated__/res-x-assets.js
5+
src/__generated__/res-x-assets.js
6+
src/__generated__/res-x-static-routes.js

0 commit comments

Comments
 (0)