bosia 0.2.3 → 0.3.1

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