Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# master

- 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.
- BREAKING: Remove `ResX.BunUtils.serveStaticFile`; static assets now go through generated `ResXAssets.staticAssetRoutes`.
- 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.

# 1.2.1
Expand Down
179 changes: 131 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,44 +135,43 @@ let port = 4444
let server = Bun.serve({
port,
development: ResX.BunUtils.isDev,
fetch: async (request, server) => {
open Bun

// Serve static files first
switch await ResX.BunUtils.serveStaticFile(request) {
| Some(staticResponse) => staticResponse
| None =>
// Handle the request using the ResX handler if this wasn't a static file request.
// Note: By default, all HTMX handler routes are prefixed with "_api", and all form action routes are prefixed with "_form".
await Handler.handler.handleRequest({
request,
setupHeaders: () => {
// 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.
Headers.makeWithInit(FromArray([("Content-Type", "text/html")]))
},
render: async ({path, requestController, headers}) => {
// This handles the actual request.
switch path {
| list{"sitemap.xml"} => <SiteMap />
| appRoutes =>
requestController.appendTitleSegment("Test App")
<Html>
<div>
{switch appRoutes {
| list{} =>
<div> {Hjsx.string("Start page!")} </div>
| list{"moved"} =>
requestController.redirect("/start", ~status=302)
| _ =>
requestController.setStatus(404)
<div>{Hjsx.string("404")}</div>
}}
</div>
</Html>
}
},
})
}
routes: Dict.assign(
dict{
"/health": {get: Bun.Static(Response.make("ok"))},
},
ResXAssets.staticAssetRoutes,
),
fetch: async (request, _server) => {
// Handle the request using the ResX handler if this wasn't a static route.
// Note: By default, all HTMX handler routes are prefixed with "_api", and all form action routes are prefixed with "_form".
await Handler.handler.handleRequest({
request,
setupHeaders: () => {
// 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.
Headers.make(~init=FromArray([("Content-Type", "text/html")]))
},
render: async ({path, requestController, headers}) => {
// This handles the actual request.
switch path {
| list{"sitemap.xml"} => <SiteMap />
| appRoutes =>
requestController.appendTitleSegment("Test App")
<Html>
<div>
{switch appRoutes {
| list{} =>
<div> {Hjsx.string("Start page!")} </div>
| list{"moved"} =>
requestController.redirect("/start", ~status=302)
| _ =>
requestController.setStatus(404)
<div>{Hjsx.string("404")}</div>
}}
</div>
</Html>
}
},
})
},
})

Expand Down Expand Up @@ -215,20 +214,97 @@ switch path {

## Static assets

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:
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`:

```rescript
fetch: async (request, server) => {
open Bun

switch await ResX.BunUtils.serveStaticFile(request) {
| Some(staticResponse) => staticResponse
| None =>
await Handler.handler.handleRequest({
...
let server = Bun.serve({
port,
routes: ResXAssets.staticAssetRoutes,
fetch: async (request, _server) =>
await Handler.handler.handleRequest({
request,
...
}),
})
```

`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.
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:

```rescript
Bun.serve({
port,
routes: Dict.assign(
dict{
"/health": {get: Bun.Static(Response.make("ok"))},
},
ResXAssets.staticAssetRoutes,
),
fetch: async (request, _server) =>
await Handler.handler.handleRequest({
request,
...
}),
})
```

If you want to configure how these generated static asset routes behave, pass `staticAssetRoutes` to the Vite plugin:

> These settings only apply to generated static asset routes, not your normal app routes.

```js
// vite.config.js
import { defineConfig } from "vite";
import resXVitePlugin from "rescript-x/res-x-vite-plugin.mjs";

export default defineConfig({
plugins: [
resXVitePlugin({
staticAssetRoutes: {
headers: {
"/assets/**": {
"Cache-Control": "public, max-age=31536000, immutable",
},
"/robots.txt": {
"Cache-Control": "public, max-age=300",
},
},
},
}),
],
});
```

`staticAssetRoutes.headers` is an object where:

- Each key is a route pattern for generated static asset routes.
- Each value is a map of response headers to apply when that pattern matches.
- Exact paths like `"/robots.txt"` match only that route.
- `*` matches a single path segment, for example `"/assets/*"`.
- `**` matches any remaining path depth, for example `"/assets/**"`.
- If multiple patterns match the same route, the last matching rule wins.

So this:

```js
headers: {
"/assets/**": {
"Cache-Control": "public, max-age=31536000, immutable",
},
"/robots.txt": {
"Cache-Control": "public, max-age=300",
},
}
```

means:

- all generated `/assets/...` routes get long-lived immutable caching
- `/robots.txt` gets a shorter cache policy
- nothing outside the generated static asset routes is affected

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.

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.

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

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

Nested paths are preserved as well:

```
// public/assets/logo.svg exists
GET /assets/logo.svg
```

### `assets` for assets that do need transformation

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.
Expand Down
3 changes: 2 additions & 1 deletion demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
lib
dist
out
src/__generated__/res-x-assets.js
src/__generated__/res-x-assets.js
src/__generated__/res-x-static-routes.js
Loading
Loading