bosia 0.2.2 → 0.3.0
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/README.md +39 -39
- package/package.json +56 -53
- package/src/ambient.d.ts +31 -0
- package/src/cli/add.ts +120 -114
- package/src/cli/build.ts +10 -10
- package/src/cli/create.ts +142 -137
- package/src/cli/dev.ts +8 -8
- package/src/cli/feat.ts +291 -132
- package/src/cli/index.ts +51 -42
- package/src/cli/registry.ts +136 -115
- package/src/cli/start.ts +17 -17
- package/src/cli/test.ts +25 -0
- package/src/core/build.ts +72 -56
- package/src/core/client/App.svelte +177 -153
- package/src/core/client/appState.svelte.ts +57 -0
- package/src/core/client/enhance.ts +112 -0
- package/src/core/client/hydrate.ts +97 -65
- package/src/core/client/prefetch.ts +101 -94
- package/src/core/client/router.svelte.ts +64 -51
- package/src/core/cookies.ts +70 -66
- package/src/core/cors.ts +44 -35
- package/src/core/csrf.ts +38 -38
- package/src/core/dedup.ts +17 -17
- package/src/core/dev.ts +165 -168
- package/src/core/env.ts +155 -128
- package/src/core/envCodegen.ts +73 -73
- package/src/core/errors.ts +48 -49
- package/src/core/hooks.ts +50 -50
- package/src/core/html.ts +192 -139
- package/src/core/matcher.ts +130 -121
- package/src/core/paths.ts +8 -10
- package/src/core/plugin.ts +113 -107
- package/src/core/prerender.ts +191 -118
- package/src/core/renderer.ts +359 -265
- package/src/core/routeFile.ts +140 -127
- package/src/core/routeTypes.ts +144 -83
- package/src/core/scanner.ts +125 -95
- package/src/core/server.ts +543 -370
- package/src/core/types.ts +25 -20
- package/src/lib/client.ts +12 -0
- package/src/lib/index.ts +8 -8
- package/src/lib/utils.ts +44 -30
- package/templates/default/.prettierignore +5 -0
- package/templates/default/.prettierrc.json +9 -0
- package/templates/default/README.md +5 -5
- package/templates/default/package.json +22 -18
- package/templates/default/src/app.css +80 -80
- package/templates/default/src/app.d.ts +3 -3
- package/templates/default/src/routes/+error.svelte +7 -10
- package/templates/default/src/routes/+layout.svelte +2 -2
- package/templates/default/src/routes/+page.svelte +31 -29
- package/templates/default/src/routes/about/+page.svelte +3 -3
- package/templates/default/tsconfig.json +20 -20
- package/templates/demo/.prettierignore +5 -0
- package/templates/demo/.prettierrc.json +9 -0
- package/templates/demo/README.md +9 -9
- package/templates/demo/package.json +22 -17
- package/templates/demo/src/app.css +80 -80
- package/templates/demo/src/app.d.ts +3 -3
- package/templates/demo/src/hooks.server.ts +9 -9
- package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
- package/templates/demo/src/routes/(public)/+page.svelte +96 -67
- package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
- package/templates/demo/src/routes/+error.svelte +10 -7
- package/templates/demo/src/routes/+layout.server.ts +4 -4
- package/templates/demo/src/routes/+layout.svelte +2 -2
- package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
- package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
- package/templates/demo/src/routes/api/hello/+server.ts +25 -25
- package/templates/demo/tsconfig.json +20 -20
- package/templates/todo/.prettierignore +5 -0
- package/templates/todo/.prettierrc.json +9 -0
- package/templates/todo/README.md +9 -9
- package/templates/todo/package.json +22 -17
- package/templates/todo/src/app.css +80 -80
- package/templates/todo/src/app.d.ts +7 -7
- package/templates/todo/src/hooks.server.ts +9 -9
- package/templates/todo/src/routes/+error.svelte +10 -7
- package/templates/todo/src/routes/+layout.server.ts +4 -4
- package/templates/todo/src/routes/+layout.svelte +2 -2
- package/templates/todo/src/routes/+page.svelte +44 -44
- package/templates/todo/template.json +1 -1
- package/templates/todo/tsconfig.json +20 -20
package/src/core/prerender.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, cpSync, existsSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import type { RouteManifest } from "./types.ts";
|
|
3
|
+
import type { RouteManifest, TrailingSlash } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
import { BOSIA_NODE_PATH } from "./paths.ts";
|
|
6
6
|
|
|
@@ -10,134 +10,207 @@ const PRERENDER_TIMEOUT = Number(process.env.PRERENDER_TIMEOUT) || 5_000; // 5s
|
|
|
10
10
|
|
|
11
11
|
// ─── Prerendering ─────────────────────────────────────────
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
13
|
+
interface PrerenderTarget {
|
|
14
|
+
path: string;
|
|
15
|
+
trailingSlash: TrailingSlash;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Pure helpers (exported for tests) ────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Substitute `[param]` and `[...rest]` placeholders in a route pattern with
|
|
22
|
+
* concrete values from an `entries()` record.
|
|
23
|
+
*/
|
|
24
|
+
export function substituteParams(pattern: string, entry: Record<string, string>): string {
|
|
25
|
+
let resolved = pattern;
|
|
26
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
27
|
+
resolved = resolved.replace(`[...${key}]`, value);
|
|
28
|
+
resolved = resolved.replace(`[${key}]`, value);
|
|
29
|
+
}
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Canonical URL to fetch during prerender, based on trailing-slash mode.
|
|
35
|
+
* Avoids hitting the server's 308 redirect mid-prerender.
|
|
36
|
+
*/
|
|
37
|
+
export function canonicalRouteFor(routePath: string, ts: TrailingSlash): string {
|
|
38
|
+
if (routePath === "/") return "/";
|
|
39
|
+
if (ts === "always") return routePath.endsWith("/") ? routePath : routePath + "/";
|
|
40
|
+
return routePath.replace(/\/$/, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Output HTML filename for a prerendered route. Strategy follows trailing-slash
|
|
45
|
+
* mode so static hosts serve the right file on direct URL hits.
|
|
46
|
+
*/
|
|
47
|
+
export function prerenderOutPath(routePath: string, ts: TrailingSlash): string {
|
|
48
|
+
if (routePath === "/") return "./dist/prerendered/index.html";
|
|
49
|
+
if (ts === "never") return `./dist/prerendered${routePath.replace(/\/$/, "")}.html`;
|
|
50
|
+
return `./dist/prerendered${routePath.replace(/\/$/, "")}/index.html`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Data-payload filename for a prerendered route — matches client `dataUrl()`. */
|
|
54
|
+
export function prerenderDataPath(routePath: string): string {
|
|
55
|
+
return routePath === "/" ? "/index.json" : `${routePath.replace(/\/$/, "")}.json`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function detectPrerenderRoutes(manifest: RouteManifest): Promise<PrerenderTarget[]> {
|
|
59
|
+
const targets: PrerenderTarget[] = [];
|
|
60
|
+
for (const route of manifest.pages) {
|
|
61
|
+
if (!route.pageServer) continue;
|
|
62
|
+
const filePath = join("src", "routes", route.pageServer);
|
|
63
|
+
const content = await Bun.file(filePath).text();
|
|
64
|
+
if (!/export\s+const\s+prerender\s*=\s*true/.test(content)) continue;
|
|
65
|
+
if (/export\s+const\s+ssr\s*=\s*false/.test(content)) {
|
|
66
|
+
console.warn(
|
|
67
|
+
` ⚠️ ${route.pattern} has prerender=true && ssr=false — contradictory, skipped`,
|
|
68
|
+
);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ts = route.trailingSlash;
|
|
73
|
+
|
|
74
|
+
if (route.pattern.includes("[")) {
|
|
75
|
+
// Dynamic route — import module and call entries() to get param values
|
|
76
|
+
try {
|
|
77
|
+
const mod = await import(join(process.cwd(), filePath));
|
|
78
|
+
if (typeof mod.entries !== "function") {
|
|
79
|
+
console.warn(
|
|
80
|
+
` ⚠️ ${route.pattern} has prerender=true but no entries() export — skipped`,
|
|
81
|
+
);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const entryList: Record<string, string>[] = await mod.entries();
|
|
85
|
+
for (const entry of entryList) {
|
|
86
|
+
targets.push({
|
|
87
|
+
path: substituteParams(route.pattern, entry),
|
|
88
|
+
trailingSlash: ts,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(` ❌ Failed to resolve entries() for ${route.pattern}:`, err);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
targets.push({ path: route.pattern, trailingSlash: ts });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return targets;
|
|
48
99
|
}
|
|
49
100
|
|
|
50
101
|
export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<void> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
102
|
+
const targets = await detectPrerenderRoutes(manifest);
|
|
103
|
+
if (targets.length === 0) return;
|
|
104
|
+
|
|
105
|
+
console.log(`\n🖨️ Prerendering ${targets.length} route(s)...`);
|
|
106
|
+
|
|
107
|
+
const port = 13572;
|
|
108
|
+
const child = Bun.spawn(["bun", "run", "./dist/server/index.js"], {
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
NODE_ENV: "production",
|
|
112
|
+
PORT: String(port),
|
|
113
|
+
NODE_PATH: BOSIA_NODE_PATH,
|
|
114
|
+
},
|
|
115
|
+
stdout: "ignore",
|
|
116
|
+
stderr: "ignore",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Poll /_health until ready (max 10s)
|
|
120
|
+
const base = `http://localhost:${port}`;
|
|
121
|
+
let ready = false;
|
|
122
|
+
for (let i = 0; i < 50; i++) {
|
|
123
|
+
await Bun.sleep(200);
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(`${base}/_health`);
|
|
126
|
+
if (res.ok) {
|
|
127
|
+
ready = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
/* not ready yet */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!ready) {
|
|
136
|
+
child.kill();
|
|
137
|
+
console.error("❌ Prerender server failed to start");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
mkdirSync("./dist/prerendered", { recursive: true });
|
|
142
|
+
|
|
143
|
+
for (const { path: routePath, trailingSlash: ts } of targets) {
|
|
144
|
+
try {
|
|
145
|
+
// Hit the canonical URL so the server doesn't 308 us mid-prerender
|
|
146
|
+
const canonicalRoute = canonicalRouteFor(routePath, ts);
|
|
147
|
+
|
|
148
|
+
const res = await fetch(`${base}${canonicalRoute}`, {
|
|
149
|
+
signal: AbortSignal.timeout(PRERENDER_TIMEOUT),
|
|
150
|
+
});
|
|
151
|
+
const html = await res.text();
|
|
152
|
+
|
|
153
|
+
// Filename strategy:
|
|
154
|
+
// never → about.html (canonical /about, served by static host as /about → about.html)
|
|
155
|
+
// always → about/index.html (canonical /about/, static host serves /about/ → about/index.html)
|
|
156
|
+
// ignore → about/index.html (single emit; both URLs resolve via server canonicalize=off)
|
|
157
|
+
// root → index.html
|
|
158
|
+
const outPath = prerenderOutPath(routePath, ts);
|
|
159
|
+
mkdirSync(outPath.substring(0, outPath.lastIndexOf("/")), { recursive: true });
|
|
160
|
+
writeFileSync(outPath, html);
|
|
161
|
+
|
|
162
|
+
// Also prerender the data payload (filename matches dataUrl() — strips trailing slash)
|
|
163
|
+
const dataPath = prerenderDataPath(routePath);
|
|
164
|
+
const dataRes = await fetch(`${base}/__bosia/data${dataPath}`, {
|
|
165
|
+
signal: AbortSignal.timeout(PRERENDER_TIMEOUT),
|
|
166
|
+
});
|
|
167
|
+
if (dataRes.ok) {
|
|
168
|
+
const dataJson = await dataRes.text();
|
|
169
|
+
const dataOutPath = `./dist/prerendered/__bosia/data${dataPath}`;
|
|
170
|
+
mkdirSync(dataOutPath.substring(0, dataOutPath.lastIndexOf("/")), {
|
|
171
|
+
recursive: true,
|
|
172
|
+
});
|
|
173
|
+
writeFileSync(dataOutPath, dataJson);
|
|
174
|
+
console.log(` ✅ ${routePath} → ${outPath} (+ data)`);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` ✅ ${routePath} → ${outPath}`);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
180
|
+
console.error(
|
|
181
|
+
` ❌ Prerender timed out for ${routePath} after ${PRERENDER_TIMEOUT / 1000}s — increase PRERENDER_TIMEOUT to raise the limit`,
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
console.error(` ❌ Failed to prerender ${routePath}:`, err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
child.kill();
|
|
190
|
+
console.log("✅ Prerendering complete");
|
|
118
191
|
}
|
|
119
192
|
|
|
120
193
|
// ─── Static Site Output ──────────────────────────────────
|
|
121
194
|
|
|
122
195
|
export function generateStaticSite(): void {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
196
|
+
if (!existsSync("./dist/prerendered")) {
|
|
197
|
+
console.log("\n⏭️ No prerendered pages — skipping static site output");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
127
200
|
|
|
128
|
-
|
|
129
|
-
|
|
201
|
+
console.log("\n📦 Generating static site...");
|
|
202
|
+
mkdirSync("./dist/static", { recursive: true });
|
|
130
203
|
|
|
131
|
-
|
|
132
|
-
|
|
204
|
+
// 1. HTML files from prerendering
|
|
205
|
+
cpSync("./dist/prerendered", "./dist/static", { recursive: true });
|
|
133
206
|
|
|
134
|
-
|
|
135
|
-
|
|
207
|
+
// 2. Client JS/CSS — preserves /dist/client/... absolute paths used in HTML
|
|
208
|
+
cpSync("./dist/client", "./dist/static/dist/client", { recursive: true });
|
|
136
209
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
210
|
+
// 3. Public assets (bosia-tw.css, favicon, etc.) — preserves /bosia-tw.css path
|
|
211
|
+
if (existsSync("./public")) {
|
|
212
|
+
cpSync("./public", "./dist/static", { recursive: true });
|
|
213
|
+
}
|
|
141
214
|
|
|
142
|
-
|
|
215
|
+
console.log("✅ Static site generated: dist/static/");
|
|
143
216
|
}
|