bosia 1.1.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.4"
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
- clientEntry ??
354
- jsFiles.find((f) => f === "hydrate.js") ??
355
- jsFiles.find((f) => f.startsWith("hydrate")) ??
356
- "hydrate.js",
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,
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
+ }
@@ -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(buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments)),
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
  }