Asset bundling
The live seat map and the JSON checkout need a bit of JavaScript on the concert page: the seat picker. Margaret works with esbuild: you build your scripts and styles with esbuild, and Margaret embeds the result in your binary and checks every asset you reference while it builds.
The seat picker
The concert page lists every seat as a checkbox. The seat picker opens the WebSocket from the previous chapter, disables the seats that are taken, pauses the updates while the tab is hidden, and posts the order as JSON when the customer buys:
import "../css/seat_picker.css";
const picker = document.querySelector<HTMLFormElement>(".seat-picker")!;
const remaining = picker.querySelector(".seat-picker__remaining")!;
const liveUrl = new URL(
`${window.location.pathname}/live`,
window.location.href,
);
liveUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const socket = new WebSocket(liveUrl);
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ id: 1, method: "watch_seats", params: {} }));
});
socket.addEventListener("message", (event) => {
const { result } = JSON.parse(event.data);
remaining.textContent = `${result.remaining} tickets left`;
for (const seat of result.seats) {
picker.querySelector<HTMLInputElement>(
`input[name="seat"][value="${seat}"]`,
)!.disabled = true;
}
});
document.addEventListener("visibilitychange", () => {
socket.send(
JSON.stringify({
method: "visibility",
params: { visible: document.visibilityState === "visible" },
}),
);
});
picker.addEventListener("submit", async (event) => {
event.preventDefault();
const form = new FormData(picker);
const seats = form.getAll("seat").map(Number);
const response = await fetch(picker.action, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: form.get("email"), seats }),
});
if (response.ok) {
window.location.href = (await response.json()).order_url;
}
});Building with esbuild
Run esbuild from your crate's directory, next to Cargo.toml. Write the output to assets/, and ask for a metafile called esbuild-meta.json:
$ esbuild resources/css/box_office.css resources/ts/seat_picker.ts \
--bundle --format=esm --outdir=assets \
--entry-names='[name]_[hash]' --metafile=esbuild-meta.jsonThe metafile tells Margaret what esbuild built. As soon as it exists, Margaret generates an asset! macro. Run esbuild before cargo build, so the build sees the latest assets.
Using assets in views
asset! takes the path of a source file. Add it to the page's AssetBag, and the layout renders the matching and tags:
use AssetBag;
use renders_view;
use singleton;
use DOCTYPE;
use Markup;
use html;
use RendersView;
use crateasset;
;
The concert page adds the seat picker, and renders the form it works with:
asset_bag.add;
self.layout.renderRoutes get a fresh AssetBag for every request by asking for one:
A path esbuild didn't build doesn't compile:
error: unknown esbuild asset input: resources/ts/seat_pickr.tsAssetBag also has image() and file() for images and fonts you reference directly in your markup.
Serving assets
The generated AssetResponder serves the files. {*path} catches the rest of the path:
use Arc;
use Response;
use constructor;
use process;
use responds_to_http;
use singleton;
use RouteMethod;
use crateAssetResponder;
Every file is embedded in the binary, so there's nothing extra to deploy. Bundled files get long-lived cache headers, since their names change whenever their content does:
$ curl -i http://127.0.0.1:8080/assets/seat_picker_ZULLZKGM.js
HTTP/1.1 200 OK
content-type: text/javascript
cache-control: public, max-age=31536000, immutable