@pracht/core 0.2.2 → 0.2.3
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/dist/index.d.mts +2 -0
- package/dist/index.mjs +97 -42
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -365,6 +365,8 @@ interface PrerenderAppOptions {
|
|
|
365
365
|
clientEntryUrl?: string;
|
|
366
366
|
/** Per-source-file CSS map produced by the vite plugin (preferred over cssUrls). */
|
|
367
367
|
cssManifest?: Record<string, string[]>;
|
|
368
|
+
/** Per-source-file JS map produced by the vite plugin for modulepreload hints. */
|
|
369
|
+
jsManifest?: Record<string, string[]>;
|
|
368
370
|
/** @deprecated Pass cssManifest instead for per-page CSS resolution. */
|
|
369
371
|
cssUrls?: string[];
|
|
370
372
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -80,6 +80,12 @@ const SAFE_METHODS = new Set(["GET", "HEAD"]);
|
|
|
80
80
|
const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
81
81
|
const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
|
|
82
82
|
const ROUTE_STATE_CACHE_CONTROL = "no-store";
|
|
83
|
+
let _renderToStringAsync;
|
|
84
|
+
async function getRenderToStringAsync() {
|
|
85
|
+
if (_renderToStringAsync) return _renderToStringAsync;
|
|
86
|
+
_renderToStringAsync = (await import("preact-render-to-string")).renderToStringAsync;
|
|
87
|
+
return _renderToStringAsync;
|
|
88
|
+
}
|
|
83
89
|
const RouteDataContext = createContext(void 0);
|
|
84
90
|
function PrachtRuntimeProvider({ children, data, params = {}, routeId, url }) {
|
|
85
91
|
const [routeData, setRouteData] = useState(data);
|
|
@@ -332,11 +338,10 @@ async function handlePrachtRequest(options) {
|
|
|
332
338
|
if (match.route.render === "spa") {
|
|
333
339
|
let body = "";
|
|
334
340
|
if (shellModule?.Shell || shellModule?.Loading) {
|
|
335
|
-
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
336
341
|
const Shell = shellModule?.Shell;
|
|
337
342
|
const Loading = shellModule?.Loading;
|
|
338
343
|
const loadingTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
339
|
-
if (loadingTree) body = await
|
|
344
|
+
if (loadingTree) body = await (await getRenderToStringAsync())(loadingTree);
|
|
340
345
|
}
|
|
341
346
|
return htmlResponse(buildHtmlDocument({
|
|
342
347
|
head,
|
|
@@ -356,21 +361,21 @@ async function handlePrachtRequest(options) {
|
|
|
356
361
|
const DefaultComponent = typeof routeModule.default === "function" ? routeModule.default : void 0;
|
|
357
362
|
const Component = routeModule.Component ?? DefaultComponent;
|
|
358
363
|
if (!Component) throw new Error("Route has no Component or default export");
|
|
359
|
-
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
360
364
|
const Shell = shellModule?.Shell;
|
|
361
365
|
const componentProps = {
|
|
362
366
|
data,
|
|
363
367
|
params: match.params
|
|
364
368
|
};
|
|
365
369
|
const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
|
|
370
|
+
const tree = h(PrachtRuntimeProvider, {
|
|
371
|
+
data,
|
|
372
|
+
params: match.params,
|
|
373
|
+
routeId: match.route.id ?? "",
|
|
374
|
+
url: url.pathname
|
|
375
|
+
}, componentTree);
|
|
366
376
|
return htmlResponse(buildHtmlDocument({
|
|
367
377
|
head,
|
|
368
|
-
body: await
|
|
369
|
-
data,
|
|
370
|
-
params: match.params,
|
|
371
|
-
routeId: match.route.id ?? "",
|
|
372
|
-
url: url.pathname
|
|
373
|
-
}, componentTree)),
|
|
378
|
+
body: await (await getRenderToStringAsync())(tree),
|
|
374
379
|
hydrationState: {
|
|
375
380
|
url: url.pathname,
|
|
376
381
|
routeId: match.route.id ?? "",
|
|
@@ -397,15 +402,42 @@ async function handlePrachtRequest(options) {
|
|
|
397
402
|
});
|
|
398
403
|
}
|
|
399
404
|
}
|
|
405
|
+
/** Strip leading `./` and `/` so all module paths share one canonical form. */
|
|
406
|
+
function normalizeModulePath(path) {
|
|
407
|
+
return path.replace(/^\.?\//, "");
|
|
408
|
+
}
|
|
409
|
+
function buildSuffixIndex(manifest) {
|
|
410
|
+
const index = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const key of Object.keys(manifest)) {
|
|
412
|
+
const normalized = normalizeModulePath(key);
|
|
413
|
+
if (!normalized) continue;
|
|
414
|
+
if (!index.has(normalized)) index.set(normalized, key);
|
|
415
|
+
for (let i = normalized.indexOf("/"); i !== -1; i = normalized.indexOf("/", i + 1)) {
|
|
416
|
+
const suffix = normalized.slice(i + 1);
|
|
417
|
+
if (suffix && !index.has(suffix)) index.set(suffix, key);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return index;
|
|
421
|
+
}
|
|
422
|
+
const suffixIndexCache = /* @__PURE__ */ new WeakMap();
|
|
423
|
+
function getSuffixIndex(manifest) {
|
|
424
|
+
let index = suffixIndexCache.get(manifest);
|
|
425
|
+
if (index) return index;
|
|
426
|
+
index = buildSuffixIndex(manifest);
|
|
427
|
+
suffixIndexCache.set(manifest, index);
|
|
428
|
+
return index;
|
|
429
|
+
}
|
|
430
|
+
function resolveManifestEntries(manifest, file) {
|
|
431
|
+
if (file in manifest) return manifest[file];
|
|
432
|
+
const resolved = getSuffixIndex(manifest).get(normalizeModulePath(file));
|
|
433
|
+
if (resolved) return manifest[resolved];
|
|
434
|
+
}
|
|
400
435
|
function resolvePageCssUrls(options, shellFile, routeFile) {
|
|
401
436
|
if (!options.cssManifest) return options.cssUrls ?? [];
|
|
402
437
|
const css = /* @__PURE__ */ new Set();
|
|
403
438
|
function addFromManifest(file) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
for (const c of cssFiles) css.add(c);
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
439
|
+
const entries = resolveManifestEntries(options.cssManifest, file);
|
|
440
|
+
if (entries) for (const c of entries) css.add(c);
|
|
409
441
|
}
|
|
410
442
|
if (shellFile) addFromManifest(shellFile);
|
|
411
443
|
addFromManifest(routeFile);
|
|
@@ -415,11 +447,8 @@ function resolvePageJsUrls(options, shellFile, routeFile) {
|
|
|
415
447
|
if (!options.jsManifest) return [];
|
|
416
448
|
const js = /* @__PURE__ */ new Set();
|
|
417
449
|
function addFromManifest(file) {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
for (const j of jsFiles) js.add(j);
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
450
|
+
const entries = resolveManifestEntries(options.jsManifest, file);
|
|
451
|
+
if (entries) for (const j of entries) js.add(j);
|
|
423
452
|
}
|
|
424
453
|
if (shellFile) addFromManifest(shellFile);
|
|
425
454
|
addFromManifest(routeFile);
|
|
@@ -517,11 +546,11 @@ function deserializeRouteError$1(error) {
|
|
|
517
546
|
return result;
|
|
518
547
|
}
|
|
519
548
|
function jsonErrorResponse(routeError, options) {
|
|
520
|
-
const
|
|
549
|
+
const headers = applySecurityAndRouteHeaders(new Headers({ "content-type": "application/json; charset=utf-8" }), options.isRouteStateRequest ? { isRouteStateRequest: true } : void 0);
|
|
550
|
+
return new Response(JSON.stringify({ error: routeError }), {
|
|
521
551
|
status: routeError.status,
|
|
522
|
-
headers
|
|
552
|
+
headers
|
|
523
553
|
});
|
|
524
|
-
return options.isRouteStateRequest ? withRouteResponseHeaders(response, { isRouteStateRequest: true }) : withDefaultSecurityHeaders(response);
|
|
525
554
|
}
|
|
526
555
|
function renderApiErrorResponse(options) {
|
|
527
556
|
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
@@ -569,14 +598,14 @@ async function renderRouteErrorResponse(options) {
|
|
|
569
598
|
const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
|
|
570
599
|
const cssUrls = resolvePageCssUrls(options.options, options.shellFile, options.routeArgs.route.file);
|
|
571
600
|
const modulePreloadUrls = resolvePageJsUrls(options.options, options.shellFile, options.routeArgs.route.file);
|
|
572
|
-
const
|
|
601
|
+
const renderToString = await getRenderToStringAsync();
|
|
573
602
|
const ErrorBoundary = options.routeModule.ErrorBoundary;
|
|
574
603
|
const Shell = shellModule?.Shell;
|
|
575
604
|
const errorValue = deserializeRouteError$1(routeErrorWithDiagnostics);
|
|
576
605
|
const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
|
|
577
606
|
return htmlResponse(buildHtmlDocument({
|
|
578
607
|
head,
|
|
579
|
-
body: await
|
|
608
|
+
body: await renderToString(h(PrachtRuntimeProvider, {
|
|
580
609
|
data: null,
|
|
581
610
|
routeId: options.routeId,
|
|
582
611
|
url: options.urlPathname
|
|
@@ -636,8 +665,8 @@ async function resolveDataFunctions(route, routeModule, registry) {
|
|
|
636
665
|
async function resolveRegistryModule(modules, file) {
|
|
637
666
|
if (!modules) return void 0;
|
|
638
667
|
if (file in modules) return modules[file]();
|
|
639
|
-
const
|
|
640
|
-
|
|
668
|
+
const resolved = getSuffixIndex(modules).get(normalizeModulePath(file));
|
|
669
|
+
if (resolved) return modules[resolved]();
|
|
641
670
|
}
|
|
642
671
|
async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
|
|
643
672
|
const shellHead = shellModule?.head ? await shellModule.head(routeArgs) : {};
|
|
@@ -679,10 +708,11 @@ function buildHtmlDocument(options) {
|
|
|
679
708
|
</html>`;
|
|
680
709
|
}
|
|
681
710
|
function htmlResponse(html, status = 200) {
|
|
682
|
-
|
|
711
|
+
const headers = applySecurityAndRouteHeaders(new Headers({ "content-type": "text/html; charset=utf-8" }), { isRouteStateRequest: false });
|
|
712
|
+
return new Response(html, {
|
|
683
713
|
status,
|
|
684
|
-
headers
|
|
685
|
-
})
|
|
714
|
+
headers
|
|
715
|
+
});
|
|
686
716
|
}
|
|
687
717
|
function applyDefaultSecurityHeaders(headers) {
|
|
688
718
|
if (!headers.has("permissions-policy")) headers.set("permissions-policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
|
@@ -691,8 +721,17 @@ function applyDefaultSecurityHeaders(headers) {
|
|
|
691
721
|
if (!headers.has("x-frame-options")) headers.set("x-frame-options", "SAMEORIGIN");
|
|
692
722
|
return headers;
|
|
693
723
|
}
|
|
724
|
+
function applySecurityAndRouteHeaders(headers, options) {
|
|
725
|
+
applyDefaultSecurityHeaders(headers);
|
|
726
|
+
if (options) {
|
|
727
|
+
appendVaryHeader(headers, ROUTE_STATE_REQUEST_HEADER);
|
|
728
|
+
if (options.isRouteStateRequest && !headers.has("cache-control")) headers.set("cache-control", ROUTE_STATE_CACHE_CONTROL);
|
|
729
|
+
}
|
|
730
|
+
return headers;
|
|
731
|
+
}
|
|
694
732
|
function withDefaultSecurityHeaders(response) {
|
|
695
|
-
const headers =
|
|
733
|
+
const headers = new Headers(response.headers);
|
|
734
|
+
applySecurityAndRouteHeaders(headers);
|
|
696
735
|
return new Response(response.body, {
|
|
697
736
|
status: response.status,
|
|
698
737
|
statusText: response.statusText,
|
|
@@ -700,9 +739,8 @@ function withDefaultSecurityHeaders(response) {
|
|
|
700
739
|
});
|
|
701
740
|
}
|
|
702
741
|
function withRouteResponseHeaders(response, options) {
|
|
703
|
-
const headers =
|
|
704
|
-
|
|
705
|
-
if (options.isRouteStateRequest && !headers.has("cache-control")) headers.set("cache-control", ROUTE_STATE_CACHE_CONTROL);
|
|
742
|
+
const headers = new Headers(response.headers);
|
|
743
|
+
applySecurityAndRouteHeaders(headers, options);
|
|
706
744
|
return new Response(response.body, {
|
|
707
745
|
status: response.status,
|
|
708
746
|
statusText: response.statusText,
|
|
@@ -730,29 +768,46 @@ async function prerenderApp(options) {
|
|
|
730
768
|
const resolved = resolveApp(options.app);
|
|
731
769
|
const results = [];
|
|
732
770
|
const isgManifest = {};
|
|
771
|
+
const work = [];
|
|
733
772
|
for (const route of resolved.routes) {
|
|
734
773
|
if (route.render !== "ssg" && route.render !== "isg") continue;
|
|
735
774
|
const paths = await collectSSGPaths(route, options.registry);
|
|
736
|
-
for (const pathname of paths) {
|
|
737
|
-
|
|
775
|
+
for (const pathname of paths) work.push({
|
|
776
|
+
pathname,
|
|
777
|
+
render: route.render,
|
|
778
|
+
revalidate: route.revalidate
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
const CONCURRENCY = 10;
|
|
782
|
+
for (let i = 0; i < work.length; i += CONCURRENCY) {
|
|
783
|
+
const batch = work.slice(i, i + CONCURRENCY);
|
|
784
|
+
const batchResults = await Promise.all(batch.map(async (item) => {
|
|
785
|
+
const url = new URL(item.pathname, "http://localhost");
|
|
738
786
|
const request = new Request(url, { method: "GET" });
|
|
739
787
|
const response = await handlePrachtRequest({
|
|
740
788
|
app: options.app,
|
|
741
789
|
request,
|
|
742
790
|
registry: options.registry,
|
|
743
791
|
clientEntryUrl: options.clientEntryUrl,
|
|
744
|
-
cssManifest: options.cssManifest
|
|
792
|
+
cssManifest: options.cssManifest,
|
|
793
|
+
jsManifest: options.jsManifest
|
|
745
794
|
});
|
|
746
795
|
if (response.status !== 200) {
|
|
747
|
-
console.warn(` Warning: ${
|
|
748
|
-
|
|
796
|
+
console.warn(` Warning: ${item.render.toUpperCase()} route "${item.pathname}" returned status ${response.status}, skipping.`);
|
|
797
|
+
return null;
|
|
749
798
|
}
|
|
750
|
-
|
|
799
|
+
return {
|
|
800
|
+
item,
|
|
801
|
+
html: await response.text()
|
|
802
|
+
};
|
|
803
|
+
}));
|
|
804
|
+
for (const result of batchResults) {
|
|
805
|
+
if (!result) continue;
|
|
751
806
|
results.push({
|
|
752
|
-
path: pathname,
|
|
753
|
-
html
|
|
807
|
+
path: result.item.pathname,
|
|
808
|
+
html: result.html
|
|
754
809
|
});
|
|
755
|
-
if (
|
|
810
|
+
if (result.item.render === "isg" && result.item.revalidate) isgManifest[result.item.pathname] = { revalidate: result.item.revalidate };
|
|
756
811
|
}
|
|
757
812
|
}
|
|
758
813
|
if (options.withISGManifest) return {
|