@pracht/core 0.2.2 → 0.2.4

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
@@ -157,6 +157,9 @@ type LoaderData<TLoader extends LoaderLike> = TLoader extends ((...args: any[])
157
157
  interface HeadArgs<TLoader extends LoaderLike = undefined, TContext = any> extends BaseRouteArgs<TContext> {
158
158
  data: LoaderData<TLoader>;
159
159
  }
160
+ interface HeadersArgs<TLoader extends LoaderLike = undefined, TContext = any> extends BaseRouteArgs<TContext> {
161
+ data: LoaderData<TLoader>;
162
+ }
160
163
  interface RouteComponentProps<TLoader extends LoaderLike = undefined> {
161
164
  data: LoaderData<TLoader>;
162
165
  params: RouteParams;
@@ -171,6 +174,7 @@ type LoaderFn<TContext = any, TData = unknown> = (args: LoaderArgs<TContext>) =>
171
174
  interface RouteModule<TContext = any, TLoader extends LoaderLike = undefined> {
172
175
  loader?: LoaderFn<TContext>;
173
176
  head?: (args: HeadArgs<TLoader, TContext>) => MaybePromise<HeadMetadata>;
177
+ headers?: (args: HeadersArgs<TLoader, TContext>) => MaybePromise<HeadersInit>;
174
178
  Component?: FunctionComponent<RouteComponentProps<TLoader>>;
175
179
  default?: FunctionComponent<RouteComponentProps<TLoader>>;
176
180
  ErrorBoundary?: FunctionComponent<ErrorBoundaryProps>;
@@ -180,6 +184,7 @@ interface ShellModule<TContext = any> {
180
184
  Shell: FunctionComponent<ShellProps>;
181
185
  Loading?: FunctionComponent;
182
186
  head?: (args: BaseRouteArgs<TContext>) => MaybePromise<HeadMetadata>;
187
+ headers?: (args: BaseRouteArgs<TContext>) => MaybePromise<HeadersInit>;
183
188
  }
184
189
  type MiddlewareResult<TContext = any> = void | Response | {
185
190
  redirect: string;
@@ -351,6 +356,7 @@ declare function applyDefaultSecurityHeaders(headers: Headers): Headers;
351
356
  interface PrerenderResult {
352
357
  path: string;
353
358
  html: string;
359
+ headers?: Record<string, string>;
354
360
  }
355
361
  interface ISGManifestEntry {
356
362
  revalidate: RouteRevalidate;
@@ -365,6 +371,8 @@ interface PrerenderAppOptions {
365
371
  clientEntryUrl?: string;
366
372
  /** Per-source-file CSS map produced by the vite plugin (preferred over cssUrls). */
367
373
  cssManifest?: Record<string, string[]>;
374
+ /** Per-source-file JS map produced by the vite plugin for modulepreload hints. */
375
+ jsManifest?: Record<string, string[]>;
368
376
  /** @deprecated Pass cssManifest instead for per-page CSS resolution. */
369
377
  cssUrls?: string[];
370
378
  }
@@ -395,4 +403,4 @@ interface InitClientRouterOptions {
395
403
  }
396
404
  declare function initClientRouter(options: InitClientRouterOptions): Promise<void>;
397
405
  //#endregion
398
- export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadMetadata, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareResult, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteMatch, type RouteMeta, type RouteModule, type RouteParams, type RouteRevalidate, type RouteStateResult, type RouteTreeNode, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
406
+ export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadMetadata, type HeadersArgs, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareResult, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteMatch, type RouteMeta, type RouteModule, type RouteParams, type RouteRevalidate, type RouteStateResult, type RouteTreeNode, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
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);
@@ -327,16 +333,16 @@ async function handlePrachtRequest(options) {
327
333
  currentPhase = "render";
328
334
  shellModule = match.route.shellFile ? await resolveRegistryModule(registry.shellModules, match.route.shellFile) : void 0;
329
335
  const head = await mergeHeadMetadata(shellModule, routeModule, routeArgs, data);
336
+ const documentHeaders = await mergeDocumentHeaders(shellModule, routeModule, routeArgs, data);
330
337
  const cssUrls = resolvePageCssUrls(options, match.route.shellFile, match.route.file);
331
338
  const modulePreloadUrls = resolvePageJsUrls(options, match.route.shellFile, match.route.file);
332
339
  if (match.route.render === "spa") {
333
340
  let body = "";
334
341
  if (shellModule?.Shell || shellModule?.Loading) {
335
- const { renderToStringAsync } = await import("preact-render-to-string");
336
342
  const Shell = shellModule?.Shell;
337
343
  const Loading = shellModule?.Loading;
338
344
  const loadingTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
339
- if (loadingTree) body = await renderToStringAsync(loadingTree);
345
+ if (loadingTree) body = await (await getRenderToStringAsync())(loadingTree);
340
346
  }
341
347
  return htmlResponse(buildHtmlDocument({
342
348
  head,
@@ -351,26 +357,26 @@ async function handlePrachtRequest(options) {
351
357
  clientEntryUrl: options.clientEntryUrl,
352
358
  cssUrls,
353
359
  modulePreloadUrls
354
- }));
360
+ }), 200, documentHeaders);
355
361
  }
356
362
  const DefaultComponent = typeof routeModule.default === "function" ? routeModule.default : void 0;
357
363
  const Component = routeModule.Component ?? DefaultComponent;
358
364
  if (!Component) throw new Error("Route has no Component or default export");
359
- const { renderToStringAsync } = await import("preact-render-to-string");
360
365
  const Shell = shellModule?.Shell;
361
366
  const componentProps = {
362
367
  data,
363
368
  params: match.params
364
369
  };
365
370
  const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
371
+ const tree = h(PrachtRuntimeProvider, {
372
+ data,
373
+ params: match.params,
374
+ routeId: match.route.id ?? "",
375
+ url: url.pathname
376
+ }, componentTree);
366
377
  return htmlResponse(buildHtmlDocument({
367
378
  head,
368
- body: await renderToStringAsync(h(PrachtRuntimeProvider, {
369
- data,
370
- params: match.params,
371
- routeId: match.route.id ?? "",
372
- url: url.pathname
373
- }, componentTree)),
379
+ body: await (await getRenderToStringAsync())(tree),
374
380
  hydrationState: {
375
381
  url: url.pathname,
376
382
  routeId: match.route.id ?? "",
@@ -380,7 +386,7 @@ async function handlePrachtRequest(options) {
380
386
  clientEntryUrl: options.clientEntryUrl,
381
387
  cssUrls,
382
388
  modulePreloadUrls
383
- }));
389
+ }), 200, documentHeaders);
384
390
  } catch (error) {
385
391
  return renderRouteErrorResponse({
386
392
  error,
@@ -397,15 +403,42 @@ async function handlePrachtRequest(options) {
397
403
  });
398
404
  }
399
405
  }
406
+ /** Strip leading `./` and `/` so all module paths share one canonical form. */
407
+ function normalizeModulePath(path) {
408
+ return path.replace(/^\.?\//, "");
409
+ }
410
+ function buildSuffixIndex(manifest) {
411
+ const index = /* @__PURE__ */ new Map();
412
+ for (const key of Object.keys(manifest)) {
413
+ const normalized = normalizeModulePath(key);
414
+ if (!normalized) continue;
415
+ if (!index.has(normalized)) index.set(normalized, key);
416
+ for (let i = normalized.indexOf("/"); i !== -1; i = normalized.indexOf("/", i + 1)) {
417
+ const suffix = normalized.slice(i + 1);
418
+ if (suffix && !index.has(suffix)) index.set(suffix, key);
419
+ }
420
+ }
421
+ return index;
422
+ }
423
+ const suffixIndexCache = /* @__PURE__ */ new WeakMap();
424
+ function getSuffixIndex(manifest) {
425
+ let index = suffixIndexCache.get(manifest);
426
+ if (index) return index;
427
+ index = buildSuffixIndex(manifest);
428
+ suffixIndexCache.set(manifest, index);
429
+ return index;
430
+ }
431
+ function resolveManifestEntries(manifest, file) {
432
+ if (file in manifest) return manifest[file];
433
+ const resolved = getSuffixIndex(manifest).get(normalizeModulePath(file));
434
+ if (resolved) return manifest[resolved];
435
+ }
400
436
  function resolvePageCssUrls(options, shellFile, routeFile) {
401
437
  if (!options.cssManifest) return options.cssUrls ?? [];
402
438
  const css = /* @__PURE__ */ new Set();
403
439
  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
- }
440
+ const entries = resolveManifestEntries(options.cssManifest, file);
441
+ if (entries) for (const c of entries) css.add(c);
409
442
  }
410
443
  if (shellFile) addFromManifest(shellFile);
411
444
  addFromManifest(routeFile);
@@ -415,11 +448,8 @@ function resolvePageJsUrls(options, shellFile, routeFile) {
415
448
  if (!options.jsManifest) return [];
416
449
  const js = /* @__PURE__ */ new Set();
417
450
  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
- }
451
+ const entries = resolveManifestEntries(options.jsManifest, file);
452
+ if (entries) for (const j of entries) js.add(j);
423
453
  }
