@bractjs/bractjs 0.1.14 → 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/bin/cli.ts +3 -0
- package/package.json +1 -1
- package/src/build/bundler.ts +7 -1
- package/src/build/manifest.ts +4 -1
- package/src/client/components/LiveReload.tsx +9 -0
- package/src/dev/server.ts +5 -0
- package/src/server/env.ts +18 -0
- package/src/server/index.ts +1 -1
- package/src/server/render.ts +3 -3
- package/src/server/serve.ts +29 -3
- package/templates/new-app/package.json +2 -2
- package/templates/new-app/tsconfig.json +20 -0
package/bin/cli.ts
CHANGED
|
@@ -71,6 +71,9 @@ switch (command) {
|
|
|
71
71
|
break;
|
|
72
72
|
|
|
73
73
|
case "build": {
|
|
74
|
+
// Force production so React's conditional exports resolve to the prod
|
|
75
|
+
// server build (react-dom/server.bun production) instead of the dev one.
|
|
76
|
+
if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
|
|
74
77
|
const { runBuild } = await import("../src/build/bundler.ts");
|
|
75
78
|
await runBuild({ port: 3000, appDir: "./app", publicDir: "./public", buildDir: "./build", manifest: { clientEntry: "", routes: {} } });
|
|
76
79
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bractjs/bractjs",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/src/build/bundler.ts
CHANGED
|
@@ -33,6 +33,12 @@ export async function runBuild(config: BractJSConfig): Promise<void> {
|
|
|
33
33
|
target: "bun",
|
|
34
34
|
outdir: "build/server",
|
|
35
35
|
sourcemap: config.sourcemap ?? "external",
|
|
36
|
+
// Force production so Bun picks the `jsx`/`jsxs` runtime instead of
|
|
37
|
+
// `jsxDEV` — `jsxDEV` only exists on react/jsx-dev-runtime, which is a
|
|
38
|
+
// no-op when bundled under NODE_ENV=production, leaving the call site
|
|
39
|
+
// calling an undefined function. Same fix applied to the client bundle
|
|
40
|
+
// implicitly via buildDefines().
|
|
41
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
36
42
|
plugins: [useClientStubPlugin],
|
|
37
43
|
});
|
|
38
44
|
if (!serverResult.success) throw new AggregateError(serverResult.logs, "Server build failed");
|
|
@@ -96,7 +102,7 @@ export async function runBuild(config: BractJSConfig): Promise<void> {
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
// ── 5. Write manifest ──────────────────────────────────────────────────
|
|
99
|
-
const manifest = generateManifest({ clientEntry, rootChunk, routeChunks });
|
|
105
|
+
const manifest = generateManifest({ clientEntry, rootChunk, routeChunks, mode: "production" });
|
|
100
106
|
await writeManifest(manifest, "build");
|
|
101
107
|
console.log("[bract] build complete →", Object.keys(manifest.routes).length, "routes");
|
|
102
108
|
}
|
package/src/build/manifest.ts
CHANGED
|
@@ -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.
|
package/src/server/index.ts
CHANGED
|
@@ -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";
|
package/src/server/render.ts
CHANGED
|
@@ -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,
|
|
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 =
|
|
39
|
-
const devOverlay =
|
|
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
|
package/src/server/serve.ts
CHANGED
|
@@ -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 {
|
|
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> = !
|
|
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 =
|
|
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.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "bractjs dev",
|
|
7
|
-
"build": "bractjs build",
|
|
8
|
-
"start": "bractjs start"
|
|
7
|
+
"build": "NODE_ENV=production bractjs build",
|
|
8
|
+
"start": "NODE_ENV=production bractjs start"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@bractjs/bractjs": "latest",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "react",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"types": ["bun-types", "react", "react-dom"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["app/**/*.ts", "app/**/*.tsx", "bractjs.config.ts"]
|
|
20
|
+
}
|