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.
Files changed (87) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -53
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +8 -8
  8. package/src/cli/feat.ts +291 -132
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -153
  15. package/src/core/client/appState.svelte.ts +57 -0
  16. package/src/core/client/enhance.ts +112 -0
  17. package/src/core/client/hydrate.ts +97 -65
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +165 -168
  25. package/src/core/env.ts +155 -128
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +192 -139
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -118
  34. package/src/core/renderer.ts +359 -265
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +543 -370
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/client.ts +12 -0
  41. package/src/lib/index.ts +8 -8
  42. package/src/lib/utils.ts +44 -30
  43. package/templates/default/.prettierignore +5 -0
  44. package/templates/default/.prettierrc.json +9 -0
  45. package/templates/default/README.md +5 -5
  46. package/templates/default/package.json +22 -18
  47. package/templates/default/src/app.css +80 -80
  48. package/templates/default/src/app.d.ts +3 -3
  49. package/templates/default/src/routes/+error.svelte +7 -10
  50. package/templates/default/src/routes/+layout.svelte +2 -2
  51. package/templates/default/src/routes/+page.svelte +31 -29
  52. package/templates/default/src/routes/about/+page.svelte +3 -3
  53. package/templates/default/tsconfig.json +20 -20
  54. package/templates/demo/.prettierignore +5 -0
  55. package/templates/demo/.prettierrc.json +9 -0
  56. package/templates/demo/README.md +9 -9
  57. package/templates/demo/package.json +22 -17
  58. package/templates/demo/src/app.css +80 -80
  59. package/templates/demo/src/app.d.ts +3 -3
  60. package/templates/demo/src/hooks.server.ts +9 -9
  61. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  62. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  63. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  64. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  65. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  67. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  68. package/templates/demo/src/routes/+error.svelte +10 -7
  69. package/templates/demo/src/routes/+layout.server.ts +4 -4
  70. package/templates/demo/src/routes/+layout.svelte +2 -2
  71. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  72. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  73. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  74. package/templates/demo/tsconfig.json +20 -20
  75. package/templates/todo/.prettierignore +5 -0
  76. package/templates/todo/.prettierrc.json +9 -0
  77. package/templates/todo/README.md +9 -9
  78. package/templates/todo/package.json +22 -17
  79. package/templates/todo/src/app.css +80 -80
  80. package/templates/todo/src/app.d.ts +7 -7
  81. package/templates/todo/src/hooks.server.ts +9 -9
  82. package/templates/todo/src/routes/+error.svelte +10 -7
  83. package/templates/todo/src/routes/+layout.server.ts +4 -4
  84. package/templates/todo/src/routes/+layout.svelte +2 -2
  85. package/templates/todo/src/routes/+page.svelte +44 -44
  86. package/templates/todo/template.json +1 -1
  87. package/templates/todo/tsconfig.json +20 -20
