bosia 0.8.13 → 0.8.14

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.8.13",
3
+ "version": "0.8.14",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
@@ -77,6 +77,8 @@
77
77
  let showLoading = $state(false);
78
78
  let currentLayoutPaths: string[] = [];
79
79
  let lastSettledPath = "";
80
+ let lastNavKey = ""; // pathname + search of the last processed nav
81
+ let lastTick = 0; // invalidationTick value last processed
80
82
  // Skip bar on the very first effect run (initial hydration — data already present)
81
83
  let firstNav = true;
82
84
  let navDoneTimer: ReturnType<typeof setTimeout> | null = null;
@@ -107,10 +109,12 @@
107
109
 
108
110
  // Subscribe to `invalidationTick` so `invalidate()` can wake the effect
109
111
  // without a URL change.
110
- void appState.invalidationTick;
112
+ const currentTick = appState.invalidationTick; // still tracks reactively
111
113
 
112
114
  const path = router.currentRoute;
113
- const pathname = path.split("?")[0].split("#")[0];
115
+ const url = new URL(path, window.location.origin);
116
+ const pathname = url.pathname;
117
+ const navKey = pathname + url.search;
114
118
  const match = findMatch(clientRoutes, pathname);
115
119
  if (!match) return;
116
120
 
@@ -121,11 +125,22 @@
121
125
  if (isFirst) {
122
126
  currentLayoutPaths = (match.route as any).layoutPaths ?? [];
123
127
  lastSettledPath = pathname;
128
+ lastNavKey = navKey;
129
+ lastTick = currentTick;
124
130
  // Restore-after-reload: router.init() staged this entry's snapshot.
125
131
  settleSnapshot();
126
132
  return; // Initial hydration — data already in SSR props, no fetch needed
127
133
  }
128
134
 
135
+ // Hash-only change (same pathname+search, no invalidation): scroll, don't
136
+ // refetch. A static host can't answer the parent-snapshot POST a refetch may
137
+ // issue, which would otherwise render the error page.
138
+ if (navKey === lastNavKey && currentTick === lastTick) {
139
+ settleScroll();
140
+ return;
141
+ }
142
+ lastTick = currentTick;
143
+
129
144
  appState.form = null;
