@bractjs/bractjs 0.1.15 → 0.1.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bractjs/bractjs",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Production-grade SSR framework for Bun + React 19. File-based routing, streaming SSR, server actions, typed routes.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/bractjs/bractjs#readme",
@@ -102,7 +102,7 @@ export async function runBuild(config: BractJSConfig): Promise<void> {
102
102
  }
103
103
 
104
104
  // ── 5. Write manifest ──────────────────────────────────────────────────
105
- const manifest = generateManifest({ clientEntry, rootChunk, routeChunks });
105
+ const manifest = generateManifest({ clientEntry, rootChunk, routeChunks, mode: "production" });
106
106
  await writeManifest(manifest, "build");
107
107
  console.log("[bract] build complete →", Object.keys(manifest.routes).length, "routes");
108
108
  }
@@ -9,6 +9,8 @@ export interface RouteManifestEntry {
9
9
 
10
10
  export interface RouteManifest {
11
11
  version: 1;
12
+ /** "production" = produced by `bractjs build`. Absent on dev-rebuilder manifests. */
13
+ mode?: "production";
12
14
  clientEntry: string;
13
15
  rootChunk?: string;
14
16
  routes: Record<string, RouteManifestEntry>;
@@ -29,12 +31,13 @@ export function generateManifest(opts: {
29
31
  clientEntry: string;
30
32
  rootChunk?: string;
31
33
  routeChunks: Map<string, string>;
34
+ mode?: "production";
32
35
  }): RouteManifest {
33
36
  const routes: Record<string, RouteManifestEntry> = {};
34
37
  for (const [pattern, chunk] of opts.routeChunks) {
35
38
  routes[pattern] = { chunk, pattern };
36
39
  }
37
- return { version: 1, clientEntry: opts.clientEntry, rootChunk: opts.rootChunk, routes };
40
+ return { version: 1, mode: opts.mode, clientEntry: opts.clientEntry, rootChunk: opts.rootChunk, routes };
38
41
  }
39
42
 
40
43
  /**
@@ -1,12 +1,21 @@
1
1
  import { type ReactElement } from "react";
2
2
  import { hmrClientScript } from "../../dev/hmr-client.ts";
3
+ import { isDevRuntime } from "../../server/env.ts";
3
4
 
4
5
  /**
5
6
  * Renders an inline WebSocket HMR client in development.
6
7
  * Returns null in production.
8
+ *
9
+ * Two gates:
10
+ * 1. Build-time `process.env.NODE_ENV === "production"` — strips the script from
11
+ * the client bundle entirely (Bun substitutes this define at build time).
12
+ * 2. Runtime `isDevRuntime()` — kills SSR injection unless the server was
13
+ * actually started via `bractjs dev`. Prevents `NODE_ENV=development
14
+ * bractjs start` from shipping an HMR client that retries WS forever.
7
15
  */
8
16
  export function LiveReload(): ReactElement | null {
9
17
  if (process.env.NODE_ENV === "production") return null;
18
+ if (!isDevRuntime()) return null;
10
19
 
11
20
  // SECURITY(low): dangerouslySetInnerHTML is safe here — hmrClientScript is a
12
21
  // build-time constant string with no user input. The NODE_ENV gate above
package/src/dev/server.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { createServer } from "../server/serve.ts";
2
+ import { setRuntimeMode } from "../server/env.ts";
2
3
  import { createHmrServer } from "./hmr-server.ts";
3
4
  import { watchApp } from "./watcher.ts";
4
5
  import { rebuildClient } from "./rebuilder.ts";
5
6
  import { filePathToPattern } from "../server/scanner.ts";
6
7
  import { basename, extname } from "node:path";
7
8
 
9
+ // Must precede any user-code import so SSR-time isDevRuntime() checks
10
+ // (e.g. inside <LiveReload>) observe the dev mode.
11
+ setRuntimeMode("dev");
12
+
8
13
  const hmr = createHmrServer(3001);
9
14
 
10
15
  // Build client bundle before the HTTP server starts accepting requests
package/src/server/env.ts CHANGED
@@ -2,6 +2,24 @@ export function isDev(): boolean {
2
2
  return Bun.env.NODE_ENV !== "production";
3
3
  }
4
4
 
5
+ /**
6
+ * Runtime mode — what the server is actually doing, independent of NODE_ENV.
7
+ * `bractjs dev` sets this to "dev". `bractjs start` leaves it at "prod".
8
+ *
9
+ * Use this (not isDev()) to gate dev-only behavior like HMR injection or
10
+ * re-reading the route manifest on every request. NODE_ENV alone is unreliable:
11
+ * a user running `NODE_ENV=development bractjs start` would otherwise get a
12
+ * production server that still ships an HMR client trying to reconnect to a
13
+ * non-existent ws://localhost:3001 forever.
14
+ */
15
+ let _runtimeMode: "dev" | "prod" = "prod";
16
+ export function setRuntimeMode(m: "dev" | "prod"): void {
17
+ _runtimeMode = m;
18
+ }
19
+ export function isDevRuntime(): boolean {
20
+ return _runtimeMode === "dev";
21
+ }
22
+
5
23
  /**
6
24
  * Strict "is development?" check used to gate sensitive output (error
7
25
  * messages, stack traces) that would otherwise leak in production.
@@ -5,4 +5,4 @@ export { renderRoute } from "./render.ts";
5
5
  export type { RenderOptions, ServerManifest } from "./render.ts";
6
6
 
7
7
  export { redirect, json, error } from "./response.ts";
8
- export { isDev, requireEnv, safeStringify } from "./env.ts";
8
+ export { isDev, isDevRuntime, setRuntimeMode, requireEnv, safeStringify } from "./env.ts";
@@ -1,7 +1,7 @@
1
1
  import { renderToReadableStream } from "react-dom/server";
2
2
  import type { ReactNode } from "react";
3
3
  import type { MetaDescriptor } from "../shared/route-types.ts";
4
- import { safeStringify, isDev } from "./env.ts";
4
+ import { safeStringify, isDevRuntime } from "./env.ts";
5
5
  import { errorOverlayScript } from "../dev/error-overlay.ts";
6
6
  import { mergeMeta, renderMetaTags } from "./meta.ts";
7
7
 
@@ -35,8 +35,8 @@ export async function renderRoute(options: RenderOptions): Promise<Response> {
35
35
  status = 200,
36
36
  } = options;
37
37
 
38
- const devFlag = isDev() ? "window.__BRACT_DEV__=true;" : "";
39
- const devOverlay = isDev() ? devFlag + errorOverlayScript + "\n" : "";
38
+ const devFlag = isDevRuntime() ? "window.__BRACT_DEV__=true;" : "";
39
+ const devOverlay = isDevRuntime() ? devFlag + errorOverlayScript + "\n" : "";
40
40
  const mergedMeta = mergeMeta(options.meta ?? []);
41
41
  // metaHtml is injected into <head> via React (the renderToReadableStream tree
42
42
  // is expected to use it). The merged descriptor array is what the client
@@ -2,7 +2,7 @@ import { scanRoutes } from "./scanner.ts";
2
2
  import { buildTrie } from "./matcher.ts";
3
3
  import { handleRequest, type HandlerConfig } from "./request-handler.ts";
4
4
  import { type ServerManifest } from "./render.ts";
5
- import { isDev, isExplicitDev } from "./env.ts";
5
+ import { isDevRuntime, isExplicitDev } from "./env.ts";
6
6
  import { loadManifest } from "../build/manifest.ts";
7
7
  import { serveStatic } from "./static.ts";
8
8
  import { handleImageRequest } from "../image/handler.ts";
@@ -68,7 +68,7 @@ export function buildFetchHandler(config: Partial<BractJSConfig>) {
68
68
  const buildDir = resolve(config.buildDir ?? "./build");
69
69
  const imageCacheDir = resolve(config.imageCacheDir ?? ".bract-image-cache");
70
70
 
71
- const manifestReady: Promise<ServerManifest> = !isDev() && !config.manifest
71
+ const manifestReady: Promise<ServerManifest> = !isDevRuntime() && !config.manifest
72
72
  ? loadManifest(buildDir).then((m) => ({
73
73
  clientEntry: m.clientEntry,
74
74
  rootChunk: m.rootChunk,
@@ -147,17 +147,43 @@ export function buildFetchHandler(config: Partial<BractJSConfig>) {
147
147
  if (staticRes) return staticRes;
148
148
 
149
149
  const trie = await trieReady;
150
- const manifest = isDev() ? await readDevManifest(buildDir) : await manifestReady;
150
+ const manifest = isDevRuntime() ? await readDevManifest(buildDir) : await manifestReady;
151
151
  const handlerConfig: HandlerConfig = { appDir, publicDir, manifest };
152
152
  return handleRequest(request, trie, handlerConfig);
153
153
  };
154
154
  }
155
155
 
156
+ /**
157
+ * In production-runtime mode, surface a warning when the manifest on disk
158
+ * wasn't produced by `bractjs build` (missing `"mode": "production"`).
159
+ * Almost always means the user is running `bractjs start` against a dev
160
+ * rebuilder's manifest, or hasn't run `bractjs build` at all.
161
+ */
162
+ async function warnIfStaleBuild(buildDir: string): Promise<void> {
163
+ const f = Bun.file(join(buildDir, "route-manifest.json"));
164
+ if (!(await f.exists())) {
165
+ console.warn(`[bract] No build found at ${buildDir}/route-manifest.json. Run \`bractjs build\` before \`bractjs start\`.`);
166
+ return;
167
+ }
168
+ try {
169
+ const m = (await f.json()) as { mode?: string };
170
+ if (m.mode !== "production") {
171
+ console.warn(`[bract] Build at ${buildDir} was not produced by \`bractjs build\` (mode=${m.mode ?? "unset"}). Re-run \`bractjs build\` for a production-ready manifest.`);
172
+ }
173
+ } catch {
174
+ // Malformed manifest — the request path will surface the real error.
175
+ }
176
+ }
177
+
156
178
  export function createServer(config?: Partial<BractJSConfig>): {
157
179
  stop(): void;
158
180
  } {
159
181
  const port = config?.port ?? 3000;
160
182
 
183
+ if (!isDevRuntime()) {
184
+ void warnIfStaleBuild(resolve(config?.buildDir ?? "./build"));
185
+ }
186
+
161
187
  const fetchHandler = buildFetchHandler(config ?? {});
162
188
 
163
189
  // Use provided adapter or fall back to the default Bun adapter.