424
454
  if (shellFile) addFromManifest(shellFile);
425
455
  addFromManifest(routeFile);
@@ -517,11 +547,11 @@ function deserializeRouteError$1(error) {
517
547
  return result;
518
548
  }
519
549
  function jsonErrorResponse(routeError, options) {
520
- const response = new Response(JSON.stringify({ error: routeError }), {
550
+ const headers = applySecurityAndRouteHeaders(new Headers({ "content-type": "application/json; charset=utf-8" }), options.isRouteStateRequest ? { isRouteStateRequest: true } : void 0);
551
+ return new Response(JSON.stringify({ error: routeError }), {
521
552
  status: routeError.status,
522
- headers: { "content-type": "application/json; charset=utf-8" }
553
+ headers
523
554
  });
524
- return options.isRouteStateRequest ? withRouteResponseHeaders(response, { isRouteStateRequest: true }) : withDefaultSecurityHeaders(response);
525
555
  }
526
556
  function renderApiErrorResponse(options) {
527
557
  const exposeDetails = shouldExposeServerErrors(options.options);
@@ -567,16 +597,17 @@ async function renderRouteErrorResponse(options) {
567
597
  if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
568
598
  const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
569
599
  const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
600
+ const documentHeaders = await mergeDocumentHeaders(shellModule, void 0, options.routeArgs, void 0);
570
601
  const cssUrls = resolvePageCssUrls(options.options, options.shellFile, options.routeArgs.route.file);
571
602
  const modulePreloadUrls = resolvePageJsUrls(options.options, options.shellFile, options.routeArgs.route.file);
572
- const { renderToStringAsync } = await import("preact-render-to-string");
603
+ const renderToString = await getRenderToStringAsync();
573
604
  const ErrorBoundary = options.routeModule.ErrorBoundary;
574
605
  const Shell = shellModule?.Shell;
575
606
  const errorValue = deserializeRouteError$1(routeErrorWithDiagnostics);
576
607
  const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
577
608
  return htmlResponse(buildHtmlDocument({
578
609
  head,
579
- body: await renderToStringAsync(h(PrachtRuntimeProvider, {
610
+ body: await renderToString(h(PrachtRuntimeProvider, {
580
611
  data: null,
581
612
  routeId: options.routeId,
582
613
  url: options.urlPathname
@@ -590,7 +621,7 @@ async function renderRouteErrorResponse(options) {
590
621
  clientEntryUrl: options.options.clientEntryUrl,
591
622
  cssUrls,
592
623
  modulePreloadUrls
593
- }), routeErrorWithDiagnostics.status);
624
+ }), routeErrorWithDiagnostics.status, documentHeaders);
594
625
  }
595
626
  async function runMiddlewareChain(options) {
596
627
  let context = options.context;
@@ -636,8 +667,8 @@ async function resolveDataFunctions(route, routeModule, registry) {
636
667
  async function resolveRegistryModule(modules, file) {
637
668
  if (!modules) return void 0;
638
669
  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]();
670
+ const resolved = getSuffixIndex(modules).get(normalizeModulePath(file));
671
+ if (resolved) return modules[resolved]();
641
672
  }
642
673
  async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
643
674
  const shellHead = shellModule?.head ? await shellModule.head(routeArgs) : {};
@@ -652,6 +683,22 @@ async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
652
683
  link: [...shellHead.link ?? [], ...routeHead.link ?? []]
653
684
  };
654
685
  }
686
+ async function mergeDocumentHeaders(shellModule, routeModule, routeArgs, data) {
687
+ const headers = new Headers();
688
+ const shellHeaders = shellModule?.headers ? await shellModule.headers(routeArgs) : void 0;
689
+ if (shellHeaders) applyHeaders(headers, shellHeaders);
690
+ const routeHeaders = routeModule?.headers ? await routeModule.headers({
691
+ ...routeArgs,
692
+ data
693
+ }) : void 0;
694
+ if (routeHeaders) applyHeaders(headers, routeHeaders);
695
+ return headers;
696
+ }
697
+ function applyHeaders(headers, init) {
698
+ new Headers(init).forEach((value, key) => {
699
+ headers.set(key, value);
700
+ });
701
+ }
655
702
  function buildHtmlDocument(options) {
656
703
  const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [] } = options;
657
704
  const titleTag = head.title ? `<title>${escapeHtml(head.title)}</title>` : "";
@@ -678,11 +725,14 @@ function buildHtmlDocument(options) {
678
725
  </body>
679
726
  </html>`;
680
727
  }
681
- function htmlResponse(html, status = 200) {
682
- return withRouteResponseHeaders(new Response(html, {
728
+ function htmlResponse(html, status = 200, initHeaders) {
729
+ const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
730
+ if (initHeaders) applyHeaders(headers, initHeaders);
731
+ applySecurityAndRouteHeaders(headers, { isRouteStateRequest: false });
732
+ return new Response(html, {
683
733
  status,
684
- headers: { "content-type": "text/html; charset=utf-8" }
685
- }), { isRouteStateRequest: false });
734
+ headers
735
+ });
686
736
  }
687
737
  function applyDefaultSecurityHeaders(headers) {
688
738
  if (!headers.has("permissions-policy")) headers.set("permissions-policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
@@ -691,8 +741,17 @@ function applyDefaultSecurityHeaders(headers) {
691
741
  if (!headers.has("x-frame-options")) headers.set("x-frame-options", "SAMEORIGIN");
692
742
  return headers;
693
743
  }
744
+ function applySecurityAndRouteHeaders(headers, options) {
745
+ applyDefaultSecurityHeaders(headers);
746
+ if (options) {
747
+ appendVaryHeader(headers, ROUTE_STATE_REQUEST_HEADER);
748
+ if (options.isRouteStateRequest && !headers.has("cache-control")) headers.set("cache-control", ROUTE_STATE_CACHE_CONTROL);
749
+ }
750
+ return headers;
751
+ }
694
752
  function withDefaultSecurityHeaders(response) {
695
- const headers = applyDefaultSecurityHeaders(new Headers(response.headers));
753
+ const headers = new Headers(response.headers);
754
+ applySecurityAndRouteHeaders(headers);
696
755
  return new Response(response.body, {
697
756
  status: response.status,
698
757
  statusText: response.statusText,
@@ -700,9 +759,8 @@ function withDefaultSecurityHeaders(response) {
700
759
  });
701
760
  }
702
761
  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);
762
+ const headers = new Headers(response.headers);
763
+ applySecurityAndRouteHeaders(headers, options);
706
764
  return new Response(response.body, {
707
765
  status: response.status,
708
766
  statusText: response.statusText,
@@ -730,29 +788,49 @@ async function prerenderApp(options) {
730
788
  const resolved = resolveApp(options.app);
731
789
  const results = [];
732
790
  const isgManifest = {};
791
+ const work = [];
733
792
  for (const route of resolved.routes) {
734
793
  if (route.render !== "ssg" && route.render !== "isg") continue;
735
794
  const paths = await collectSSGPaths(route, options.registry);
736
- for (const pathname of paths) {
737
- const url = new URL(pathname, "http://localhost");
795
+ for (const pathname of paths) work.push({
796
+ pathname,
797
+ render: route.render,
798
+ revalidate: route.revalidate
799
+ });
800
+ }
801
+ const CONCURRENCY = 10;
802
+ for (let i = 0; i < work.length; i += CONCURRENCY) {
803
+ const batch = work.slice(i, i + CONCURRENCY);
804
+ const batchResults = await Promise.all(batch.map(async (item) => {
805
+ const url = new URL(item.pathname, "http://localhost");
738
806
  const request = new Request(url, { method: "GET" });
739
807
  const response = await handlePrachtRequest({
740
808
  app: options.app,
741
809
  request,
742
810
  registry: options.registry,
743
811
  clientEntryUrl: options.clientEntryUrl,
744
- cssManifest: options.cssManifest
812
+ cssManifest: options.cssManifest,
813
+ jsManifest: options.jsManifest
745
814
  });
746
815
  if (response.status !== 200) {
747
- console.warn(` Warning: ${route.render.toUpperCase()} route "${pathname}" returned status ${response.status}, skipping.`);
748
- continue;
816
+ console.warn(` Warning: ${item.render.toUpperCase()} route "${item.pathname}" returned status ${response.status}, skipping.`);
817
+ return null;
749
818
  }
750
819
  const html = await response.text();
820
+ return {
821
+ headers: Object.fromEntries(response.headers),
822
+ html,
823
+ item
824
+ };
825
+ }));
826
+ for (const result of batchResults) {
827
+ if (!result) continue;
751
828
  results.push({
752
- path: pathname,
753
- html
829
+ path: result.item.pathname,
830
+ html: result.html,
831
+ headers: result.headers
754
832
  });
755
- if (route.render === "isg" && route.revalidate) isgManifest[pathname] = { revalidate: route.revalidate };
833
+ if (result.item.render === "isg" && result.item.revalidate) isgManifest[result.item.pathname] = { revalidate: result.item.revalidate };
756
834
  }
757
835
  }
758
836
  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.4",
4
4
  "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
5
5
  "bugs": {
6
6
  "url": "https://github.com/JoviDeCroock/pracht/issues"