@@ -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
- async function detectPrerenderRoutes(manifest: RouteManifest): Promise<string[]> {
14
- const paths: string[] = [];
15
- for (const route of manifest.pages) {
16
- if (!route.pageServer) continue;
17
- const filePath = join("src", "routes", route.pageServer);
18
- const content = await Bun.file(filePath).text();
19
- if (!/export\s+const\s+prerender\s*=\s*true/.test(content)) continue;
20
-
21
- if (route.pattern.includes("[")) {
22
- // Dynamic route import module and call entries() to get param values
23
- try {
24
- const mod = await import(join(process.cwd(), filePath));
25
- if (typeof mod.entries !== "function") {
26
- console.warn(` ⚠️ ${route.pattern} has prerender=true but no entries() export — skipped`);
27
- continue;
28
- }
29
- const entryList: Record<string, string>[] = await mod.entries();
30
- for (const entry of entryList) {
31
- let resolved = route.pattern;
32
- for (const [key, value] of Object.entries(entry)) {
33
- // [...slug] → value (rest param)
34
- resolved = resolved.replace(`[...${key}]`, value);
35
- // [param] value
36
- resolved = resolved.replace(`[${key}]`, value);
37
- }
38
- paths.push(resolved);
39
- }
40
- } catch (err) {
41
- console.error(` ❌ Failed to resolve entries() for ${route.pattern}:`, err);
42
- }
43
- } else {
44
- paths.push(route.pattern);
45
- }
46
- }
47
- return paths;
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
- const paths = await detectPrerenderRoutes(manifest);
52
- if (paths.length === 0) return;
53
-
54
- console.log(`\n🖨️ Prerendering ${paths.length} route(s)...`);
55
-
56
- const port = 13572;
57
- const child = Bun.spawn(
58
- ["bun", "run", "./dist/server/index.js"],
59
- {
60
- env: { ...process.env, NODE_ENV: "production", PORT: String(port), NODE_PATH: BOSIA_NODE_PATH },
61
- stdout: "ignore",
62
- stderr: "ignore",
63
- },
64
- );
65
-
66
- // Poll /_health until ready (max 10s)
67
- const base = `http://localhost:${port}`;
68
- let ready = false;
69
- for (let i = 0; i < 50; i++) {
70
- await Bun.sleep(200);
71
- try {
72
- const res = await fetch(`${base}/_health`);
73
- if (res.ok) { ready = true; break; }
74
- } catch { /* not ready yet */ }
75
- }
76
-
77
- if (!ready) {
78
- child.kill();
79
- console.error("❌ Prerender server failed to start");
80
- return;
81
- }
82
-
83
- mkdirSync("./dist/prerendered", { recursive: true });
84
-
85
- for (const routePath of paths) {
86
- try {
87
- const res = await fetch(`${base}${routePath}`, { signal: AbortSignal.timeout(PRERENDER_TIMEOUT) });
88
- const html = await res.text();
89
- const outPath = routePath === "/"
90
- ? "./dist/prerendered/index.html"
91
- : `./dist/prerendered${routePath}/index.html`;
92
- mkdirSync(outPath.substring(0, outPath.lastIndexOf("/")), { recursive: true });
93
- writeFileSync(outPath, html);
94
-
95
- // Also prerender the data payload
96
- const dataPath = routePath === "/" ? "/index.json" : `${routePath.replace(/\/$/, "")}.json`;
97
- const dataRes = await fetch(`${base}/__bosia/data${dataPath}`, { signal: AbortSignal.timeout(PRERENDER_TIMEOUT) });
98
- if (dataRes.ok) {
99
- const dataJson = await dataRes.text();
100
- const dataOutPath = `./dist/prerendered/__bosia/data${dataPath}`;
101
- mkdirSync(dataOutPath.substring(0, dataOutPath.lastIndexOf("/")), { recursive: true });
102
- writeFileSync(dataOutPath, dataJson);
103
- console.log(` ✅ ${routePath} ${outPath} (+ data)`);
104
- } else {
105
- console.log(` ✅ ${routePath} ${outPath}`);
106
- }
107
- } catch (err) {
108
- if (err instanceof DOMException && err.name === "TimeoutError") {
109
- console.error(` ❌ Prerender timed out for ${routePath} after ${PRERENDER_TIMEOUT / 1000}s — increase PRERENDER_TIMEOUT to raise the limit`);
110
- } else {
111
- console.error(` ❌ Failed to prerender ${routePath}:`, err);
112
- }
113
- }
114
- }
115
-
116
- child.kill();
117
- console.log("✅ Prerendering complete");
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
- if (!existsSync("./dist/prerendered")) {
124
- console.log("\n⏭️ No prerendered pages — skipping static site output");
125
- return;
126
- }
196
+ if (!existsSync("./dist/prerendered")) {
197
+ console.log("\n⏭️ No prerendered pages — skipping static site output");
198
+ return;
199
+ }
127
200
 
128
- console.log("\n📦 Generating static site...");
129
- mkdirSync("./dist/static", { recursive: true });
201
+ console.log("\n📦 Generating static site...");
202
+ mkdirSync("./dist/static", { recursive: true });
130
203
 
131
- // 1. HTML files from prerendering
132
- cpSync("./dist/prerendered", "./dist/static", { recursive: true });
204
+ // 1. HTML files from prerendering
205
+ cpSync("./dist/prerendered", "./dist/static", { recursive: true });
133
206
 
134
- // 2. Client JS/CSS — preserves /dist/client/... absolute paths used in HTML
135
- cpSync("./dist/client", "./dist/static/dist/client", { recursive: true });
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
- // 3. Public assets (bosia-tw.css, favicon, etc.) — preserves /bosia-tw.css path
138
- if (existsSync("./public")) {
139
- cpSync("./public", "./dist/static", { recursive: true });
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
- console.log("✅ Static site generated: dist/static/");
215
+ console.log("✅ Static site generated: dist/static/");
143
216
  }