bosia 0.9.7 → 0.9.9
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 +2 -2
- package/src/ambient.d.ts +7 -0
- package/src/cli/build.ts +6 -1
- package/src/cli/index.ts +3 -3
- package/src/cli/start.ts +9 -1
- package/src/cli/sync.ts +9 -2
- package/src/core/appHtml.ts +3 -11
- package/src/core/artifacts.ts +21 -0
- package/src/core/build.ts +105 -6
- package/src/core/cache.ts +10 -6
- package/src/core/client/App.svelte +5 -2
- package/src/core/client/prefetch.ts +3 -1
- package/src/core/config.ts +19 -8
- package/src/core/dev.ts +1 -2
- package/src/core/env.ts +0 -20
- package/src/core/hooks.ts +28 -0
- package/src/core/html.ts +29 -24
- package/src/core/paths.ts +4 -2
- package/src/core/platform.ts +24 -0
- package/src/core/plugin.ts +26 -1
- package/src/core/prerender.ts +6 -6
- package/src/core/renderer.ts +15 -8
- package/src/core/routeFile.ts +4 -0
- package/src/core/scanner.ts +64 -0
- package/src/core/server.bun.ts +129 -0
- package/src/core/server.ts +129 -203
- package/src/core/server.workers.ts +47 -0
- package/src/core/types/plugin.ts +5 -0
- package/src/core/types.ts +2 -0
- package/src/core/workersCodegen.ts +110 -0
- package/src/core/workersGuard.ts +107 -0
- package/src/lib/index.ts +3 -0
- package/templates/default/_gitignore +2 -0
- package/templates/demo/_gitignore +2 -0
- package/templates/shop/_gitignore +2 -0
- package/templates/store/_gitignore +2 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite
|
|
5
|
+
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing. No Node.js, no Vite. Runs on Bun or Cloudflare Workers.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bun",
|
|
8
8
|
"svelte",
|
package/src/ambient.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ declare module "bosia:routes" {
|
|
|
11
11
|
layouts: Loader[];
|
|
12
12
|
hasServerData: boolean;
|
|
13
13
|
trailingSlash: TrailingSlash;
|
|
14
|
+
prerender: boolean;
|
|
14
15
|
}>;
|
|
15
16
|
|
|
16
17
|
export const serverRoutes: Array<{
|
|
@@ -29,3 +30,9 @@ declare module "bosia:routes" {
|
|
|
29
30
|
|
|
30
31
|
export const errorPage: Loader | null;
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
// Workers only — backed by .bosia/runtime.workers.ts (see core/workersCodegen.ts).
|
|
35
|
+
declare module "bosia:workers-runtime" {
|
|
36
|
+
export const handle: import("./core/hooks.ts").Handle | null;
|
|
37
|
+
export const config: import("./core/types/plugin.ts").BosiaConfig;
|
|
38
|
+
}
|
package/src/cli/build.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { spawn } from "bun";
|
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { loadEnv } from "../core/env.ts";
|
|
4
4
|
|
|
5
|
-
export async function runBuild() {
|
|
5
|
+
export async function runBuild(args: string[] = []) {
|
|
6
6
|
loadEnv("production");
|
|
7
|
+
// --target=workers or --target workers; overrides bosia.config's `target`.
|
|
8
|
+
const eq = args.find((a) => a.startsWith("--target="));
|
|
9
|
+
const i = args.indexOf("--target");
|
|
10
|
+
const target = eq ? eq.slice("--target=".length) : i !== -1 ? args[i + 1] : undefined;
|
|
11
|
+
if (target) process.env.BOSIA_TARGET = target;
|
|
7
12
|
const buildScript = resolve(import.meta.dir, "../core/build.ts");
|
|
8
13
|
const proc = spawn(["bun", "run", buildScript], {
|
|
9
14
|
stdout: "inherit",
|
package/src/cli/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ─── Bosia CLI ────────────────────────────────────────────
|
|
3
3
|
// bun x bosia@latest create <name> scaffold a new project
|
|
4
4
|
// bun x bosia dev start the development server
|
|
5
|
-
// bun x bosia build
|
|
5
|
+
// bun x bosia build [--target=workers] build for production (Bun or Cloudflare Workers)
|
|
6
6
|
// bun x bosia start run the production server
|
|
7
7
|
// bun x bosia@latest add <name> add a UI component from the registry
|
|
8
8
|
// bun x bosia@latest feat <name> add a feature scaffold from the registry
|
|
@@ -40,7 +40,7 @@ async function main() {
|
|
|
40
40
|
}
|
|
41
41
|
case "build": {
|
|
42
42
|
const { runBuild } = await import("./build.ts");
|
|
43
|
-
await runBuild();
|
|
43
|
+
await runBuild(args);
|
|
44
44
|
break;
|
|
45
45
|
}
|
|
46
46
|
case "sync": {
|
|
@@ -114,7 +114,7 @@ Usage:
|
|
|
114
114
|
Commands:
|
|
115
115
|
create <name> [--template <t>] Scaffold a new Bosia project
|
|
116
116
|
dev Start the development server
|
|
117
|
-
build Build for production
|
|
117
|
+
build Build for production (--target=workers for Cloudflare)
|
|
118
118
|
sync Generate .bosia/ codegen (routes, $types, env) without building
|
|
119
119
|
start Run the production server
|
|
120
120
|
test [args] Run tests with bun test (auto-loads .env.test, sets BOSIA_ENV=test)
|
package/src/cli/start.ts
CHANGED
|
@@ -6,12 +6,20 @@ export async function runStart() {
|
|
|
6
6
|
loadEnv("production");
|
|
7
7
|
|
|
8
8
|
let serverEntry = "index.js";
|
|
9
|
+
let target = "bun";
|
|
9
10
|
try {
|
|
10
11
|
const manifest = await Bun.file(`${OUT_DIR}/manifest.json`).json();
|
|
11
12
|
serverEntry = manifest.serverEntry ?? "index.js";
|
|
13
|
+
target = manifest.target ?? "bun";
|
|
12
14
|
} catch {}
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
// A Workers build runs in workerd, locally via wrangler (fetched on first use).
|
|
17
|
+
const cmd =
|
|
18
|
+
target === "workers"
|
|
19
|
+
? ["bunx", "wrangler", "dev", ...(process.env.PORT ? ["--port", process.env.PORT] : [])]
|
|
20
|
+
: ["bun", "run", `${OUT_DIR}/server/${serverEntry}`];
|
|
21
|
+
|
|
22
|
+
const proc = spawn(cmd, {
|
|
15
23
|
stdout: "inherit",
|
|
16
24
|
stderr: "inherit",
|
|
17
25
|
cwd: process.cwd(),
|
package/src/cli/sync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanRoutes } from "../core/scanner.ts";
|
|
1
|
+
import { scanRoutes, RouteConflictError } from "../core/scanner.ts";
|
|
2
2
|
import { generateRoutesFile } from "../core/routeFile.ts";
|
|
3
3
|
import { generateRouteTypes, ensureRootDirs } from "../core/routeTypes.ts";
|
|
4
4
|
import { loadEnv, classifyEnvVars } from "../core/env.ts";
|
|
@@ -8,7 +8,14 @@ import { findBrandPlaceholders, BRAND_SENTINEL } from "../core/brandGuard.ts";
|
|
|
8
8
|
export async function runSync() {
|
|
9
9
|
const envMode = process.env.NODE_ENV === "production" ? "production" : "development";
|
|
10
10
|
const classifiedEnv = classifyEnvVars(loadEnv(envMode));
|
|
11
|
-
|
|
11
|
+
let manifest;
|
|
12
|
+
try {
|
|
13
|
+
manifest = scanRoutes();
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (!(err instanceof RouteConflictError)) throw err;
|
|
16
|
+
console.error(`❌ ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
12
19
|
generateRoutesFile(manifest);
|
|
13
20
|
generateRouteTypes(manifest);
|
|
14
21
|
ensureRootDirs();
|
package/src/core/appHtml.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
|
|
4
4
|
import { OUT_DIR } from "./paths.ts";
|
|
5
|
+
import { readArtifact } from "./artifacts.ts";
|
|
5
6
|
import { rebaseHtmlAttrs } from "./basePath.ts";
|
|
6
7
|
import { currentBase } from "./appBase.ts";
|
|
7
8
|
|
|
@@ -86,16 +87,6 @@ export function writeAppHtmlSegments(segments: AppHtmlSegments, outDir: string =
|
|
|
86
87
|
return target;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
function readPersistedSegments(cwd: string): AppHtmlSegments | undefined {
|
|
90
|
-
const persistedPath = join(cwd, OUT_DIR, "app-html.json");
|
|
91
|
-
if (!existsSync(persistedPath)) return undefined;
|
|
92
|
-
try {
|
|
93
|
-
return JSON.parse(readFileSync(persistedPath, "utf-8")) as AppHtmlSegments;
|
|
94
|
-
} catch {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
90
|
// ─── Cached Getter ────────────────────────────────────────
|
|
100
91
|
|
|
101
92
|
export function getAppHtmlSegments(cwd: string = process.cwd()): AppHtmlSegments {
|
|
@@ -104,7 +95,8 @@ export function getAppHtmlSegments(cwd: string = process.cwd()): AppHtmlSegments
|
|
|
104
95
|
}
|
|
105
96
|
// Prefer persisted dist artifact (production runtime — no `src/` in image).
|
|
106
97
|
// Fall back to parsing `src/app.html` directly (dev mode, build step).
|
|
107
|
-
cachedSegments =
|
|
98
|
+
cachedSegments =
|
|
99
|
+
readArtifact<AppHtmlSegments>("app-html.json", join(cwd, OUT_DIR)) ?? loadAppHtmlTemplate(cwd);
|
|
108
100
|
return cachedSegments;
|
|
109
101
|
}
|
|
110
102
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
import { OUT_DIR } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
// ─── Build Artifacts ─────────────────────────────────────
|
|
7
|
+
// The one place the runtime reads the JSON the build left in OUT_DIR
|
|
8
|
+
// (manifest.json, app-html.json, route-manifest.json). Runtimes without a
|
|
9
|
+
// filesystem (Cloudflare Workers) swap this module for `.bosia/artifacts.ts`,
|
|
10
|
+
// which holds the same JSON inlined — see artifactCodegen.ts and plugin.ts.
|
|
11
|
+
|
|
12
|
+
/** Parsed `<dir>/<name>`, or undefined when it is missing or unreadable. */
|
|
13
|
+
export function readArtifact<T>(name: string, dir: string = OUT_DIR): T | undefined {
|
|
14
|
+
const p = join(dir, name);
|
|
15
|
+
if (!existsSync(p)) return undefined;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(p, "utf-8")) as T;
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/core/build.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { writeFileSync, readFileSync, rmSync, mkdirSync, existsSync } from "fs";
|
|
|
2
2
|
import { basename, join, relative } from "path";
|
|
3
3
|
import type { RouteManifest } from "./types.ts";
|
|
4
4
|
|
|
5
|
-
import { scanRoutes } from "./scanner.ts";
|
|
5
|
+
import { scanRoutes, RouteConflictError } from "./scanner.ts";
|
|
6
6
|
import { generateRoutesFile } from "./routeFile.ts";
|
|
7
7
|
import { generateRouteTypes, ensureRootDirs } from "./routeTypes.ts";
|
|
8
8
|
import { makeBosiaPlugin } from "./plugin.ts";
|
|
@@ -14,9 +14,15 @@ import { generateEnvModules } from "./envCodegen.ts";
|
|
|
14
14
|
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin, toPosix } from "./paths.ts";
|
|
15
15
|
import { currentBase } from "./appBase.ts";
|
|
16
16
|
import { finalizeTailwindCss, TW_TEMP_BASENAME } from "./twHash.ts";
|
|
17
|
-
import { loadPlugins } from "./config.ts";
|
|
18
|
-
import type { BuildContext } from "./types/plugin.ts";
|
|
17
|
+
import { loadBosiaConfig, loadPlugins } from "./config.ts";
|
|
18
|
+
import type { BuildContext, RuntimeTarget } from "./types/plugin.ts";
|
|
19
19
|
import { loadAppHtmlTemplate, writeAppHtmlSegments } from "./appHtml.ts";
|
|
20
|
+
import {
|
|
21
|
+
generateArtifactsModule,
|
|
22
|
+
generateWorkersRuntime,
|
|
23
|
+
generateWranglerConfig,
|
|
24
|
+
} from "./workersCodegen.ts";
|
|
25
|
+
import { workersGuardReport } from "./workersGuard.ts";
|
|
20
26
|
|
|
21
27
|
// Resolved from this file's location inside the bosia package
|
|
22
28
|
const CORE_DIR = import.meta.dir;
|
|
@@ -42,6 +48,23 @@ const buildCtx: BuildContext = {
|
|
|
42
48
|
cwd: process.cwd(),
|
|
43
49
|
};
|
|
44
50
|
|
|
51
|
+
// 0-bis. Runtime target: `bosia build --target=` (BOSIA_TARGET) beats bosia.config.
|
|
52
|
+
const target = (process.env.BOSIA_TARGET ||
|
|
53
|
+
(await loadBosiaConfig()).target ||
|
|
54
|
+
"bun") as RuntimeTarget;
|
|
55
|
+
if (target !== "bun" && target !== "workers") {
|
|
56
|
+
console.error(`❌ Unknown target "${target}". Use "bun" or "workers".`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (target !== "bun") console.log(`🎯 Target: ${target}`);
|
|
60
|
+
if (target === "workers") {
|
|
61
|
+
const guard = workersGuardReport();
|
|
62
|
+
if (guard) {
|
|
63
|
+
console.error(`❌ ${guard}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
for (const p of userPlugins) {
|
|
46
69
|
if (p.build?.preBuild) {
|
|
47
70
|
await p.build.preBuild(buildCtx);
|
|
@@ -77,6 +100,8 @@ for (const p of [
|
|
|
77
100
|
".bosia/routes.client.ts",
|
|
78
101
|
".bosia/env.server.ts",
|
|
79
102
|
".bosia/env.client.ts",
|
|
103
|
+
".bosia/artifacts.ts",
|
|
104
|
+
".bosia/runtime.workers.ts",
|
|
80
105
|
".bosia/types",
|
|
81
106
|
]) {
|
|
82
107
|
try {
|
|
@@ -85,7 +110,14 @@ for (const p of [
|
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
// 1. Scan routes (or reuse the cached manifest — see 0b-pre)
|
|
88
|
-
|
|
113
|
+
let manifest: RouteManifest;
|
|
114
|
+
try {
|
|
115
|
+
manifest = cachedManifest ?? scanRoutes();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (!(err instanceof RouteConflictError)) throw err;
|
|
118
|
+
console.error(`❌ ${err.message}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
89
121
|
buildCtx.manifest = manifest;
|
|
90
122
|
console.log(
|
|
91
123
|
`📂 Found ${manifest.pages.length} page route(s)${cachedManifest ? " (cached scan)" : ""}:`,
|
|
@@ -180,7 +212,9 @@ const clientPromise = Bun.build({
|
|
|
180
212
|
target: "browser",
|
|
181
213
|
conditions: ["svelte"],
|
|
182
214
|
splitting: true,
|
|
183
|
-
|
|
215
|
+
// Chunks are named after their source, which for routes is `+page` — and
|
|
216
|
+
// Cloudflare's asset server answers a `+` in the path with a redirect.
|
|
217
|
+
naming: { entry: "[name]-[hash].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
184
218
|
minify: isProduction,
|
|
185
219
|
sourcemap: isProduction ? "none" : "linked",
|
|
186
220
|
define: {
|
|
@@ -191,7 +225,7 @@ const clientPromise = Bun.build({
|
|
|
191
225
|
});
|
|
192
226
|
|
|
193
227
|
const serverPromise = Bun.build({
|
|
194
|
-
entrypoints: [join(CORE_DIR, "server.ts")],
|
|
228
|
+
entrypoints: [join(CORE_DIR, "server.bun.ts")],
|
|
195
229
|
outdir: `${OUT_DIR}/server`,
|
|
196
230
|
target: "bun",
|
|
197
231
|
conditions: ["svelte"],
|
|
@@ -285,10 +319,14 @@ const distManifest = {
|
|
|
285
319
|
jsFiles.find((f) => f.startsWith("hydrate")) ??
|
|
286
320
|
"hydrate.js",
|
|
287
321
|
serverEntry,
|
|
322
|
+
target,
|
|
288
323
|
tw: twFile,
|
|
289
324
|
// The CSS urls and the client route table are baked in with this prefix.
|
|
290
325
|
// Stamped so the server can warn when it boots with a different one.
|
|
291
326
|
basePath: currentBase(),
|
|
327
|
+
// Names of PUBLIC_* runtime vars the page may expose to the browser. The
|
|
328
|
+
// server is its own process, so it can't see which names loadEnv() declared.
|
|
329
|
+
publicEnv: Object.keys(classifiedEnv.publicDynamic),
|
|
292
330
|
};
|
|
293
331
|
writeFileSync(`${OUT_DIR}/manifest.json`, JSON.stringify(distManifest, null, 2));
|
|
294
332
|
console.log(`✅ Client bundle: ${jsFiles.join(", ")}`);
|
|
@@ -315,6 +353,10 @@ await prerenderStaticRoutes(manifest);
|
|
|
315
353
|
// 10. Generate static site output (HTML + client assets + public → dist/static/)
|
|
316
354
|
generateStaticSite();
|
|
317
355
|
|
|
356
|
+
// 11. Workers target: a second server bundle for Cloudflare. The Bun one above
|
|
357
|
+
// still exists — prerender just booted it to crawl static routes.
|
|
358
|
+
if (target === "workers") await buildWorker();
|
|
359
|
+
|
|
318
360
|
for (const p of userPlugins) {
|
|
319
361
|
if (p.build?.postBuild) {
|
|
320
362
|
await p.build.postBuild(buildCtx);
|
|
@@ -325,6 +367,63 @@ console.log(`\n🎉 Build complete in ${Math.round(performance.now() - buildStar
|
|
|
325
367
|
|
|
326
368
|
// ─── Helpers ─────────────────────────────────────────────
|
|
327
369
|
|
|
370
|
+
// Dev-only plugins in bosia.config.ts (the inspector) import svelte/compiler, which
|
|
371
|
+
// would put ~820KB of never-run code in the worker, 60% of the demo's bundle.
|
|
372
|
+
// Nothing compiles Svelte at runtime, so every export becomes a function that throws.
|
|
373
|
+
function stubSvelteCompiler(): import("bun").BunPlugin {
|
|
374
|
+
return {
|
|
375
|
+
name: "bosia-stub-svelte-compiler",
|
|
376
|
+
setup(build) {
|
|
377
|
+
build.onResolve({ filter: /^svelte\/compiler$/ }, () => ({
|
|
378
|
+
path: "svelte/compiler",
|
|
379
|
+
namespace: "bosia-stub",
|
|
380
|
+
}));
|
|
381
|
+
build.onLoad({ filter: /.*/, namespace: "bosia-stub" }, async () => {
|
|
382
|
+
const names = Object.keys(await import("svelte/compiler"));
|
|
383
|
+
const fail = `() => { throw new Error("The Svelte compiler isn't available on Cloudflare Workers"); }`;
|
|
384
|
+
return {
|
|
385
|
+
loader: "js",
|
|
386
|
+
contents: names.map((n) => `export const ${n} = ${fail};`).join("\n"),
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function buildWorker(): Promise<void> {
|
|
394
|
+
// The isolate has no filesystem: inline the artifacts and static-import the
|
|
395
|
+
// user's hooks + config instead of reading them off disk at boot.
|
|
396
|
+
generateArtifactsModule();
|
|
397
|
+
generateWorkersRuntime();
|
|
398
|
+
// target "node" would emit createRequire(import.meta.url), and import.meta.url
|
|
399
|
+
// is undefined in workerd. Node builtins stay `node:` imports (nodejs_compat).
|
|
400
|
+
const result = await Bun.build({
|
|
401
|
+
entrypoints: [join(CORE_DIR, "server.workers.ts")],
|
|
402
|
+
outdir: `${OUT_DIR}/worker`,
|
|
403
|
+
target: "browser",
|
|
404
|
+
format: "esm",
|
|
405
|
+
conditions: ["workerd", "worker", "svelte"],
|
|
406
|
+
naming: { entry: "index.[ext]" },
|
|
407
|
+
minify: isProduction,
|
|
408
|
+
external: ["node:*"],
|
|
409
|
+
define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "development") },
|
|
410
|
+
plugins: [
|
|
411
|
+
stubSvelteCompiler(),
|
|
412
|
+
makeBosiaPlugin("bun", "workers"),
|
|
413
|
+
...userServerBunPlugins,
|
|
414
|
+
makeBosiaSvelteCompiler("bun"),
|
|
415
|
+
],
|
|
416
|
+
});
|
|
417
|
+
if (!result.success) {
|
|
418
|
+
console.error("❌ Worker build failed:");
|
|
419
|
+
for (const msg of result.logs) console.error(msg);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
const kb = Math.round((result.outputs[0]?.size ?? 0) / 1024);
|
|
423
|
+
console.log(`✅ Worker entry: ${OUT_DIR}/worker/index.js (${kb}KB)`);
|
|
424
|
+
if (generateWranglerConfig()) console.log("☁️ Wrote wrangler.jsonc");
|
|
425
|
+
}
|
|
426
|
+
|
|
328
427
|
async function readUserDependencyNames(cwd: string): Promise<string[]> {
|
|
329
428
|
try {
|
|
330
429
|
const pkg = (await Bun.file(join(cwd, "package.json")).json()) as {
|
package/src/core/cache.ts
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
//
|
|
6
6
|
// See docs/guides/response-cache.md.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// node:crypto / node:zlib rather than Bun.* — the same code runs on Bun and on
|
|
9
|
+
// Cloudflare Workers (nodejs_compat), and both stay sync there.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { brotliCompressSync, gzipSync, constants as zlibConstants } from "node:zlib";
|
|
9
12
|
import type { Cookies, LoaderDeps } from "./hooks.ts";
|
|
10
13
|
import type { CookieJar } from "./cookies.ts";
|
|
11
14
|
import { dedupKey } from "./dedup.ts";
|
|
15
|
+
import { compressionOn, PRECOMPRESSED } from "./html.ts";
|
|
12
16
|
|
|
13
17
|
// ─── Config ──────────────────────────────────────────────
|
|
14
18
|
|
|
@@ -125,7 +129,7 @@ const pathIndex = new Map<string, Set<string>>(); // pathname → cacheKeys
|
|
|
125
129
|
|
|
126
130
|
/** SHA-256 truncated to 64 bits — identity buckets must not collide across users. */
|
|
127
131
|
function identityDigest(s: string): string {
|
|
128
|
-
return
|
|
132
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
export function computeIdentityHash(req: Request, cookies: Pick<CookieJar, "peek">): string {
|
|
@@ -282,11 +286,11 @@ export function buildCompressedVariants(body: Bytes): {
|
|
|
282
286
|
brotli: Bytes | null;
|
|
283
287
|
} {
|
|
284
288
|
const COMPRESS_MIN_BYTES = 2048;
|
|
285
|
-
if (body.length < COMPRESS_MIN_BYTES) return { gzip: null, brotli: null };
|
|
289
|
+
if (!compressionOn || body.length < COMPRESS_MIN_BYTES) return { gzip: null, brotli: null };
|
|
286
290
|
let gzip: Bytes | null = null;
|
|
287
291
|
let brotli: Bytes | null = null;
|
|
288
292
|
try {
|
|
289
|
-
gzip =
|
|
293
|
+
gzip = new Uint8Array(gzipSync(body)) as Bytes;
|
|
290
294
|
} catch {
|
|
291
295
|
gzip = null;
|
|
292
296
|
}
|
|
@@ -360,11 +364,11 @@ export function serveCached(entry: CacheEntry, req: Request): Response {
|
|
|
360
364
|
};
|
|
361
365
|
if (entry.brotli && accept.includes("br")) {
|
|
362
366
|
headers["content-encoding"] = "br";
|
|
363
|
-
return new Response(entry.brotli, { status: entry.status, headers });
|
|
367
|
+
return new Response(entry.brotli, { ...PRECOMPRESSED, status: entry.status, headers });
|
|
364
368
|
}
|
|
365
369
|
if (entry.gzip && accept.includes("gzip")) {
|
|
366
370
|
headers["content-encoding"] = "gzip";
|
|
367
|
-
return new Response(entry.gzip, { status: entry.status, headers });
|
|
371
|
+
return new Response(entry.gzip, { ...PRECOMPRESSED, status: entry.status, headers });
|
|
368
372
|
}
|
|
369
373
|
return new Response(entry.raw, { status: entry.status, headers });
|
|
370
374
|
}
|
|
@@ -219,7 +219,10 @@
|
|
|
219
219
|
// Forward cached parent data for skipped layers so downstream loaders see
|
|
220
220
|
// real parent() data, not {}. POST only when there's something to carry —
|
|
221
221
|
// keeps the no-skip case a cacheable/dedupable GET.
|
|
222
|
-
|
|
222
|
+
// A prerendered route's data is a fixed file, so there is nothing to skip —
|
|
223
|
+
// and on Workers the asset server answers anything but GET with 405.
|
|
224
|
+
const prerendered = match.route.prerender;
|
|
225
|
+
const snapshots = prerendered ? {} : buildParentSnapshots(path, maskBits);
|
|
223
226
|
const dataInit: RequestInit =
|
|
224
227
|
Object.keys(snapshots).length > 0
|
|
225
228
|
? {
|
|
@@ -231,7 +234,7 @@
|
|
|
231
234
|
const dataFetch = cached
|
|
232
235
|
? Promise.resolve(cached)
|
|
233
236
|
: match.route.hasServerData
|
|
234
|
-
? fetch(dataUrl(path, maskBits), dataInit)
|
|
237
|
+
? fetch(dataUrl(path, prerendered ? undefined : maskBits), dataInit)
|
|
235
238
|
.then(readDataResponse)
|
|
236
239
|
// Only a failed request reaches here now — offline, DNS, aborted.
|
|
237
240
|
// A response that arrived is read for what it says, not discarded.
|
|
@@ -17,7 +17,9 @@ export function buildMaskBits(path: string): string | null {
|
|
|
17
17
|
const url = new URL(path, window.location.origin);
|
|
18
18
|
const pathname = url.pathname;
|
|
19
19
|
const match = findMatch(clientRoutes, pathname);
|
|
20
|
-
|
|
20
|
+
// Prerendered data is a fixed file: nothing to skip, and on Workers only a
|
|
21
|
+
// plain GET reaches it (the asset server answers a POST with 405).
|
|
22
|
+
if (!match || match.route.prerender) return null;
|
|
21
23
|
const ctx = liveContext(pathname, match.params, url);
|
|
22
24
|
const layoutIds = (match.route as any).layoutIds as (string | null)[];
|
|
23
25
|
const pageId = (match.route as any).pageId as string | null;
|
package/src/core/config.ts
CHANGED
|
@@ -9,7 +9,7 @@ let cachedFromPath: string | null = null;
|
|
|
9
9
|
|
|
10
10
|
const CONFIG_NAMES = ["bosia.config.ts", "bosia.config.js", "bosia.config.mjs"];
|
|
11
11
|
|
|
12
|
-
function findConfigPath(cwd: string): string | null {
|
|
12
|
+
export function findConfigPath(cwd: string): string | null {
|
|
13
13
|
for (const name of CONFIG_NAMES) {
|
|
14
14
|
const p = join(cwd, name);
|
|
15
15
|
if (existsSync(p)) return p;
|
|
@@ -39,9 +39,7 @@ export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<Bosi
|
|
|
39
39
|
`${prebuiltPath} must export a default object (use \`export default defineConfig({...})\`).`,
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const plugins = rawPlugins.filter((p): p is BosiaPlugin => Boolean(p));
|
|
44
|
-
cached = { plugins };
|
|
42
|
+
cached = normalizeConfig(config);
|
|
45
43
|
cachedFromPath = cwd;
|
|
46
44
|
return cached;
|
|
47
45
|
}
|
|
@@ -89,13 +87,26 @@ export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<Bosi
|
|
|
89
87
|
);
|
|
90
88
|
}
|
|
91
89
|
|
|
90
|
+
cached = normalizeConfig(config);
|
|
91
|
+
cachedFromPath = cwd;
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Keep every field — only `plugins` needs cleaning. Rebuilding the object as
|
|
96
|
+
// `{ plugins }` silently dropped `strictImports` and anything added later.
|
|
97
|
+
function normalizeConfig(config: BosiaConfig): BosiaConfig {
|
|
92
98
|
const rawPlugins = Array.isArray(config.plugins) ? config.plugins : [];
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
return { ...config, plugins: rawPlugins.filter((p): p is BosiaPlugin => Boolean(p)) };
|
|
100
|
+
}
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Hand over an already-imported config. For runtimes that can't load
|
|
104
|
+
* `bosia.config.ts` off disk (Workers bundles it statically); call before
|
|
105
|
+
* anything reads the config.
|
|
106
|
+
*/
|
|
107
|
+
export function setBosiaConfig(config: BosiaConfig, cwd: string = process.cwd()): void {
|
|
108
|
+
cached = normalizeConfig(config);
|
|
97
109
|
cachedFromPath = cwd;
|
|
98
|
-
return normalized;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
/** Test-only — drops the in-memory cache so tests can reload fresh config files. */
|
package/src/core/dev.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, type Subprocess } from "bun";
|
|
2
2
|
import { readdirSync, statSync, watch, type Dirent } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { loadEnv
|
|
4
|
+
import { loadEnv } from "./env.ts";
|
|
5
5
|
import { BOSIA_NODE_PATH } from "./paths.ts";
|
|
6
6
|
import { pidsOnPort } from "./port.ts";
|
|
7
7
|
import { affectsRouteManifest, shouldIgnoreForRebuild } from "./devWatch.ts";
|
|
@@ -31,7 +31,6 @@ function reloadEnv() {
|
|
|
31
31
|
for (const [k, v] of Object.entries(SHELL_ENV_SNAPSHOT)) {
|
|
32
32
|
if (v !== undefined) process.env[k] = v;
|
|
33
33
|
}
|
|
34
|
-
resetDeclaredKeys();
|
|
35
34
|
loadEnv("development");
|
|
36
35
|
}
|
|
37
36
|
|
package/src/core/env.ts
CHANGED
|
@@ -162,11 +162,6 @@ export function loadEnv(mode: string, dir?: string): Record<string, string> {
|
|
|
162
162
|
for (const key of Object.keys(parsed)) declaredNames.add(key);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// Track declared keys so html.ts only exposes .env-declared PUBLIC_* vars
|
|
166
|
-
for (const key of declaredNames) {
|
|
167
|
-
_declaredKeys.add(key);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
165
|
// Apply to process.env — system env wins (don't overwrite existing).
|
|
171
166
|
// Name-only keys stay absent so `process.env.X ?? default` still falls back.
|
|
172
167
|
for (const [key, value] of Object.entries(merged)) {
|
|
@@ -186,21 +181,6 @@ export function loadEnv(mode: string, dir?: string): Record<string, string> {
|
|
|
186
181
|
return result;
|
|
187
182
|
}
|
|
188
183
|
|
|
189
|
-
// ─── Declared Key Tracking ───────────────────────────
|
|
190
|
-
// Track which keys were declared in .env files so html.ts only exposes those to the client.
|
|
191
|
-
|
|
192
|
-
const _declaredKeys = new Set<string>();
|
|
193
|
-
|
|
194
|
-
/** Returns the set of env var keys that were declared in .env files. */
|
|
195
|
-
export function getDeclaredEnvKeys(): ReadonlySet<string> {
|
|
196
|
-
return _declaredKeys;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Clear the declared-keys set. Call before re-running `loadEnv` on hot-reload so removed PUBLIC_* keys stop leaking to the client. */
|
|
200
|
-
export function resetDeclaredKeys(): void {
|
|
201
|
-
_declaredKeys.clear();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
184
|
// ─── Classifier ──────────────────────────────────────────
|
|
205
185
|
|
|
206
186
|
export interface ClassifiedEnv {
|
package/src/core/hooks.ts
CHANGED
|
@@ -31,6 +31,19 @@ export interface Cookies {
|
|
|
31
31
|
|
|
32
32
|
// ─── Event Types ──────────────────────────────────────────
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Runtime bindings (Cloudflare Workers: D1, KV, R2, vars). Augment to type them:
|
|
36
|
+
* `declare module "bosia" { interface PlatformEnv { DB: D1Database } }`
|
|
37
|
+
*/
|
|
38
|
+
export interface PlatformEnv {
|
|
39
|
+
[key: string]: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the host runtime hands the app. */
|
|
43
|
+
export interface Platform {
|
|
44
|
+
env: PlatformEnv;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
export type RequestEvent = {
|
|
35
48
|
request: Request;
|
|
36
49
|
url: URL;
|
|
@@ -56,6 +69,11 @@ export type RequestEvent = {
|
|
|
56
69
|
* authorization: a check that runs on one kind and not the other is a hole.
|
|
57
70
|
*/
|
|
58
71
|
isDataRequest: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Runtime platform context. On Cloudflare Workers, `platform.env` holds the
|
|
74
|
+
* bindings (D1, KV, R2, …). `undefined` on Bun.
|
|
75
|
+
*/
|
|
76
|
+
platform?: Platform;
|
|
59
77
|
};
|
|
60
78
|
|
|
61
79
|
export type LoadEvent = {
|
|
@@ -73,6 +91,11 @@ export type LoadEvent = {
|
|
|
73
91
|
* namespaced (e.g. `"app:user"`).
|
|
74
92
|
*/
|
|
75
93
|
depends: (...keys: string[]) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Runtime platform context. On Cloudflare Workers, `platform.env` holds the
|
|
96
|
+
* bindings (D1, KV, R2, …). `undefined` on Bun.
|
|
97
|
+
*/
|
|
98
|
+
platform?: Platform;
|
|
76
99
|
/**
|
|
77
100
|
* Set response headers for this request. Headers accumulate across
|
|
78
101
|
* layout and page loaders and land on both the SSR HTML response and
|
|
@@ -150,6 +173,11 @@ export type MetadataEvent = {
|
|
|
150
173
|
locals: Record<string, any>;
|
|
151
174
|
cookies: Cookies;
|
|
152
175
|
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
176
|
+
/**
|
|
177
|
+
* Runtime platform context. On Cloudflare Workers, `platform.env` holds the
|
|
178
|
+
* bindings (D1, KV, R2, …). `undefined` on Bun.
|
|
179
|
+
*/
|
|
180
|
+
platform?: Platform;
|
|
153
181
|
};
|
|
154
182
|
|
|
155
183
|
export type Metadata = {
|