@pracht/core 0.6.0 → 0.7.0
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 +62 -8
- package/dist/index.mjs +138 -15
- package/package.json +14 -1
package/dist/index.d.mts
CHANGED
|
@@ -23,6 +23,53 @@ type RegisteredContext = Register extends {
|
|
|
23
23
|
} ? T : unknown;
|
|
24
24
|
type RenderMode = "spa" | "ssr" | "ssg" | "isg";
|
|
25
25
|
type RouteParams = Record<string, string>;
|
|
26
|
+
type RouteParamInput = string | number | boolean;
|
|
27
|
+
type SearchParamPrimitive = string | number | boolean;
|
|
28
|
+
type SearchParamValue = SearchParamPrimitive | null | undefined | readonly (SearchParamPrimitive | null | undefined)[];
|
|
29
|
+
type SearchParamsInput = string | URLSearchParams | Record<string, SearchParamValue>;
|
|
30
|
+
interface BuildHrefOptions {
|
|
31
|
+
params?: Record<string, RouteParamInput>;
|
|
32
|
+
search?: SearchParamsInput;
|
|
33
|
+
hash?: string;
|
|
34
|
+
}
|
|
35
|
+
interface NavigateOptions {
|
|
36
|
+
replace?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface HrefRouteDefinition {
|
|
39
|
+
id?: string;
|
|
40
|
+
path: string;
|
|
41
|
+
segments?: readonly RouteSegment[];
|
|
42
|
+
}
|
|
43
|
+
type RegisteredRouteMap = Register extends {
|
|
44
|
+
routes: infer TRoutes;
|
|
45
|
+
} ? TRoutes extends Record<string, unknown> ? TRoutes : {} : {};
|
|
46
|
+
type HasRegisteredRoutes = keyof RegisteredRouteMap extends never ? false : true;
|
|
47
|
+
type EmptyRouteParams = Record<never, never>;
|
|
48
|
+
type IsEmptyRouteParams<TParams> = keyof TParams extends never ? true : false;
|
|
49
|
+
type RouteId = HasRegisteredRoutes extends true ? Extract<keyof RegisteredRouteMap, string> : string;
|
|
50
|
+
type RouteParamsFor<TRoute extends RouteId> = HasRegisteredRoutes extends true ? TRoute extends keyof RegisteredRouteMap ? RegisteredRouteMap[TRoute] extends {
|
|
51
|
+
params: infer TParams;
|
|
52
|
+
} ? TParams extends Record<string, unknown> ? TParams : EmptyRouteParams : EmptyRouteParams : never : Record<string, RouteParamInput>;
|
|
53
|
+
type RouteSearchFor<TRoute extends RouteId> = HasRegisteredRoutes extends true ? TRoute extends keyof RegisteredRouteMap ? RegisteredRouteMap[TRoute] extends {
|
|
54
|
+
search: infer TSearch;
|
|
55
|
+
} ? TSearch : SearchParamsInput : never : SearchParamsInput;
|
|
56
|
+
type TypedHrefOptions<TRoute extends RouteId> = IsEmptyRouteParams<RouteParamsFor<TRoute>> extends true ? {
|
|
57
|
+
params?: never;
|
|
58
|
+
search?: RouteSearchFor<TRoute>;
|
|
59
|
+
hash?: string;
|
|
60
|
+
} : {
|
|
61
|
+
params: RouteParamsFor<TRoute>;
|
|
62
|
+
search?: RouteSearchFor<TRoute>;
|
|
63
|
+
hash?: string;
|
|
64
|
+
};
|
|
65
|
+
type HrefOptions<TRoute extends RouteId = RouteId> = HasRegisteredRoutes extends true ? TRoute extends RouteId ? TypedHrefOptions<TRoute> : never : BuildHrefOptions;
|
|
66
|
+
type HrefArgs<TRoute extends RouteId = RouteId> = HasRegisteredRoutes extends true ? TRoute extends RouteId ? IsEmptyRouteParams<RouteParamsFor<TRoute>> extends true ? [options?: TypedHrefOptions<TRoute>] : [options: TypedHrefOptions<TRoute>] : never : [options?: BuildHrefOptions];
|
|
67
|
+
type RouteTarget<TRoute extends RouteId = RouteId> = HasRegisteredRoutes extends true ? TRoute extends RouteId ? {
|
|
68
|
+
route: TRoute;
|
|
69
|
+
} & TypedHrefOptions<TRoute> : never : {
|
|
70
|
+
route: string;
|
|
71
|
+
} & BuildHrefOptions;
|
|
72
|
+
type HrefFn = <TRoute extends RouteId>(route: TRoute, ...args: HrefArgs<TRoute>) => string;
|
|
26
73
|
/**
|
|
27
74
|
* A reference to a module file — either a plain string path or a lazy import
|
|
28
75
|
* function. Using `() => import("./path")` enables IDE click-to-navigate.
|
|
@@ -67,6 +114,7 @@ interface RouteMeta {
|
|
|
67
114
|
middleware?: string[];
|
|
68
115
|
revalidate?: RouteRevalidate;
|
|
69
116
|
prefetch?: PrefetchStrategy;
|
|
117
|
+
hasLoader?: boolean;
|
|
70
118
|
}
|
|
71
119
|
interface GroupMeta {
|
|
72
120
|
shell?: string;
|
|
@@ -241,7 +289,9 @@ declare function group(meta: GroupMeta, routes: RouteTreeNode[]): GroupDefinitio
|
|
|
241
289
|
declare function defineApp(config: PrachtAppConfig): PrachtApp;
|
|
242
290
|
declare function resolveApp(app: PrachtApp): ResolvedPrachtApp;
|
|
243
291
|
declare function matchAppRoute(app: PrachtApp | ResolvedPrachtApp, pathname: string): RouteMatch | undefined;
|
|
244
|
-
declare function buildPathFromSegments(segments: RouteSegment[], params: RouteParams): string;
|
|
292
|
+
declare function buildPathFromSegments(segments: readonly RouteSegment[], params: RouteParams): string;
|
|
293
|
+
declare function buildHref<TRoute extends RouteId>(routes: readonly HrefRouteDefinition[], routeId: TRoute, ...args: HrefArgs<TRoute>): string;
|
|
294
|
+
declare function createHref(routes: readonly HrefRouteDefinition[]): HrefFn;
|
|
245
295
|
/**
|
|
246
296
|
* Convert a list of file paths from `import.meta.glob` into resolved API routes.
|
|
247
297
|
*
|
|
@@ -310,22 +360,22 @@ interface FormProps extends Omit<JSX.HTMLAttributes<HTMLFormElement>, "action" |
|
|
|
310
360
|
action?: string;
|
|
311
361
|
method?: string;
|
|
312
362
|
}
|
|
363
|
+
type LinkProps<TRoute extends RouteId = RouteId> = Omit<JSX.HTMLAttributes<HTMLAnchorElement>, "href"> & RouteTarget<TRoute>;
|
|
313
364
|
interface Location {
|
|
314
365
|
pathname: string;
|
|
315
366
|
search: string;
|
|
316
367
|
}
|
|
317
368
|
declare global {
|
|
369
|
+
var __PRACHT_ROUTE_DEFINITIONS__: readonly HrefRouteDefinition[] | undefined;
|
|
318
370
|
interface Window {
|
|
319
371
|
__PRACHT_STATE__?: PrachtHydrationState;
|
|
320
|
-
__PRACHT_NAVIGATE__?: (to: string, options?: {
|
|
321
|
-
replace?: boolean;
|
|
322
|
-
}) => Promise<void>;
|
|
323
372
|
}
|
|
324
373
|
}
|
|
325
374
|
interface PrachtRuntimeValue {
|
|
326
375
|
data: unknown;
|
|
327
376
|
params: RouteParams;
|
|
328
377
|
routeId: string;
|
|
378
|
+
routes?: readonly HrefRouteDefinition[];
|
|
329
379
|
url: string;
|
|
330
380
|
setData: (data: unknown) => void;
|
|
331
381
|
}
|
|
@@ -334,6 +384,7 @@ declare function PrachtRuntimeProvider<TData>({
|
|
|
334
384
|
data,
|
|
335
385
|
params,
|
|
336
386
|
routeId,
|
|
387
|
+
routes,
|
|
337
388
|
stateVersion,
|
|
338
389
|
url
|
|
339
390
|
}: {
|
|
@@ -341,6 +392,7 @@ declare function PrachtRuntimeProvider<TData>({
|
|
|
341
392
|
data: TData;
|
|
342
393
|
params?: RouteParams;
|
|
343
394
|
routeId: string;
|
|
395
|
+
routes?: readonly HrefRouteDefinition[];
|
|
344
396
|
stateVersion?: number;
|
|
345
397
|
url: string;
|
|
346
398
|
}): _$preact.VNode<_$preact.Attributes & {
|
|
@@ -354,6 +406,7 @@ declare function useRouteData<TData = unknown>(): TData;
|
|
|
354
406
|
declare function useLocation(): Location;
|
|
355
407
|
declare function useParams(): RouteParams;
|
|
356
408
|
declare function useRevalidate(): () => Promise<unknown>;
|
|
409
|
+
declare function Link<TRoute extends RouteId>(props: LinkProps<TRoute>): _$preact.VNode<_$preact.ClassAttributes<HTMLAnchorElement> & h.JSX.HTMLAttributes<HTMLAnchorElement>>;
|
|
357
410
|
declare function Form(props: FormProps): _$preact.VNode<_$preact.ClassAttributes<HTMLFormElement> & h.JSX.HTMLAttributes<HTMLFormElement>>;
|
|
358
411
|
//#endregion
|
|
359
412
|
//#region src/runtime-client-fetch.d.ts
|
|
@@ -422,9 +475,10 @@ declare global {
|
|
|
422
475
|
}
|
|
423
476
|
}
|
|
424
477
|
type ModuleMap = Record<string, () => Promise<unknown>>;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
478
|
+
interface NavigateFn {
|
|
479
|
+
(to: string, options?: NavigateOptions): Promise<void>;
|
|
480
|
+
<TRoute extends RouteId>(to: RouteTarget<TRoute>, options?: NavigateOptions): Promise<void>;
|
|
481
|
+
}
|
|
428
482
|
declare function useNavigate(): NavigateFn;
|
|
429
483
|
interface InitClientRouterOptions {
|
|
430
484
|
app: ResolvedPrachtApp;
|
|
@@ -436,4 +490,4 @@ interface InitClientRouterOptions {
|
|
|
436
490
|
}
|
|
437
491
|
declare function initClientRouter(options: InitClientRouterOptions): Promise<void>;
|
|
438
492
|
//#endregion
|
|
439
|
-
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 HeadAttributes, type HeadMetadata, type HeadScriptDescriptor, 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, useRouteData };
|
|
493
|
+
export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type BuildHrefOptions, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadAttributes, type HeadMetadata, type HeadScriptDescriptor, type HeadersArgs, type HrefArgs, type HrefFn, type HrefOptions, type HrefRouteDefinition, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, Link, type LinkProps, 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 NavigateOptions, 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 RouteId, type RouteMatch, type RouteMeta, type RouteModule, type RouteParamInput, type RouteParams, type RouteParamsFor, type RouteRevalidate, type RouteSearchFor, type RouteStateResult, type RouteTarget, type RouteTreeNode, type SearchParamPrimitive, type SearchParamValue, type SearchParamsInput, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildHref, buildPathFromSegments, createHref, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|
package/dist/index.mjs
CHANGED
|
@@ -22,6 +22,7 @@ function route(path, fileOrConfig, meta = {}) {
|
|
|
22
22
|
path: normalizeRoutePath(path),
|
|
23
23
|
file: resolveModuleRef(component),
|
|
24
24
|
loaderFile: resolveModuleRef(loader),
|
|
25
|
+
hasLoader: !!loader,
|
|
25
26
|
...routeMeta
|
|
26
27
|
};
|
|
27
28
|
}
|
|
@@ -97,6 +98,7 @@ function flattenRouteNode(app, node, inherited, routes) {
|
|
|
97
98
|
path: fullPath,
|
|
98
99
|
file: node.file,
|
|
99
100
|
loaderFile: node.loaderFile,
|
|
101
|
+
hasLoader: node.loaderFile ? true : node.hasLoader,
|
|
100
102
|
shell,
|
|
101
103
|
shellFile: shell ? app.shells[shell] : void 0,
|
|
102
104
|
render: node.render ?? inherited.render,
|
|
@@ -188,6 +190,54 @@ function buildPathFromSegments(segments, params) {
|
|
|
188
190
|
return (params[segment.name] ?? params["*"] ?? "").split("/").map((part) => encodeDynamicPathSegment(part)).join("/");
|
|
189
191
|
}).join("/"));
|
|
190
192
|
}
|
|
193
|
+
function buildHref(routes, routeId, ...args) {
|
|
194
|
+
return buildHrefUntyped(routes, String(routeId), args[0]);
|
|
195
|
+
}
|
|
196
|
+
function createHref(routes) {
|
|
197
|
+
return ((routeId, options) => buildHrefUntyped(routes, routeId, options));
|
|
198
|
+
}
|
|
199
|
+
function buildHrefUntyped(routes, routeId, options = {}) {
|
|
200
|
+
const route = routes.find((candidate) => candidate.id === routeId);
|
|
201
|
+
if (!route) throw new Error(`Unknown pracht route id: ${routeId}.`);
|
|
202
|
+
const segments = route.segments ?? parseRouteSegments(route.path);
|
|
203
|
+
return `${buildPathFromSegments(segments, normalizeHrefParams(segments, options.params ?? {}))}${serializeSearch(options.search)}${serializeHash(options.hash)}`;
|
|
204
|
+
}
|
|
205
|
+
function normalizeHrefParams(segments, params) {
|
|
206
|
+
const expected = new Set(segments.filter((segment) => segment.type === "param" || segment.type === "catchall").map((segment) => segment.name));
|
|
207
|
+
for (const name of expected) if (params[name] == null) throw new Error(`Missing route param: ${name}.`);
|
|
208
|
+
for (const name of Object.keys(params)) if (!expected.has(name)) throw new Error(`Unexpected route param: ${name}.`);
|
|
209
|
+
const normalized = {};
|
|
210
|
+
for (const name of expected) normalized[name] = String(params[name]);
|
|
211
|
+
return normalized;
|
|
212
|
+
}
|
|
213
|
+
function serializeSearch(search) {
|
|
214
|
+
if (search == null) return "";
|
|
215
|
+
if (typeof search === "string") {
|
|
216
|
+
if (!search) return "";
|
|
217
|
+
return search.startsWith("?") ? search : `?${search}`;
|
|
218
|
+
}
|
|
219
|
+
const serialized = (search instanceof URLSearchParams ? search : objectToSearchParams(search)).toString();
|
|
220
|
+
return serialized ? `?${serialized}` : "";
|
|
221
|
+
}
|
|
222
|
+
function objectToSearchParams(search) {
|
|
223
|
+
const params = new URLSearchParams();
|
|
224
|
+
for (const [key, value] of Object.entries(search)) {
|
|
225
|
+
if (Array.isArray(value)) {
|
|
226
|
+
for (const item of value) appendSearchValue(params, key, item);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
appendSearchValue(params, key, value);
|
|
230
|
+
}
|
|
231
|
+
return params;
|
|
232
|
+
}
|
|
233
|
+
function appendSearchValue(params, key, value) {
|
|
234
|
+
if (value == null) return;
|
|
235
|
+
params.append(key, String(value));
|
|
236
|
+
}
|
|
237
|
+
function serializeHash(hash) {
|
|
238
|
+
if (!hash) return "";
|
|
239
|
+
return hash.startsWith("#") ? hash : `#${hash}`;
|
|
240
|
+
}
|
|
191
241
|
/**
|
|
192
242
|
* Encode one dynamic URL path segment for SSG/ISG output. `encodeURIComponent`
|
|
193
243
|
* leaves unreserved characters (including `.`) intact, and even percent-encoded
|
|
@@ -532,6 +582,10 @@ function parseSafeNavigationUrl(location, base) {
|
|
|
532
582
|
if (!SAFE_NAVIGATION_PROTOCOLS.has(targetUrl.protocol)) return null;
|
|
533
583
|
return targetUrl;
|
|
534
584
|
}
|
|
585
|
+
function routeNeedsServerFetch(route) {
|
|
586
|
+
if (route.hasLoader === false && route.middlewareFiles.length === 0) return false;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
535
589
|
function buildRouteStateUrl(url) {
|
|
536
590
|
return `${url}${url.includes("?") ? "&" : "?"}_data=1`;
|
|
537
591
|
}
|
|
@@ -589,6 +643,10 @@ async function navigateToClientLocation(location, options) {
|
|
|
589
643
|
const CACHE_TTL_MS = 3e4;
|
|
590
644
|
const MAX_PREFETCH_CACHE_ENTRIES = 100;
|
|
591
645
|
const MAX_MATCH_CACHE_ENTRIES = 250;
|
|
646
|
+
const EMPTY_ROUTE_STATE_PROMISE = Promise.resolve({
|
|
647
|
+
type: "data",
|
|
648
|
+
data: void 0
|
|
649
|
+
});
|
|
592
650
|
const prefetchCache = /* @__PURE__ */ new Map();
|
|
593
651
|
function clearPrefetchCache() {
|
|
594
652
|
prefetchCache.clear();
|
|
@@ -604,7 +662,8 @@ function getCachedRouteState(url) {
|
|
|
604
662
|
prefetchCache.set(url, entry);
|
|
605
663
|
return entry.promise;
|
|
606
664
|
}
|
|
607
|
-
function prefetchRouteState(url) {
|
|
665
|
+
function prefetchRouteState(url, route) {
|
|
666
|
+
if (route && !routeNeedsServerFetch(route)) return EMPTY_ROUTE_STATE_PROMISE;
|
|
608
667
|
const cached = getCachedRouteState(url);
|
|
609
668
|
if (cached) return cached;
|
|
610
669
|
sweepPrefetchCache();
|
|
@@ -657,10 +716,9 @@ function setupPrefetching(app, warmModules) {
|
|
|
657
716
|
return entry;
|
|
658
717
|
}
|
|
659
718
|
function prefetchHref(href) {
|
|
660
|
-
prefetchRouteState(href);
|
|
661
|
-
if (!warmModules) return;
|
|
662
719
|
const match = getMatchEntry(href).match;
|
|
663
|
-
|
|
720
|
+
prefetchRouteState(href, match?.route);
|
|
721
|
+
if (warmModules && match) warmModules(match);
|
|
664
722
|
}
|
|
665
723
|
document.addEventListener("mouseenter", (e) => {
|
|
666
724
|
const anchor = e.target.closest?.("a");
|
|
@@ -734,7 +792,8 @@ function trimMapToSize(map, maxEntries) {
|
|
|
734
792
|
//#endregion
|
|
735
793
|
//#region src/runtime-hooks.ts
|
|
736
794
|
const RouteDataContext = createContext(void 0);
|
|
737
|
-
function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, routeId, stateVersion = 0, url }) {
|
|
795
|
+
function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, routeId, routes, stateVersion = 0, url }) {
|
|
796
|
+
registerRuntimeRoutes(routes);
|
|
738
797
|
const [routeDataState, setRouteDataState] = useState({
|
|
739
798
|
data,
|
|
740
799
|
stateVersion
|
|
@@ -755,6 +814,7 @@ function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, ro
|
|
|
755
814
|
data: routeData,
|
|
756
815
|
params,
|
|
757
816
|
routeId,
|
|
817
|
+
routes,
|
|
758
818
|
setData: (nextData) => setRouteDataState({
|
|
759
819
|
data: nextData,
|
|
760
820
|
stateVersion
|
|
@@ -764,6 +824,7 @@ function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, ro
|
|
|
764
824
|
routeData,
|
|
765
825
|
params,
|
|
766
826
|
routeId,
|
|
827
|
+
routes,
|
|
767
828
|
stateVersion,
|
|
768
829
|
url
|
|
769
830
|
]);
|
|
@@ -811,6 +872,23 @@ function useRevalidate() {
|
|
|
811
872
|
return result.data;
|
|
812
873
|
};
|
|
813
874
|
}
|
|
875
|
+
function Link(props) {
|
|
876
|
+
const routes = useContext(RouteDataContext)?.routes ?? globalThis.__PRACHT_ROUTE_DEFINITIONS__;
|
|
877
|
+
if (!routes) throw new Error("<Link route=...> must render inside a pracht route tree.");
|
|
878
|
+
const { route, params, search, hash, ...anchorProps } = props;
|
|
879
|
+
return h("a", {
|
|
880
|
+
...anchorProps,
|
|
881
|
+
href: buildHref(routes, route, {
|
|
882
|
+
params,
|
|
883
|
+
search,
|
|
884
|
+
hash
|
|
885
|
+
})
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function registerRuntimeRoutes(routes) {
|
|
889
|
+
if (!routes) return;
|
|
890
|
+
globalThis.__PRACHT_ROUTE_DEFINITIONS__ = routes;
|
|
891
|
+
}
|
|
814
892
|
function Form(props) {
|
|
815
893
|
const { onSubmit, method, ...rest } = props;
|
|
816
894
|
return h("form", {
|
|
@@ -1199,6 +1277,7 @@ async function renderRouteErrorResponse(options) {
|
|
|
1199
1277
|
body: await renderToString(h(PrachtRuntimeProvider, {
|
|
1200
1278
|
data: null,
|
|
1201
1279
|
routeId: options.routeId,
|
|
1280
|
+
routes: options.routes,
|
|
1202
1281
|
url: options.requestPath
|
|
1203
1282
|
}, componentTree)),
|
|
1204
1283
|
hydrationState: {
|
|
@@ -1326,6 +1405,7 @@ async function handlePrachtRequest(options) {
|
|
|
1326
1405
|
if (hasDataParam) url.searchParams.delete("_data");
|
|
1327
1406
|
const requestPath = getRequestPath(url);
|
|
1328
1407
|
const registry = options.registry ?? {};
|
|
1408
|
+
const resolvedApp = getResolvedApp(options.app);
|
|
1329
1409
|
const headerSignalsRouteState = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1";
|
|
1330
1410
|
const dataParamIsFirstParty = hasDataParam && isFirstPartyFetch(options.request);
|
|
1331
1411
|
const isRouteStateRequest = headerSignalsRouteState || dataParamIsFirstParty;
|
|
@@ -1380,7 +1460,7 @@ async function handlePrachtRequest(options) {
|
|
|
1380
1460
|
}
|
|
1381
1461
|
}
|
|
1382
1462
|
}
|
|
1383
|
-
const match = matchAppRoute(
|
|
1463
|
+
const match = matchAppRoute(resolvedApp, url.pathname);
|
|
1384
1464
|
if (!match) {
|
|
1385
1465
|
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Not found", 404, {
|
|
1386
1466
|
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
@@ -1462,11 +1542,18 @@ async function handlePrachtRequest(options) {
|
|
|
1462
1542
|
const modulePreloadUrls = resolvePageJsUrls(options.jsManifest, match.route.shellFile, match.route.file);
|
|
1463
1543
|
if (match.route.render === "spa") {
|
|
1464
1544
|
let body = "";
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1545
|
+
const Shell = shellModule?.Shell;
|
|
1546
|
+
const Loading = shellModule?.Loading;
|
|
1547
|
+
const loadingTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
1548
|
+
if (loadingTree) {
|
|
1549
|
+
const tree = h(PrachtRuntimeProvider, {
|
|
1550
|
+
data: null,
|
|
1551
|
+
params: match.params,
|
|
1552
|
+
routeId: match.route.id ?? "",
|
|
1553
|
+
routes: resolvedApp.routes,
|
|
1554
|
+
url: requestPath
|
|
1555
|
+
}, loadingTree);
|
|
1556
|
+
body = await (await getRenderToStringAsync())(tree);
|
|
1470
1557
|
}
|
|
1471
1558
|
return htmlResponse(buildHtmlDocument({
|
|
1472
1559
|
head,
|
|
@@ -1498,6 +1585,7 @@ async function handlePrachtRequest(options) {
|
|
|
1498
1585
|
data,
|
|
1499
1586
|
params: match.params,
|
|
1500
1587
|
routeId: match.route.id ?? "",
|
|
1588
|
+
routes: resolvedApp.routes,
|
|
1501
1589
|
url: requestPath
|
|
1502
1590
|
}, componentTree);
|
|
1503
1591
|
return htmlResponse(buildHtmlDocument({
|
|
@@ -1523,6 +1611,7 @@ async function handlePrachtRequest(options) {
|
|
|
1523
1611
|
routeArgs,
|
|
1524
1612
|
routeId: match.route.id ?? "",
|
|
1525
1613
|
routeModule,
|
|
1614
|
+
routes: resolvedApp.routes,
|
|
1526
1615
|
shellFile: match.route.shellFile,
|
|
1527
1616
|
shellModule,
|
|
1528
1617
|
requestPath
|
|
@@ -1532,8 +1621,24 @@ async function handlePrachtRequest(options) {
|
|
|
1532
1621
|
function getRequestPath(url) {
|
|
1533
1622
|
return `${url.pathname}${url.search}`;
|
|
1534
1623
|
}
|
|
1624
|
+
function getResolvedApp(app) {
|
|
1625
|
+
const routes = app.routes;
|
|
1626
|
+
if (routes.length === 0 || isHrefRouteDefinition(routes[0])) return app;
|
|
1627
|
+
return resolveApp(app);
|
|
1628
|
+
}
|
|
1629
|
+
function isHrefRouteDefinition(value) {
|
|
1630
|
+
return Boolean(value && typeof value === "object" && "path" in value && "segments" in value && Array.isArray(value.segments));
|
|
1631
|
+
}
|
|
1535
1632
|
//#endregion
|
|
1536
1633
|
//#region src/prerender.ts
|
|
1634
|
+
const DANGEROUS_PRERENDER_HEADER_NAMES = new Set([
|
|
1635
|
+
"authorization",
|
|
1636
|
+
"proxy-authenticate",
|
|
1637
|
+
"proxy-authorization",
|
|
1638
|
+
"set-cookie",
|
|
1639
|
+
"www-authenticate"
|
|
1640
|
+
]);
|
|
1641
|
+
const SECRET_SHAPED_PRERENDER_HEADER_RE = /^x-.*(?:api[-_]?key|client[-_]?secret|credential|jwt[-_]?secret|password|private[-_]?key|refresh[-_]?token|secret|session[-_]?secret|token|webhook[-_]?secret)(?:$|[-_])/i;
|
|
1537
1642
|
async function prerenderApp(options) {
|
|
1538
1643
|
const resolved = resolveApp(options.app);
|
|
1539
1644
|
const results = [];
|
|
@@ -1567,6 +1672,7 @@ async function prerenderApp(options) {
|
|
|
1567
1672
|
console.warn(` Warning: ${item.render.toUpperCase()} route "${item.pathname}" returned status ${response.status}, skipping.`);
|
|
1568
1673
|
return null;
|
|
1569
1674
|
}
|
|
1675
|
+
assertSafePrerenderHeaders(response.headers, item);
|
|
1570
1676
|
const html = await response.text();
|
|
1571
1677
|
return {
|
|
1572
1678
|
headers: Object.fromEntries(response.headers),
|
|
@@ -1590,6 +1696,16 @@ async function prerenderApp(options) {
|
|
|
1590
1696
|
};
|
|
1591
1697
|
return results;
|
|
1592
1698
|
}
|
|
1699
|
+
function assertSafePrerenderHeaders(headers, item) {
|
|
1700
|
+
const dangerous = [...headers.keys()].filter(isDangerousPrerenderHeader);
|
|
1701
|
+
if (dangerous.length === 0) return;
|
|
1702
|
+
const names = dangerous.map((name) => `"${name}"`).join(", ");
|
|
1703
|
+
throw new Error(`Refusing to prerender ${item.render.toUpperCase()} route "${item.pathname}" because its document headers include ${names}. SSG/ISG document headers are serialized into public static output and replayed for every visitor. Move cookies/authentication headers to API routes, loaders, middleware responses, or SSR-only routes.`);
|
|
1704
|
+
}
|
|
1705
|
+
function isDangerousPrerenderHeader(name) {
|
|
1706
|
+
const normalized = name.toLowerCase();
|
|
1707
|
+
return DANGEROUS_PRERENDER_HEADER_NAMES.has(normalized) || SECRET_SHAPED_PRERENDER_HEADER_RE.test(normalized);
|
|
1708
|
+
}
|
|
1593
1709
|
async function collectSSGPaths(route, registry) {
|
|
1594
1710
|
if (!route.segments.some((s) => s.type === "param" || s.type === "catchall")) return [route.path];
|
|
1595
1711
|
const routeModule = await resolveRegistryModule(registry?.routeModules, route.file);
|
|
@@ -1708,6 +1824,7 @@ async function initClientRouter(options) {
|
|
|
1708
1824
|
data,
|
|
1709
1825
|
params,
|
|
1710
1826
|
routeId,
|
|
1827
|
+
routes: app.routes,
|
|
1711
1828
|
stateVersion: version,
|
|
1712
1829
|
url
|
|
1713
1830
|
}, componentTree));
|
|
@@ -1789,9 +1906,10 @@ async function initClientRouter(options) {
|
|
|
1789
1906
|
activeNavigationAbort?.abort();
|
|
1790
1907
|
const abortController = new AbortController();
|
|
1791
1908
|
activeNavigationAbort = abortController;
|
|
1792
|
-
const
|
|
1909
|
+
const navigationTarget = typeof to === "string" ? to : buildHref(app.routes, to.route, to);
|
|
1910
|
+
const target = resolveBrowserRouteTarget(navigationTarget);
|
|
1793
1911
|
if (!target) {
|
|
1794
|
-
window.location.href =
|
|
1912
|
+
window.location.href = navigationTarget;
|
|
1795
1913
|
return;
|
|
1796
1914
|
}
|
|
1797
1915
|
const match = matchAppRoute(app, target.pathname);
|
|
@@ -1799,7 +1917,12 @@ async function initClientRouter(options) {
|
|
|
1799
1917
|
window.location.href = target.browserUrl;
|
|
1800
1918
|
return;
|
|
1801
1919
|
}
|
|
1802
|
-
|
|
1920
|
+
let statePromise;
|
|
1921
|
+
if (routeNeedsServerFetch(match.route)) statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl, { signal: abortController.signal });
|
|
1922
|
+
else statePromise = Promise.resolve({
|
|
1923
|
+
type: "data",
|
|
1924
|
+
data: void 0
|
|
1925
|
+
});
|
|
1803
1926
|
const routeModPromise = startRouteImport(match);
|
|
1804
1927
|
const shellModPromise = startShellImport(match);
|
|
1805
1928
|
let state = {
|
|
@@ -1963,4 +2086,4 @@ var PrachtHttpError = class extends Error {
|
|
|
1963
2086
|
}
|
|
1964
2087
|
};
|
|
1965
2088
|
//#endregion
|
|
1966
|
-
export { Form, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|
|
2089
|
+
export { Form, Link, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildHref, buildPathFromSegments, createHref, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pracht/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Core runtime for Pracht, a full-stack Preact framework with per-route SSR, SSG, ISG, SPA, loaders, and API routes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pracht",
|
|
7
|
+
"preact",
|
|
8
|
+
"framework",
|
|
9
|
+
"full-stack",
|
|
10
|
+
"ssr",
|
|
11
|
+
"ssg",
|
|
12
|
+
"isg",
|
|
13
|
+
"routing",
|
|
14
|
+
"loaders",
|
|
15
|
+
"api-routes"
|
|
16
|
+
],
|
|
4
17
|
"license": "MIT",
|
|
5
18
|
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
|
|
6
19
|
"bugs": {
|