@kasperrt/smolter 0.4.1 → 0.4.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.
Files changed (86) hide show
  1. package/.generated/client/deployment.ts +40 -25
  2. package/.generated/core/environment.ts +1 -0
  3. package/.generated/core/navigation.ts +1 -0
  4. package/.generated/core/resource-types.ts +4 -0
  5. package/.generated/core/resources.ts +2 -0
  6. package/.generated/core/responses.ts +12 -0
  7. package/.generated/core/utils.ts +5 -2
  8. package/.generated/files/index.ts +5 -3
  9. package/.generated/preact/await-fallback.tsx +33 -0
  10. package/.generated/preact/await.tsx +23 -5
  11. package/.generated/preact/client.tsx +15 -4
  12. package/.generated/preact/deferred-hydration.ts +14 -1
  13. package/.generated/preact/document.tsx +10 -0
  14. package/.generated/preact/index.ts +6 -2
  15. package/.generated/preact/lazy-route.ts +7 -0
  16. package/.generated/preact/navigation.tsx +6 -0
  17. package/.generated/preact/root-context.ts +4 -0
  18. package/.generated/preact/route-hooks.tsx +12 -2
  19. package/.generated/preact/route-types.ts +14 -0
  20. package/.generated/preact/router.tsx +3 -0
  21. package/.generated/react/data.ts +5 -1
  22. package/.generated/react/index.ts +11 -1
  23. package/.generated/react/outlet.ts +48 -0
  24. package/.generated/react/resources/components.tsx +12 -0
  25. package/.generated/server/application.tsx +13 -0
  26. package/.generated/server/assets.ts +7 -0
  27. package/.generated/server/handler.ts +3 -0
  28. package/.generated/server/http.ts +2 -0
  29. package/.generated/server/node.ts +5 -2
  30. package/.generated/server/preact.ts +5 -2
  31. package/.generated/vite/application/lazy-files.ts +10 -17
  32. package/.generated/vite/index.ts +14 -0
  33. package/CHANGELOG.md +18 -0
  34. package/README.md +38 -18
  35. package/dist/client/deployment.js +30 -22
  36. package/dist/core/environment.d.ts +1 -0
  37. package/dist/core/navigation.d.ts +1 -0
  38. package/dist/core/resource-types.d.ts +4 -0
  39. package/dist/core/resources.d.ts +2 -0
  40. package/dist/core/resources.js +2 -0
  41. package/dist/core/responses.d.ts +12 -0
  42. package/dist/core/responses.js +5 -0
  43. package/dist/core/utils.d.ts +5 -2
  44. package/dist/core/utils.js +1 -2
  45. package/dist/files/index.js +4 -3
  46. package/dist/preact/await-fallback.d.ts +4 -0
  47. package/dist/preact/await-fallback.js +29 -0
  48. package/dist/preact/await.d.ts +5 -1
  49. package/dist/preact/await.js +22 -3
  50. package/dist/preact/client.d.ts +9 -1
  51. package/dist/preact/client.js +8 -3
  52. package/dist/preact/deferred-hydration.d.ts +1 -1
  53. package/dist/preact/deferred-hydration.js +7 -1
  54. package/dist/preact/document.d.ts +10 -0
  55. package/dist/preact/document.js +10 -1
  56. package/dist/preact/index.d.ts +6 -2
  57. package/dist/preact/lazy-route.d.ts +2 -0
  58. package/dist/preact/lazy-route.js +6 -0
  59. package/dist/preact/navigation.d.ts +6 -0
  60. package/dist/preact/navigation.js +2 -0
  61. package/dist/preact/root-context.d.ts +1 -0
  62. package/dist/preact/route-hooks.d.ts +10 -0
  63. package/dist/preact/route-hooks.js +13 -3
  64. package/dist/preact/route-types.d.ts +14 -0
  65. package/dist/preact/router.d.ts +3 -0
  66. package/dist/preact/router.js +3 -1
  67. package/dist/react/data.d.ts +5 -1
  68. package/dist/react/index.d.ts +6 -1
  69. package/dist/react/index.js +2 -1
  70. package/dist/react/outlet.d.ts +16 -0
  71. package/dist/react/outlet.js +26 -0
  72. package/dist/react/resources/components.d.ts +12 -0
  73. package/dist/react/resources/components.js +11 -0
  74. package/dist/server/application.d.ts +3 -0
  75. package/dist/server/application.js +10 -0
  76. package/dist/server/assets.d.ts +2 -0
  77. package/dist/server/assets.js +6 -0
  78. package/dist/server/handler.d.ts +3 -0
  79. package/dist/server/http.d.ts +2 -0
  80. package/dist/server/http.js +2 -0
  81. package/dist/server/node.d.ts +5 -1
  82. package/dist/server/preact.d.ts +5 -1
  83. package/dist/vite/application/lazy-files.js +5 -11
  84. package/dist/vite/index.js +6 -0
  85. package/jsr.json +1 -1
  86. package/package.json +6 -5
