Template
The starting point for an app with a React frontend. Same server, env, database and API skeleton as template-htmx — the two differ only in what is served at / — with a React 19 SPA, Tailwind v4 and shadcn/ui on top. Carried from a working project rather than designed in the abstract, and carrying the lessons that cost it time: - The dev script states NODE_ENV=development rather than inheriting it. React resolves its runtime against NODE_ENV: Bun emits jsxDEV() in development and React's production jsx-dev-runtime defines jsxDEV as void 0. Inherit production while transforming for development and the app dies at first paint. Verified: the dev bundle resolves the development runtime and contains no void-0 stub. - Asset URLs in components are relative and <base href="/"> is in the HTML, so a path-prefix proxy can rewrite one attribute and every asset follows. A test enforces it. - Routing is in the hash, because the app may be mounted under a prefix it only learns at runtime and a hash means the same thing at either mount. - index.html holds __PUBLIC_URL__ and gen-index.ts substitutes it, because Bun's HTML bundler fails the build on a root-relative favicon href. - APP_ENV failing closed, env files before the ambient environment, the database enforced by name, one Postgres client per process — as in the htmx template. The database has no tables. The App is a shell with one tab and no features, named as a placeholder. Renaming `app` is the first step and the README lists every place it appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
35 lines
1.4 KiB
TypeScript
35 lines
1.4 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
// Guards the one rule that broke the preview: an asset URL starting with "/" escapes the proxy prefix.
|
|
//
|
|
// Served through a path-prefix proxy the app lives under /<prefix>/, and only the HTML is
|
|
// rewritten — a root-absolute path inside a component reaches the platform root, which answers
|
|
// 200 text/html. The image is broken and the network tab shows no error, so this is worth a test
|
|
// rather than a note someone reads after losing an afternoon to it.
|
|
|
|
const frontendDir = dirname(fileURLToPath(import.meta.url));
|
|
const templatePath = resolve(frontendDir, "index.html");
|
|
|
|
describe("asset URLs", () => {
|
|
test("index.html declares a base so relative URLs can be rewritten", async () => {
|
|
const html = await Bun.file(templatePath).text();
|
|
expect(html).toMatch(/<base\s+href="\/"\s*\/?>/);
|
|
});
|
|
|
|
test("no component uses a root-absolute asset URL", async () => {
|
|
const offenders: string[] = [];
|
|
|
|
for await (const relative of new Bun.Glob("**/*.tsx").scan({ cwd: frontendDir })) {
|
|
const source = await Bun.file(resolve(frontendDir, relative)).text();
|
|
|
|
for (const [match] of source.matchAll(/\b(?:src|srcSet|poster)=["']\/[^/][^"']*["']/g)) {
|
|
offenders.push(`${relative}: ${match}`);
|
|
}
|
|
}
|
|
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
});
|