@bractjs/bractjs 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +18 -1
- package/package.json +3 -2
- package/src/__tests__/codegen-write.test.ts +67 -0
- package/src/__tests__/codegen.test.ts +29 -2
- package/src/__tests__/compile-safety.test.ts +4 -0
- package/src/__tests__/csp.test.ts +10 -0
- package/src/__tests__/define-actions.test.ts +69 -0
- package/src/__tests__/env.test.ts +18 -0
- package/src/__tests__/fetcher-store.test.ts +67 -0
- package/src/__tests__/fixtures/app/root.tsx +7 -2
- package/src/__tests__/fixtures/app/routes/boom.tsx +9 -0
- package/src/__tests__/fixtures/app/routes/client-only.tsx +16 -0
- package/src/__tests__/fixtures/app/routes/counter.tsx +16 -0
- package/src/__tests__/fixtures/app/routes/data-only.tsx +16 -0
- package/src/__tests__/fixtures/app/routes/features-demo.tsx +28 -0
- package/src/__tests__/fixtures/app/routes/intent-demo.tsx +46 -0
- package/src/__tests__/fixtures/app/routes/protected-client-only.tsx +15 -0
- package/src/__tests__/fixtures/app/routes/search-demo.tsx +39 -0
- package/src/__tests__/form-data-helpers.test.ts +43 -0
- package/src/__tests__/headers.test.ts +111 -0
- package/src/__tests__/integration.test.ts +90 -0
- package/src/__tests__/layout-registry.test.ts +7 -3
- package/src/__tests__/loader.test.ts +32 -1
- package/src/__tests__/matcher.test.ts +29 -0
- package/src/__tests__/module-registry.test.ts +2 -3
- package/src/__tests__/nav-utils.test.ts +46 -0
- package/src/__tests__/prerender.test.ts +102 -0
- package/src/__tests__/programmatic-api.test.ts +20 -1
- package/src/__tests__/revalidation.test.ts +65 -0
- package/src/__tests__/route-lint.test.ts +79 -0
- package/src/__tests__/route-middleware.test.ts +84 -0
- package/src/__tests__/route-table.test.ts +33 -0
- package/src/__tests__/safe-validate.test.ts +96 -0
- package/src/__tests__/scanner.test.ts +46 -1
- package/src/__tests__/scroll-restoration.test.ts +66 -0
- package/src/__tests__/search-serializer.test.ts +42 -0
- package/src/__tests__/search-validation.test.ts +125 -0
- package/src/__tests__/security-fixes.test.ts +201 -0
- package/src/__tests__/security.test.ts +110 -1
- package/src/__tests__/selective-ssr.test.ts +85 -0
- package/src/__tests__/spa-mode.test.ts +77 -0
- package/src/__tests__/typed-routing.test.ts +51 -1
- package/src/__tests__/use-matches.test.ts +54 -0
- package/src/build/bundler.ts +33 -0
- package/src/build/prerender.ts +88 -0
- package/src/build/route-lint.ts +49 -0
- package/src/client/ClientRouter.tsx +339 -47
- package/src/client/cache.ts +8 -0
- package/src/client/components/Await.tsx +9 -2
- package/src/client/components/Form.tsx +23 -34
- package/src/client/components/Link.tsx +80 -9
- package/src/client/components/Outlet.tsx +8 -2
- package/src/client/components/ScrollRestoration.tsx +125 -0
- package/src/client/entry.tsx +39 -2
- package/src/client/fetcher-store.ts +61 -0
- package/src/client/form-utils.ts +3 -0
- package/src/client/hooks/useActionData.ts +7 -3
- package/src/client/hooks/useFetcher.ts +116 -33
- package/src/client/hooks/useFetchers.ts +23 -0
- package/src/client/hooks/useLoaderData.ts +8 -4
- package/src/client/hooks/useLocation.ts +27 -0
- package/src/client/hooks/useMatches.ts +32 -0
- package/src/client/hooks/useNavigate.ts +11 -6
- package/src/client/hooks/useRevalidator.ts +26 -0
- package/src/client/hooks/useSearch.ts +73 -0
- package/src/client/hooks/useSearchParams.ts +7 -2
- package/src/client/nav-utils.ts +26 -0
- package/src/client/prefetch.ts +110 -15
- package/src/client/registry.ts +24 -0
- package/src/client/revalidation.ts +25 -0
- package/src/client/router.tsx +34 -1
- package/src/client/rpc.ts +11 -1
- package/src/client/scroll-restoration.ts +48 -0
- package/src/client/search-serializer.ts +40 -0
- package/src/client/types.ts +6 -0
- package/src/codegen/module-registry.ts +13 -21
- package/src/codegen/route-codegen.ts +148 -10
- package/src/config/load.ts +22 -0
- package/src/dev/hmr-client.ts +3 -1
- package/src/dev/route-table.ts +27 -0
- package/src/dev/server.ts +106 -8
- package/src/dev/watcher.ts +25 -3
- package/src/index.ts +38 -6
- package/src/server/action-handler.ts +3 -13
- package/src/server/action-registry.ts +35 -0
- package/src/server/adapter.ts +16 -0
- package/src/server/api-route.ts +47 -0
- package/src/server/csp.ts +19 -4
- package/src/server/csrf.ts +36 -3
- package/src/server/env.ts +26 -5
- package/src/server/headers.ts +49 -0
- package/src/server/layout.ts +43 -20
- package/src/server/loader.ts +14 -8
- package/src/server/matcher.ts +29 -2
- package/src/server/matches.ts +50 -0
- package/src/server/middleware.ts +66 -0
- package/src/server/proto-guard.ts +56 -0
- package/src/server/render.ts +51 -18
- package/src/server/request-handler.ts +111 -29
- package/src/server/scanner.ts +45 -3
- package/src/server/search.ts +47 -0
- package/src/server/serve.ts +116 -4
- package/src/server/session.ts +12 -1
- package/src/server/spa.ts +62 -0
- package/src/server/stream-handler.ts +10 -1
- package/src/server/validate.ts +89 -14
- package/src/shared/context.ts +7 -0
- package/src/shared/define-actions.ts +39 -0
- package/src/shared/form-data.ts +34 -0
- package/src/shared/route-types.ts +191 -2
- package/templates/new-app/app/root.tsx +2 -1
- package/templates/new-app/bractjs.config.ts +7 -12
- package/types/config.d.ts +24 -0
- package/types/index.d.ts +182 -9
- package/types/route.d.ts +138 -3
- package/LICENSE +0 -21
- package/README.md +0 -1125
package/src/build/bundler.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface BuildConfig {
|
|
|
19
19
|
minify?: boolean;
|
|
20
20
|
clientEnv?: string[];
|
|
21
21
|
plugins?: BunPlugin[];
|
|
22
|
+
/** SPA mode: when `false`, the build also emits the static document shell. */
|
|
23
|
+
ssr?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export async function runBuild(config: BuildConfig): Promise<void> {
|
|
@@ -30,6 +32,16 @@ export async function runBuild(config: BuildConfig): Promise<void> {
|
|
|
30
32
|
const routeFilePaths = routes.map((r) => join(appDir, r.filePath));
|
|
31
33
|
const rootFilePath = join(appDir, "root.tsx");
|
|
32
34
|
|
|
35
|
+
// Static route-module lint: surface empty routes and miscased exports at
|
|
36
|
+
// build time (no execution — just source analysis).
|
|
37
|
+
const { lintRouteModuleSource } = await import("./route-lint.ts");
|
|
38
|
+
for (const r of routes) {
|
|
39
|
+
const src = await Bun.file(join(appDir, r.filePath)).text().catch(() => "");
|
|
40
|
+
for (const warning of lintRouteModuleSource(src, r.filePath)) {
|
|
41
|
+
console.warn(`[bract] ${warning}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
// ── 1. Clean stale artefacts ────────────────────────────────────────────
|
|
34
46
|
const buildDir = config.buildDir ?? "build";
|
|
35
47
|
await Promise.all([
|
|
@@ -140,5 +152,26 @@ export async function runBuild(config: BuildConfig): Promise<void> {
|
|
|
140
152
|
// ── 5. Write manifest ──────────────────────────────────────────────────
|
|
141
153
|
const manifest = generateManifest({ clientEntry, rootChunk, routeChunks, mode: "production" });
|
|
142
154
|
await writeManifest(manifest, "build");
|
|
155
|
+
|
|
156
|
+
// ── 6. SPA shell (ssr: false) ───────────────────────────────────────────
|
|
157
|
+
// Emit the static document shell every document GET will serve in SPA mode.
|
|
158
|
+
if (config.ssr === false) {
|
|
159
|
+
const { renderSpaShell } = await import("../server/spa.ts");
|
|
160
|
+
const { installUseClientServerStub } = await import("../server/use-client-runtime.ts");
|
|
161
|
+
// root.tsx is imported from source here — "use client" components inside
|
|
162
|
+
// it must null-render exactly as they do on the running server.
|
|
163
|
+
installUseClientServerStub(appDir);
|
|
164
|
+
const serverManifest = {
|
|
165
|
+
clientEntry,
|
|
166
|
+
rootChunk,
|
|
167
|
+
routes: Object.fromEntries(
|
|
168
|
+
Object.entries(manifest.routes).map(([pat, e]) => [pat, { file: e.chunk, chunk: e.chunk }]),
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
const html = await renderSpaShell(appDir, serverManifest);
|
|
172
|
+
await Bun.write(join(buildDir, "client", "__spa.html"), html);
|
|
173
|
+
console.log("[bract] SPA shell → build/client/__spa.html");
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
console.log("[bract] build complete →", Object.keys(manifest.routes).length, "routes");
|
|
144
177
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { buildFetchHandler } from "../server/serve.ts";
|
|
3
|
+
import type { ServerManifest } from "../server/render.ts";
|
|
4
|
+
|
|
5
|
+
export interface PrerenderOptions {
|
|
6
|
+
/** Concrete paths to prerender (or a function resolving them, e.g. from a DB). */
|
|
7
|
+
prerender: string[] | (() => string[] | Promise<string[]>);
|
|
8
|
+
appDir?: string;
|
|
9
|
+
publicDir?: string;
|
|
10
|
+
buildDir?: string;
|
|
11
|
+
/** Override the manifest instead of loading `<buildDir>/route-manifest.json`. */
|
|
12
|
+
manifest?: ServerManifest;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PrerenderResult {
|
|
16
|
+
written: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where a path's prerendered files live under `<buildDir>/client/_prerender`.
|
|
21
|
+
* Throws on anything that isn't a clean absolute path — these strings come
|
|
22
|
+
* from user config but become filesystem writes.
|
|
23
|
+
*/
|
|
24
|
+
export function prerenderPaths(path: string): { html: string; data: string } {
|
|
25
|
+
if (!path.startsWith("/")) {
|
|
26
|
+
throw new Error(`[bractjs] prerender: paths must start with "/", got ${JSON.stringify(path)}`);
|
|
27
|
+
}
|
|
28
|
+
if (path.includes(":") || path.includes("[") || path.includes("*")) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`[bractjs] prerender: ${JSON.stringify(path)} looks like a route PATTERN — ` +
|
|
31
|
+
`expand dynamic routes to concrete paths (e.g. "/blog/intro", not "/blog/:slug").`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const segments = path.split("/").filter(Boolean);
|
|
35
|
+
if (segments.some((s) => s === ".." || s === ".")) {
|
|
36
|
+
throw new Error(`[bractjs] prerender: refusing path with dot segments: ${JSON.stringify(path)}`);
|
|
37
|
+
}
|
|
38
|
+
const dir = segments.join("/");
|
|
39
|
+
return {
|
|
40
|
+
html: dir === "" ? "index.html" : `${dir}/index.html`,
|
|
41
|
+
data: dir === "" ? "_data.json" : `${dir}/_data.json`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build-time prerendering (SSG): run the production fetch handler in-process
|
|
47
|
+
* against each configured path and write the HTML document plus its `/_data`
|
|
48
|
+
* payload (used by client navigations INTO a prerendered page) under
|
|
49
|
+
* `<buildDir>/client/_prerender/`. The production server serves these before
|
|
50
|
+
* falling back to dynamic SSR — query-carrying requests stay dynamic.
|
|
51
|
+
*
|
|
52
|
+
* Loaders run for real at build time: anything they need (DB, env) must be
|
|
53
|
+
* available to the build.
|
|
54
|
+
*/
|
|
55
|
+
export async function runPrerender(options: PrerenderOptions): Promise<PrerenderResult> {
|
|
56
|
+
const buildDir = options.buildDir ?? "./build";
|
|
57
|
+
const paths = typeof options.prerender === "function" ? await options.prerender() : options.prerender;
|
|
58
|
+
|
|
59
|
+
const handler = buildFetchHandler({
|
|
60
|
+
appDir: options.appDir ?? "./app",
|
|
61
|
+
publicDir: options.publicDir,
|
|
62
|
+
buildDir,
|
|
63
|
+
manifest: options.manifest,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const written: string[] = [];
|
|
67
|
+
for (const path of paths) {
|
|
68
|
+
const out = prerenderPaths(path);
|
|
69
|
+
|
|
70
|
+
const htmlRes = await handler(new Request("http://prerender.local" + path));
|
|
71
|
+
if (htmlRes.status !== 200) {
|
|
72
|
+
throw new Error(`[bractjs] prerender: GET ${path} returned ${htmlRes.status}`);
|
|
73
|
+
}
|
|
74
|
+
const htmlFile = join(buildDir, "client", "_prerender", out.html);
|
|
75
|
+
await Bun.write(htmlFile, await htmlRes.text());
|
|
76
|
+
written.push(htmlFile);
|
|
77
|
+
|
|
78
|
+
const dataRes = await handler(
|
|
79
|
+
new Request("http://prerender.local/_data?path=" + encodeURIComponent(path)),
|
|
80
|
+
);
|
|
81
|
+
if (dataRes.status === 200) {
|
|
82
|
+
const dataFile = join(buildDir, "client", "_prerender", out.data);
|
|
83
|
+
await Bun.write(dataFile, await dataRes.text());
|
|
84
|
+
written.push(dataFile);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { written };
|
|
88
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { extractExports } from "./directives.ts";
|
|
2
|
+
|
|
3
|
+
// The canonical route-module export names. A route file exporting a near-miss
|
|
4
|
+
// (wrong case) of one of these is almost always a mistake — the framework's
|
|
5
|
+
// projection is case-sensitive, so `Loader` is silently ignored.
|
|
6
|
+
export const ROUTE_EXPORT_NAMES = [
|
|
7
|
+
"default", "loader", "action", "clientLoader", "clientAction", "meta", "headers",
|
|
8
|
+
"middleware", "beforeLoad", "shouldRevalidate", "searchSchema", "ssr", "Fallback",
|
|
9
|
+
"handle", "ErrorBoundary", "config", "loaderDeps", "context",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
const CANONICAL_LOWER = new Map(ROUTE_EXPORT_NAMES.map((n) => [n.toLowerCase(), n]));
|
|
13
|
+
const CANONICAL_SET = new Set<string>(ROUTE_EXPORT_NAMES);
|
|
14
|
+
|
|
15
|
+
/** A route is "renderable or does work" if it has any of these. */
|
|
16
|
+
const MEANINGFUL = ["default", "loader", "action", "beforeLoad"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Static lint of a route module's SOURCE (no execution). Returns human-readable
|
|
20
|
+
* warning strings. Used by the dev rebuilder and the production build to catch
|
|
21
|
+
* two common, silent mistakes: a route that renders nothing, and an export
|
|
22
|
+
* whose casing doesn't match a framework export (so it's ignored).
|
|
23
|
+
*/
|
|
24
|
+
export function lintRouteModuleSource(src: string, filePath: string): string[] {
|
|
25
|
+
const warnings: string[] = [];
|
|
26
|
+
const names = extractExports(src);
|
|
27
|
+
// extractExports misses anonymous `export default () => …` / `export default function() {}`.
|
|
28
|
+
const hasAnonDefault = /^export\s+default\b/m.test(src) && !names.includes("default");
|
|
29
|
+
const exportSet = new Set(names);
|
|
30
|
+
if (hasAnonDefault) exportSet.add("default");
|
|
31
|
+
|
|
32
|
+
if (!MEANINGFUL.some((n) => exportSet.has(n))) {
|
|
33
|
+
warnings.push(
|
|
34
|
+
`${filePath}: route has no default/loader/action/beforeLoad export — it renders an empty page.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const name of exportSet) {
|
|
39
|
+
if (CANONICAL_SET.has(name)) continue;
|
|
40
|
+
const canonical = CANONICAL_LOWER.get(name.toLowerCase());
|
|
41
|
+
if (canonical && canonical !== name) {
|
|
42
|
+
warnings.push(
|
|
43
|
+
`${filePath}: export "${name}" looks like "${canonical}" — route exports are case-sensitive, so "${name}" is ignored.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return warnings;
|
|
49
|
+
}
|