@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 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 renderToStringAsync(loadingTree);
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 renderToStringAsync(h(PrachtRuntimeProvider, {
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 suffix = file.replace(/^\.\//, "");
405
- for (const [key, cssFiles] of Object.entries(options.cssManifest)) if (key === file || key.endsWith(`/${suffix}`) || key.endsWith(suffix)) {
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 suffix = file.replace(/^\.\//, "");
419
- for (const [key, jsFiles] of Object.entries(options.jsManifest)) if (key === file || key.endsWith(`/${suffix}`) || key.endsWith(suffix)) {
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 response = new Response(JSON.stringify({ error: routeError }), {
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: { "content-type": "application/json; charset=utf-8" }
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 { renderToStringAsync } = await import("preact-render-to-string");
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 renderToStringAsync(h(PrachtRuntimeProvider, {
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 suffix = file.replace(/^\.\//, "");
640
- for (const key of Object.keys(modules)) if (key.endsWith(`/${suffix}`) || key.endsWith(suffix)) return modules[key]();
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
- return withRouteResponseHeaders(new Response(html, {
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: { "content-type": "text/html; charset=utf-8" }
685
- }), { isRouteStateRequest: false });
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 = applyDefaultSecurityHeaders(new Headers(response.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 = applyDefaultSecurityHeaders(new Headers(response.headers));
704
- appendVaryHeader(headers, ROUTE_STATE_REQUEST_HEADER);
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
- const url = new URL(pathname, "http://localhost");
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: ${route.render.toUpperCase()} route "${pathname}" returned status ${response.status}, skipping.`);
748
- continue;
796
+ console.warn(` Warning: ${item.render.toUpperCase()} route "${item.pathname}" returned status ${response.status}, skipping.`);
797
+ return null;
749
798
  }
750
- const html = await response.text();
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 (route.render === "isg" && route.revalidate) isgManifest[pathname] = { revalidate: route.revalidate };
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/core",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
5
5
  "bugs": {
6
6
  "url": "https://github.com/JoviDeCroock/pracht/issues"