bosia 0.6.16 → 0.6.18

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": "bosia",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/core/build.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { writeFileSync, rmSync, mkdirSync } from "fs";
1
+ import { writeFileSync, rmSync, mkdirSync, existsSync } from "fs";
2
2
  import { join, relative } from "path";
3
3
 
4
4
  import { scanRoutes } from "./scanner.ts";
@@ -17,6 +17,9 @@ import { loadAppHtmlTemplate, writeAppHtmlSegments } from "./appHtml.ts";
17
17
  // Resolved from this file's location inside the bosia package
18
18
  const CORE_DIR = import.meta.dir;
19
19
 
20
+ // Runtime externals: never bundled into dist/hooks.server.js or dist/bosia.config.js
21
+ const BOSIA_RUNTIME_EXTERNALS = ["bosia", "elysia", "bun", "svelte", "svelte/server"];
22
+
20
23
  // ─── Entry Point ─────────────────────────────────────────
21
24
 
22
25
  const isProduction = process.env.NODE_ENV === "production";
@@ -255,6 +258,13 @@ writeFileSync(`${OUT_DIR}/route-manifest.json`, JSON.stringify(manifest, null, 2
255
258
  // falls back to parsing `src/app.html` for dev.
256
259
  writeAppHtmlSegments(appHtml);
257
260
 
261
+ // 8d. Bundle user `src/hooks.server.ts` and `bosia.config.{ts,js,mjs}` into
262
+ // `dist/` so production images can copy only `dist/` + `node_modules/` —
263
+ // `src/` is never required at runtime. The runtime (server.ts, config.ts)
264
+ // prefers these artifacts over the source files. npm packages stay external
265
+ // so they resolve against the app's node_modules at runtime.
266
+ await bundleRuntimeUserFiles(process.cwd());
267
+
258
268
  // 9. Prerender static routes
259
269
  await prerenderStaticRoutes(manifest);
260
270
 
@@ -268,3 +278,75 @@ for (const p of userPlugins) {
268
278
  }
269
279
 
270
280
  console.log(`\n🎉 Build complete in ${Math.round(performance.now() - buildStart)}ms!`);
281
+
282
+ // ─── Helpers ─────────────────────────────────────────────
283
+
284
+ async function readUserDependencyNames(cwd: string): Promise<string[]> {
285
+ try {
286
+ const pkg = (await Bun.file(join(cwd, "package.json")).json()) as {
287
+ dependencies?: Record<string, string>;
288
+ peerDependencies?: Record<string, string>;
289
+ optionalDependencies?: Record<string, string>;
290
+ };
291
+ return Array.from(
292
+ new Set([
293
+ ...Object.keys(pkg.dependencies ?? {}),
294
+ ...Object.keys(pkg.peerDependencies ?? {}),
295
+ ...Object.keys(pkg.optionalDependencies ?? {}),
296
+ ]),
297
+ );
298
+ } catch {
299
+ return [];
300
+ }
301
+ }
302
+
303
+ async function bundleRuntimeUserFiles(cwd: string): Promise<void> {
304
+ const userDeps = await readUserDependencyNames(cwd);
305
+ // Externalize every npm package + every subpath (e.g. `bosia/plugins/inspector`).
306
+ // Bun.build's `external` accepts globs.
307
+ const externalNames = Array.from(new Set([...BOSIA_RUNTIME_EXTERNALS, ...userDeps]));
308
+ const external = externalNames.flatMap((n) => [n, `${n}/*`]);
309
+
310
+ // 1) src/hooks.server.ts → dist/hooks.server.js
311
+ const hooksSrc = join(cwd, "src", "hooks.server.ts");
312
+ if (existsSync(hooksSrc)) {
313
+ const result = await Bun.build({
314
+ entrypoints: [hooksSrc],
315
+ outdir: OUT_DIR,
316
+ target: "bun",
317
+ format: "esm",
318
+ naming: { entry: "hooks.server.[ext]" },
319
+ minify: isProduction,
320
+ sourcemap: isProduction ? "none" : "linked",
321
+ external,
322
+ });
323
+ if (!result.success) {
324
+ console.error("❌ hooks.server bundle failed:");
325
+ for (const msg of result.logs) console.error(msg);
326
+ process.exit(1);
327
+ }
328
+ console.log("🪝 Bundled hooks.server → " + OUT_DIR + "/hooks.server.js");
329
+ }
330
+
331
+ // 2) bosia.config.{ts,js,mjs} → dist/bosia.config.js
332
+ const configCandidates = ["bosia.config.ts", "bosia.config.js", "bosia.config.mjs"];
333
+ const configSrc = configCandidates.map((n) => join(cwd, n)).find((p) => existsSync(p));
334
+ if (configSrc) {
335
+ const result = await Bun.build({
336
+ entrypoints: [configSrc],
337
+ outdir: OUT_DIR,
338
+ target: "bun",
339
+ format: "esm",
340
+ naming: { entry: "bosia.config.[ext]" },
341
+ minify: isProduction,
342
+ sourcemap: isProduction ? "none" : "linked",
343
+ external,
344
+ });
345
+ if (!result.success) {
346
+ console.error("❌ bosia.config bundle failed:");
347
+ for (const msg of result.logs) console.error(msg);
348
+ process.exit(1);
349
+ }
350
+ console.log("⚙️ Bundled bosia.config → " + OUT_DIR + "/bosia.config.js");
351
+ }
352
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
+ import { OUT_DIR } from "./paths.ts";
4
5
  import type { BosiaConfig, BosiaPlugin } from "./types/plugin.ts";
5
6
 
6
7
  let cached: BosiaConfig | null = null;
@@ -26,6 +27,25 @@ function findConfigPath(cwd: string): string | null {
26
27
  export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<BosiaConfig> {
27
28
  if (cached && cachedFromPath === cwd) return cached;
28
29
 
30
+ // Production runtime: prefer the pre-bundled `${OUT_DIR}/bosia.config.js`
31
+ // produced by the build. Avoids needing `bosia.config.ts` in the image and
32
+ // skips an on-the-fly Bun.build at server startup.
33
+ const prebuiltPath = join(cwd, OUT_DIR, "bosia.config.js");
34
+ if (existsSync(prebuiltPath)) {
35
+ const mod = (await import(prebuiltPath)) as { default?: BosiaConfig };
36
+ const config = mod.default;
37
+ if (!config || typeof config !== "object") {
38
+ throw new Error(
39
+ `${prebuiltPath} must export a default object (use \`export default defineConfig({...})\`).`,
40
+ );
41
+ }
42
+ const rawPlugins = Array.isArray(config.plugins) ? config.plugins : [];
43
+ const plugins = rawPlugins.filter((p): p is BosiaPlugin => Boolean(p));
44
+ cached = { plugins };
45
+ cachedFromPath = cwd;
46
+ return cached;
47
+ }
48
+
29
49
  const configPath = findConfigPath(cwd);
30
50
  if (!configPath) {
31
51
  cached = { plugins: [] };
@@ -295,24 +295,32 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
295
295
  // ─── Static Site Output ──────────────────────────────────
296
296
 
297
297
  export function generateStaticSite(): void {
298
- if (!existsSync(`${OUT_DIR}/prerendered`)) {
299
- console.log("\n⏭️ No prerendered pages — skipping static site output");
298
+ const hasPublic = existsSync("./public");
299
+ const hasPrerender = existsSync(`${OUT_DIR}/prerendered`);
300
+
301
+ // Mirror `public/` → `dist/static/` on every build (not only SSG builds) so
302
+ // production containers can ship dist/ alone. Without this, apps with zero
303
+ // prerendered routes (pure SSR) would lose bosia-tw.css and favicons when
304
+ // public/ is dropped from the image.
305
+ if (!hasPublic && !hasPrerender) {
306
+ console.log("\n⏭️ No public/ or prerendered pages — skipping static site output");
300
307
  return;
301
308
  }
302
309
 
303
310
  console.log("\n📦 Generating static site...");
304
311
  mkdirSync(`${OUT_DIR}/static`, { recursive: true });
305
312
 
306
- // 1. HTML files from prerendering
307
- cpSync(`${OUT_DIR}/prerendered`, `${OUT_DIR}/static`, { recursive: true });
308
-
309
- // 2. Client JS/CSS — preserves /dist/client/... absolute paths used in HTML
310
- cpSync(`${OUT_DIR}/client`, `${OUT_DIR}/static/dist/client`, { recursive: true });
311
-
312
- // 3. Public assets (bosia-tw.css, favicon, etc.) — preserves /bosia-tw.css path
313
- if (existsSync("./public")) {
313
+ // 1. Public assets (bosia-tw.css, favicon, etc.) — preserves /bosia-tw.css path
314
+ if (hasPublic) {
314
315
  cpSync("./public", `${OUT_DIR}/static`, { recursive: true });
315
316
  }
316
317
 
318
+ // 2. HTML files from prerendering (SSG output only)
319
+ if (hasPrerender) {
320
+ cpSync(`${OUT_DIR}/prerendered`, `${OUT_DIR}/static`, { recursive: true });
321
+ // 3. Client JS/CSS — preserves /dist/client/... absolute paths used in HTML
322
+ cpSync(`${OUT_DIR}/client`, `${OUT_DIR}/static/dist/client`, { recursive: true });
323
+ }
324
+
317
325
  console.log(`✅ Static site generated: ${OUT_DIR}/static/`);
318
326
  }
@@ -45,21 +45,32 @@ import {
45
45
  import { getServerTime } from "../lib/utils.ts";
46
46
 
47
47
  // ─── User Hooks ──────────────────────────────────────────
48
- // Load src/hooks.server.ts if present. Uses process.cwd() so
49
- // Bun can resolve it at runtime without bundling user code.
48
+ // Load user hooks. Production prefers the pre-bundled `${OUT_DIR}/hooks.server.js`
49
+ // emitted by the build (single-file, all relative imports inlined, npm deps left
50
+ // external) so production images can ship only `dist/` + `node_modules/` without
51
+ // the `src/` tree. Dev (and any environment lacking the artifact) falls back to
52
+ // importing `src/hooks.server.ts` directly so edits hot-reload without a build.
50
53
 
51
54
  let userHandle: Handle | null = null;
52
55
 
53
- const hooksPath = join(process.cwd(), "src", "hooks.server.ts");
54
- if (existsSync(hooksPath)) {
56
+ const prebuiltHooksPath = join(process.cwd(), OUT_DIR, "hooks.server.js");
57
+ const srcHooksPath = join(process.cwd(), "src", "hooks.server.ts");
58
+ const hooksPath = existsSync(prebuiltHooksPath)
59
+ ? prebuiltHooksPath
60
+ : existsSync(srcHooksPath)
61
+ ? srcHooksPath
62
+ : null;
63
+ if (hooksPath) {
55
64
  try {
56
65
  const mod = await import(hooksPath);
57
66
  if (typeof mod.handle === "function") {
58
67
  userHandle = mod.handle as Handle;
59
- console.log("🪝 Loaded hooks.server.ts");
68
+ console.log(
69
+ `🪝 Loaded ${hooksPath === prebuiltHooksPath ? "dist/hooks.server.js" : "src/hooks.server.ts"}`,
70
+ );
60
71
  }
61
72
  } catch (err) {
62
- console.warn("⚠️ Failed to load hooks.server.ts:", err);
73
+ console.warn("⚠️ Failed to load hooks.server:", err);
63
74
  }
64
75
  }
65
76
 
@@ -64,6 +64,16 @@ export function buildStaticManifest(outDir: string): StaticManifest {
64
64
  }
65
65
  }
66
66
 
67
+ // `dist/static/` mirrors `public/` (the build copies it for SSG output).
68
+ // Walk it too so production images can drop `public/` and ship only `dist/`.
69
+ // `addOnce` keeps the `public/` source canonical when both exist (dev).
70
+ const staticRoot = join(outAbs, "static");
71
+ if (existsSync(staticRoot)) {
72
+ for (const { abs, rel } of walk(staticRoot)) {
73
+ addOnce(manifest, `/${rel}`, { absPath: abs });
74
+ }
75
+ }
76
+
67
77
  if (existsSync(outAbs)) {
68
78
  let rootEntries: Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>;
69
79
  try {