130
145
  if (navDoneTimer) {
131
146
  clearTimeout(navDoneTimer);
@@ -160,7 +175,6 @@
160
175
  // For each layout depth + the page, compare the cached entry (if any)
161
176
  // against the live URL/params and the dirty set. If everything is
162
177
  // cacheable, skip the fetch entirely.
163
- const url = new URL(path, window.location.origin);
164
178
  const ctx = liveContext(pathname, match.params, url);
165
179
  const layoutIds = (match.route as any).layoutIds as (string | null)[];
166
180
  const pageId = (match.route as any).pageId as string | null;
@@ -243,6 +257,7 @@
243
257
  LoadingComponent = null;
244
258
  currentLayoutPaths = destLayoutPaths;
245
259
  lastSettledPath = pathname;
260
+ lastNavKey = navKey;
246
261
  navDoneTimer = setTimeout(() => {
247
262
  navDone = false;
248
263
  }, 400);
@@ -27,6 +27,7 @@ export function getEphemeralPort(): Promise<number> {
27
27
  const CORE_DIR = import.meta.dir;
28
28
 
29
29
  const PRERENDER_TIMEOUT = Number(process.env.PRERENDER_TIMEOUT) || 5_000; // 5s default
30
+ const PRERENDER_CONCURRENCY = Number(process.env.PRERENDER_CONCURRENCY) || 6;
30
31
 
31
32
  // ─── Prerendering ─────────────────────────────────────────
32
33
 
@@ -97,79 +98,62 @@ export function prerenderApiOutPath(routePath: string): string {
97
98
  return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}.json`;
98
99
  }
99
100
 
101
+ /** Dynamic route → import module, call entries(), expand to concrete targets. */
102
+ async function expandDynamicRoute(
103
+ pattern: string,
104
+ filePath: string,
105
+ kind: "page" | "api",
106
+ ts: TrailingSlash,
107
+ ): Promise<PrerenderTarget[]> {
108
+ try {
109
+ const mod = await import(join(process.cwd(), filePath));
110
+ if (typeof mod.entries !== "function") {
111
+ console.warn(` ⚠️ ${pattern} has prerender=true but no entries() export — skipped`);
112
+ return [];
113
+ }
114
+ const entryList: Record<string, string>[] = await mod.entries();
115
+ return entryList.map((entry) => ({
116
+ path: substituteParams(pattern, entry),
117
+ kind,
118
+ trailingSlash: ts,
119
+ }));
120
+ } catch (err) {
121
+ console.error(` ❌ Failed to resolve entries() for ${pattern}:`, err);
122
+ return [];
123
+ }
124
+ }
125
+
100
126
  async function detectPrerenderRoutes(manifest: RouteManifest): Promise<PrerenderTarget[]> {
101
- const targets: PrerenderTarget[] = [];
102
- for (const route of manifest.pages) {
103
- if (!route.pageServer) continue;
127
+ const pageTasks = manifest.pages.map(async (route): Promise<PrerenderTarget[]> => {
128
+ if (!route.pageServer) return [];
104
129
  const filePath = join("src", "routes", route.pageServer);
105
130
  const content = await Bun.file(filePath).text();
106
- if (!/export\s+const\s+prerender\s*=\s*true/.test(content)) continue;
131
+ if (!/export\s+const\s+prerender\s*=\s*true/.test(content)) return [];
107
132
  if (/export\s+const\s+ssr\s*=\s*false/.test(content)) {
108
133
  console.warn(
109
134
  ` ⚠️ ${route.pattern} has prerender=true && ssr=false — contradictory, skipped`,
110
135
  );
111
- continue;
136
+ return [];
112
137
  }
113
-
114
138
  const ts = route.trailingSlash;
139
+ if (route.pattern.includes("[")) return expandDynamicRoute(route.pattern, filePath, "page", ts);
140
+ return [{ path: route.pattern, kind: "page", trailingSlash: ts }];
141
+ });
115
142
 
116
- if (route.pattern.includes("[")) {
117
- // Dynamic route — import module and call entries() to get param values
118
- try {
119
- const mod = await import(join(process.cwd(), filePath));
120
- if (typeof mod.entries !== "function") {
121
- console.warn(
122
- ` ⚠️ ${route.pattern} has prerender=true but no entries() export — skipped`,
123
- );
124
- continue;
125
- }
126
- const entryList: Record<string, string>[] = await mod.entries();
127
- for (const entry of entryList) {
128
- targets.push({
129
- path: substituteParams(route.pattern, entry),
130
- kind: "page",
131
- trailingSlash: ts,
132
- });
133
- }
134
- } catch (err) {
135
- console.error(` ❌ Failed to resolve entries() for ${route.pattern}:`, err);
136
- }
137
- } else {
138
- targets.push({ path: route.pattern, kind: "page", trailingSlash: ts });
139
- }
140
- }
141
-
142
- for (const route of manifest.apis) {
143
+ const apiTasks = manifest.apis.map(async (route): Promise<PrerenderTarget[]> => {
143
144
  const filePath = join("src", "routes", route.server);
144
145
  const content = await Bun.file(filePath).text();
145
- if (!/export\s+const\s+prerender\s*=\s*true/.test(content)) continue;
146
-
147
- if (route.pattern.includes("[")) {
148
- try {
149
- const mod = await import(join(process.cwd(), filePath));
150
- if (typeof mod.entries !== "function") {
151
- console.warn(
152
- ` ⚠️ ${route.pattern} has prerender=true but no entries() export — skipped`,
153
- );
154
- continue;
155
- }
156
- const entryList: Record<string, string>[] = await mod.entries();
157
- for (const entry of entryList) {
158
- targets.push({
159
- path: substituteParams(route.pattern, entry),
160
- kind: "api",
161
- trailingSlash: "never",
162
- });
163
- }
164
- } catch (err) {
165
- console.error(` ❌ Failed to resolve entries() for ${route.pattern}:`, err);
166
- }
167
- } else {
168
- targets.push({ path: route.pattern, kind: "api", trailingSlash: "never" });
169
- }
170
- }
146
+ if (!/export\s+const\s+prerender\s*=\s*true/.test(content)) return [];
147
+ if (route.pattern.includes("["))
148
+ return expandDynamicRoute(route.pattern, filePath, "api", "never");
149
+ return [{ path: route.pattern, kind: "api", trailingSlash: "never" }];
150
+ });
171
151
 
172
- return targets;
152
+ // Unbounded Promise.all over all routes; each task holds only small text reads
153
+ // and route metadata, so RAM stays trivial. Chunk it if route counts ever hit
154
+ // tens of thousands (open file descriptors would be the first limit).
155
+ const results = await Promise.all([...pageTasks, ...apiTasks]);
156
+ return results.flat();
173
157
  }
174
158
 
175
159
  export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<void> {
@@ -198,11 +182,12 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
198
182
  for (const sig of signals) process.once(sig, onSignal);
199
183
 
200
184
  try {
201
- // Poll /_health until ready (max 10s)
185
+ // Poll /_health until ready (max 10s). Check first, sleep only on failure —
186
+ // avoids a guaranteed floor when the server is already up.
202
187
  const base = `http://localhost:${port}`;
203
188
  let ready = false;
204
- for (let i = 0; i < 50; i++) {
205
- await Bun.sleep(200);
189
+ const deadline = Date.now() + 10_000;
190
+ while (Date.now() < deadline) {
206
191
  try {
207
192
  const res = await fetch(`${base}/_health`);
208
193
  if (res.ok) {
@@ -212,6 +197,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
212
197
  } catch {
213
198
  /* not ready yet */
214
199
  }
200
+ await Bun.sleep(50);
215
201
  }
216
202
 
217
203
  if (!ready) {
@@ -221,7 +207,11 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
221
207
 
222
208
  mkdirSync(`${OUT_DIR}/prerendered`, { recursive: true });
223
209
 
224
- for (const { path: routePath, kind, trailingSlash: ts } of targets) {
210
+ const prerenderOne = async ({
211
+ path: routePath,
212
+ kind,
213
+ trailingSlash: ts,
214
+ }: PrerenderTarget): Promise<void> => {
225
215
  try {
226
216
  if (kind === "api") {
227
217
  // APIs: fetch the bare route URL, write body to `<path>.json`.
@@ -233,7 +223,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
233
223
  mkdirSync(outPath.substring(0, outPath.lastIndexOf("/")), { recursive: true });
234
224
  writeFileSync(outPath, body);
235
225
  console.log(` ✅ ${routePath} → ${outPath}`);
236
- continue;
226
+ return;
237
227
  }
238
228
 
239
229
  // Hit the canonical URL so the server doesn't 308 us mid-prerender
@@ -278,7 +268,19 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
278
268
  console.error(` ❌ Failed to prerender ${routePath}:`, err);
279
269
  }
280
270
  }
281
- }
271
+ };
272
+
273
+ // Bounded worker pool: N workers pull from a shared index until targets drain.
274
+ let next = 0;
275
+ const worker = async () => {
276
+ while (next < targets.length) {
277
+ const target = targets[next++];
278
+ if (target) await prerenderOne(target);
279
+ }
280
+ };
281
+ await Promise.all(
282
+ Array.from({ length: Math.min(PRERENDER_CONCURRENCY, targets.length) }, worker),
283
+ );
282
284
 
283
285
  console.log("✅ Prerendering complete");
284
286
  } finally {