bosia 1.1.0 → 1.1.2
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/build.ts +14 -5
- package/src/core/client/App.svelte +4 -1
- package/src/core/html.ts +26 -3
- package/src/core/preloadMap.ts +58 -0
- package/src/core/renderer.ts +11 -2
package/package.json
CHANGED
package/src/core/build.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { generateRouteTypes, ensureRootDirs } from "./routeTypes.ts";
|
|
|
17
17
|
import { makeBosiaPlugin } from "./plugin.ts";
|
|
18
18
|
import { makeBosiaSvelteCompiler, svelteMapCache } from "./svelteCompiler.ts";
|
|
19
19
|
import { finalizeComponentCss } from "./componentCss.ts";
|
|
20
|
+
import { buildPreloadMap } from "./preloadMap.ts";
|
|
20
21
|
import { prerenderStaticRoutes, generateStaticSite } from "./prerender.ts";
|
|
21
22
|
import { loadEnv, classifyEnvVars } from "./env.ts";
|
|
22
23
|
import { generateEnvModules } from "./envCodegen.ts";
|
|
@@ -252,6 +253,8 @@ const clientPromise = Bun.build({
|
|
|
252
253
|
// Chunks are named after their source, which for routes is `+page` — and
|
|
253
254
|
// Cloudflare's asset server answers a `+` in the path with a redirect.
|
|
254
255
|
naming: { entry: "[name]-[hash].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
256
|
+
// Read below to map each route to the chunks it needs (see preloadMap.ts).
|
|
257
|
+
metafile: true,
|
|
255
258
|
minify: isProduction,
|
|
256
259
|
sourcemap: isProduction ? "none" : "linked",
|
|
257
260
|
define: {
|
|
@@ -346,14 +349,20 @@ const serverEntry = serverEntryOutput ? basename(serverEntryOutput.path) : "inde
|
|
|
346
349
|
|
|
347
350
|
// 8. Write dist/manifest.json
|
|
348
351
|
mkdirSync(OUT_DIR, { recursive: true });
|
|
352
|
+
const entryFile =
|
|
353
|
+
clientEntry ??
|
|
354
|
+
jsFiles.find((f) => f === "hydrate.js") ??
|
|
355
|
+
jsFiles.find((f) => f.startsWith("hydrate")) ??
|
|
356
|
+
"hydrate.js";
|
|
349
357
|
const distManifest = {
|
|
350
358
|
js: jsFiles,
|
|
351
359
|
css: cssFiles,
|
|
352
|
-
entry:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
360
|
+
entry: entryFile,
|
|
361
|
+
// Per-route modulepreload list, so a page's code downloads with the entry
|
|
362
|
+
// instead of after it.
|
|
363
|
+
preload: clientResult.metafile
|
|
364
|
+
? buildPreloadMap(clientResult.metafile, manifest.pages, entryFile)
|
|
365
|
+
: {},
|
|
357
366
|
serverEntry,
|
|
358
367
|
target,
|
|
359
368
|
tw: twFile,
|
|
@@ -145,6 +145,9 @@
|
|
|
145
145
|
settleScroll();
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
|
+
// A prefetch saved before an invalidation holds pre-mutation data — after a
|
|
149
|
+
// `use:enhance` success or `invalidate*()` it must not stand in for the refetch.
|
|
150
|
+
const invalidated = currentTick !== lastTick || appState.dirty.all;
|
|
148
151
|
lastTick = currentTick;
|
|
149
152
|
|
|
150
153
|
// Only a real navigation drops the action result. An invalidation re-run
|
|
@@ -217,7 +220,7 @@
|
|
|
217
220
|
|
|
218
221
|
// Load components + data in parallel, then update state atomically
|
|
219
222
|
// to avoid a flash of stale/empty data before the fetch completes.
|
|
220
|
-
const cached = match.route.hasServerData ? consumePrefetch(path) : null;
|
|
223
|
+
const cached = match.route.hasServerData && !invalidated ? consumePrefetch(path) : null;
|
|
221
224
|
prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
|
|
222
225
|
// Forward cached parent data for skipped layers so downstream loaders see
|
|
223
226
|
// real parent() data, not {}. POST only when there's something to carry —
|
package/src/core/html.ts
CHANGED
|
@@ -24,6 +24,8 @@ export const distManifest: {
|
|
|
24
24
|
basePath?: string;
|
|
25
25
|
/** PUBLIC_* (non-static) names declared in .env files, stamped by the build. */
|
|
26
26
|
publicEnv?: string[];
|
|
27
|
+
/** Client chunks per route pattern, stamped by the build (see preloadMap.ts). */
|
|
28
|
+
preload?: Record<string, string[]>;
|
|
27
29
|
} = readArtifact("manifest.json") ?? { js: [], css: [], entry: "hydrate.js" };
|
|
28
30
|
|
|
29
31
|
export const isDev = process.env.NODE_ENV !== "production";
|
|
@@ -55,6 +57,17 @@ export function baseScript(nonce?: string): string {
|
|
|
55
57
|
: "";
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** modulepreload links for the chunks `pattern` needs to hydrate, so they
|
|
61
|
+
* download alongside the entry instead of after it has run. Hashed names, so
|
|
62
|
+
* no buster (same reason as ENTRY). Unknown pattern or an older dist/ without
|
|
63
|
+
* the `preload` field → nothing, and the page loads as it did before. */
|
|
64
|
+
export function routePreloadLinks(pattern?: string): string {
|
|
65
|
+
if (!pattern) return "";
|
|
66
|
+
return (distManifest.preload?.[pattern] ?? [])
|
|
67
|
+
.map((f) => `\n <link rel="modulepreload" href="${DIST}/${f}">`)
|
|
68
|
+
.join("");
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
/** Tailwind stylesheet link. Content-hashed name needs no cache buster — the
|
|
59
72
|
* hash IS the buster. Fallback keeps older dist/ artifacts (no `tw` field) styled. */
|
|
60
73
|
function twCssLink(): string {
|
|
@@ -161,6 +174,7 @@ export function buildHtml(
|
|
|
161
174
|
bodyEndExtras?: string[],
|
|
162
175
|
segments?: AppHtmlSegments,
|
|
163
176
|
metadata?: Metadata | null,
|
|
177
|
+
pattern?: string,
|
|
164
178
|
): string {
|
|
165
179
|
// An app writes <a href="/masuk">; under a base the browser has to be handed
|
|
166
180
|
// /sso/masuk or it walks off this app entirely. Only the rendered markup is
|
|
@@ -208,6 +222,11 @@ export function buildHtml(
|
|
|
208
222
|
|
|
209
223
|
const bodyEnd = bodyEndExtras?.length ? "\n " + bodyEndExtras.join("\n ") : "";
|
|
210
224
|
|
|
225
|
+
// Same hints the streaming shell sends; skipped when no JS runs at all.
|
|
226
|
+
const preloads = csr
|
|
227
|
+
? ` <link rel="modulepreload" href="${ENTRY}">${routePreloadLinks(pattern)}\n`
|
|
228
|
+
: "";
|
|
229
|
+
|
|
211
230
|
if (segments) {
|
|
212
231
|
const safeKey = safeLang(lang);
|
|
213
232
|
const headOpenInterpolated = interpolateSegment(segments.headOpen, {
|
|
@@ -225,6 +244,7 @@ export function buildHtml(
|
|
|
225
244
|
`\n ${faviconLine}${twCssLink()}\n` +
|
|
226
245
|
componentCssLinks() +
|
|
227
246
|
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
247
|
+
preloads +
|
|
228
248
|
` ${fallbackTitle}${metaTags}${head}` +
|
|
229
249
|
headCloseInterpolated +
|
|
230
250
|
(body ? "" : `\n${SPINNER}`) +
|
|
@@ -243,7 +263,7 @@ export function buildHtml(
|
|
|
243
263
|
${metaTags} ${head}
|
|
244
264
|
${twCssLink()}
|
|
245
265
|
${componentCssLinks()} <script${n}>${THEME_INIT_JS}</script>
|
|
246
|
-
</head>
|
|
266
|
+
${preloads}</head>
|
|
247
267
|
<body>
|
|
248
268
|
<div id="app">${body}</div>${scripts}${bodyEnd}
|
|
249
269
|
</body>
|
|
@@ -257,6 +277,7 @@ export function buildHtmlShellOpen(
|
|
|
257
277
|
lang?: string,
|
|
258
278
|
nonce?: string,
|
|
259
279
|
segments?: AppHtmlSegments,
|
|
280
|
+
pattern?: string,
|
|
260
281
|
): string {
|
|
261
282
|
const key = safeLang(lang);
|
|
262
283
|
const n = nonceAttr(nonce);
|
|
@@ -270,7 +291,8 @@ export function buildHtmlShellOpen(
|
|
|
270
291
|
`\n ${faviconLine}${twCssLink()}\n` +
|
|
271
292
|
componentCssLinks() +
|
|
272
293
|
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
273
|
-
` <link rel="modulepreload" href="${ENTRY}">`
|
|
294
|
+
` <link rel="modulepreload" href="${ENTRY}">` +
|
|
295
|
+
routePreloadLinks(pattern)
|
|
274
296
|
);
|
|
275
297
|
}
|
|
276
298
|
|
|
@@ -282,7 +304,8 @@ export function buildHtmlShellOpen(
|
|
|
282
304
|
` ${twCssLink()}\n` +
|
|
283
305
|
componentCssLinks() +
|
|
284
306
|
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
285
|
-
` <link rel="modulepreload" href="${ENTRY}">`
|
|
307
|
+
` <link rel="modulepreload" href="${ENTRY}">` +
|
|
308
|
+
routePreloadLinks(pattern)
|
|
286
309
|
);
|
|
287
310
|
}
|
|
288
311
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { toPosix } from "./paths.ts";
|
|
2
|
+
import type { PageRoute } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/** The slice of Bun's `BuildMetafile` this module reads. */
|
|
5
|
+
export interface PreloadMetafile {
|
|
6
|
+
outputs: Record<string, { entryPoint?: string; imports?: { path: string; kind: string }[] }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client chunks each page route needs before it can hydrate, keyed by route
|
|
11
|
+
* pattern, as paths relative to `dist/client` (same shape as `manifest.js`).
|
|
12
|
+
*
|
|
13
|
+
* Without this the browser only learns about a route's code after the entry has
|
|
14
|
+
* run and `import()`ed it, and only learns about that chunk's own imports after
|
|
15
|
+
* it arrives — a serial waterfall during which clicks hit dead buttons. Emitting
|
|
16
|
+
* these as modulepreload links lets them download alongside the entry.
|
|
17
|
+
*
|
|
18
|
+
* Route chunks are found by their metafile `entryPoint` (Bun makes every
|
|
19
|
+
* dynamically imported route file its own entry chunk, named relative to the
|
|
20
|
+
* app root), then followed through static imports only — a `dynamic-import`
|
|
21
|
+
* edge is code the page may never ask for. The client entry is left out: the
|
|
22
|
+
* shell already preloads it.
|
|
23
|
+
*/
|
|
24
|
+
export function buildPreloadMap(
|
|
25
|
+
metafile: PreloadMetafile,
|
|
26
|
+
pages: Pick<PageRoute, "pattern" | "page" | "layouts">[],
|
|
27
|
+
entry: string,
|
|
28
|
+
): Record<string, string[]> {
|
|
29
|
+
const clean = (p: string) => toPosix(p).replace(/^\.\//, "");
|
|
30
|
+
const outputs = new Map<string, { path: string; kind: string }[]>();
|
|
31
|
+
const chunkBySource = new Map<string, string>();
|
|
32
|
+
for (const [out, info] of Object.entries(metafile.outputs)) {
|
|
33
|
+
if (!out.endsWith(".js")) continue;
|
|
34
|
+
outputs.set(clean(out), info.imports ?? []);
|
|
35
|
+
if (info.entryPoint) chunkBySource.set(clean(info.entryPoint), clean(out));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const map: Record<string, string[]> = {};
|
|
39
|
+
for (const route of pages) {
|
|
40
|
+
const seen = new Set<string>([entry]);
|
|
41
|
+
const files: string[] = [];
|
|
42
|
+
const visit = (chunk: string) => {
|
|
43
|
+
if (seen.has(chunk)) return;
|
|
44
|
+
seen.add(chunk);
|
|
45
|
+
files.push(chunk);
|
|
46
|
+
for (const imp of outputs.get(chunk) ?? []) {
|
|
47
|
+
if (imp.kind === "import-statement") visit(clean(imp.path));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// Layouts first: they wrap the page, so hydration needs them just as much.
|
|
51
|
+
for (const src of [...route.layouts, route.page]) {
|
|
52
|
+
const chunk = chunkBySource.get(`src/routes/${toPosix(src)}`);
|
|
53
|
+
if (chunk) visit(chunk);
|
|
54
|
+
}
|
|
55
|
+
if (files.length) map[route.pattern] = files;
|
|
56
|
+
}
|
|
57
|
+
return map;
|
|
58
|
+
}
|
package/src/core/renderer.ts
CHANGED
|
@@ -761,7 +761,7 @@ export async function renderSSRStream(
|
|
|
761
761
|
);
|
|
762
762
|
}
|
|
763
763
|
const html =
|
|
764
|
-
buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments) +
|
|
764
|
+
buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments, route.pattern) +
|
|
765
765
|
buildMetadataChunk(metadata, headExtras, appHtmlSegments) +
|
|
766
766
|
buildHtmlTail(
|
|
767
767
|
"",
|
|
@@ -819,7 +819,14 @@ export async function renderSSRStream(
|
|
|
819
819
|
|
|
820
820
|
// Pre-compute all chunks; pull-based stream gives Bun native backpressure.
|
|
821
821
|
const chunks: Uint8Array[] = [
|
|
822
|
-
enc.encode(
|
|
822
|
+
enc.encode(
|
|
823
|
+
buildHtmlShellOpen(
|
|
824
|
+
metadata?.lang,
|
|
825
|
+
nonce,
|
|
826
|
+
appHtmlSegments,
|
|
827
|
+
data.csr ? route.pattern : undefined,
|
|
828
|
+
),
|
|
829
|
+
),
|
|
823
830
|
enc.encode(buildMetadataChunk(metadata, headExtras, appHtmlSegments)),
|
|
824
831
|
enc.encode(
|
|
825
832
|
buildHtmlTail(
|
|
@@ -1031,6 +1038,7 @@ export async function renderPageWithFormData(
|
|
|
1031
1038
|
bodyEndExtras,
|
|
1032
1039
|
appHtmlSegments,
|
|
1033
1040
|
metadata,
|
|
1041
|
+
route.pattern,
|
|
1034
1042
|
);
|
|
1035
1043
|
return compress(html, "text/html; charset=utf-8", req, status, data.loaderHeaders);
|
|
1036
1044
|
}
|
|
@@ -1065,6 +1073,7 @@ export async function renderPageWithFormData(
|
|
|
1065
1073
|
bodyEndExtras,
|
|
1066
1074
|
appHtmlSegments,
|
|
1067
1075
|
metadata,
|
|
1076
|
+
route.pattern,
|
|
1068
1077
|
);
|
|
1069
1078
|
return compress(html, "text/html; charset=utf-8", req, status, data.loaderHeaders);
|
|
1070
1079
|
}
|