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 +1 -1
- package/src/core/client/App.svelte +18 -3
- package/src/core/prerender.ts +68 -66
package/package.json
CHANGED
|
@@ -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
|
-
|
|
112
|
+
const currentTick = appState.invalidationTick; // still tracks reactively
|
|
111
113
|
|
|
112
114
|
const path = router.currentRoute;
|
|
113
|
-
const
|
|
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);
|
package/src/core/prerender.ts
CHANGED
|
@@ -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
|
|
102
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|