From 8551e1d86847edf253ffa505186c2cb8d6c5164b Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 8 Mar 2026 22:03:35 +0100 Subject: [PATCH 1/2] move to proper static asset build time emission --- CHANGELOG.md | 1 + README.md | 179 ++++++++---- demo/.gitignore | 3 +- demo/package-lock.json | 206 +++++++++++--- demo/package.json | 4 +- demo/src/DeferredComponent.js | 28 +- demo/src/Demo.js | 190 ++++++------- demo/src/Demo.res | 61 ++-- demo/src/ErrorMessage.js | 8 +- demo/src/Exporter.js | 6 +- demo/src/FailingComponent.js | 6 +- demo/src/FailingComponent.res | 2 +- demo/src/FourOhFour.js | 18 +- demo/src/Head.js | 16 +- demo/src/Hello.js | 123 ++++----- demo/src/Html.js | 58 ++-- demo/src/HtmxHandler.js | 10 +- demo/src/Navigation.js | 42 +-- demo/src/SiteMap.js | 62 ++--- demo/src/StreamTest.js | 46 ++-- demo/src/U.js | 4 +- demo/src/UserFriends.js | 4 +- demo/src/UserPage.js | 280 ++++++++++--------- demo/src/UserPage.res | 2 +- demo/src/UserRoutes.js | 30 +- demo/src/__generated__/ResXAssets.js | 8 +- demo/src/__generated__/ResXAssets.res | 4 +- package-lock.json | 42 +++ package.json | 1 + res-x-vite-plugin.mjs | 383 ++++++++++++++++++++++++-- src/BunUtils.js | 49 ---- src/BunUtils.res | 70 ----- src/BunUtils.resi | 2 - test/StaticAssetRoutes.test.js | 357 ++++++++++++++++++++++++ 34 files changed, 1558 insertions(+), 747 deletions(-) create mode 100644 test/StaticAssetRoutes.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2710f..b68d4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` 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 diff --git a/README.md b/README.md index cf4adcc..667828f 100644 --- a/README.md +++ b/README.md @@ -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"} => - | appRoutes => - requestController.appendTitleSegment("Test App") - -
- {switch appRoutes { - | list{} => -
{Hjsx.string("Start page!")}
- | list{"moved"} => - requestController.redirect("/start", ~status=302) - | _ => - requestController.setStatus(404) -
{Hjsx.string("404")}
- }} -
- - } - }, - }) - } + 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"} => + | appRoutes => + requestController.appendTitleSegment("Test App") + +
+ {switch appRoutes { + | list{} => +
{Hjsx.string("Start page!")}
+ | list{"moved"} => + requestController.redirect("/start", ~status=302) + | _ => + requestController.setStatus(404) +
{Hjsx.string("404")}
+ }} +
+ + } + }, + }) }, }) @@ -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: @@ -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. diff --git a/demo/.gitignore b/demo/.gitignore index 4d97799..63e43fe 100644 --- a/demo/.gitignore +++ b/demo/.gitignore @@ -2,4 +2,5 @@ node_modules lib dist out -src/__generated__/res-x-assets.js \ No newline at end of file +src/__generated__/res-x-assets.js +src/__generated__/res-x-static-routes.js diff --git a/demo/package-lock.json b/demo/package-lock.json index 934a498..7536680 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -12,8 +12,8 @@ "@rescript/core": "1.6.0", "concurrently": "^8.2.1", "fast-glob": "^3.3.1", - "rescript": "11.1.4", - "rescript-bun": "0.5.0", + "rescript": "12.0.0-rc.3", + "rescript-bun": "2.1.0", "rescript-x": "../", "vite": "^4.4.11" }, @@ -25,21 +25,20 @@ }, "..": { "name": "rescript-x", - "version": "0.4.0", + "version": "1.2.1", "license": "MIT", "dependencies": { + "chokidar": "^4.0.3", "fast-glob": "^3.3.1" }, "devDependencies": { - "@rescript/core": "1.6.0", "fast-glob": "^3.3.1", - "rescript": "11.1.4", - "rescript-bun": "0.5.0" + "rescript": "12.0.0-rc.3", + "rescript-bun": "2.1.0" }, "peerDependencies": { - "@rescript/core": ">=1.0.0", - "rescript": ">=11.1.0", - "rescript-bun": ">=0.4.1", + "rescript": ">=12.0.0-0 <13.0.0", + "rescript-bun": ">=2.1.0", "vite": ">=4.4.11" } }, @@ -511,6 +510,86 @@ "rescript": ">=11.1.0" } }, + "node_modules/@rescript/darwin-arm64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/darwin-arm64/-/darwin-arm64-12.0.0-rc.3.tgz", + "integrity": "sha512-d6OMJWnW8I9DgYPNw+9Gy72tOcgRqry+NkpVWYYU4+Ay/LAfCs1W+AWUmsKTlxwTe2JTLhXRX3HzrqXSQoC6tg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@rescript/darwin-x64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/darwin-x64/-/darwin-x64-12.0.0-rc.3.tgz", + "integrity": "sha512-ualm4FZRztHaxY7YbEbSgNQwBPRPEsyn30/xHSkuCY2CV7W7808H29X33bqnDkQZ1XxmnG3z2bz9UoZcJJnf6A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@rescript/linux-arm64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/linux-arm64/-/linux-arm64-12.0.0-rc.3.tgz", + "integrity": "sha512-StPyP+KHY1k2CQrVLV52VnQvgeQIKz8woEYB0JRL4LtYDb6g6jVj9aXiq6ZWyVo9vxas1zSCzCvzrXubIsgkmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@rescript/linux-x64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/linux-x64/-/linux-x64-12.0.0-rc.3.tgz", + "integrity": "sha512-HB9K4huyr8zCET5BnUJQgskMpiJXERFc7P2mO0vDgMKgmDEmEeOVTUfFiO9aqCwFP/ujGFsWb47SEUxTmHOCcA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@rescript/runtime": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/runtime/-/runtime-12.0.0-rc.3.tgz", + "integrity": "sha512-Ihdnppw7A+ArLwfnLPcz04jh6ZaKADo0Rc6vMHYQ2+Dw+snO5VbxJ2As5ahVt9nUVL56w1k9pWgrIoLtb1eHcA==" + }, + "node_modules/@rescript/win32-x64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/win32-x64/-/win32-x64-12.0.0-rc.3.tgz", + "integrity": "sha512-Eyy61o6Wk3Cgiy/99zFCzxumL1WHOGEdbxU+ksqghuD3s2RgBWBobSygf3nhcoNBibXI0Fhpu+XXXoTOsTiQ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.11.0" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -1622,26 +1701,48 @@ } }, "node_modules/rescript": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", - "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", - "hasInstallScript": true, + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-12.0.0-rc.3.tgz", + "integrity": "sha512-8EOLvO6p5MxpprqpN4tbblfk6uJbz2Q5g/VHA7ii7TJu98JFNLehRjPNHhlEk2G4Lx+PZraSnGudTxBVmyLNoQ==", + "license": "SEE LICENSE IN LICENSE", + "workspaces": [ + "packages/playground", + "packages/@rescript/*", + "tests/dependencies/**", + "tests/analysis_tests/**", + "tests/docstring_tests", + "tests/gentype_tests/**", + "tests/tools_tests", + "scripts/res" + ], + "dependencies": { + "@rescript/runtime": "12.0.0-rc.3" + }, "bin": { - "bsc": "bsc", - "bstracing": "lib/bstracing", - "rescript": "rescript" + "bsc": "cli/bsc.js", + "bstracing": "cli/bstracing.js", + "rescript": "cli/rescript.js", + "rescript-legacy": "cli/rescript-legacy.js", + "rescript-tools": "cli/rescript-tools.js" }, "engines": { - "node": ">=10" + "node": ">=20.11.0" + }, + "optionalDependencies": { + "@rescript/darwin-arm64": "12.0.0-rc.3", + "@rescript/darwin-x64": "12.0.0-rc.3", + "@rescript/linux-arm64": "12.0.0-rc.3", + "@rescript/linux-x64": "12.0.0-rc.3", + "@rescript/win32-x64": "12.0.0-rc.3" } }, "node_modules/rescript-bun": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/rescript-bun/-/rescript-bun-0.5.0.tgz", - "integrity": "sha512-y7GK+RgQeaC2a8Qam0jZZ+ApTHW32s2bpVOjryeU2pHoNjtNXel7txZyywwEfC5o5rXvF7b12688YW5lO6C5jg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rescript-bun/-/rescript-bun-2.1.0.tgz", + "integrity": "sha512-8INIikN97UlmJgPy5DAnx0fzkqq8Ux6Glj6iYE+SURfLr66PsgaVTgpmlpXBrBjTM/bhaAL82Ulc0gI60YdFYg==", + "license": "MIT", "peerDependencies": { - "@rescript/core": ">=1.3.0", - "rescript": ">=11.1.0" + "rescript": ">= 12.0.0-alpha.4" } }, "node_modules/rescript-x": { @@ -2526,6 +2627,41 @@ "integrity": "sha512-Qx8G9NVSJlf6Z8yWwmy1k1kw3qf6okZdOJ7BLtWS0eFx5WYur4H5OXsnBzEc+W5WQ0uXgOrH965HQQHm0bpLSA==", "requires": {} }, + "@rescript/darwin-arm64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/darwin-arm64/-/darwin-arm64-12.0.0-rc.3.tgz", + "integrity": "sha512-d6OMJWnW8I9DgYPNw+9Gy72tOcgRqry+NkpVWYYU4+Ay/LAfCs1W+AWUmsKTlxwTe2JTLhXRX3HzrqXSQoC6tg==", + "optional": true + }, + "@rescript/darwin-x64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/darwin-x64/-/darwin-x64-12.0.0-rc.3.tgz", + "integrity": "sha512-ualm4FZRztHaxY7YbEbSgNQwBPRPEsyn30/xHSkuCY2CV7W7808H29X33bqnDkQZ1XxmnG3z2bz9UoZcJJnf6A==", + "optional": true + }, + "@rescript/linux-arm64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/linux-arm64/-/linux-arm64-12.0.0-rc.3.tgz", + "integrity": "sha512-StPyP+KHY1k2CQrVLV52VnQvgeQIKz8woEYB0JRL4LtYDb6g6jVj9aXiq6ZWyVo9vxas1zSCzCvzrXubIsgkmg==", + "optional": true + }, + "@rescript/linux-x64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/linux-x64/-/linux-x64-12.0.0-rc.3.tgz", + "integrity": "sha512-HB9K4huyr8zCET5BnUJQgskMpiJXERFc7P2mO0vDgMKgmDEmEeOVTUfFiO9aqCwFP/ujGFsWb47SEUxTmHOCcA==", + "optional": true + }, + "@rescript/runtime": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/runtime/-/runtime-12.0.0-rc.3.tgz", + "integrity": "sha512-Ihdnppw7A+ArLwfnLPcz04jh6ZaKADo0Rc6vMHYQ2+Dw+snO5VbxJ2As5ahVt9nUVL56w1k9pWgrIoLtb1eHcA==" + }, + "@rescript/win32-x64": { + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rescript/win32-x64/-/win32-x64-12.0.0-rc.3.tgz", + "integrity": "sha512-Eyy61o6Wk3Cgiy/99zFCzxumL1WHOGEdbxU+ksqghuD3s2RgBWBobSygf3nhcoNBibXI0Fhpu+XXXoTOsTiQ9w==", + "optional": true + }, "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -3246,23 +3382,31 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "rescript": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", - "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==" + "version": "12.0.0-rc.3", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-12.0.0-rc.3.tgz", + "integrity": "sha512-8EOLvO6p5MxpprqpN4tbblfk6uJbz2Q5g/VHA7ii7TJu98JFNLehRjPNHhlEk2G4Lx+PZraSnGudTxBVmyLNoQ==", + "requires": { + "@rescript/darwin-arm64": "12.0.0-rc.3", + "@rescript/darwin-x64": "12.0.0-rc.3", + "@rescript/linux-arm64": "12.0.0-rc.3", + "@rescript/linux-x64": "12.0.0-rc.3", + "@rescript/runtime": "12.0.0-rc.3", + "@rescript/win32-x64": "12.0.0-rc.3" + } }, "rescript-bun": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/rescript-bun/-/rescript-bun-0.5.0.tgz", - "integrity": "sha512-y7GK+RgQeaC2a8Qam0jZZ+ApTHW32s2bpVOjryeU2pHoNjtNXel7txZyywwEfC5o5rXvF7b12688YW5lO6C5jg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rescript-bun/-/rescript-bun-2.1.0.tgz", + "integrity": "sha512-8INIikN97UlmJgPy5DAnx0fzkqq8Ux6Glj6iYE+SURfLr66PsgaVTgpmlpXBrBjTM/bhaAL82Ulc0gI60YdFYg==", "requires": {} }, "rescript-x": { "version": "file:..", "requires": { - "@rescript/core": "1.6.0", + "chokidar": "^4.0.3", "fast-glob": "^3.3.1", - "rescript": "11.1.4", - "rescript-bun": "0.5.0" + "rescript": "12.0.0-rc.3", + "rescript-bun": "2.1.0" } }, "resolve": { diff --git a/demo/package.json b/demo/package.json index 44e660a..e9fd813 100644 --- a/demo/package.json +++ b/demo/package.json @@ -22,9 +22,9 @@ "concurrently": "^8.2.1", "fast-glob": "^3.3.1", "rescript-x": "../", - "rescript": "11.1.4", + "rescript": "12.0.0-rc.3", "vite": "^4.4.11", - "rescript-bun": "0.5.0" + "rescript-bun": "2.1.0" }, "devDependencies": { "autoprefixer": "^10.4.16", diff --git a/demo/src/DeferredComponent.js b/demo/src/DeferredComponent.js index f7410ff..258c91f 100644 --- a/demo/src/DeferredComponent.js +++ b/demo/src/DeferredComponent.js @@ -1,31 +1,29 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); function wait() { - return new Promise((function (resolve, _reject) { - setTimeout((function () { - resolve(); - }), 1000); - })); + return new Promise((resolve, _reject) => { + setTimeout(() => resolve(), 1000); + }); } async function make(param) { await wait(); return Hjsx$ResX.Elements.jsxs("div", { - children: [ - "This was deferred.", - Hjsx$ResX.Elements.jsx("div", { - children: param.children - }) - ] - }); + children: [ + "This was deferred.", + Hjsx$ResX.Elements.jsx("div", { + children: param.children + }) + ] + }); } -var DeferredComponent = make; +let DeferredComponent = make; -var make$1 = DeferredComponent; +let make$1 = DeferredComponent; exports.wait = wait; exports.make = make$1; diff --git a/demo/src/Demo.js b/demo/src/Demo.js index 2eea441..201b612 100644 --- a/demo/src/Demo.js +++ b/demo/src/Demo.js @@ -1,115 +1,105 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Html = require("./Html.js"); -var SiteMap = require("./SiteMap.js"); -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var FourOhFour = require("./FourOhFour.js"); -var $$Navigation = require("./Navigation.js"); -var UserRoutes = require("./UserRoutes.js"); -var Caml_option = require("rescript/lib/js/caml_option.js"); -var HtmxHandler = require("./HtmxHandler.js"); -var BunUtils$ResX = require("rescript-x/src/BunUtils.js"); -var Handlers$ResX = require("rescript-x/src/Handlers.js"); -var RequestController$ResX = require("rescript-x/src/RequestController.js"); +let Html = require("./Html.js"); +let SiteMap = require("./SiteMap.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let FourOhFour = require("./FourOhFour.js"); +let Navigation = require("./Navigation.js"); +let ResXAssets = require("./__generated__/ResXAssets.js"); +let UserRoutes = require("./UserRoutes.js"); +let HtmxHandler = require("./HtmxHandler.js"); +let BunUtils$ResX = require("rescript-x/src/BunUtils.js"); +let Handlers$ResX = require("rescript-x/src/Handlers.js"); +let RequestController$ResX = require("rescript-x/src/RequestController.js"); -var server = Bun.serve({ - development: BunUtils$ResX.isDev, - port: 4444, - fetch: (async function (request, _server) { - var staticResponse = await BunUtils$ResX.serveStaticFile(request); - if (staticResponse !== undefined) { - return Caml_option.valFromOption(staticResponse); - } else { - return await Handlers$ResX.handleRequest(HtmxHandler.handler, { - request: request, - render: (async function (param) { - var requestController = param.requestController; - var path = param.path; - var headers = param.headers; - var exit = 0; - if (path && path.hd === "sitemap.xml") { - if (!path.tl) { - return Hjsx$ResX.jsx(SiteMap.make, {}); - } - exit = 1; - } else { - exit = 1; - } - if (exit === 1) { - RequestController$ResX.appendTitleSegment(requestController, "Test App"); - var tmp; - var exit$1 = 0; - if (path) { - var exit$2 = 0; - switch (path.hd) { - case "moved" : - tmp = path.tl ? Hjsx$ResX.jsx(FourOhFour.make, { - setGenericTitle: true - }) : RequestController$ResX.redirect(requestController, "/start", 302); - break; - case "" : - case "start" : - exit$2 = 3; - break; - case "user" : - tmp = UserRoutes.match(path.tl, headers, requestController); - break; - default: - tmp = Hjsx$ResX.jsx(FourOhFour.make, { - setGenericTitle: true - }); - } - if (exit$2 === 3) { - if (path.tl) { - tmp = Hjsx$ResX.jsx(FourOhFour.make, { - setGenericTitle: true - }); - } else { - exit$1 = 2; - } - } - - } else { - exit$1 = 2; - } - if (exit$1 === 2) { - headers.set("Cache-Control", "public, immutable, max-age=900"); - tmp = Hjsx$ResX.Elements.jsx("div", { - children: "Start page!" - }); - } - return Hjsx$ResX.jsxs(Html.make, { - children: [ - Hjsx$ResX.jsx($$Navigation.make, {}), - Hjsx$ResX.Elements.jsx("div", { - children: tmp - }) - ] - }); - } - - }), - setupHeaders: (function () { - return new Headers([[ - "Content-Type", - "text/html" - ]]); - }) - }); +let server = Bun.serve({ + development: BunUtils$ResX.isDev, + port: 4444, + fetch: async (request, _server) => await Handlers$ResX.handleRequest(HtmxHandler.handler, { + request: request, + render: async param => { + let requestController = param.requestController; + let path = param.path; + let headers = param.headers; + let exit = 0; + if (path !== 0 && path.hd === "sitemap.xml") { + if (path.tl === 0) { + return Hjsx$ResX.jsx(SiteMap.make, {}); + } + exit = 1; + } else { + exit = 1; + } + if (exit === 1) { + RequestController$ResX.appendTitleSegment(requestController, "Test App"); + let tmp; + let exit$1 = 0; + if (path !== 0) { + let exit$2 = 0; + switch (path.hd) { + case "moved" : + tmp = path.tl !== 0 ? Hjsx$ResX.jsx(FourOhFour.make, { + setGenericTitle: true + }) : RequestController$ResX.redirect(requestController, "/start", 302); + break; + case "" : + case "start" : + exit$2 = 3; + break; + case "user" : + tmp = UserRoutes.match(path.tl, headers, requestController); + break; + default: + tmp = Hjsx$ResX.jsx(FourOhFour.make, { + setGenericTitle: true + }); } - }) - }); + if (exit$2 === 3) { + if (path.tl !== 0) { + tmp = Hjsx$ResX.jsx(FourOhFour.make, { + setGenericTitle: true + }); + } else { + exit$1 = 2; + } + } + } else { + exit$1 = 2; + } + if (exit$1 === 2) { + headers.set("Cache-Control", "public, immutable, max-age=900"); + tmp = Hjsx$ResX.Elements.jsx("div", { + children: "Start page!" + }); + } + return Hjsx$ResX.jsxs(Html.make, { + children: [ + Hjsx$ResX.jsx(Navigation.make, {}), + Hjsx$ResX.Elements.jsx("div", { + children: tmp + }) + ] + }); + } + }, + setupHeaders: () => new Headers([[ + "Content-Type", + "text/html" + ]]) + }), + routes: ResXAssets.staticAssetRoutes +}); -var portString = server.port.toString(); +let portString = server.port.toString(); -console.log("Listening! on localhost:" + portString); +console.log(`Listening! on localhost:` + portString); if (BunUtils$ResX.isDev) { BunUtils$ResX.runDevServer(4444); } -var port = 4444; +let port = 4444; exports.port = port; exports.server = server; diff --git a/demo/src/Demo.res b/demo/src/Demo.res index 7bff602..231ab88 100644 --- a/demo/src/Demo.res +++ b/demo/src/Demo.res @@ -3,39 +3,36 @@ let port = 4444 let server = Bun.serve({ port, development: ResX.BunUtils.isDev, + routes: ResXAssets.staticAssetRoutes, fetch: async (request, _server) => { - switch await ResX.BunUtils.serveStaticFile(request) { - | Some(staticResponse) => staticResponse - | None => - await HtmxHandler.handler.handleRequest({ - request, - setupHeaders: () => { - Headers.make(~init=FromArray([("Content-Type", "text/html")])) - }, - render: async ({path, requestController, headers}) => { - switch path { - | list{"sitemap.xml"} => - | appRoutes => - requestController.appendTitleSegment("Test App") - - -
- {switch appRoutes { - | list{"start" | ""} | list{} => - headers->Headers.set("Cache-Control", "public, immutable, max-age=900") -
{Hjsx.string("Start page!")}
- | list{"moved"} => - requestController.redirect("/start", ~status=302) - | list{"user", ...userRoutes} => - userRoutes->UserRoutes.match(~headers, ~requestController) - | _ => - }} -
- - } - }, - }) - } + await HtmxHandler.handler.handleRequest({ + request, + setupHeaders: () => { + Headers.make(~init=FromArray([("Content-Type", "text/html")])) + }, + render: async ({path, requestController, headers}) => { + switch path { + | list{"sitemap.xml"} => + | appRoutes => + requestController.appendTitleSegment("Test App") + + +
+ {switch appRoutes { + | list{"start" | ""} | list{} => + headers->Headers.set("Cache-Control", "public, immutable, max-age=900") +
{Hjsx.string("Start page!")}
+ | list{"moved"} => + requestController.redirect("/start", ~status=302) + | list{"user", ...userRoutes} => + userRoutes->UserRoutes.match(~headers, ~requestController) + | _ => + }} +
+ + } + }, + }) }, }) diff --git a/demo/src/ErrorMessage.js b/demo/src/ErrorMessage.js index 857be94..6008a61 100644 --- a/demo/src/ErrorMessage.js +++ b/demo/src/ErrorMessage.js @@ -1,15 +1,15 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); function ErrorMessage(props) { return Hjsx$ResX.Elements.jsx("div", { - children: props.message - }); + children: props.message + }); } -var make = ErrorMessage; +let make = ErrorMessage; exports.make = make; /* Hjsx-ResX Not a pure module */ diff --git a/demo/src/Exporter.js b/demo/src/Exporter.js index 651e969..8ad2ce9 100644 --- a/demo/src/Exporter.js +++ b/demo/src/Exporter.js @@ -1,10 +1,10 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Demo = require("./Demo.js"); -var StaticExporter$ResX = require("rescript-x/src/StaticExporter.js"); +let Demo = require("./Demo.js"); +let StaticExporter$ResX = require("rescript-x/src/StaticExporter.js"); -var urls = [ +let urls = [ "/", "/start", "/user/1" diff --git a/demo/src/FailingComponent.js b/demo/src/FailingComponent.js index 601348c..842b044 100644 --- a/demo/src/FailingComponent.js +++ b/demo/src/FailingComponent.js @@ -1,13 +1,13 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Js_exn = require("rescript/lib/js/js_exn.js"); +let Stdlib_JsError = require("@rescript/runtime/lib/js/Stdlib_JsError.js"); function FailingComponent(props) { - return Js_exn.raiseError("Error!"); + return Stdlib_JsError.throwWithMessage("Error!"); } -var make = FailingComponent; +let make = FailingComponent; exports.make = make; /* No side effect */ diff --git a/demo/src/FailingComponent.res b/demo/src/FailingComponent.res index 3f3d74a..b7c6624 100644 --- a/demo/src/FailingComponent.res +++ b/demo/src/FailingComponent.res @@ -1,4 +1,4 @@ @jsx.component let make = () => { - Exn.raiseError("Error!") + JsError.throwWithMessage("Error!") } diff --git a/demo/src/FourOhFour.js b/demo/src/FourOhFour.js index dd83a40..a5e825e 100644 --- a/demo/src/FourOhFour.js +++ b/demo/src/FourOhFour.js @@ -1,24 +1,24 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var HtmxHandler = require("./HtmxHandler.js"); -var RequestController$ResX = require("rescript-x/src/RequestController.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let HtmxHandler = require("./HtmxHandler.js"); +let RequestController$ResX = require("rescript-x/src/RequestController.js"); function FourOhFour(props) { - var __setGenericTitle = props.setGenericTitle; - var setGenericTitle = __setGenericTitle !== undefined ? __setGenericTitle : false; - var context = HtmxHandler.useContext(); + let __setGenericTitle = props.setGenericTitle; + let setGenericTitle = __setGenericTitle !== undefined ? __setGenericTitle : false; + let context = HtmxHandler.useContext(); RequestController$ResX.setStatus(context.requestController, 404); if (setGenericTitle) { RequestController$ResX.setFullTitle(context.requestController, "Not Found"); } return Hjsx$ResX.Elements.jsx("div", { - children: "404" - }); + children: "404" + }); } -var make = FourOhFour; +let make = FourOhFour; exports.make = make; /* Hjsx-ResX Not a pure module */ diff --git a/demo/src/Head.js b/demo/src/Head.js index 6a136e1..f937584 100644 --- a/demo/src/Head.js +++ b/demo/src/Head.js @@ -1,27 +1,27 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var Handlers$ResX = require("rescript-x/src/Handlers.js"); -var RequestController$ResX = require("rescript-x/src/RequestController.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Handlers$ResX = require("rescript-x/src/Handlers.js"); +let RequestController$ResX = require("rescript-x/src/RequestController.js"); function Head(props) { return Hjsx$ResX.Elements.jsx("head", { - children: props.children - }); + children: props.children + }); } function Head$RenderInHead(props) { - var ctx = Handlers$ResX.useContext(props.handler); + let ctx = Handlers$ResX.useContext(props.handler); RequestController$ResX.appendToHead(ctx.requestController, props.children); return null; } -var RenderInHead = { +let RenderInHead = { make: Head$RenderInHead }; -var make = Head; +let make = Head; exports.make = make; exports.RenderInHead = RenderInHead; diff --git a/demo/src/Hello.js b/demo/src/Hello.js index db7c04f..c524352 100644 --- a/demo/src/Hello.js +++ b/demo/src/Hello.js @@ -1,84 +1,81 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Js_exn = require("rescript/lib/js/js_exn.js"); -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var Htmx$ResX = require("rescript-x/src/Htmx.js"); -var Caml_option = require("rescript/lib/js/caml_option.js"); -var HtmxHandler = require("./HtmxHandler.js"); -var ErrorMessage = require("./ErrorMessage.js"); -var Handlers$ResX = require("rescript-x/src/Handlers.js"); -var Caml_js_exceptions = require("rescript/lib/js/caml_js_exceptions.js"); -var SecurityPolicy$ResX = require("rescript-x/src/SecurityPolicy.js"); -var FormDataHelpers$ResX = require("rescript-x/src/FormDataHelpers.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Htmx$ResX = require("rescript-x/src/Htmx.js"); +let Stdlib_Exn = require("@rescript/runtime/lib/js/Stdlib_Exn.js"); +let HtmxHandler = require("./HtmxHandler.js"); +let ErrorMessage = require("./ErrorMessage.js"); +let Handlers$ResX = require("rescript-x/src/Handlers.js"); +let Primitive_option = require("@rescript/runtime/lib/js/Primitive_option.js"); +let SecurityPolicy$ResX = require("rescript-x/src/SecurityPolicy.js"); +let FormDataHelpers$ResX = require("rescript-x/src/FormDataHelpers.js"); +let Primitive_exceptions = require("@rescript/runtime/lib/js/Primitive_exceptions.js"); function myVariantFromString(a) { - if (!(!(a instanceof File) && typeof a !== "string") && typeof a === "string") { + if (typeof a === "string") { switch (a) { case "one" : - return { - TAG: "Ok", - _0: "One" - }; + return { + TAG: "Ok", + _0: "One" + }; case "two" : - return { - TAG: "Ok", - _0: "Two" - }; - default: - + return { + TAG: "Ok", + _0: "Two" + }; } } return { - TAG: "Error", - _0: "Unknown value: \"" + String(a) + "\"" - }; + TAG: "Error", + _0: `Unknown value: "` + String(a) + `"` + }; } -var onButtonBlick = Handlers$ResX.hxPost(HtmxHandler.handler, "/button-click", SecurityPolicy$ResX.allow, (async function (param) { - try { - var formData = await param.request.formData(); - var firstName = FormDataHelpers$ResX.expectString(formData, "firstName", undefined); - var lastName = FormDataHelpers$ResX.expectString(formData, "lastName", undefined); - FormDataHelpers$ResX.expectCustom(formData, "myVariant", myVariantFromString); - return Hjsx$ResX.Elements.jsx("span", { - children: "Hi " + firstName + " " + lastName + "!" - }); - } - catch (raw_exn){ - var exn = Caml_js_exceptions.internalToOCamlException(raw_exn); - if (exn.RE_EXN_ID === Js_exn.$$Error) { - return Hjsx$ResX.jsx(ErrorMessage.make, { - message: "Something went wrong..." - }); - } - throw exn; - } - })); +let onButtonBlick = Handlers$ResX.hxPost(HtmxHandler.handler, "/button-click", SecurityPolicy$ResX.allow, async param => { + try { + let formData = await param.request.formData(); + let firstName = FormDataHelpers$ResX.expectString(formData, "firstName", undefined); + let lastName = FormDataHelpers$ResX.expectString(formData, "lastName", undefined); + FormDataHelpers$ResX.expectCustom(formData, "myVariant", myVariantFromString); + return Hjsx$ResX.Elements.jsx("span", { + children: "Hi " + firstName + " " + lastName + "!" + }); + } catch (raw_exn) { + let exn = Primitive_exceptions.internalToException(raw_exn); + if (exn.RE_EXN_ID === Stdlib_Exn.$$Error) { + return Hjsx$ResX.jsx(ErrorMessage.make, { + message: "Something went wrong..." + }); + } + throw exn; + } +}, undefined); function Hello(props) { return Hjsx$ResX.Elements.jsxs("form", { - children: [ - Hjsx$ResX.Elements.jsx("button", { - children: "Hello " + props.name, - "hx-post": Caml_option.some(onButtonBlick), - "hx-swap": Caml_option.some(Htmx$ResX.Swap.make("innerHTML", "Transition")) - }), - Hjsx$ResX.Elements.jsx("input", { - name: "firstName", - type: "text", - value: "" - }), - Hjsx$ResX.Elements.jsx("input", { - name: "lastName", - type: "text", - value: "" - }) - ] - }); + children: [ + Hjsx$ResX.Elements.jsx("button", { + children: "Hello " + props.name, + "hx-post": Primitive_option.some(onButtonBlick), + "hx-swap": Primitive_option.some(Htmx$ResX.Swap.make("innerHTML", "Transition")) + }), + Hjsx$ResX.Elements.jsx("input", { + name: "firstName", + type: "text", + value: "" + }), + Hjsx$ResX.Elements.jsx("input", { + name: "lastName", + type: "text", + value: "" + }) + ] + }); } -var make = Hello; +let make = Hello; exports.myVariantFromString = myVariantFromString; exports.onButtonBlick = onButtonBlick; diff --git a/demo/src/Html.js b/demo/src/Html.js index 3a2ee60..6195807 100644 --- a/demo/src/Html.js +++ b/demo/src/Html.js @@ -1,40 +1,40 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Dev$ResX = require("rescript-x/src/Dev.js"); -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var ResXAssets = require("./__generated__/ResXAssets.js"); +let Dev$ResX = require("rescript-x/src/Dev.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let ResXAssets = require("./__generated__/ResXAssets.js"); function Html(props) { return Hjsx$ResX.Elements.jsxs("html", { - children: [ - Hjsx$ResX.Elements.jsx("head", { - children: Hjsx$ResX.Elements.jsx("link", { - href: ResXAssets.assets.styles_css, - rel: "stylesheet", - type: "text/css" - }) - }), - Hjsx$ResX.Elements.jsxs("body", { - children: [ - props.children, - Hjsx$ResX.jsx(Dev$ResX.make, {}), - Hjsx$ResX.Elements.jsx("script", { - src: "https://unpkg.com/htmx.org@1.9.5" - }), - Hjsx$ResX.Elements.jsx("script", { - async: true, - src: ResXAssets.assets.resXClient_js - }) - ], - className: "bg-orange-200 p-10", - "hx-boost": true - }) - ] - }); + children: [ + Hjsx$ResX.Elements.jsx("head", { + children: Hjsx$ResX.Elements.jsx("link", { + href: ResXAssets.assets.styles_css, + rel: "stylesheet", + type: "text/css" + }) + }), + Hjsx$ResX.Elements.jsxs("body", { + children: [ + props.children, + Hjsx$ResX.jsx(Dev$ResX.make, {}), + Hjsx$ResX.Elements.jsx("script", { + src: "https://unpkg.com/htmx.org@1.9.5" + }), + Hjsx$ResX.Elements.jsx("script", { + async: true, + src: ResXAssets.assets.resXClient_js + }) + ], + className: "bg-orange-200 p-10", + "hx-boost": true + }) + ] + }); } -var make = Html; +let make = Html; exports.make = make; /* Dev-ResX Not a pure module */ diff --git a/demo/src/HtmxHandler.js b/demo/src/HtmxHandler.js index 2381c75..5e94a04 100644 --- a/demo/src/HtmxHandler.js +++ b/demo/src/HtmxHandler.js @@ -1,13 +1,11 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Handlers$ResX = require("rescript-x/src/Handlers.js"); +let Handlers$ResX = require("rescript-x/src/Handlers.js"); -var handler = Handlers$ResX.make((async function (_request) { - return { - userId: undefined - }; - }), undefined); +let handler = Handlers$ResX.make(async _request => ({ + userId: undefined +}), undefined); function useContext() { return Handlers$ResX.useContext(handler); diff --git a/demo/src/Navigation.js b/demo/src/Navigation.js index e58271c..743c785 100644 --- a/demo/src/Navigation.js +++ b/demo/src/Navigation.js @@ -1,34 +1,34 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var U = require("./U.js"); -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var HtmxHandler = require("./HtmxHandler.js"); +let U = require("./U.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let HtmxHandler = require("./HtmxHandler.js"); -function $$Navigation(props) { - var context = HtmxHandler.useContext(); - var match = context.path; - var tmp; - if (match && match.hd === "user") { - var match$1 = match.tl; - tmp = match$1 && match$1.hd === "1" ? "font-bold" : ""; +function Navigation(props) { + let context = HtmxHandler.useContext(); + let match = context.path; + let tmp; + if (match !== 0 && match.hd === "user") { + let match$1 = match.tl; + tmp = match$1 !== 0 && match$1.hd === "1" ? "font-bold" : ""; } else { tmp = ""; } return Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("a", { - children: "To User 1", - className: U.tw([ - tmp, - "underline text-blue-700 visited:text-purple-700" - ]), - href: "/user/1", - "hx-boost": true - }) - }); + children: Hjsx$ResX.Elements.jsx("a", { + children: "To User 1", + className: U.tw([ + tmp, + "underline text-blue-700 visited:text-purple-700" + ]), + href: "/user/1", + "hx-boost": true + }) + }); } -var make = $$Navigation; +let make = Navigation; exports.make = make; /* Hjsx-ResX Not a pure module */ diff --git a/demo/src/SiteMap.js b/demo/src/SiteMap.js index 26e0dbb..15e2938 100644 --- a/demo/src/SiteMap.js +++ b/demo/src/SiteMap.js @@ -1,44 +1,44 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var Utils$ResX = require("rescript-x/src/Utils.js"); -var HtmxHandler = require("./HtmxHandler.js"); -var RequestController$ResX = require("rescript-x/src/RequestController.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Utils$ResX = require("rescript-x/src/Utils.js"); +let HtmxHandler = require("./HtmxHandler.js"); +let RequestController$ResX = require("rescript-x/src/RequestController.js"); function SiteMap(props) { - var context = HtmxHandler.useContext(); - RequestController$ResX.setDocHeader(context.requestController, ""); + let context = HtmxHandler.useContext(); + RequestController$ResX.setDocHeader(context.requestController, ``); context.headers.set("Content-Type", "application/xml; charset=UTF-8"); context.headers.set("Cache-Control", Utils$ResX.CacheControl.make(undefined, "public", [{ - kind: "max-age", - _0: { - TAG: "Days", - _0: 1 - } - }], undefined, undefined, undefined)); + kind: "max-age", + _0: { + TAG: "Days", + _0: 1 + } + }], undefined, undefined, undefined)); return Hjsx$ResX.Elements.jsx("urlset", { - children: Hjsx$ResX.Elements.jsxs("url", { - children: [ - Hjsx$ResX.Elements.jsx("loc", { - children: "https://www.example.com/" - }), - Hjsx$ResX.Elements.jsx("lastmod", { - children: "2023-10-15" - }), - Hjsx$ResX.Elements.jsx("changefreq", { - children: "weekly" - }), - Hjsx$ResX.Elements.jsx("priority", { - children: "1.0" - }) - ] - }), - xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" - }); + children: Hjsx$ResX.Elements.jsxs("url", { + children: [ + Hjsx$ResX.Elements.jsx("loc", { + children: "https://www.example.com/" + }), + Hjsx$ResX.Elements.jsx("lastmod", { + children: "2023-10-15" + }), + Hjsx$ResX.Elements.jsx("changefreq", { + children: "weekly" + }), + Hjsx$ResX.Elements.jsx("priority", { + children: "1.0" + }) + ] + }), + xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" + }); } -var make = SiteMap; +let make = SiteMap; exports.make = make; /* Hjsx-ResX Not a pure module */ diff --git a/demo/src/StreamTest.js b/demo/src/StreamTest.js index fd1e690..5583131 100644 --- a/demo/src/StreamTest.js +++ b/demo/src/StreamTest.js @@ -1,36 +1,34 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var BunUtils$ResX = require("rescript-x/src/BunUtils.js"); +let BunUtils$ResX = require("rescript-x/src/BunUtils.js"); function wait() { - return new Promise((function (resolve, param) { - setTimeout((function () { - resolve(); - }), 2000); - })); + return new Promise((resolve, param) => { + setTimeout(() => resolve(), 2000); + }); } -var server = Bun.serve({ - development: BunUtils$ResX.isDev, - port: 5555, - fetch: (async function (_request, _server) { - var match = new TransformStream({ - transform: (function (chunk, controller) { - controller.enqueue(chunk); - }) - }); - var writer = match.writable.getWriter(); - var textEncoder = new TextEncoder(); - writer.write(textEncoder.encode("Hello!")); - writer.close(); - var response = new Response(match.readable); - response.headers.set("Content-Type", "text/html"); - return response; - }) +let server = Bun.serve({ + development: BunUtils$ResX.isDev, + port: 5555, + fetch: async (_request, _server) => { + let match = new TransformStream({ + transform: (chunk, controller) => { + controller.enqueue(chunk); + } }); + let writer = match.writable.getWriter(); + let textEncoder = new TextEncoder(); + writer.write(textEncoder.encode("Hello!")); + writer.close(); + let response = new Response(match.readable); + response.headers.set("Content-Type", "text/html"); + return response; + } +}); -var port = 5555; +let port = 5555; exports.port = port; exports.wait = wait; diff --git a/demo/src/U.js b/demo/src/U.js index b6729fa..30edd6a 100644 --- a/demo/src/U.js +++ b/demo/src/U.js @@ -3,9 +3,7 @@ function tw(classes) { - return classes.filter(function (c) { - return c !== ""; - }).join(" "); + return classes.filter(c => c !== "").join(" "); } exports.tw = tw; diff --git a/demo/src/UserFriends.js b/demo/src/UserFriends.js index c3a5d05..b6710be 100644 --- a/demo/src/UserFriends.js +++ b/demo/src/UserFriends.js @@ -1,13 +1,13 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); function UserFriends(props) { return Hjsx$ResX.Elements.jsx("div", {}); } -var make = UserFriends; +let make = UserFriends; exports.make = make; /* Hjsx-ResX Not a pure module */ diff --git a/demo/src/UserPage.js b/demo/src/UserPage.js index 41d3469..ad56677 100644 --- a/demo/src/UserPage.js +++ b/demo/src/UserPage.js @@ -1,154 +1,152 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var Js_exn = require("rescript/lib/js/js_exn.js"); -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var Htmx$ResX = require("rescript-x/src/Htmx.js"); -var ResXAssets = require("./__generated__/ResXAssets.js"); -var Caml_option = require("rescript/lib/js/caml_option.js"); -var Client$ResX = require("rescript-x/src/Client.js"); -var HtmxHandler = require("./HtmxHandler.js"); -var Core__Option = require("@rescript/core/src/Core__Option.js"); -var Handlers$ResX = require("rescript-x/src/Handlers.js"); -var FailingComponent = require("./FailingComponent.js"); -var Caml_js_exceptions = require("rescript/lib/js/caml_js_exceptions.js"); -var ErrorBoundary$ResX = require("rescript-x/src/ErrorBoundary.js"); -var SecurityPolicy$ResX = require("rescript-x/src/SecurityPolicy.js"); -var FormDataHelpers$ResX = require("rescript-x/src/FormDataHelpers.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let Htmx$ResX = require("rescript-x/src/Htmx.js"); +let ResXAssets = require("./__generated__/ResXAssets.js"); +let Stdlib_Exn = require("@rescript/runtime/lib/js/Stdlib_Exn.js"); +let Client$ResX = require("rescript-x/src/Client.js"); +let HtmxHandler = require("./HtmxHandler.js"); +let Core__Option = require("@rescript/core/src/Core__Option.js"); +let Stdlib_JsExn = require("@rescript/runtime/lib/js/Stdlib_JsExn.js"); +let Handlers$ResX = require("rescript-x/src/Handlers.js"); +let FailingComponent = require("./FailingComponent.js"); +let Primitive_option = require("@rescript/runtime/lib/js/Primitive_option.js"); +let ErrorBoundary$ResX = require("rescript-x/src/ErrorBoundary.js"); +let SecurityPolicy$ResX = require("rescript-x/src/SecurityPolicy.js"); +let FormDataHelpers$ResX = require("rescript-x/src/FormDataHelpers.js"); +let Primitive_exceptions = require("@rescript/runtime/lib/js/Primitive_exceptions.js"); -var onForm = Handlers$ResX.hxPost(HtmxHandler.handler, "/user-single", SecurityPolicy$ResX.allow, (async function (param) { - var formData = await param.request.formData(); - try { - var name = FormDataHelpers$ResX.expectString(formData, "name", undefined); - var active = FormDataHelpers$ResX.expectCheckbox(formData, "active"); - return Hjsx$ResX.Elements.jsx("div", { - children: "Some user " + name + " is " + ( - active ? "active" : "not active" - ) - }); - } - catch (raw_err){ - var err = Caml_js_exceptions.internalToOCamlException(raw_err); - if (err.RE_EXN_ID === Js_exn.$$Error) { - console.error(err._1); - return Hjsx$ResX.Elements.jsx("div", { - children: "Failed" - }); - } - throw err; - } - })); +let onForm = Handlers$ResX.hxPost(HtmxHandler.handler, "/user-single", SecurityPolicy$ResX.allow, async param => { + let formData = await param.request.formData(); + try { + let name = FormDataHelpers$ResX.expectString(formData, "name", undefined); + let active = FormDataHelpers$ResX.expectCheckbox(formData, "active"); + return Hjsx$ResX.Elements.jsx("div", { + children: `Some user ` + name + ` is ` + ( + active ? "active" : "not active" + ) + }); + } catch (raw_err) { + let err = Primitive_exceptions.internalToException(raw_err); + if (err.RE_EXN_ID === Stdlib_Exn.$$Error) { + console.error(err._1); + return Hjsx$ResX.Elements.jsx("div", { + children: "Failed" + }); + } + throw err; + } +}, undefined); function UserPage(props) { - var ctx = HtmxHandler.useContext(); + let ctx = HtmxHandler.useContext(); ctx.headers.set("Content-Type", "text/html"); return Hjsx$ResX.Elements.jsxs("div", { - children: [ - Hjsx$ResX.Elements.jsxs("form", { - children: [ - Hjsx$ResX.Elements.jsx("img", { - src: ResXAssets.assets.images__test_img_jpeg - }), - Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("div", { - children: "User 123 3333 " + props.userId, - className: "text-2xl bg-slate-200 text-gray-500" - }), - id: "user-single" - }), - Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("input", { - className: "p-2", - name: "name", - type: "text" - }), - className: "p-2" - }), - Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("input", { - className: "invalid:border-green-400 border border-gray-500", - name: "lastName", - required: true, - type: "text", - "resx-validity-message": Caml_option.some(Client$ResX.ValidityMessage.make({ - valueMissing: "Yo, you need to fill this in!" - })) - }), - className: "p-2" - }), - Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("input", { - name: "active", - type: "checkbox" - }), - className: "p-2" - }), - Hjsx$ResX.Elements.jsxs("div", { - children: [ - Hjsx$ResX.Elements.jsxs("label", { - children: [ - Hjsx$ResX.Elements.jsx("input", { - name: "status", - type: "radio", - value: "on" - }), - "On" - ] - }), - Hjsx$ResX.Elements.jsxs("label", { - children: [ - Hjsx$ResX.Elements.jsx("input", { - name: "status", - type: "radio", - value: "off" - }), - "Off" - ] - }) - ], - className: "p-2" - }), - Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("textarea", { - name: "description" - }), - className: "p-2" - }), - Hjsx$ResX.Elements.jsx("div", { - children: Hjsx$ResX.Elements.jsx("button", { - children: "Submit form", - id: "test", - "resx-onclick": Caml_option.some(Client$ResX.Actions.make([{ - kind: "ToggleClass", - target: "This", - className: "text-xl" - }])) - }), - className: "p-2" - }), - Hjsx$ResX.jsx(ErrorBoundary$ResX.make, { - children: Hjsx$ResX.jsx(FailingComponent.make, {}), - renderError: (function (err) { - return Hjsx$ResX.Elements.jsx("div", { - children: "Oops, failed! " + Core__Option.getOr(err.message, "-") - }); - }) - }) - ], - "hx-post": Caml_option.some(onForm), - "hx-swap": Caml_option.some(Htmx$ResX.Swap.make("innerHTML", undefined)), - "hx-target": Caml_option.some(Htmx$ResX.Target.make({ - TAG: "CssSelector", - _0: "#user-single" - })) - }), - props.innerContent - ], - className: "p-8" - }); + children: [ + Hjsx$ResX.Elements.jsxs("form", { + children: [ + Hjsx$ResX.Elements.jsx("img", { + src: ResXAssets.assets.images__test_img_jpeg + }), + Hjsx$ResX.Elements.jsx("div", { + children: Hjsx$ResX.Elements.jsx("div", { + children: `User 123 3333 ` + props.userId, + className: "text-2xl bg-slate-200 text-gray-500" + }), + id: "user-single" + }), + Hjsx$ResX.Elements.jsx("div", { + children: Hjsx$ResX.Elements.jsx("input", { + className: "p-2", + name: "name", + type: "text" + }), + className: "p-2" + }), + Hjsx$ResX.Elements.jsx("div", { + children: Hjsx$ResX.Elements.jsx("input", { + className: "invalid:border-green-400 border border-gray-500", + name: "lastName", + required: true, + type: "text", + "resx-validity-message": Primitive_option.some(Client$ResX.ValidityMessage.make({ + valueMissing: "Yo, you need to fill this in!" + })) + }), + className: "p-2" + }), + Hjsx$ResX.Elements.jsx("div", { + children: Hjsx$ResX.Elements.jsx("input", { + name: "active", + type: "checkbox" + }), + className: "p-2" + }), + Hjsx$ResX.Elements.jsxs("div", { + children: [ + Hjsx$ResX.Elements.jsxs("label", { + children: [ + Hjsx$ResX.Elements.jsx("input", { + name: "status", + type: "radio", + value: "on" + }), + "On" + ] + }), + Hjsx$ResX.Elements.jsxs("label", { + children: [ + Hjsx$ResX.Elements.jsx("input", { + name: "status", + type: "radio", + value: "off" + }), + "Off" + ] + }) + ], + className: "p-2" + }), + Hjsx$ResX.Elements.jsx("div", { + children: Hjsx$ResX.Elements.jsx("textarea", { + name: "description" + }), + className: "p-2" + }), + Hjsx$ResX.Elements.jsx("div", { + children: Hjsx$ResX.Elements.jsx("button", { + children: "Submit form", + id: "test", + "resx-onclick": Primitive_option.some(Client$ResX.Actions.make([{ + kind: "ToggleClass", + target: "This", + className: "text-xl" + }])) + }), + className: "p-2" + }), + Hjsx$ResX.jsx(ErrorBoundary$ResX.make, { + children: Hjsx$ResX.jsx(FailingComponent.make, {}), + renderError: err => Hjsx$ResX.Elements.jsx("div", { + children: "Oops, failed! " + Core__Option.getOr(Stdlib_JsExn.message(err), "-") + }) + }) + ], + "hx-post": Primitive_option.some(onForm), + "hx-swap": Primitive_option.some(Htmx$ResX.Swap.make("innerHTML", undefined)), + "hx-target": Primitive_option.some(Htmx$ResX.Target.make({ + TAG: "CssSelector", + _0: "#user-single" + })) + }), + props.innerContent + ], + className: "p-8" + }); } -var make = UserPage; +let make = UserPage; exports.onForm = onForm; exports.make = make; diff --git a/demo/src/UserPage.res b/demo/src/UserPage.res index 0278659..45df765 100644 --- a/demo/src/UserPage.res +++ b/demo/src/UserPage.res @@ -85,7 +85,7 @@ let make = (~innerContent, ~userId) => { { -
{Hjsx.string("Oops, failed! " ++ err->Exn.message->Option.getOr("-"))}
+
{Hjsx.string("Oops, failed! " ++ err->JsExn.message->Option.getOr("-"))}
}}>
diff --git a/demo/src/UserRoutes.js b/demo/src/UserRoutes.js index e1ddd1f..618a971 100644 --- a/demo/src/UserRoutes.js +++ b/demo/src/UserRoutes.js @@ -1,27 +1,27 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var UserPage = require("./UserPage.js"); -var Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -var FourOhFour = require("./FourOhFour.js"); -var UserFriends = require("./UserFriends.js"); -var RequestController$ResX = require("rescript-x/src/RequestController.js"); +let UserPage = require("./UserPage.js"); +let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); +let FourOhFour = require("./FourOhFour.js"); +let UserFriends = require("./UserFriends.js"); +let RequestController$ResX = require("rescript-x/src/RequestController.js"); function match(userRoutes, headers, requestController) { RequestController$ResX.appendTitleSegment(requestController, "Users"); - if (userRoutes) { - var userRoutes$1 = userRoutes.tl; - var userId = userRoutes.hd; + if (userRoutes !== 0) { + let userRoutes$1 = userRoutes.tl; + let userId = userRoutes.hd; RequestController$ResX.appendTitleSegment(requestController, userId); headers.set("Cache-Control", "private"); - var tmp; - tmp = userRoutes$1 && userRoutes$1.hd === "friends" && !userRoutes$1.tl ? Hjsx$ResX.jsx(UserFriends.make, { - userId: userId - }) : null; + let tmp; + tmp = userRoutes$1 !== 0 && userRoutes$1.hd === "friends" && userRoutes$1.tl === 0 ? Hjsx$ResX.jsx(UserFriends.make, { + userId: userId + }) : null; return Hjsx$ResX.jsx(UserPage.make, { - innerContent: tmp, - userId: userId - }); + innerContent: tmp, + userId: userId + }); } RequestController$ResX.appendTitleSegment(requestController, "Not found"); return Hjsx$ResX.jsx(FourOhFour.make, {}); diff --git a/demo/src/__generated__/ResXAssets.js b/demo/src/__generated__/ResXAssets.js index 9c416ef..fe01051 100644 --- a/demo/src/__generated__/ResXAssets.js +++ b/demo/src/__generated__/ResXAssets.js @@ -1,9 +1,13 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -var ResXAssetsJs = require("./res-x-assets.js"); +let ResXAssetsJs = require("./res-x-assets.js"); +let ResXStaticRoutesJs = require("./res-x-static-routes.js"); -var assets = ResXAssetsJs.assets; +let assets = ResXAssetsJs.assets; + +let staticAssetRoutes = ResXStaticRoutesJs.staticAssetRoutes; exports.assets = assets; +exports.staticAssetRoutes = staticAssetRoutes; /* assets Not a pure module */ diff --git a/demo/src/__generated__/ResXAssets.res b/demo/src/__generated__/ResXAssets.res index 2e0fb6f..1623fb0 100644 --- a/demo/src/__generated__/ResXAssets.res +++ b/demo/src/__generated__/ResXAssets.res @@ -26,4 +26,6 @@ type assets = { images__nested__test_image2_jpeg_: string, } -@module("./res-x-assets.js") external assets: assets = "assets" \ No newline at end of file +@module("./res-x-assets.js") external assets: assets = "assets" + +@module("./res-x-static-routes.js") external staticAssetRoutes: Dict.t = "staticAssetRoutes" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fb2bc22..38a549e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.2.1", "license": "MIT", "dependencies": { + "chokidar": "^4.0.3", "fast-glob": "^3.3.1" }, "devDependencies": { @@ -698,6 +699,21 @@ "node": ">=8" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -935,6 +951,19 @@ } ] }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/rescript": { "version": "12.0.0-rc.3", "resolved": "https://registry.npmjs.org/rescript/-/rescript-12.0.0-rc.3.tgz", @@ -1459,6 +1488,14 @@ "fill-range": "^7.0.1" } }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "requires": { + "readdirp": "^4.0.1" + } + }, "esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -1609,6 +1646,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, "rescript": { "version": "12.0.0-rc.3", "resolved": "https://registry.npmjs.org/rescript/-/rescript-12.0.0-rc.3.tgz", diff --git a/package.json b/package.json index 34c7ca4..e48e548 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "rescript-bun": "2.1.0" }, "dependencies": { + "chokidar": "^4.0.3", "fast-glob": "^3.3.1" } } diff --git a/res-x-vite-plugin.mjs b/res-x-vite-plugin.mjs index 9a5dc1b..4275418 100644 --- a/res-x-vite-plugin.mjs +++ b/res-x-vite-plugin.mjs @@ -3,14 +3,23 @@ import fs from "fs"; import fg from "fast-glob"; import chokidar from "chokidar"; +const defaultStaticAssetRoutesConfig = Object.freeze({ + headers: Object.freeze({}), +}); + export default function resXVitePlugin(options = {}) { const { generated = "src/__generated__", serverUri = "http://localhost:4444", resXClientLocation = "node_modules/rescript-x/src/ResXClient.js", + staticAssetRoutes: staticAssetRoutesOptions = defaultStaticAssetRoutesConfig, } = options; + const staticAssetRoutes = createStaticAssetRoutesConfig( + staticAssetRoutesOptions + ); const assetDir = "assets"; + const publicDir = "public"; let outDir = "dist"; let isBuild = false; let watcher; @@ -24,10 +33,19 @@ export default function resXVitePlugin(options = {}) { if (!isBuild) { const dummyDevFile = getDummyDevFile(assetDir, resXClientLocation); writeIfChanged(getAssetJsFileLoc(generated), dummyDevFile); + writeIfChanged( + getStaticAssetRoutesJsFileLoc(generated), + getDummyStaticAssetRoutesFile( + assetDir, + publicDir, + resXClientLocation, + staticAssetRoutes + ) + ); const content = getAssetDirContent(assetDir); // TODO: Check if really needed - content.forEach((c) => { + content.forEach(c => { context.load({ id: path.resolve(c), }); @@ -68,7 +86,7 @@ export default function resXVitePlugin(options = {}) { proxy["/"] = { target: serverUri, changeOrigin: true, - bypass: (req) => { + bypass: req => { if ( req.url?.startsWith("/assets/") || req.url?.includes("@vite/client") || @@ -99,17 +117,17 @@ export default function resXVitePlugin(options = {}) { regenerate(this); if (!isBuild) { - // Watch the asset directory for changes in development mode - watcher = chokidar.watch(assetDir, { + // Watch static asset roots for add/remove changes in development mode. + watcher = chokidar.watch([assetDir, publicDir], { persistent: true, ignoreInitial: true, }); - watcher.on("add", (_) => { + watcher.on("add", _ => { regenerate(this); }); - watcher.on("unlink", (_) => { + watcher.on("unlink", _ => { regenerate(this); }); } @@ -131,13 +149,13 @@ export default function resXVitePlugin(options = {}) { const map = {}; const content = getAssetDirContent(assetDir); - const contentResolved = content.map((c) => path.resolve(assetDir, c)); + const contentResolved = content.map(c => path.resolve(assetDir, c)); contentResolved.push(fs.realpathSync(resXClientLocation)); Object.entries(bundle).forEach(([key, v]) => { if (v.facadeModuleId != null) { const moduleId = v.facadeModuleId.split("?")[0]; - const path = key; + const bundlePath = key; const importedCss = v.viteMetadata?.importedCss.values().next().value; if (importedCss != null) { @@ -150,7 +168,7 @@ export default function resXVitePlugin(options = {}) { if (importedAsset != null) { map[moduleId] = importedAsset.toString(); } else { - map[moduleId] = path.toString(); + map[moduleId] = bundlePath.toString(); } } } @@ -170,19 +188,6 @@ export default function resXVitePlugin(options = {}) { return acc; }, {}); - // TODO: Fix cleanup? - // Do some basic cleanup of assets Vite emits that we're not really interested in. - /*Object.keys(bundle).forEach((key) => { - const entry = bundle[key]; - if ( - key.includes("/_virtual/@res-x-index-") || - (key.endsWith(".js") && original[entry.name] != null) - ) { - delete bundle[key]; - } - });*/ - - // Write JS file const jsFileContent = `export const assets = ${JSON.stringify( transformed, null, @@ -190,14 +195,40 @@ export default function resXVitePlugin(options = {}) { )}`; writeIfChanged(getAssetJsFileLoc(generated), jsFileContent); + writeIfChanged( + getStaticAssetRoutesJsFileLoc(generated), + getBuildStaticAssetRoutesFile( + transformed, + publicDir, + outDir, + staticAssetRoutes + ) + ); }, }; } function getAssetDirContent(assetDir) { + return getDirContent(assetDir); +} + +function getPublicDirContent(publicDir) { + return getDirContent(publicDir, { + dot: true, + }); +} + +function getDirContent(dir, options = {}) { + const { dot = false } = options; + const cwd = path.resolve(dir); + + if (!fs.existsSync(cwd)) { + return []; + } + return fg.globSync(["**/*"], { - dot: false, - cwd: path.resolve(assetDir), + dot, + cwd, }); } @@ -205,6 +236,10 @@ function getAssetJsFileLoc(generated) { return path.resolve(generated, "res-x-assets.js"); } +function getStaticAssetRoutesJsFileLoc(generated) { + return path.resolve(generated, "res-x-static-routes.js"); +} + function getAssetResFileLoc(generated) { return path.resolve(generated, "ResXAssets.res"); } @@ -251,6 +286,7 @@ function writeResTypeFile(assetDir, generated) { .join("\n")}\n}\n\n`; content += `@module("./res-x-assets.js") external assets: assets = "assets"`; + content += `\n\n@module("./res-x-static-routes.js") external staticAssetRoutes: Dict.t = "staticAssetRoutes"`; const assetResFileLoc = getAssetResFileLoc(generated); writeIfChanged(assetResFileLoc, content); @@ -270,26 +306,303 @@ function getDummyFile(assetDir, resXClientLocation) { .join(",\n")}\n}` ); - const txt = text.join("\n"); - return txt; + return text.join("\n"); } function getDummyDevFile(assetDir, resXClientLocation) { const content = getAssetDirContent(assetDir); let text = [`// Generated by ResX, do not edit manually\n`]; - const mapped = Object.fromEntries(content.map((k) => [k, k])); + const mapped = Object.fromEntries(content.map(k => [k, k])); text.push( `export const assets = {\n "resXClient_js": "/${resXClientLocation}",\n${content - .map((c) => { + .map(c => { const [_, transformed] = toRescriptFieldName(c, mapped); return ` "${transformed}": "/assets/${c}"`; }) .join(",\n")}\n}` ); - const txt = text.join("\n"); - return txt; + return text.join("\n"); +} + +function createStaticAssetRoutesConfig(staticAssetRoutes = {}) { + if ( + staticAssetRoutes == null || + typeof staticAssetRoutes !== "object" || + Array.isArray(staticAssetRoutes) + ) { + throw new Error( + "`staticAssetRoutes` must be an object when passed to resXVitePlugin." + ); + } + + if (staticAssetRoutes.assets != null) { + throw new Error( + "`staticAssetRoutes.assets` is no longer supported. ResX now always generates exact static asset routes. If you need a different asset loading strategy, implement your own asset pipeline." + ); + } + + const headers = staticAssetRoutes.headers || {}; + + if (typeof headers !== "object" || Array.isArray(headers)) { + throw new Error( + "`staticAssetRoutes.headers` must be an object whose keys are route patterns and whose values are header maps." + ); + } + + Object.entries(headers).forEach(([pattern, value]) => { + if (typeof pattern !== "string") { + throw new Error("`staticAssetRoutes.headers` keys must be strings."); + } + + if (value == null || typeof value !== "object" || Array.isArray(value)) { + throw new Error( + `\`staticAssetRoutes.headers.${pattern}\` must be an object of header name/value pairs.` + ); + } + }); + + return { + headers, + }; +} + +function getDummyStaticAssetRoutesFile( + assetDir, + publicDir, + resXClientLocation, + staticAssetRoutes +) { + return getStaticAssetRoutesFileContent({ + exactEntries: buildStaticAssetRouteEntries({ + publicEntries: getPublicRouteEntries({ + baseDir: publicDir, + fileDir: publicDir, + }), + assetEntries: getAssetDirContent(assetDir).map(assetPath => ({ + routePath: toRoutePath(path.join("assets", assetPath)), + filePath: toBunFilePath(path.join(assetDir, assetPath)), + })), + exactEntries: [ + { + routePath: toRoutePath(resXClientLocation), + filePath: toBunFilePath(resXClientLocation), + }, + ], + headers: staticAssetRoutes.headers, + }), + }); +} + +function getBuildStaticAssetRoutesFile( + assetMap, + publicDir, + outDir, + staticAssetRoutes +) { + return getStaticAssetRoutesFileContent({ + exactEntries: buildStaticAssetRouteEntries({ + publicEntries: getPublicRouteEntries({ + baseDir: publicDir, + fileDir: outDir, + }), + assetEntries: Object.values(assetMap).map(assetUrl => ({ + routePath: assetUrl, + filePath: toBunFilePath( + path.join(outDir, assetUrl.replace(/^\//, "")) + ), + })), + headers: staticAssetRoutes.headers, + }), + }); +} + +function buildStaticAssetRouteEntries({ + publicEntries, + assetEntries, + exactEntries = [], + headers, +}) { + return dedupeStaticAssetRouteEntries([ + ...publicEntries, + ...exactEntries, + ...assetEntries, + ]).map(entry => ({ + ...entry, + headers: getStaticAssetRouteHeaders(headers, entry.routePath), + })); +} + +function dedupeStaticAssetRouteEntries(entries) { + const dedupedEntries = new Map(); + + entries.forEach(entry => { + dedupedEntries.set(entry.routePath, entry); + }); + + return Array.from(dedupedEntries.values()); +} + +function getPublicRouteEntries({ baseDir, fileDir }) { + return getPublicDirContent(baseDir) + .map(publicPath => ({ + routePath: toRoutePath(publicPath), + filePath: toBunFilePath(path.join(fileDir, publicPath)), + })); +} + +function getStaticAssetRouteHeaders(headers, routePath) { + let matchedHeaders = null; + + Object.entries(headers).forEach(([pattern, value]) => { + if (matchesStaticAssetRoutePattern(pattern, routePath)) { + matchedHeaders = value; + } + }); + + return matchedHeaders; +} + +function matchesStaticAssetRoutePattern(pattern, routePath) { + return matchStaticAssetRouteSegments( + splitStaticAssetRoutePath(pattern), + splitStaticAssetRoutePath(routePath) + ); +} + +function matchStaticAssetRouteSegments(patternSegments, routeSegments) { + if (patternSegments.length === 0) { + return routeSegments.length === 0; + } + + const [segment, ...restPatternSegments] = patternSegments; + + if (segment === "**") { + if (restPatternSegments.length === 0) { + return true; + } + + for (let index = 0; index <= routeSegments.length; index++) { + if ( + matchStaticAssetRouteSegments( + restPatternSegments, + routeSegments.slice(index) + ) + ) { + return true; + } + } + + return false; + } + + if (routeSegments.length === 0) { + return false; + } + + if (segment === "*") { + return matchStaticAssetRouteSegments( + restPatternSegments, + routeSegments.slice(1) + ); + } + + if (segment !== routeSegments[0]) { + return false; + } + + return matchStaticAssetRouteSegments( + restPatternSegments, + routeSegments.slice(1) + ); +} + +function splitStaticAssetRoutePath(routePath) { + const normalized = toRoutePath(routePath); + + if (normalized === "/") { + return []; + } + + return normalized + .slice(1) + .split("/") + .filter(segment => segment.length > 0); +} + +function getStaticAssetRoutesFileContent({ exactEntries }) { + const sharedHeaderNames = new Map(); + const headerDefinitions = []; + let nextHeaderId = 0; + + const getHeaderName = headers => { + if (headers == null) { + return null; + } + + const headerKey = JSON.stringify(headers); + const existingHeaderName = sharedHeaderNames.get(headerKey); + + if (existingHeaderName != null) { + return existingHeaderName; + } + + const headerName = `staticAssetHeaders${nextHeaderId}`; + nextHeaderId += 1; + sharedHeaderNames.set(headerKey, headerName); + headerDefinitions.push({ + name: headerName, + value: headers, + }); + return headerName; + }; + + const exactRoutes = exactEntries + .sort((left, right) => left.routePath.localeCompare(right.routePath)) + .map( + ({ routePath, filePath, headers }) => { + const headerName = getHeaderName(headers); + const responseOptions = + headerName == null ? "" : `, { headers: ${headerName} }`; + + return ` ${JSON.stringify(routePath)}: {\n GET: new Response(Bun.file(${JSON.stringify( + filePath + )})${responseOptions}),\n HEAD: new Response(Bun.file(${JSON.stringify( + filePath + )})${responseOptions}),\n }`; + } + ); + + const headerConstants = + headerDefinitions.length === 0 + ? "" + : `${headerDefinitions + .map( + ({ name, value }) => + `const ${name} = ${JSON.stringify(value, null, 2)};` + ) + .join("\n")}\n\n`; + + return `// Generated by ResX, do not edit manually + +${headerConstants}export const staticAssetRoutes = { +${exactRoutes.join(",\n")} +} +`; +} + +function normalizePath(filePath) { + return filePath.replaceAll(path.sep, "/"); +} + +function toBunFilePath(filePath) { + const normalized = normalizePath(filePath); + return path.isAbsolute(filePath) ? normalized : "./" + normalized; +} + +function toRoutePath(filePath) { + return "/" + normalizePath(filePath).replace(/^\/+/, ""); } function toRescriptFieldName(fieldName, existingObj) { @@ -307,3 +620,13 @@ function toRescriptFieldName(fieldName, existingObj) { return [fieldName, transformedFieldName]; } + +export const __test = { + createStaticAssetRoutesConfig, + buildStaticAssetRouteEntries, + getBuildStaticAssetRoutesFile, + getDummyStaticAssetRoutesFile, + getStaticAssetRouteHeaders, + getStaticAssetRoutesFileContent, + matchesStaticAssetRoutePattern, +}; diff --git a/src/BunUtils.js b/src/BunUtils.js index 55dd3bc..41b8809 100644 --- a/src/BunUtils.js +++ b/src/BunUtils.js @@ -1,57 +1,9 @@ // Generated by ReScript, PLEASE EDIT WITH CARE 'use strict'; -let FastGlob = require("fast-glob"); let isDev = process.env.NODE_ENV !== "production"; -async function loadStaticFiles(root) { - return await FastGlob.glob(isDev ? [ - "public/**/*", - "assets/**/*" - ] : ["dist/**/*"], { - dot: true, - cwd: root !== undefined ? root : process.cwd() - }); -} - -let staticFiles = { - contents: undefined -}; - -async function serveStaticFile(request) { - let s = staticFiles.contents; - let staticFiles$1; - if (s !== undefined) { - staticFiles$1 = s; - } else { - let files = await loadStaticFiles(undefined); - let files$1 = new Map(files.map(f => [ - isDev ? ( - f.startsWith("public/") ? f.slice(7) : f - ) : ( - f.startsWith("dist/") ? f.slice(5) : f - ), - f - ])); - staticFiles.contents = files$1; - staticFiles$1 = files$1; - } - let url = new URL(request.url); - let pathname = url.pathname; - let path = pathname.split("/").filter(p => p !== ""); - let joined = path.join("/"); - let fileLoc = staticFiles$1.get(joined); - if (fileLoc === undefined) { - return; - } - let bunFile = Bun.file("./" + fileLoc); - let match = bunFile.size; - return match !== 0 ? new Response(bunFile) : new Response("", { - status: 404 - }); -} - function runDevServer(port) { Bun.serve({ development: true, @@ -79,7 +31,6 @@ let URLSearchParams$1 = { copy: copy }; -exports.serveStaticFile = serveStaticFile; exports.runDevServer = runDevServer; exports.isDev = isDev; exports.URLSearchParams = URLSearchParams$1; diff --git a/src/BunUtils.res b/src/BunUtils.res index b89ba94..2dd409e 100644 --- a/src/BunUtils.res +++ b/src/BunUtils.res @@ -2,76 +2,6 @@ external process: 'process = "process" let isDev = process["env"]["NODE_ENV"] !== "production" -type globConfig = { - dot?: bool, - cwd?: string, -} - -@module("fast-glob") -external glob: (array, globConfig) => promise> = "glob" - -let loadStaticFiles = async (~root=?) => { - await glob( - switch isDev { - | true => ["public/**/*", "assets/**/*"] - | false => ["dist/**/*"] - }, - { - dot: true, - cwd: switch root { - | None => process["cwd"]() - | Some(cwd) => cwd - }, - }, - ) -} - -let staticFiles = ref(None) - -let serveStaticFile = async request => { - open Bun - - let staticFiles = switch staticFiles.contents { - | None => - let files = await loadStaticFiles() - let files = - files - ->Array.map(f => { - ( - switch isDev { - | true if f->String.startsWith("public/") => f->String.slice(~start=7) - | false if f->String.startsWith("dist/") => f->String.slice(~start=5) - | _ => f - }, - f, - ) - }) - ->Map.fromArray - staticFiles := Some(files) - files - | Some(s) => s - } - - let url = request->Request.url->URL.make - let pathname = url->URL.pathname - - let path = pathname->String.split("/")->Array.filter(p => p !== "") - let joined = path->Array.join("/") - - switch staticFiles->Map.get(joined) { - | None => None - | Some(fileLoc) => - let bunFile = Bun.file("./" ++ fileLoc) - - Some( - switch bunFile->BunFile.size { - | 0. => Response.make("", ~options={status: 404}) - | _ => Response.makeFromFile(bunFile) - }, - ) - } -} - let runDevServer = (~port) => { let _devServer = Bun.serveWithWebSocket({ port: port + 1, diff --git a/src/BunUtils.resi b/src/BunUtils.resi index 9dd86eb..0d811d2 100644 --- a/src/BunUtils.resi +++ b/src/BunUtils.resi @@ -1,5 +1,3 @@ -let serveStaticFile: Request.t => promise> - let runDevServer: (~port: int) => unit let isDev: bool diff --git a/test/StaticAssetRoutes.test.js b/test/StaticAssetRoutes.test.js new file mode 100644 index 0000000..41e4839 --- /dev/null +++ b/test/StaticAssetRoutes.test.js @@ -0,0 +1,357 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { pathToFileURL } = require("url"); +const { describe, expect, test } = require("bun:test"); +const TestUtils = require("./TestUtils.js"); + +async function getPluginTestHelpers() { + return (await import("../res-x-vite-plugin.mjs")).__test; +} + +async function withTempDir(run) { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), "resx-static-asset-routes-") + ); + + try { + return await run(tempDir); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +async function importGeneratedModule(modulePath) { + return import(`${pathToFileURL(modulePath).href}?t=${Date.now()}`); +} + +async function withStaticAssetServer(routes, run) { + const [port, releasePort] = TestUtils.getPort(); + const server = Bun.serve({ + development: true, + port, + routes, + fetch: async request => + new Response(`app:${new URL(request.url).pathname}`, { + status: 200, + }), + }); + + try { + return await run({ + baseUrl: `http://localhost:${port}`, + }); + } finally { + server.stop(true); + releasePort(); + } +} + +describe("static asset routes", () => { + test("matches static asset route patterns", async () => { + const { matchesStaticAssetRoutePattern } = await getPluginTestHelpers(); + + expect(matchesStaticAssetRoutePattern("/assets/**", "/assets/app.css")).toBe( + true + ); + expect( + matchesStaticAssetRoutePattern( + "/assets/**", + "/assets/images/icons/logo.svg" + ) + ).toBe(true); + expect( + matchesStaticAssetRoutePattern("/assets/*", "/assets/app.css") + ).toBe(true); + expect( + matchesStaticAssetRoutePattern( + "/assets/*", + "/assets/images/icons/logo.svg" + ) + ).toBe(false); + expect(matchesStaticAssetRoutePattern("/robots.txt", "/robots.txt")).toBe( + true + ); + expect(matchesStaticAssetRoutePattern("/robots.txt", "/favicon.ico")).toBe( + false + ); + }); + + test("uses the last matching headers rule without merging", async () => { + const { getStaticAssetRouteHeaders } = await getPluginTestHelpers(); + + const headers = getStaticAssetRouteHeaders( + { + "/assets/**": { + "Cache-Control": "public, max-age=31536000, immutable", + "X-Base": "1", + }, + "/assets/app.css": { + "Cache-Control": "public, max-age=60", + }, + }, + "/assets/app.css" + ); + + expect(headers).toEqual({ + "Cache-Control": "public, max-age=60", + }); + }); + + test("always emits exact routes for managed assets", async () => { + const { buildStaticAssetRouteEntries, createStaticAssetRoutesConfig } = + await getPluginTestHelpers(); + + const exactEntries = buildStaticAssetRouteEntries({ + publicEntries: [ + { + routePath: "/robots.txt", + filePath: "./public/robots.txt", + }, + ], + assetEntries: [ + { + routePath: "/assets/a.css", + filePath: "./dist/assets/a.css", + }, + { + routePath: "/assets/b.css", + filePath: "./dist/assets/b.css", + }, + ], + headers: createStaticAssetRoutesConfig().headers, + }); + + expect(exactEntries).toEqual([ + { + routePath: "/robots.txt", + filePath: "./public/robots.txt", + headers: null, + }, + { + routePath: "/assets/a.css", + filePath: "./dist/assets/a.css", + headers: null, + }, + { + routePath: "/assets/b.css", + filePath: "./dist/assets/b.css", + headers: null, + }, + ]); + }); + + test("rejects the removed assets config", async () => { + const { createStaticAssetRoutesConfig } = await getPluginTestHelpers(); + + expect(() => + createStaticAssetRoutesConfig({ + assets: { + strategy: "exact", + }, + }) + ).toThrow( + "`staticAssetRoutes.assets` is no longer supported. ResX now always generates exact static asset routes. If you need a different asset loading strategy, implement your own asset pipeline." + ); + }); + + test("includes public assets under /assets routes", async () => { + const { getDummyStaticAssetRoutesFile, createStaticAssetRoutesConfig } = + await getPluginTestHelpers(); + + await withTempDir(async tempDir => { + const assetDir = path.join(tempDir, "assets"); + const publicDir = path.join(tempDir, "public"); + const resXClientPath = path.join(tempDir, "ResXClient.js"); + const modulePath = path.join(tempDir, "routes.mjs"); + + fs.mkdirSync(assetDir, { recursive: true }); + fs.mkdirSync(path.join(publicDir, "assets"), { recursive: true }); + fs.writeFileSync( + path.join(publicDir, "assets", "from-public.txt"), + "public asset" + ); + fs.writeFileSync(resXClientPath, "export {};\n"); + fs.writeFileSync( + modulePath, + getDummyStaticAssetRoutesFile( + assetDir, + publicDir, + resXClientPath, + createStaticAssetRoutesConfig() + ) + ); + + const { staticAssetRoutes } = await importGeneratedModule(modulePath); + const response = staticAssetRoutes["/assets/from-public.txt"].GET; + + expect(await response.text()).toBe("public asset"); + }); + }); + + test("generated exact routes serve correctly through Bun.serve", async () => { + const { getDummyStaticAssetRoutesFile, createStaticAssetRoutesConfig } = + await getPluginTestHelpers(); + + await withTempDir(async tempDir => { + const assetDir = path.join(tempDir, "assets"); + const publicDir = path.join(tempDir, "public"); + const resXClientPath = path.join(tempDir, "ResXClient.js"); + const modulePath = path.join(tempDir, "routes.mjs"); + + fs.mkdirSync(assetDir, { recursive: true }); + fs.mkdirSync(path.join(publicDir, "assets"), { recursive: true }); + fs.writeFileSync(path.join(assetDir, "app.css"), "body { color: red; }"); + fs.writeFileSync(path.join(publicDir, "robots.txt"), "User-agent: *"); + fs.writeFileSync( + path.join(publicDir, "assets", "from-public.txt"), + "public asset" + ); + fs.writeFileSync(resXClientPath, "export {};\n"); + fs.writeFileSync( + modulePath, + getDummyStaticAssetRoutesFile( + assetDir, + publicDir, + resXClientPath, + createStaticAssetRoutesConfig({ + headers: { + "/assets/**": { + "Cache-Control": "public, max-age=31536000, immutable", + }, + "/robots.txt": { + "Cache-Control": "public, max-age=300", + }, + }, + }) + ) + ); + + const { staticAssetRoutes } = await importGeneratedModule(modulePath); + const routes = Object.assign( + { + "/health": { + GET: new Response("ok"), + }, + }, + staticAssetRoutes + ); + + await withStaticAssetServer(routes, async ({ baseUrl }) => { + const robotsResponse = await fetch(`${baseUrl}/robots.txt`); + expect(robotsResponse.status).toBe(200); + expect(robotsResponse.headers.get("cache-control")).toBe( + "public, max-age=300" + ); + expect(await robotsResponse.text()).toBe("User-agent: *"); + + const assetResponse = await fetch(`${baseUrl}/assets/app.css`); + expect(assetResponse.status).toBe(200); + expect(assetResponse.headers.get("cache-control")).toBe( + "public, max-age=31536000, immutable" + ); + expect(await assetResponse.text()).toBe("body { color: red; }"); + + const publicAssetResponse = await fetch( + `${baseUrl}/assets/from-public.txt` + ); + expect(publicAssetResponse.status).toBe(200); + expect(await publicAssetResponse.text()).toBe("public asset"); + + const healthResponse = await fetch(`${baseUrl}/health`); + expect(healthResponse.status).toBe(200); + expect(await healthResponse.text()).toBe("ok"); + + const appResponse = await fetch(`${baseUrl}/app`); + expect(appResponse.status).toBe(200); + expect(await appResponse.text()).toBe("app:/app"); + }); + }); + }); + + test("generated exact routes apply configured headers", async () => { + const { getStaticAssetRoutesFileContent } = await getPluginTestHelpers(); + + await withTempDir(async tempDir => { + const robotsPath = path.join(tempDir, "robots.txt"); + const modulePath = path.join(tempDir, "routes.mjs"); + + fs.writeFileSync(robotsPath, "/\nDisallow: *"); + fs.writeFileSync( + modulePath, + getStaticAssetRoutesFileContent({ + exactEntries: [ + { + routePath: "/robots.txt", + filePath: robotsPath, + headers: { + "Cache-Control": "public, max-age=300", + }, + }, + ], + }) + ); + + const { staticAssetRoutes } = await importGeneratedModule(modulePath); + const response = staticAssetRoutes["/robots.txt"].GET; + + expect(response.headers.get("cache-control")).toBe("public, max-age=300"); + expect(await response.text()).toBe("/\nDisallow: *"); + }); + }); + + test("generated exact routes serve zero-byte files", async () => { + const { getStaticAssetRoutesFileContent } = await getPluginTestHelpers(); + + await withTempDir(async tempDir => { + const emptyPath = path.join(tempDir, "empty.txt"); + const modulePath = path.join(tempDir, "routes.mjs"); + + fs.writeFileSync(emptyPath, ""); + fs.writeFileSync( + modulePath, + getStaticAssetRoutesFileContent({ + exactEntries: [ + { + routePath: "/empty.txt", + filePath: emptyPath, + headers: null, + }, + ], + }) + ); + + const { staticAssetRoutes } = await importGeneratedModule(modulePath); + const response = staticAssetRoutes["/empty.txt"].GET; + + expect(response.status).toBe(200); + expect(await response.text()).toBe(""); + }); + }); + + test("generated exact routes resolve headers at build time", async () => { + const { getStaticAssetRoutesFileContent } = await getPluginTestHelpers(); + + const content = getStaticAssetRoutesFileContent({ + exactEntries: [ + { + routePath: "/robots.txt", + filePath: "./dist/robots.txt", + headers: { + "Cache-Control": "public, max-age=300", + }, + }, + ], + }); + + expect(content.includes("matchStaticAssetRouteSegments")).toBe(false); + expect(content.includes("getStaticAssetRouteHeaders(")).toBe(false); + expect(content.includes("decodeWildcardStaticAssetPath")).toBe(false); + expect(content.includes("const staticAssetHeaders0 =")).toBe(true); + expect( + content.includes( + 'new Response(Bun.file("./dist/robots.txt"), { headers: staticAssetHeaders0 })' + ) + ).toBe(true); + }); +}); From 9f6b9f4a089984cbc6961a20d00990d39ddeff7c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 8 Mar 2026 22:07:10 +0100 Subject: [PATCH 2/2] Rebuild demo JS after rebase --- demo/src/Demo.js | 8 +++----- demo/src/FourOhFour.js | 5 ++--- demo/src/Head.js | 6 ++---- demo/src/Hello.js | 3 +-- demo/src/HtmxHandler.js | 2 +- demo/src/SiteMap.js | 3 +-- demo/src/UserPage.js | 3 +-- demo/src/UserRoutes.js | 7 +++---- 8 files changed, 14 insertions(+), 23 deletions(-) diff --git a/demo/src/Demo.js b/demo/src/Demo.js index 201b612..38592b7 100644 --- a/demo/src/Demo.js +++ b/demo/src/Demo.js @@ -10,13 +10,11 @@ let ResXAssets = require("./__generated__/ResXAssets.js"); let UserRoutes = require("./UserRoutes.js"); let HtmxHandler = require("./HtmxHandler.js"); let BunUtils$ResX = require("rescript-x/src/BunUtils.js"); -let Handlers$ResX = require("rescript-x/src/Handlers.js"); -let RequestController$ResX = require("rescript-x/src/RequestController.js"); let server = Bun.serve({ development: BunUtils$ResX.isDev, port: 4444, - fetch: async (request, _server) => await Handlers$ResX.handleRequest(HtmxHandler.handler, { + fetch: async (request, _server) => await HtmxHandler.handler.handleRequest({ request: request, render: async param => { let requestController = param.requestController; @@ -32,7 +30,7 @@ let server = Bun.serve({ exit = 1; } if (exit === 1) { - RequestController$ResX.appendTitleSegment(requestController, "Test App"); + requestController.appendTitleSegment("Test App"); let tmp; let exit$1 = 0; if (path !== 0) { @@ -41,7 +39,7 @@ let server = Bun.serve({ case "moved" : tmp = path.tl !== 0 ? Hjsx$ResX.jsx(FourOhFour.make, { setGenericTitle: true - }) : RequestController$ResX.redirect(requestController, "/start", 302); + }) : requestController.redirect("/start", 302); break; case "" : case "start" : diff --git a/demo/src/FourOhFour.js b/demo/src/FourOhFour.js index a5e825e..c45746a 100644 --- a/demo/src/FourOhFour.js +++ b/demo/src/FourOhFour.js @@ -3,15 +3,14 @@ let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); let HtmxHandler = require("./HtmxHandler.js"); -let RequestController$ResX = require("rescript-x/src/RequestController.js"); function FourOhFour(props) { let __setGenericTitle = props.setGenericTitle; let setGenericTitle = __setGenericTitle !== undefined ? __setGenericTitle : false; let context = HtmxHandler.useContext(); - RequestController$ResX.setStatus(context.requestController, 404); + context.requestController.setStatus(404); if (setGenericTitle) { - RequestController$ResX.setFullTitle(context.requestController, "Not Found"); + context.requestController.setFullTitle("Not Found"); } return Hjsx$ResX.Elements.jsx("div", { children: "404" diff --git a/demo/src/Head.js b/demo/src/Head.js index f937584..ea9addc 100644 --- a/demo/src/Head.js +++ b/demo/src/Head.js @@ -2,8 +2,6 @@ 'use strict'; let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); -let Handlers$ResX = require("rescript-x/src/Handlers.js"); -let RequestController$ResX = require("rescript-x/src/RequestController.js"); function Head(props) { return Hjsx$ResX.Elements.jsx("head", { @@ -12,8 +10,8 @@ function Head(props) { } function Head$RenderInHead(props) { - let ctx = Handlers$ResX.useContext(props.handler); - RequestController$ResX.appendToHead(ctx.requestController, props.children); + let ctx = props.handler.useContext(); + ctx.requestController.appendToHead(props.children); return null; } diff --git a/demo/src/Hello.js b/demo/src/Hello.js index c524352..ce09927 100644 --- a/demo/src/Hello.js +++ b/demo/src/Hello.js @@ -6,7 +6,6 @@ let Htmx$ResX = require("rescript-x/src/Htmx.js"); let Stdlib_Exn = require("@rescript/runtime/lib/js/Stdlib_Exn.js"); let HtmxHandler = require("./HtmxHandler.js"); let ErrorMessage = require("./ErrorMessage.js"); -let Handlers$ResX = require("rescript-x/src/Handlers.js"); let Primitive_option = require("@rescript/runtime/lib/js/Primitive_option.js"); let SecurityPolicy$ResX = require("rescript-x/src/SecurityPolicy.js"); let FormDataHelpers$ResX = require("rescript-x/src/FormDataHelpers.js"); @@ -33,7 +32,7 @@ function myVariantFromString(a) { }; } -let onButtonBlick = Handlers$ResX.hxPost(HtmxHandler.handler, "/button-click", SecurityPolicy$ResX.allow, async param => { +let onButtonBlick = HtmxHandler.handler.hxPost("/button-click", SecurityPolicy$ResX.allow, async param => { try { let formData = await param.request.formData(); let firstName = FormDataHelpers$ResX.expectString(formData, "firstName", undefined); diff --git a/demo/src/HtmxHandler.js b/demo/src/HtmxHandler.js index 5e94a04..aaf2640 100644 --- a/demo/src/HtmxHandler.js +++ b/demo/src/HtmxHandler.js @@ -8,7 +8,7 @@ let handler = Handlers$ResX.make(async _request => ({ }), undefined); function useContext() { - return Handlers$ResX.useContext(handler); + return handler.useContext(); } exports.handler = handler; diff --git a/demo/src/SiteMap.js b/demo/src/SiteMap.js index 15e2938..01b5c0d 100644 --- a/demo/src/SiteMap.js +++ b/demo/src/SiteMap.js @@ -4,11 +4,10 @@ let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); let Utils$ResX = require("rescript-x/src/Utils.js"); let HtmxHandler = require("./HtmxHandler.js"); -let RequestController$ResX = require("rescript-x/src/RequestController.js"); function SiteMap(props) { let context = HtmxHandler.useContext(); - RequestController$ResX.setDocHeader(context.requestController, ``); + context.requestController.setDocHeader(``); context.headers.set("Content-Type", "application/xml; charset=UTF-8"); context.headers.set("Cache-Control", Utils$ResX.CacheControl.make(undefined, "public", [{ kind: "max-age", diff --git a/demo/src/UserPage.js b/demo/src/UserPage.js index ad56677..8a990f2 100644 --- a/demo/src/UserPage.js +++ b/demo/src/UserPage.js @@ -9,7 +9,6 @@ let Client$ResX = require("rescript-x/src/Client.js"); let HtmxHandler = require("./HtmxHandler.js"); let Core__Option = require("@rescript/core/src/Core__Option.js"); let Stdlib_JsExn = require("@rescript/runtime/lib/js/Stdlib_JsExn.js"); -let Handlers$ResX = require("rescript-x/src/Handlers.js"); let FailingComponent = require("./FailingComponent.js"); let Primitive_option = require("@rescript/runtime/lib/js/Primitive_option.js"); let ErrorBoundary$ResX = require("rescript-x/src/ErrorBoundary.js"); @@ -17,7 +16,7 @@ let SecurityPolicy$ResX = require("rescript-x/src/SecurityPolicy.js"); let FormDataHelpers$ResX = require("rescript-x/src/FormDataHelpers.js"); let Primitive_exceptions = require("@rescript/runtime/lib/js/Primitive_exceptions.js"); -let onForm = Handlers$ResX.hxPost(HtmxHandler.handler, "/user-single", SecurityPolicy$ResX.allow, async param => { +let onForm = HtmxHandler.handler.hxPost("/user-single", SecurityPolicy$ResX.allow, async param => { let formData = await param.request.formData(); try { let name = FormDataHelpers$ResX.expectString(formData, "name", undefined); diff --git a/demo/src/UserRoutes.js b/demo/src/UserRoutes.js index 618a971..df3404a 100644 --- a/demo/src/UserRoutes.js +++ b/demo/src/UserRoutes.js @@ -5,14 +5,13 @@ let UserPage = require("./UserPage.js"); let Hjsx$ResX = require("rescript-x/src/Hjsx.js"); let FourOhFour = require("./FourOhFour.js"); let UserFriends = require("./UserFriends.js"); -let RequestController$ResX = require("rescript-x/src/RequestController.js"); function match(userRoutes, headers, requestController) { - RequestController$ResX.appendTitleSegment(requestController, "Users"); + requestController.appendTitleSegment("Users"); if (userRoutes !== 0) { let userRoutes$1 = userRoutes.tl; let userId = userRoutes.hd; - RequestController$ResX.appendTitleSegment(requestController, userId); + requestController.appendTitleSegment(userId); headers.set("Cache-Control", "private"); let tmp; tmp = userRoutes$1 !== 0 && userRoutes$1.hd === "friends" && userRoutes$1.tl === 0 ? Hjsx$ResX.jsx(UserFriends.make, { @@ -23,7 +22,7 @@ function match(userRoutes, headers, requestController) { userId: userId }); } - RequestController$ResX.appendTitleSegment(requestController, "Not found"); + requestController.appendTitleSegment("Not found"); return Hjsx$ResX.jsx(FourOhFour.make, {}); }