bosia 1.1.4 → 1.1.5
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/ambient.d.ts +3 -0
- package/src/cli/add.ts +15 -12
- package/src/cli/block.ts +18 -16
- package/src/cli/feat.ts +31 -11
- package/src/cli/page.ts +18 -16
- package/src/cli/registry.ts +117 -20
- package/src/cli/theme.ts +7 -2
- package/src/core/backend.ts +8 -1
- package/src/core/build.ts +7 -2
- package/src/core/cache.ts +28 -6
- package/src/core/client/App.svelte +15 -26
- package/src/core/client/page.svelte.ts +15 -0
- package/src/core/client/prefetch.ts +55 -60
- package/src/core/dev.ts +11 -1
- package/src/core/etag.ts +13 -0
- package/src/core/hooks.ts +17 -0
- package/src/core/html.ts +109 -14
- package/src/core/plugins/inspector/bun-plugin.ts +31 -18
- package/src/core/renderer.ts +99 -76
- package/src/core/routeFile.ts +36 -20
- package/src/core/scanner.ts +2 -0
- package/src/core/server.ts +294 -94
- package/src/core/staticManifest.ts +60 -15
- package/src/core/svelteCompiler.ts +96 -27
- package/src/core/types.ts +4 -0
- package/templates/shop/.env.example +3 -0
- package/templates/shop/src/hooks.server.ts +12 -2
- package/templates/shop/src/routes/(private)/+layout.server.ts +2 -6
- package/templates/store/.env.example +3 -0
- package/templates/store/src/hooks.server.ts +12 -2
- package/templates/store/src/routes/(private)/+layout.server.ts +2 -6
package/src/core/renderer.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
coalesceMiss,
|
|
16
16
|
collectTags,
|
|
17
17
|
computeCacheKey,
|
|
18
|
-
concatChunks,
|
|
19
18
|
deferCacheWrite,
|
|
20
19
|
serveCached,
|
|
21
20
|
} from "./cache.ts";
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
compressBytes,
|
|
36
35
|
encodeForRequest,
|
|
37
36
|
isDev,
|
|
37
|
+
withPreloadLink,
|
|
38
38
|
type Encoded,
|
|
39
39
|
} from "./html.ts";
|
|
40
40
|
import type { Metadata } from "./hooks.ts";
|
|
@@ -358,20 +358,17 @@ function makeDepends(deps: LoaderDeps): (...keys: string[]) => void {
|
|
|
358
358
|
// Runs layout + page server loaders for a given URL.
|
|
359
359
|
// Used by both SSR and the /__bosia/data JSON endpoint.
|
|
360
360
|
//
|
|
361
|
-
// `mask` controls
|
|
362
|
-
// - undefined →
|
|
363
|
-
// - layouts[i] ===
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
// data* contributed by previously-cached layers. The client already holds
|
|
367
|
-
// each skipped layer's data in its loader cache and forwards it as
|
|
368
|
-
// `parentSnapshots` (depth → data) in the request body, so downstream
|
|
369
|
-
// loaders that DO re-run see real parent() data, not `{}`. The response
|
|
370
|
-
// slot stays `null` (client renders that layer from its cache).
|
|
361
|
+
// `mask` controls which layers' data the client data endpoint sends back:
|
|
362
|
+
// - undefined → send everything (SSR, first nav)
|
|
363
|
+
// - layouts[i] === false → the layout still RUNS, but its slot is emitted as
|
|
364
|
+
// null (the client renders that layer from its loader cache)
|
|
365
|
+
// - page === false → page loader is skipped, emit null
|
|
371
366
|
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
367
|
+
// Layout loaders are never skipped on the server. They are where apps gate a
|
|
368
|
+
// route group (`if (!locals.user) throw redirect(...)`), and the mask comes from
|
|
369
|
+
// the client — honouring it would let any caller switch a guard off with
|
|
370
|
+
// `?_invalidated=…`. Their output also feeds `parent()` for the loaders below,
|
|
371
|
+
// so it has to be the server's own, never data the client sends.
|
|
375
372
|
|
|
376
373
|
export type LoaderMask = {
|
|
377
374
|
page: boolean;
|
|
@@ -383,10 +380,9 @@ export async function loadRouteData(
|
|
|
383
380
|
locals: Record<string, any>,
|
|
384
381
|
req: Request,
|
|
385
382
|
cookies: Cookies,
|
|
386
|
-
metadataData: Record<string, any> | null = null,
|
|
383
|
+
metadataData: Record<string, any> | null | Promise<Record<string, any> | null> = null,
|
|
387
384
|
match?: RouteMatch<(typeof serverRoutes)[number]> | null,
|
|
388
385
|
mask?: LoaderMask,
|
|
389
|
-
parentSnapshots?: Record<number, Record<string, any>>,
|
|
390
386
|
) {
|
|
391
387
|
match ??= findMatch(serverRoutes, url.pathname);
|
|
392
388
|
if (!match) return null;
|
|
@@ -404,18 +400,9 @@ export async function loadRouteData(
|
|
|
404
400
|
|
|
405
401
|
// Run layout server loaders root → leaf, each gets parent() data
|
|
406
402
|
for (const ls of route.layoutServers) {
|
|
407
|
-
|
|
403
|
+
// Runs regardless of the mask — see the trust note above loadRouteData.
|
|
404
|
+
const omit = mask && mask.layouts[ls.depth] === false;
|
|
408
405
|
try {
|
|
409
|
-
if (skip) {
|
|
410
|
-
layoutData[ls.depth] = null;
|
|
411
|
-
layoutDeps[ls.depth] = null;
|
|
412
|
-
// Skipped layers contribute their client-cached data (forwarded as
|
|
413
|
-
// parentSnapshots) to the parent chain, so downstream loaders that DO
|
|
414
|
-
// re-run see real parent() data. Falls back to {} when no snapshot was
|
|
415
|
-
// sent. Perf hint only — never authoritative for authz (use locals).
|
|
416
|
-
parentData = { ...parentData, ...(parentSnapshots?.[ls.depth] ?? {}) };
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
406
|
const mod = await ls.loader();
|
|
420
407
|
if (typeof mod.load === "function" && !warmingUp) {
|
|
421
408
|
// Snapshot per layer so loaders cannot mutate the shared accumulator,
|
|
@@ -427,6 +414,7 @@ export async function loadRouteData(
|
|
|
427
414
|
(await withTimeout(
|
|
428
415
|
mod.load({
|
|
429
416
|
params: trackedParams(params, deps),
|
|
417
|
+
route: { id: route.id },
|
|
430
418
|
url: trackedUrl(url, deps),
|
|
431
419
|
locals,
|
|
432
420
|
cookies: trackedCookies(cookies, deps),
|
|
@@ -440,12 +428,12 @@ export async function loadRouteData(
|
|
|
440
428
|
LOAD_TIMEOUT,
|
|
441
429
|
`layout load (depth=${ls.depth}, ${url.pathname})`,
|
|
442
430
|
)) ?? {};
|
|
443
|
-
layoutData[ls.depth] = result;
|
|
444
|
-
layoutDeps[ls.depth] = deps;
|
|
431
|
+
layoutData[ls.depth] = omit ? null : result;
|
|
432
|
+
layoutDeps[ls.depth] = omit ? null : deps;
|
|
445
433
|
parentData = { ...parentData, ...result };
|
|
446
434
|
} else {
|
|
447
|
-
layoutData[ls.depth] = {};
|
|
448
|
-
layoutDeps[ls.depth] = emptyDeps();
|
|
435
|
+
layoutData[ls.depth] = omit ? null : {};
|
|
436
|
+
layoutDeps[ls.depth] = omit ? null : emptyDeps();
|
|
449
437
|
}
|
|
450
438
|
} catch (err) {
|
|
451
439
|
if (isRedirect(err)) throw err;
|
|
@@ -494,12 +482,13 @@ export async function loadRouteData(
|
|
|
494
482
|
(await withTimeout(
|
|
495
483
|
mod.load({
|
|
496
484
|
params: trackedParams(params, deps),
|
|
485
|
+
route: { id: route.id },
|
|
497
486
|
url: trackedUrl(url, deps),
|
|
498
487
|
locals,
|
|
499
488
|
cookies: trackedCookies(cookies, deps),
|
|
500
489
|
parent,
|
|
501
490
|
fetch: trackedFetch(fetch, origin, deps),
|
|
502
|
-
metadata: metadataData,
|
|
491
|
+
metadata: await metadataData,
|
|
503
492
|
depends: makeDepends(deps),
|
|
504
493
|
platform: getPlatform(),
|
|
505
494
|
setHeaders,
|
|
@@ -564,7 +553,15 @@ export async function loadMetadata(
|
|
|
564
553
|
const fetch = makeFetch(req, url);
|
|
565
554
|
return (
|
|
566
555
|
(await withTimeout(
|
|
567
|
-
mod.metadata({
|
|
556
|
+
mod.metadata({
|
|
557
|
+
params,
|
|
558
|
+
route: { id: route.id },
|
|
559
|
+
url,
|
|
560
|
+
locals,
|
|
561
|
+
cookies,
|
|
562
|
+
fetch,
|
|
563
|
+
platform: getPlatform(),
|
|
564
|
+
}),
|
|
568
565
|
METADATA_TIMEOUT,
|
|
569
566
|
`metadata (${url.pathname})`,
|
|
570
567
|
)) ?? null
|
|
@@ -646,12 +643,53 @@ export async function renderSSRStream(
|
|
|
646
643
|
// below must stay inside this try; the cache-write path hands the release
|
|
647
644
|
// off to its deferred cacheSet by nulling releaseMiss first.
|
|
648
645
|
try {
|
|
649
|
-
// ── Pre-stream phase:
|
|
650
|
-
//
|
|
646
|
+
// ── Pre-stream phase: metadata(), loaders, module imports and plugin
|
|
647
|
+
// fragments all start together, and all settle before committing to a 200,
|
|
648
|
+
// so a Redirect/HttpError from any of them still gets a proper response.
|
|
649
|
+
// Layout loaders still run in order before the page load(), which also
|
|
650
|
+
// waits for metadata.data (see loadRouteData).
|
|
651
|
+
const metadataP = loadMetadata(route, params, url, locals, cookies, req).then(
|
|
652
|
+
(value) => ({ ok: true as const, value }),
|
|
653
|
+
(err: unknown) => ({ ok: false as const, err }),
|
|
654
|
+
);
|
|
655
|
+
const dataP = Promise.all([
|
|
656
|
+
loadRouteData(
|
|
657
|
+
url,
|
|
658
|
+
locals,
|
|
659
|
+
req,
|
|
660
|
+
cookies,
|
|
661
|
+
// A failed metadata() hands load() null, as before — and when the
|
|
662
|
+
// failure ends the request, the result below is never read.
|
|
663
|
+
metadataP.then((m) => (m.ok ? (m.value?.data ?? null) : null)),
|
|
664
|
+
match,
|
|
665
|
+
),
|
|
666
|
+
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
667
|
+
// pageMod is already loaded when the cache flag was unknown.
|
|
668
|
+
pageMod ?? route.pageModule(),
|
|
669
|
+
]);
|
|
670
|
+
// Settled by the early returns below without being read.
|
|
671
|
+
dataP.catch(() => {});
|
|
672
|
+
const fragmentsP = metadataP.then((m) => {
|
|
673
|
+
const renderCtx: RenderContext = {
|
|
674
|
+
request: req,
|
|
675
|
+
url,
|
|
676
|
+
route: { pattern: route.pattern },
|
|
677
|
+
metadata: m.ok ? m.value : null,
|
|
678
|
+
};
|
|
679
|
+
return Promise.all([
|
|
680
|
+
pluginRenderFragments("head", renderCtx),
|
|
681
|
+
pluginRenderFragments("bodyEnd", renderCtx),
|
|
682
|
+
]);
|
|
683
|
+
});
|
|
684
|
+
fragmentsP.catch(() => {});
|
|
685
|
+
|
|
686
|
+
// Metadata errors return a proper error response with correct status code.
|
|
651
687
|
let metadata: Metadata | null = null;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
688
|
+
const meta = await metadataP;
|
|
689
|
+
if (meta.ok) {
|
|
690
|
+
metadata = meta.value;
|
|
691
|
+
} else {
|
|
692
|
+
const err = meta.err;
|
|
655
693
|
if (isRedirect(err)) {
|
|
656
694
|
// Not Response.redirect(): it rejects relative URLs on Workers.
|
|
657
695
|
return new Response(null, { status: err.status, headers: { Location: err.location } });
|
|
@@ -675,21 +713,12 @@ export async function renderSSRStream(
|
|
|
675
713
|
// Continue with null metadata — don't break the page for a metadata failure
|
|
676
714
|
}
|
|
677
715
|
|
|
678
|
-
// ── Pre-stream phase: run load() + module imports in parallel before committing to a 200 ──
|
|
679
|
-
// This ensures HttpError/Redirect from load() can return a proper response before any bytes are sent.
|
|
680
|
-
const metadataData = metadata?.data ?? null;
|
|
681
716
|
let data: Awaited<ReturnType<typeof loadRouteData>>;
|
|
682
717
|
let layoutMods: any[];
|
|
683
718
|
|
|
684
719
|
try {
|
|
685
|
-
// pageMod is already loaded when the cache flag was unknown; otherwise
|
|
686
|
-
// fold its import into this parallel block instead of a serial await.
|
|
687
720
|
let pm: any;
|
|
688
|
-
[data, layoutMods, pm] = await
|
|
689
|
-
loadRouteData(url, locals, req, cookies, metadataData, match),
|
|
690
|
-
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
691
|
-
pageMod ?? route.pageModule(),
|
|
692
|
-
]);
|
|
721
|
+
[data, layoutMods, pm] = await dataP;
|
|
693
722
|
pageMod = pm;
|
|
694
723
|
} catch (err) {
|
|
695
724
|
if (isRedirect(err))
|
|
@@ -741,16 +770,7 @@ export async function renderSSRStream(
|
|
|
741
770
|
nonce,
|
|
742
771
|
);
|
|
743
772
|
|
|
744
|
-
const
|
|
745
|
-
request: req,
|
|
746
|
-
url,
|
|
747
|
-
route: { pattern: route.pattern },
|
|
748
|
-
metadata,
|
|
749
|
-
};
|
|
750
|
-
const [headExtras, bodyEndExtras] = await Promise.all([
|
|
751
|
-
pluginRenderFragments("head", renderCtx),
|
|
752
|
-
pluginRenderFragments("bodyEnd", renderCtx),
|
|
753
|
-
]);
|
|
773
|
+
const [headExtras, bodyEndExtras] = await fragmentsP;
|
|
754
774
|
|
|
755
775
|
// SSR always runs every loader, so coerce types from the optional sparse shape.
|
|
756
776
|
const layoutDataFull = (data.layoutData as Record<string, any>[]).map((d) => d ?? {});
|
|
@@ -781,7 +801,13 @@ export async function renderSSRStream(
|
|
|
781
801
|
data.layoutDeps,
|
|
782
802
|
appHtmlSegments,
|
|
783
803
|
);
|
|
784
|
-
return compress(
|
|
804
|
+
return compress(
|
|
805
|
+
html,
|
|
806
|
+
"text/html; charset=utf-8",
|
|
807
|
+
req,
|
|
808
|
+
200,
|
|
809
|
+
withPreloadLink(data.loaderHeaders, route.pattern, true),
|
|
810
|
+
);
|
|
785
811
|
}
|
|
786
812
|
|
|
787
813
|
// Render-first: run render() before committing to a 200. Failure → proper error page
|
|
@@ -823,17 +849,15 @@ export async function renderSSRStream(
|
|
|
823
849
|
// compressed — a stream here never streamed, it only skipped compression.
|
|
824
850
|
// Truly progressive SSR (ROADMAP) would pipe a stream through
|
|
825
851
|
// CompressionStream instead.
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
enc.encode(buildMetadataChunk(metadata, headExtras, appHtmlSegments)),
|
|
836
|
-
enc.encode(
|
|
852
|
+
// One string, encoded once: three encodes plus a concat copied every byte twice.
|
|
853
|
+
const fullBody = enc.encode(
|
|
854
|
+
buildHtmlShellOpen(
|
|
855
|
+
metadata?.lang,
|
|
856
|
+
nonce,
|
|
857
|
+
appHtmlSegments,
|
|
858
|
+
data.csr ? route.pattern : undefined,
|
|
859
|
+
) +
|
|
860
|
+
buildMetadataChunk(metadata, headExtras, appHtmlSegments) +
|
|
837
861
|
buildHtmlTail(
|
|
838
862
|
body,
|
|
839
863
|
head,
|
|
@@ -848,10 +872,9 @@ export async function renderSSRStream(
|
|
|
848
872
|
data.layoutDeps,
|
|
849
873
|
appHtmlSegments,
|
|
850
874
|
),
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const fullBody = concatChunks(chunks);
|
|
875
|
+
) as Uint8Array<ArrayBuffer>;
|
|
876
|
+
// Stored with the cache entry too, so hits carry the same Link header.
|
|
877
|
+
const headers = withPreloadLink(data.loaderHeaders, route.pattern, data.csr) ?? {};
|
|
855
878
|
// Set only on a cache write: the client's variant, encoded once at cache
|
|
856
879
|
// quality and shared by the response and the cache entry.
|
|
857
880
|
let sent: Encoded | null | undefined;
|
|
@@ -890,7 +913,7 @@ export async function renderSSRStream(
|
|
|
890
913
|
brotli,
|
|
891
914
|
contentType: "text/html; charset=utf-8",
|
|
892
915
|
status: 200,
|
|
893
|
-
extraHeaders:
|
|
916
|
+
extraHeaders: headers,
|
|
894
917
|
tags,
|
|
895
918
|
},
|
|
896
919
|
cookies,
|
|
@@ -902,7 +925,7 @@ export async function renderSSRStream(
|
|
|
902
925
|
}
|
|
903
926
|
}
|
|
904
927
|
|
|
905
|
-
return compressBytes(fullBody, "text/html; charset=utf-8", req, 200,
|
|
928
|
+
return compressBytes(fullBody, "text/html; charset=utf-8", req, 200, headers, sent);
|
|
906
929
|
} finally {
|
|
907
930
|
releaseMiss?.();
|
|
908
931
|
}
|
package/src/core/routeFile.ts
CHANGED
|
@@ -48,7 +48,7 @@ function sortRoutes<T extends { pattern: string }>(routes: T[]): T[] {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function generateRoutesFile(manifest: RouteManifest): void {
|
|
51
|
-
const lines: string[] = ["// AUTO-GENERATED by bosia build — do not edit\n"];
|
|
51
|
+
const lines: string[] = ["// AUTO-GENERATED by bosia build — do not edit\n", ONCE_HELPER];
|
|
52
52
|
|
|
53
53
|
const pages = sortRoutes(manifest.pages);
|
|
54
54
|
const apis = sortRoutes(manifest.apis);
|
|
@@ -56,6 +56,7 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
56
56
|
// clientRoutes
|
|
57
57
|
lines.push("export const clientRoutes: Array<{");
|
|
58
58
|
lines.push(" pattern: string;");
|
|
59
|
+
lines.push(" id: string;");
|
|
59
60
|
lines.push(" page: () => Promise<any>;");
|
|
60
61
|
lines.push(" layouts: (() => Promise<any>)[];");
|
|
61
62
|
lines.push(" errorPages: { loader: () => Promise<any>; depth: number }[];");
|
|
@@ -92,6 +93,7 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
92
93
|
.join(", ");
|
|
93
94
|
lines.push(" {");
|
|
94
95
|
lines.push(` pattern: ${JSON.stringify(clientPattern(r.pattern))},`);
|
|
96
|
+
lines.push(` id: ${JSON.stringify(r.id)},`);
|
|
95
97
|
lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
|
|
96
98
|
lines.push(` layouts: [${layoutImports}],`);
|
|
97
99
|
lines.push(` errorPages: [${errorPageImports}],`);
|
|
@@ -111,6 +113,7 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
111
113
|
// serverRoutes
|
|
112
114
|
lines.push("export const serverRoutes: Array<{");
|
|
113
115
|
lines.push(" pattern: string;");
|
|
116
|
+
lines.push(" id: string;");
|
|
114
117
|
lines.push(" pageModule: () => Promise<any>;");
|
|
115
118
|
lines.push(" layoutModules: (() => Promise<any>)[];");
|
|
116
119
|
lines.push(" pageServer: (() => Promise<any>) | null;");
|
|
@@ -120,28 +123,19 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
120
123
|
lines.push(" cache: boolean | null;");
|
|
121
124
|
lines.push("}> = [");
|
|
122
125
|
for (const r of pages) {
|
|
123
|
-
const layoutImports = r.layouts
|
|
124
|
-
.map((l) => `() => import(${JSON.stringify(toImportPath(l))})`)
|
|
125
|
-
.join(", ");
|
|
126
|
+
const layoutImports = r.layouts.map((l) => serverImport(l)).join(", ");
|
|
126
127
|
const layoutServerImports = r.layoutServers
|
|
127
|
-
.map(
|
|
128
|
-
(ls) =>
|
|
129
|
-
`{ loader: () => import(${JSON.stringify(toImportPath(ls.path))}), depth: ${ls.depth} }`,
|
|
130
|
-
)
|
|
128
|
+
.map((ls) => `{ loader: ${serverImport(ls.path)}, depth: ${ls.depth} }`)
|
|
131
129
|
.join(", ");
|
|
132
130
|
const errorPageImports = r.errorPages
|
|
133
|
-
.map(
|
|
134
|
-
(ep) =>
|
|
135
|
-
`{ loader: () => import(${JSON.stringify(toImportPath(ep.path))}), depth: ${ep.depth} }`,
|
|
136
|
-
)
|
|
131
|
+
.map((ep) => `{ loader: ${serverImport(ep.path)}, depth: ${ep.depth} }`)
|
|
137
132
|
.join(", ");
|
|
138
133
|
lines.push(" {");
|
|
139
134
|
lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
|
|
140
|
-
lines.push(`
|
|
135
|
+
lines.push(` id: ${JSON.stringify(r.id)},`);
|
|
136
|
+
lines.push(` pageModule: ${serverImport(r.page)},`);
|
|
141
137
|
lines.push(` layoutModules: [${layoutImports}],`);
|
|
142
|
-
lines.push(
|
|
143
|
-
` pageServer: ${r.pageServer ? `() => import(${JSON.stringify(toImportPath(r.pageServer))})` : "null"},`,
|
|
144
|
-
);
|
|
138
|
+
lines.push(` pageServer: ${r.pageServer ? serverImport(r.pageServer) : "null"},`);
|
|
145
139
|
lines.push(` layoutServers: [${layoutServerImports}],`);
|
|
146
140
|
lines.push(` errorPages: [${errorPageImports}],`);
|
|
147
141
|
lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
|
|
@@ -153,12 +147,14 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
153
147
|
// apiRoutes
|
|
154
148
|
lines.push("export const apiRoutes: Array<{");
|
|
155
149
|
lines.push(" pattern: string;");
|
|
150
|
+
lines.push(" id: string;");
|
|
156
151
|
lines.push(" module: () => Promise<any>;");
|
|
157
152
|
lines.push("}> = [");
|
|
158
153
|
for (const r of apis) {
|
|
159
154
|
lines.push(" {");
|
|
160
155
|
lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
|
|
161
|
-
lines.push(`
|
|
156
|
+
lines.push(` id: ${JSON.stringify(r.id)},`);
|
|
157
|
+
lines.push(` module: ${serverImport(r.server)},`);
|
|
162
158
|
lines.push(" },");
|
|
163
159
|
}
|
|
164
160
|
lines.push("];\n");
|
|
@@ -166,9 +162,7 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
166
162
|
// errorPage
|
|
167
163
|
const ep = manifest.errorPage;
|
|
168
164
|
lines.push(
|
|
169
|
-
`export const errorPage: (() => Promise<any>) | null = ${
|
|
170
|
-
ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
|
|
171
|
-
};\n`,
|
|
165
|
+
`export const errorPage: (() => Promise<any>) | null = ${ep ? serverImport(ep) : "null"};\n`,
|
|
172
166
|
);
|
|
173
167
|
|
|
174
168
|
mkdirSync(".bosia", { recursive: true });
|
|
@@ -197,6 +191,7 @@ function generateClientRoutesFile(
|
|
|
197
191
|
|
|
198
192
|
lines.push("export const clientRoutes: Array<{");
|
|
199
193
|
lines.push(" pattern: string;");
|
|
194
|
+
lines.push(" id: string;");
|
|
200
195
|
lines.push(" page: () => Promise<any>;");
|
|
201
196
|
lines.push(" layouts: (() => Promise<any>)[];");
|
|
202
197
|
lines.push(" errorPages: { loader: () => Promise<any>; depth: number }[];");
|
|
@@ -227,6 +222,7 @@ function generateClientRoutesFile(
|
|
|
227
222
|
.join(", ");
|
|
228
223
|
lines.push(" {");
|
|
229
224
|
lines.push(` pattern: ${JSON.stringify(clientPattern(r.pattern))},`);
|
|
225
|
+
lines.push(` id: ${JSON.stringify(r.id)},`);
|
|
230
226
|
lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
|
|
231
227
|
lines.push(` layouts: [${layoutImports}],`);
|
|
232
228
|
lines.push(` errorPages: [${errorPageImports}],`);
|
|
@@ -253,6 +249,26 @@ function generateClientRoutesFile(
|
|
|
253
249
|
writeFileSync(".bosia/routes.client.ts", lines.join("\n"));
|
|
254
250
|
}
|
|
255
251
|
|
|
252
|
+
// The renderer calls these thunks several times per request (metadata, load,
|
|
253
|
+
// render), and a repeated `import()` still resolves the specifier each time even
|
|
254
|
+
// though the module is cached — ~8% of CPU on an uncached SSR request. Each thunk
|
|
255
|
+
// keeps its first promise instead. A rejected import is dropped, so the next
|
|
256
|
+
// request retries it rather than replaying the failure forever.
|
|
257
|
+
const ONCE_HELPER = `function once<T>(load: () => Promise<T>): () => Promise<T> {
|
|
258
|
+
let p: Promise<T> | undefined;
|
|
259
|
+
return () =>
|
|
260
|
+
(p ??= load().catch((err) => {
|
|
261
|
+
p = undefined;
|
|
262
|
+
throw err;
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
/** A memoized server-side import thunk for `src/routes/<routePath>`. */
|
|
268
|
+
function serverImport(routePath: string): string {
|
|
269
|
+
return `once(() => import(${JSON.stringify(toImportPath(routePath))}))`;
|
|
270
|
+
}
|
|
271
|
+
|
|
256
272
|
// Import path from .bosia/routes.ts to src/routes/<routePath>
|
|
257
273
|
function toImportPath(routePath: string): string {
|
|
258
274
|
return "../src/routes/" + toPosix(routePath);
|
package/src/core/scanner.ts
CHANGED
|
@@ -142,6 +142,7 @@ export function scanRoutes(): RouteManifest {
|
|
|
142
142
|
if (items.some((i) => i.isFile() && i.name === "+server.ts")) {
|
|
143
143
|
apis.push({
|
|
144
144
|
pattern: toUrlPath(urlSegments),
|
|
145
|
+
id: `/${dir}`,
|
|
145
146
|
server: rel("+server.ts"),
|
|
146
147
|
});
|
|
147
148
|
}
|
|
@@ -158,6 +159,7 @@ export function scanRoutes(): RouteManifest {
|
|
|
158
159
|
const pageFile = rel("+page.svelte");
|
|
159
160
|
pages.push({
|
|
160
161
|
pattern: toUrlPath(urlSegments),
|
|
162
|
+
id: `/${dir}`,
|
|
161
163
|
page: pageFile,
|
|
162
164
|
layouts: [...currentLayouts],
|
|
163
165
|
pageServer: pageServerFile,
|