@@ -7,30 +7,38 @@ import type {
7
7
  import type { DataResponse } from "../core/responses.ts";
8
8
  import type { ComponentType, ReactNode } from "preact/compat";
9
9
 
10
+ /** Request, route parameters and cancellation signal supplied to a loader. */
10
11
  export interface LoaderFunctionArgs extends DataArgs {
11
12
  context?: unknown;
12
13
  }
13
14
 
15
+ /** Request, route parameters and cancellation signal supplied to an action. */
14
16
  export type ActionFunctionArgs = LoaderFunctionArgs;
15
17
 
18
+ /** Loader arguments with a serverLoader callback for the server transport. */
16
19
  export interface ClientLoaderFunctionArgs extends LoaderFunctionArgs {
17
20
  serverLoader: <T = unknown>() => Promise<T>;
18
21
  }
19
22
 
23
+ /** Action arguments with a serverAction callback for the server transport. */
20
24
  export interface ClientActionFunctionArgs extends LoaderFunctionArgs {
21
25
  serverAction: <T = unknown>() => Promise<T>;
22
26
  }
23
27
 
28
+ /** Navigation or mutation context used to decide whether loader data should refresh. */
24
29
  export type ShouldRevalidateFunctionArgs = RevalidationArgs;
25
30
 
26
31
  type UnwrapData<T> = T extends DataResponse<infer V> ? V : T;
27
32
 
33
+ /** Infer successful loader data, unwrapping promises and data responses. */
28
34
  export type RouteData<T> = T extends (...args: never[]) => infer R
29
35
  ? UnwrapData<Exclude<Awaited<R>, Response | Error>>
30
36
  : T;
31
37
 
38
+ /** A document metadata descriptor, such as a title, meta tag or structured-data record. */
32
39
  export type MetaDescriptor = Record<string, unknown>;
33
40
 
41
+ /** Loader data, error, parameters and location supplied to a route metadata function. */
34
42
  export interface MetaArgs<T> {
35
43
  data: RouteData<T> | undefined;
36
44
  loaderData: RouteData<T> | undefined;
@@ -39,10 +47,13 @@ export interface MetaArgs<T> {
39
47
  location: URL;
40
48
  }
41
49
 
50
+ /** Derive document metadata descriptors from the current route data and location. */
42
51
  export type MetaFunction<T = unknown> = (args: MetaArgs<T>) => MetaDescriptor[];
43
52
 
53
+ /** Return link descriptors rendered in the document head. */
44
54
  export type LinksFunction = () => Record<string, string>[];
45
55
 
56
+ /** Data route with optional lazy component, preload and document metadata integration. */
46
57
  export interface AppRoute extends Route {
47
58
  resolveComponent?: () => ComponentType | undefined;
48
59
  preload?: () => Promise<void>;
@@ -51,6 +62,7 @@ export interface AppRoute extends Route {
51
62
  meta?: MetaFunction;
52
63
  }
53
64
 
65
+ /** Root document components, loader, metadata and revalidation policy for a Preact application. */
54
66
  export interface RootModule {
55
67
  /** Route identity used by targeted revalidation. Defaults to root. */
56
68
  id?: string;
@@ -65,12 +77,14 @@ export interface RootModule {
65
77
  shouldRevalidate?: (args: RevalidationArgs) => boolean;
66
78
  }
67
79
 
80
+ /** Serialized root data and route snapshot used to hydrate without repeating initial loaders. */
68
81
  export interface Hydration {
69
82
  rootError?: boolean;
70
83
  root: unknown;
71
84
  snapshot: Snapshot;
72
85
  }
73
86
 
87
+ /** Transform a data transport response using the current request context. */
74
88
  export type HandleDataRequestFunction = (
75
89
  response: Response,
76
90
  args: LoaderFunctionArgs,
@@ -14,11 +14,13 @@ import type { AppRoute } from "./route-types.ts";
14
14
  import { Routes } from "./routes.ts";
15
15
  import { useContext, useMemo, useSyncExternalStore } from "./runtime.ts";
16
16
 
17
+ /** Optional children and context shared with descendants through useOutletContext. */
17
18
  export interface OutletProps {
18
19
  context?: unknown;
19
20
  children?: ReactNode;
20
21
  }
21
22
 
23
+ /** Render supplied children or the active route, and provide outlet context to descendants. */
22
24
  export function Outlet({
23
25
  context,
24
26
  children,
@@ -37,6 +39,7 @@ export interface ApplicationProps extends ApplicationContext {
37
39
  rootStore?: ReturnType<typeof createRootStore>;
38
40
  }
39
41
 
42
+ /** Render a Preact root document and active route with shared root data and application context. */
40
43
  export function RouterApplication(
41
44
  props: ApplicationProps,
42
45
  ): import("preact").JSX.Element {
@@ -1,4 +1,8 @@
1
- /** Resource subscriptions independent of URL navigation. @module */
1
+ /**
2
+ * Independent React resource data, forms and subscriptions.
3
+ *
4
+ * @module
5
+ */
2
6
  export type {
3
7
  ActionFunctionArgs as ResourceActionArgs,
4
8
  DataResult as ResourceResult,
@@ -1,4 +1,8 @@
1
- /** Framework components and typed data hooks. @module */
1
+ /**
2
+ * React routing components, typed data hooks and forms.
3
+ *
4
+ * @module
5
+ */
2
6
  export type * from "../core/index.ts";
3
7
  export {
4
8
  createDataRouter,
@@ -14,4 +18,10 @@ export * from "./fetcher.ts";
14
18
  export * from "./form.ts";
15
19
  export * from "./hooks.ts";
16
20
  export * from "./link.ts";
21
+ export {
22
+ Outlet,
23
+ type OutletProps,
24
+ useMatches,
25
+ useOutletContext,
26
+ } from "./outlet.ts";
17
27
  export * from "./routes.ts";
@@ -0,0 +1,48 @@
1
+ import { useRouter, useSnapshot } from "./context.ts";
2
+ import { Routes } from "./routes.ts";
3
+ import {
4
+ createContext,
5
+ createElement,
6
+ type ReactNode,
7
+ useContext,
8
+ } from "./runtime.ts";
9
+
10
+ const OutletContext = createContext<unknown>(undefined);
11
+
12
+ /** Optional children and context shared with descendants through useOutletContext. */
13
+ export interface OutletProps {
14
+ context?: unknown;
15
+ children?: ReactNode;
16
+ }
17
+
18
+ /** Render the active route under a layout's shared context. */
19
+ export function Outlet({ context, children }: OutletProps): ReactNode {
20
+ const inherited = useContext(OutletContext);
21
+ return createElement(
22
+ OutletContext.Provider,
23
+ { value: context ?? inherited },
24
+ children ??
25
+ createElement(Routes, {
26
+ fallback: createElement("p", { role: "status" }, "Loading…"),
27
+ }),
28
+ );
29
+ }
30
+
31
+ /** Read the nearest Outlet's context. */
32
+ export function useOutletContext<T>(): T {
33
+ return useContext(OutletContext) as T;
34
+ }
35
+
36
+ /** Read the active flat route match; this is not a nested layout match chain. */
37
+ export function useMatches(): {
38
+ id: string;
39
+ data: unknown;
40
+ params: import("../core/index.ts").Params;
41
+ }[] {
42
+ const router = useRouter();
43
+ const snapshot = useSnapshot();
44
+ const route = router.routes.find((item) => item.path === snapshot.path);
45
+ return [
46
+ { id: route?.id ?? "", data: snapshot.data, params: snapshot.params },
47
+ ];
48
+ }
@@ -23,6 +23,7 @@ import {
23
23
  useSyncExternalStore,
24
24
  } from "react";
25
25
 
26
+ /** Resource handlers with optional React content, pending UI and retry boundary. */
26
27
  export interface ResourceRouteModule extends ResourceModule {
27
28
  default?: ComponentType;
28
29
  ErrorBoundary?: ComponentType<{ retry: () => void }>;
@@ -42,6 +43,7 @@ interface RouterProviderProps extends PropsWithChildren {
42
43
  router: ResourceRouter;
43
44
  }
44
45
 
46
+ /** Provide a resource router and load initial matches; dispose it when unmounted. */
45
47
  export function RouterProvider({
46
48
  children,
47
49
  initialMatches = EMPTY_MATCHES,
@@ -66,6 +68,7 @@ interface RouteProps extends PropsWithChildren {
66
68
  routeId: string;
67
69
  }
68
70
 
71
+ /** Provide a resource route and render its content, loading state or retry boundary. */
69
72
  export function Route({
70
73
  children,
71
74
  params = {},
@@ -121,12 +124,14 @@ export function Route({
121
124
  );
122
125
  }
123
126
 
127
+ /** Read the current resource route loader result. */
124
128
  export function useLoaderData<Data>(): Data | null {
125
129
  const route = useCurrentRoute();
126
130
 
127
131
  return useRouteLoaderData<Data>(route.routeId, route.params);
128
132
  }
129
133
 
134
+ /** Read loader data for a registered resource route. */
130
135
  export function useRouteLoaderData<Data>(
131
136
  routeId: string,
132
137
  params: Params = {},
@@ -136,6 +141,7 @@ export function useRouteLoaderData<Data>(
136
141
  return state.loaderData as Data | null;
137
142
  }
138
143
 
144
+ /** Read the current resource route action result. */
139
145
  export function useActionData<Data>(): Data | null {
140
146
  const route = useCurrentRoute();
141
147
  const state = useRouteState(route.routeId, route.params, false);
@@ -143,6 +149,7 @@ export function useActionData<Data>(): Data | null {
143
149
  return state.actionData as Data | null;
144
150
  }
145
151
 
152
+ /** Read the current page navigation or submission state. */
146
153
  export function useNavigation(): { state: NavigationState } {
147
154
  const route = useCurrentRoute();
148
155
  const state = useRouteState(route.routeId, route.params, false);
@@ -150,6 +157,7 @@ export function useNavigation(): { state: NavigationState } {
150
157
  return { state: state.navigationState };
151
158
  }
152
159
 
160
+ /** Return the resource route revalidation function. */
153
161
  export function useRevalidator(): {
154
162
  revalidate: () => Promise<DataResult<unknown>>;
155
163
  state: string;
@@ -183,6 +191,7 @@ interface FormProps extends PropsWithChildren {
183
191
  params?: Params;
184
192
  }
185
193
 
194
+ /** Submit a resource action from a form without document navigation. */
186
195
  export function Form({
187
196
  action,
188
197
  children,
@@ -208,6 +217,7 @@ export function Form({
208
217
  return <FormContext.Provider value={submit}>{children}</FormContext.Provider>;
209
218
  }
210
219
 
220
+ /** Return a function that submits form data through the current router. */
211
221
  export function useSubmit(): (input?: unknown) => Promise<DataResult<unknown>> {
212
222
  const submit = useContext(FormContext);
213
223
 
@@ -216,6 +226,7 @@ export function useSubmit(): (input?: unknown) => Promise<DataResult<unknown>> {
216
226
  return submit;
217
227
  }
218
228
 
229
+ /** Read or mutate a resource independently of the rendered route. */
219
230
  export function useFetcher<Data = unknown>(
220
231
  options: FetcherOptions = {},
221
232
  ): readonly [
@@ -312,6 +323,7 @@ export function useFetcher<Data = unknown>(
312
323
  return [fetcherState, fetcherOperations] as const;
313
324
  }
314
325
 
326
+ /** Read the resource router supplied by its provider. */
315
327
  export function useRouter(): ResourceRouter {
316
328
  const router = useContext(RouterContext);
317
329
 
@@ -9,17 +9,21 @@ import {
9
9
  import type { AppRoute, RootModule } from "../preact/route-types.ts";
10
10
  import { prepareRoutes, RouterApplication } from "../preact/router.tsx";
11
11
  import { loadRouter } from "./index.ts";
12
+ import type { ComponentChildren } from "preact";
13
+ import { Suspense } from "preact/compat";
12
14
  import { renderToReadableStream } from "preact-render-to-string/stream";
13
15
  import { deferredHydration } from "./deferred-hydration.ts";
14
16
  import { privateResponse } from "./private-response.ts";
15
17
  import { progressiveDocument } from "./progressive-document.ts";
16
18
  import { resolveHydration, streamDocument } from "./stream-document.ts";
17
19
 
20
+ /** SSR settings for CSP nonce and early deferred-data hydration. */
18
21
  export interface RenderApplicationOptions {
19
22
  nonce?: string;
20
23
  earlyHydration?: boolean;
21
24
  }
22
25
 
26
+ /** Render a Preact application response with root and route data, streaming deferred content when enabled. */
23
27
  export async function renderApplication(
24
28
  request: Request,
25
29
  routes: AppRoute[],
@@ -147,6 +151,7 @@ export async function renderApplication(
147
151
  entry={entry}
148
152
  rootError={Boolean(rootError)}
149
153
  streaming
154
+ deferredBoundary={options.earlyHydration ? undefined : deferredBoundary}
150
155
  initialData={deferred?.initial}
151
156
  document
152
157
  />,
@@ -181,6 +186,13 @@ export async function renderApplication(
181
186
  );
182
187
  }
183
188
 
189
+ function deferredBoundary(
190
+ children: ComponentChildren,
191
+ fallback: ComponentChildren,
192
+ ) {
193
+ return <Suspense fallback={fallback}>{children}</Suspense>;
194
+ }
195
+
184
196
  async function dispatch(
185
197
  request: Request,
186
198
  routes: AppRoute[],
@@ -250,6 +262,7 @@ async function dispatch(
250
262
  return Response.json((await normalizeResult(value)) ?? null);
251
263
  }
252
264
 
265
+ /** Dispatch a normalized application URL to its route loader or action and return an HTTP response. */
253
266
  export async function dispatchRoute(
254
267
  request: Request,
255
268
  routes: AppRoute[],
@@ -1,11 +1,18 @@
1
+ /**
2
+ * Production route asset discovery for SSR preload hints.
3
+ *
4
+ * @module
5
+ */
1
6
  import { matchRoutes } from "../core/index.ts";
2
7
 
8
+ /** Emitted JavaScript and stylesheet URLs associated with a route. */
3
9
  export interface RouteAssets {
4
10
  path: string;
5
11
  files: string[];
6
12
  styles?: string[];
7
13
  }
8
14
 
15
+ /** Collect matched-route JavaScript and stylesheet assets from the production route manifest. */
9
16
  export function routePreloads(
10
17
  routes: RouteAssets[],
11
18
  pathname: string,
@@ -8,6 +8,7 @@ export interface ServerBuild {
8
8
  handleDataRequest: (request: Request) => Response | Promise<Response>;
9
9
  }
10
10
 
11
+ /** SSR build, browser entry URL and application-scoped data endpoint for Node or Express. */
11
12
  export interface RequestHandlerOptions {
12
13
  build: ServerBuild;
13
14
  /** Browser entry URL resolved from Vite's production manifest. */
@@ -16,12 +17,14 @@ export interface RequestHandlerOptions {
16
17
  dataEndpoint?: string;
17
18
  }
18
19
 
20
+ /** Node request with optional Express protocol and original URL information. */
19
21
  export interface ServerIncomingMessage extends IncomingMessage {
20
22
  /** Express supplies this using the application's trust proxy policy. */
21
23
  protocol?: string;
22
24
  originalUrl?: string;
23
25
  }
24
26
 
27
+ /** Express-compatible callback for forwarding middleware errors. */
25
28
  export type NextHandler = (error?: unknown) => void;
26
29
 
27
30
  /** Node/Express middleware; mount after metrics, tracing and static assets. */
@@ -3,6 +3,7 @@ import { Readable, Transform } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import type { ServerIncomingMessage } from "./handler.ts";
5
5
 
6
+ /** Convert a Node request to a Web Request with cancellation and an optional normalized route target. */
6
7
  export async function webRequest(
7
8
  incoming: ServerIncomingMessage,
8
9
  outgoing: ServerResponse,
@@ -40,6 +41,7 @@ interface StreamingResponse extends ServerResponse {
40
41
  flush?: () => void;
41
42
  }
42
43
 
44
+ /** Write a Web Response to Node with streaming backpressure and disconnect cancellation. */
43
45
  export async function send(response: Response, outgoing: StreamingResponse) {
44
46
  outgoing.statusCode = response.status;
45
47
  response.headers.forEach((value, name) => {
@@ -1,4 +1,7 @@
1
- /** Optional application integration. @module */
2
-
1
+ /**
2
+ * Streaming Node and Express adapters for Web requests and responses.
3
+ *
4
+ * @module
5
+ */
3
6
  export * from "./handler.ts";
4
7
  export * from "./http.ts";
@@ -1,5 +1,8 @@
1
- /** Preact server rendering and HTTP data dispatch. @module */
2
-
1
+ /**
2
+ * Preact SSR rendering and route data dispatch.
3
+ *
4
+ * @module
5
+ */
3
6
  export {
4
7
  dispatchRoute,
5
8
  type RenderApplicationOptions,
@@ -87,10 +87,7 @@ export function lazyFiles(
87
87
  const source = await readFile(route.file, "utf8");
88
88
  const transformed = await transformWithOxc(source, route.file);
89
89
  const ast = this.parse(transformed.code);
90
- const fields: Record<string, string | boolean> = {
91
- path: route.path,
92
- dataEndpoint,
93
- };
90
+ const fields: Record<string, string | boolean> = {};
94
91
 
95
92
  for (const statement of ast.body) {
96
93
  if (statement.type === "ExportDefaultDeclaration") {
@@ -120,22 +117,18 @@ export function lazyFiles(
120
117
  for (const declaration of statement.declaration.declarations) {
121
118
  if (declaration.id.type === "Identifier")
122
119
  fields[declaration.id.name] = true;
123
-
124
- if (
125
- declaration.id.type !== "Identifier" ||
126
- declaration.init?.type !== "Literal" ||
127
- typeof declaration.init.value !== "string"
128
- )
129
- continue;
130
-
131
- const name = declaration.id.name;
132
-
133
- if (name === "id") fields.id = declaration.init.value;
134
-
135
- if (name === "routePath") fields.path = declaration.init.value;
136
120
  }
137
121
  }
138
122
 
123
+ if (fields.id || fields.routePath)
124
+ return this.error(
125
+ `File routes derive identity and paths from filenames: remove id/routePath exports from ${route.file}. Use [.] for a literal dot.`,
126
+ );
127
+
128
+ // Export discovery records capabilities, never routing configuration.
129
+ fields.path = route.path;
130
+ fields.dataEndpoint = dataEndpoint;
131
+
139
132
  const file = `${route.file.replaceAll("\\", "/")}?smolter-client`;
140
133
 
141
134
  if (fields.default) modules.set(file, String(fields.path));
@@ -152,6 +152,20 @@ export function routerPlugin({
152
152
  for (const statement of ast.body) {
153
153
  if (statement.type !== "ExportNamedDeclaration") continue;
154
154
 
155
+ const declared = statement.declaration;
156
+ const names = statement.specifiers.map((specifier) =>
157
+ specifier.exported.type === "Identifier"
158
+ ? specifier.exported.name
159
+ : specifier.exported.value,
160
+ );
161
+ if (declared?.type === "VariableDeclaration")
162
+ for (const item of declared.declarations)
163
+ if (item.id.type === "Identifier") names.push(item.id.name);
164
+ if (names.some((name) => name === "id" || name === "routePath"))
165
+ return this.error(
166
+ `File routes derive identity and paths from filenames: remove id/routePath exports from ${file}. Use [.] for a literal dot.`,
167
+ );
168
+
155
169
  for (const specifier of statement.specifiers) {
156
170
  const name =
157
171
  specifier.exported.type === "Identifier"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3
4
+
5
+ - Prefetch individual guide articles on pointer, keyboard and touch intent. Keep the shared header, sidebar and current article visible during cold guide navigation; show a quiet pending status without replacing the layout. Link the header directly to the canonical Getting started guide.
6
+ - Publish compact React/Preact and SSR compatibility indicators in guide navigation and detailed support information on each page.
7
+ - Document every public JSR entrypoint and exported symbol. Validate the generated documentation before publishing, preserving JSR-before-npm release ordering.
8
+
9
+ - Add React `Outlet`, `useMatches` and `useOutletContext`; allow Preact matches under a plain RouterProvider. Show renderer and SSR compatibility in documentation navigation and on each page. Integrated document/early-hydration APIs remain Preact-only.
10
+
11
+ - Remove the public `safeWrapAsync` utility; retain it internally for router and server error handling. Existing consumers of this export must provide their own error handling. `useMatches` and `useOutletContext` remain public Preact APIs.
12
+
13
+ ## 0.4.2
14
+
15
+ - Add integrated `Await fallback` for early hydration: pending and resolved content belongs to the mounted component, so navigation, root revalidation and late settlement cannot leave abandoned content behind. Migrate early-hydrated deferred sections from outer Preact `Suspense` to this prop, including root sections. Native Preact streamed Suspense remains unsupported for this ownership contract; it is not patched. Ordinary non-early SSR still streams resolved HTML.
16
+ - Preserve deployment recovery ownership across overlapping page/fetcher submissions and watcher replacement, including old cleanup calls. Mutations are not replayed and pending submissions continue to prevent recovery reloads.
17
+ - Bound initial deferred-data hydration with one total deadline (30 seconds by default); validate integer millisecond budgets, reject outstanding promises on expiry and clear lifecycle state on disposal. This is not an inactivity timer or an immediate network-disconnect detector.
18
+ - Support literal dots in flat route filenames and folders: `robots[.]txt` maps to `/robots.txt`. Generated file routes reject `id` and `routePath` overrides; derive identity from their filename and use their path for targeted revalidation. Manually defined route arrays retain explicit identities.
19
+ - Keep the thirteen-case ownership matrix, streaming/CSP/head-preservation checks, scoped transports, deployment recovery and route HMR as production regression gates. Preserve the native Preact diagnostic separately; passing Smolter tests do not claim native Preact or Vibes/device parity.
20
+
3
21
  ## 0.4.1
4
22
 
5
23
  - Invalidate pending deployment recovery when an independent fetcher submits. Prevent recovery reloads while any tracked fetcher submission is pending, without blocking ordinary client navigation. Install the watcher before creating fetchers; mountApplication already does this.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # smolter _
2
2
 
3
3
  <!-- bundlejs:start -->
4
- [![react gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.1%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.1%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
5
- [![preact gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.1%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.1%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
4
+ [![react gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.3%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.3%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
5
+ [![preact gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.3%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.3%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
6
6
  <!-- bundlejs:end -->
7
7
 
8
8
  A typed data router for React and Preact. **Zero runtime dependencies.**
@@ -14,9 +14,9 @@ loader; `shouldRevalidate` decides when that work is needed.
14
14
  Inspired by [wouter](https://github.com/molefrog/wouter)
15
15
  and [React Router](https://reactrouter.com/).
16
16
 
17
- [Read the documentation](https://kasperrt.github.io/smolter/react/docs/) for guides and the complete API reference. The site itself uses smolter routes.
17
+ [Read the documentation](https://kasperrt.github.io/smolter/docs/) for guides and the complete API reference. The prerendered site uses Preact and Smolter routes. Articles load individually; React examples remain available in the repository.
18
18
 
19
- Start with [loaders](https://kasperrt.github.io/smolter/react/docs/loaders/), [actions](https://kasperrt.github.io/smolter/react/docs/actions/), [forms and validation](https://kasperrt.github.io/smolter/react/docs/forms/), or [fetcher reads and mutations](https://kasperrt.github.io/smolter/react/docs/fetchers/). Each guide connects the handler to an actual route URL and its consuming view.
19
+ Start with [loaders](https://kasperrt.github.io/smolter/docs/loaders/), [actions](https://kasperrt.github.io/smolter/docs/actions/), [forms and validation](https://kasperrt.github.io/smolter/docs/forms/), or [fetcher reads and mutations](https://kasperrt.github.io/smolter/docs/fetchers/). Each guide connects the handler to an actual route URL and its consuming view.
20
20
 
21
21
  ## Contents
22
22
 
@@ -123,7 +123,11 @@ route folders are ignored. `.ts`, `.tsx`, `.js`, and `.jsx` are supported.
123
123
  **Only flat matching:** there are no nested outlets, inherited loaders, or layout
124
124
  chains. A pathless group module itself is not rendered or executed. Group names
125
125
  do not provide authentication. Static paths are ordered before parameters, then
126
- splats. Conflicting patterns and unsupported optional/escaped syntax are rejected.
126
+ splats. Escape a literal dot with `[.]`: `robots[.]txt.ts` and
127
+ `robots[.]txt/route.ts` both map to `/robots.txt`. File routes derive their
128
+ identity and URL from their filename; do not export `id` or `routePath`.
129
+ Target revalidation with the generated path (or `root` for root data).
130
+ Conflicting patterns and unsupported optional/bracket syntax are rejected.
127
131
 
128
132
  For the virtual module, add this declaration to your app:
129
133
 
@@ -441,18 +445,18 @@ Choose one renderer for your application. Both expose the same routing API:
441
445
  | SSR and HTTP data dispatch | `@kasperrt/smolter/react/server` | `@kasperrt/smolter/preact/server` |
442
446
  | Vite plugin and flat-file discovery | `@kasperrt/smolter/react/vite` | `@kasperrt/smolter/preact/vite` |
443
447
 
444
- `@kasperrt/smolter/utils` contains browser-safe matching and error helpers.
448
+ `@kasperrt/smolter/utils` contains browser-safe matching and URL helpers.
445
449
  The main renderer entries never import server or build modules. Named imports
446
450
  are tree-shaken; optional transport is included only when you import `/client`.
447
451
  Server entries do not import either UI renderer. Vite entries are build-time only.
448
452
 
449
453
  **Migrating renderer subpaths:** replace `/core` imports for types, router factories, and
450
454
  `defineRoute` with your renderer entry. Use `/utils` for `matchRoutes`,
451
- `internalURL` and `safeWrapAsync`. Move `/client`, `/server`,
455
+ and `internalURL`. Move `/client`, `/server`,
452
456
  `/files`, and `/vite` imports to the corresponding renderer-prefixed path above.
453
457
  The bare package root and old top-level subpaths are no longer exported.
454
458
 
455
- See the [0.3 migration guide](https://kasperrt.github.io/smolter/react/docs/migration-0.3/) and [changelog](./CHANGELOG.md).
459
+ See the [0.3 migration guide](https://kasperrt.github.io/smolter/docs/migration-0.3/) and [changelog](./CHANGELOG.md).
456
460
 
457
461
  **Router factory migration:** code that previously used `createRouter` with
458
462
  loaders, actions, fetchers or revalidation must now use `createDataRouter`.
@@ -521,7 +525,7 @@ re-exports and installs server proxies. `entry.client.tsx` and `entry.server.tsx
521
525
  are application-owned files wiring those manifests to hydration and SSR.
522
526
  Manual tables can be imported directly instead.
523
527
 
524
- See the [virtual routes guide](https://kasperrt.github.io/smolter/react/docs/virtual-routes)
528
+ See the [virtual routes guide](https://kasperrt.github.io/smolter/docs/virtual-routes)
525
529
  for the exact contract, TypeScript declaration, and both entrypoint examples.
526
530
 
527
531
  ### Express hosting
@@ -533,7 +537,7 @@ server entry and `entry` is the browser entry URL from Vite's manifest. Do not
533
537
  consume request bodies with Express body parsers before this handler.
534
538
  The adapter has no Express dependency and forwards failures to `next(error)`.
535
539
 
536
- See the [Express guide](https://kasperrt.github.io/smolter/react/docs/express)
540
+ See the [Express guide](https://kasperrt.github.io/smolter/docs/express)
537
541
  for a complete server example and streaming and hydration behavior.
538
542
 
539
543
  ## Resource subscriptions
@@ -603,12 +607,12 @@ a claim of identical features.
603
607
  | `React Router: data routing subset` | 29780 |
604
608
  | `React: routing-only named exports` | 1245 |
605
609
  | `Preact: routing-only named exports` | 1405 |
606
- | `@kasperrt/smolter/react` | 2948 |
607
- | `@kasperrt/smolter/preact` | 4842 |
608
- | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 4186 |
609
- | `@kasperrt/smolter/preact + server transport` | 6033 |
610
- | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 9034 |
611
- | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 9314 |
610
+ | `@kasperrt/smolter/react` | 3082 |
611
+ | `@kasperrt/smolter/preact` | 5016 |
612
+ | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 4419 |
613
+ | `@kasperrt/smolter/preact + server transport` | 6291 |
614
+ | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 9398 |
615
+ | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 9685 |
612
616
  | `@kasperrt/smolter/react/data (optional resource API)` | 2962 |
613
617
  | `React: common routing/data exports` | 2761 |
614
618
  | `Preact: common routing/data exports` | 3046 |
@@ -731,7 +735,7 @@ normally, and a session marker prevents reload loops. Development/SSR builds
731
735
  disable the helper. This optional client module adds nothing to the routing-only
732
736
  entrypoints and never replays form submissions.
733
737
 
734
- See [deployment updates](https://kasperrt.github.io/smolter/react/docs/deployments/)
738
+ See [deployment updates](https://kasperrt.github.io/smolter/docs/deployments/)
735
739
  for Express, CDN paths, TypeScript declarations and unsaved-state handling.
736
740
 
737
741
 
@@ -748,7 +752,7 @@ import { mountApplication } from "@kasperrt/smolter/preact/client";
748
752
  await mountApplication(routes, root, { deployment });
749
753
  ```
750
754
 
751
- Preact SSR starts root and route loaders concurrently. Return nested promises and render them with `Await` inside `Suspense` to stream a shell before optional data resolves. The Node/Express adapter streams the response; hydration starts after the document stream finishes by default. Pass `{ earlyHydration: true }` to `renderApplication` to hydrate the shell while optional promises stream independently. Root-loader failures render the root error boundary. Vite development HTML transformation still buffers the document.
755
+ Preact SSR starts root and route loaders concurrently. Return nested promises and use `Await` with an explicit `fallback` for component-owned pending content during early hydration. The Node/Express adapter streams the response; hydration starts after the document stream finishes by default. Pass `{ earlyHydration: true }` to `renderApplication` to hydrate the shell while optional promises stream independently. Initial deferred values share a total 30-second budget after their payload is read, not an inactivity timeout or reliable disconnect detector. Settlements do not extend it; configure `mountApplication` with `{ deferredTimeout: 60_000 }` for intentionally slower data (integer milliseconds from 1 through 2147483647; invalid values throw RangeError). A broken document can emit no completion event, so this deadline rejects unfinished values without retrying requests. Root-loader failures render the root error boundary. Vite development HTML transformation still buffers the document.
752
756
 
753
757
  Successful action redirects refresh registered root data without replaying the mutation. Loader arguments expose `reason` to distinguish initial loads, navigation, revalidation and fetcher reads without comparing browser URLs.
754
758
 
@@ -780,3 +784,19 @@ The generated route asset manifest follows static imports for each route and lea
780
784
  The file-router build emits `.vite/route-assets.json` with each route's static JavaScript dependency closure and its CSS in `styles`. `routePreloads` from `@kasperrt/smolter/utils/assets` produces modulepreload hints for JavaScript and `preload; as=style; crossorigin` hints for CSS (matching Vite’s anonymous CORS mode), respecting your CDN/base URL. Set these hints on the document response or send Early Hints from your host. Stylesheet preloads fetch files; they do not apply styles. Keep the application stylesheet links and Vite's CSS handling in place. Optional dynamic imports are excluded, and unrelated routes stay lazy.
781
785
 
782
786
  Root stylesheet URL imports such as `import stylesUrl from "./index.css?url"` are not route CSS imports. Keep rendering `<link rel="stylesheet" href={stylesUrl}>` in the root document; an empty route `styles` array is expected when all CSS is loaded this way. Smolter does not infer stylesheet use from arbitrary asset URLs. If measurements show late root CSS discovery, your host can send an explicit preload or Early Hint for that same resolved URL, with the same CORS mode as the stylesheet link. Preloading does not replace the stylesheet link. Do not reorganize CSS merely to populate the route manifest.
787
+
788
+
789
+ ### Component-owned deferred loading
790
+
791
+ For early Preact hydration, pass pending content directly to the built-in `Await`:
792
+
793
+ ```tsx
794
+ import { Await } from "@kasperrt/smolter/preact";
795
+
796
+ <Await resolve={data.stats} fallback={<p>Loading statistics…</p>}
797
+ errorElement={<p>Statistics unavailable.</p>}>
798
+ {(stats) => <Stats value={stats} />}
799
+ </Await>
800
+ ```
801
+
802
+ Replace the surrounding native `Suspense` for each deferred section, including root sections. This uses ordinary component-owned fallback HTML and the existing deferred data stream, avoiding Preact 11.0.1's abandoned streamed-boundary problem. No separate entrypoint, dependency patch, DOM cleanup, extra request or polling is needed. Late results cannot update an unmounted owner. Early-mode resolved content requires JavaScript; ordinary SSR without early hydration still streams resolved HTML. Existing `Await` without `fallback` keeps its Suspense semantics and its known early-hydration limitation. This does not repair arbitrary native Suspense/lazy boundaries.