bosia 0.6.15 → 0.6.17
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 +1 -1
- package/src/core/appHtml.ts +29 -3
- package/src/core/build.ts +89 -2
- package/src/core/config.ts +20 -0
- package/src/core/server.ts +17 -6
- package/src/core/staticManifest.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
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/appHtml.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
|
|
4
|
+
import { OUT_DIR } from "./paths.ts";
|
|
3
5
|
|
|
4
6
|
// ─── Types ────────────────────────────────────────────────
|
|
5
7
|
|
|
@@ -70,13 +72,37 @@ function replaceStaticPlaceholders(template: string): string {
|
|
|
70
72
|
return template;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// ─── Persisted Segments (production runtime) ──────────────
|
|
76
|
+
// At build time we serialize the parsed segments to `${OUT_DIR}/app-html.json`
|
|
77
|
+
// so the production runtime can read them without needing `src/app.html` in
|
|
78
|
+
// the image. This lets minimal Docker images copy only `dist/`.
|
|
79
|
+
|
|
80
|
+
export function writeAppHtmlSegments(segments: AppHtmlSegments, outDir: string = OUT_DIR): string {
|
|
81
|
+
const target = join(outDir, "app-html.json");
|
|
82
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
83
|
+
writeFileSync(target, JSON.stringify(segments));
|
|
84
|
+
return target;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readPersistedSegments(cwd: string): AppHtmlSegments | undefined {
|
|
88
|
+
const persistedPath = join(cwd, OUT_DIR, "app-html.json");
|
|
89
|
+
if (!existsSync(persistedPath)) return undefined;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(readFileSync(persistedPath, "utf-8")) as AppHtmlSegments;
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
// ─── Cached Getter ────────────────────────────────────────
|
|
74
98
|
|
|
75
99
|
export function getAppHtmlSegments(cwd: string = process.cwd()): AppHtmlSegments {
|
|
76
100
|
if (cachedSegments !== undefined) {
|
|
77
101
|
return cachedSegments;
|
|
78
102
|
}
|
|
79
|
-
|
|
103
|
+
// Prefer persisted dist artifact (production runtime — no `src/` in image).
|
|
104
|
+
// Fall back to parsing `src/app.html` directly (dev mode, build step).
|
|
105
|
+
cachedSegments = readPersistedSegments(cwd) ?? loadAppHtmlTemplate(cwd);
|
|
80
106
|
return cachedSegments;
|
|
81
107
|
}
|
|
82
108
|
|
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";
|
|
@@ -12,11 +12,14 @@ import { generateEnvModules } from "./envCodegen.ts";
|
|
|
12
12
|
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin } from "./paths.ts";
|
|
13
13
|
import { loadPlugins } from "./config.ts";
|
|
14
14
|
import type { BuildContext } from "./types/plugin.ts";
|
|
15
|
-
import { loadAppHtmlTemplate } from "./appHtml.ts";
|
|
15
|
+
import { loadAppHtmlTemplate, writeAppHtmlSegments } from "./appHtml.ts";
|
|
16
16
|
|
|
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";
|
|
@@ -250,6 +253,18 @@ console.log(`✅ Server entry: ${OUT_DIR}/server/${serverEntry}`);
|
|
|
250
253
|
// 8b. Persist route manifest for runtime plugins (backend.after consumers like OpenAPI).
|
|
251
254
|
writeFileSync(`${OUT_DIR}/route-manifest.json`, JSON.stringify(manifest, null, 2));
|
|
252
255
|
|
|
256
|
+
// 8c. Persist parsed app.html segments so the production runtime doesn't need
|
|
257
|
+
// `src/app.html` in the image. Renderer reads `${OUT_DIR}/app-html.json` first,
|
|
258
|
+
// falls back to parsing `src/app.html` for dev.
|
|
259
|
+
writeAppHtmlSegments(appHtml);
|
|
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
|
+
|
|
253
268
|
// 9. Prerender static routes
|
|
254
269
|
await prerenderStaticRoutes(manifest);
|
|
255
270
|
|
|
@@ -263,3 +278,75 @@ for (const p of userPlugins) {
|
|
|
263
278
|
}
|
|
264
279
|
|
|
265
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
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -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: [] };
|
package/src/core/server.ts
CHANGED
|
@@ -45,21 +45,32 @@ import {
|
|
|
45
45
|
import { getServerTime } from "../lib/utils.ts";
|
|
46
46
|
|
|
47
47
|
// ─── User Hooks ──────────────────────────────────────────
|
|
48
|
-
// Load
|
|
49
|
-
//
|
|
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
|
|
54
|
-
|
|
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(
|
|
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
|
|
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 {
|