@kasperrt/smolter 0.3.1 → 0.3.2

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 (67) hide show
  1. package/.generated/client/deployment-check.ts +70 -0
  2. package/.generated/client/deployment.ts +21 -58
  3. package/.generated/client/index.ts +8 -2
  4. package/.generated/core/responses.ts +8 -3
  5. package/.generated/core/router.ts +32 -0
  6. package/.generated/core/types.ts +2 -0
  7. package/.generated/preact/client.tsx +48 -6
  8. package/.generated/preact/document.tsx +4 -2
  9. package/.generated/preact/lazy-route.ts +2 -0
  10. package/.generated/preact/root-context.ts +2 -0
  11. package/.generated/preact/route-types.ts +1 -0
  12. package/.generated/preact/router.tsx +9 -3
  13. package/.generated/server/application.tsx +83 -19
  14. package/.generated/server/handler.ts +8 -2
  15. package/.generated/server/http.ts +24 -2
  16. package/.generated/server/index.ts +17 -5
  17. package/.generated/server/preact.ts +1 -0
  18. package/.generated/server/private-response.ts +11 -0
  19. package/.generated/server/stream-document.ts +121 -0
  20. package/.generated/vite/application/lazy-files.ts +8 -2
  21. package/.generated/vite/application/vite.ts +9 -3
  22. package/.generated/vite/index.ts +4 -2
  23. package/.generated/vite/transport.ts +4 -1
  24. package/CHANGELOG.md +10 -0
  25. package/README.md +29 -11
  26. package/dist/client/deployment-check.d.ts +6 -0
  27. package/dist/client/deployment-check.js +51 -0
  28. package/dist/client/deployment.d.ts +2 -0
  29. package/dist/client/deployment.js +11 -39
  30. package/dist/client/index.d.ts +3 -1
  31. package/dist/client/index.js +3 -3
  32. package/dist/core/responses.d.ts +1 -1
  33. package/dist/core/responses.js +3 -3
  34. package/dist/core/router.js +16 -0
  35. package/dist/core/types.d.ts +2 -0
  36. package/dist/preact/client.d.ts +6 -1
  37. package/dist/preact/client.js +27 -8
  38. package/dist/preact/document.js +4 -2
  39. package/dist/preact/lazy-route.d.ts +1 -0
  40. package/dist/preact/lazy-route.js +2 -1
  41. package/dist/preact/root-context.d.ts +2 -0
  42. package/dist/preact/route-types.d.ts +1 -0
  43. package/dist/preact/router.d.ts +1 -1
  44. package/dist/preact/router.js +3 -3
  45. package/dist/server/application.d.ts +4 -1
  46. package/dist/server/application.js +47 -13
  47. package/dist/server/handler.d.ts +3 -1
  48. package/dist/server/handler.js +5 -3
  49. package/dist/server/http.d.ts +5 -1
  50. package/dist/server/http.js +10 -1
  51. package/dist/server/index.d.ts +1 -1
  52. package/dist/server/index.js +12 -2
  53. package/dist/server/preact.d.ts +1 -1
  54. package/dist/server/private-response.d.ts +2 -0
  55. package/dist/server/private-response.js +13 -0
  56. package/dist/server/stream-document.d.ts +9 -0
  57. package/dist/server/stream-document.js +78 -0
  58. package/dist/vite/application/lazy-files.d.ts +1 -1
  59. package/dist/vite/application/lazy-files.js +5 -2
  60. package/dist/vite/application/vite.d.ts +2 -1
  61. package/dist/vite/application/vite.js +7 -4
  62. package/dist/vite/index.d.ts +2 -1
  63. package/dist/vite/index.js +6 -3
  64. package/dist/vite/transport.d.ts +1 -1
  65. package/dist/vite/transport.js +3 -2
  66. package/jsr.json +1 -1
  67. package/package.json +2 -2
@@ -19,7 +19,7 @@ function redirect(location, init = 302) {
19
19
  function isRouteErrorResponse(value) {
20
20
  return value instanceof Error && "status" in value;
21
21
  }
22
- async function normalizeResult(value) {
22
+ async function normalizeResult(value, resolveDeferred = true) {
23
23
  if (value && typeof value === "object" && "__routeError" in value) {
24
24
  const error = value.__routeError;
25
25
  return Object.assign(Error(error.message), error);
@@ -32,8 +32,8 @@ async function normalizeResult(value) {
32
32
  });
33
33
  return value.json();
34
34
  }
35
- if (value && typeof value === "object" && "__routeData" in value) return resolveData(value.value);
36
- return resolveData(value);
35
+ if (value && typeof value === "object" && "__routeData" in value) return resolveDeferred ? resolveData(value.value) : value.value;
36
+ return resolveDeferred ? resolveData(value) : value;
37
37
  }
38
38
  async function resolveData(value) {
39
39
  const resolved = await value;
@@ -71,6 +71,7 @@ function createDataRouter(routes, options = {}) {
71
71
  const fn = match.route[body ? "clientAction" : "clientLoader"] || match.route[body ? "action" : "loader"];
72
72
  if (!fn) return body && Error("No action");
73
73
  return fn({
74
+ reason: body ? "action" : navigation !== void 0 ? "navigation" : local ? "fetcher" : previous.loaded ? "revalidation" : "initial",
74
75
  request: new Request(url, {
75
76
  signal,
76
77
  method: body && method,
@@ -84,6 +85,21 @@ function createDataRouter(routes, options = {}) {
84
85
  const redirectTo = result.data instanceof Response && result.data.headers.get("Location");
85
86
  if (redirectTo && redirects) {
86
87
  await main.load(new URL(redirectTo, url).href, void 0, { replace: true }, true, redirects - 1);
88
+ if (body) {
89
+ const destination = new URL(main.getSnapshot().url);
90
+ const args = {
91
+ url: destination,
92
+ currentUrl: destination,
93
+ nextUrl: destination,
94
+ defaultShouldRevalidate: true,
95
+ reason: "action",
96
+ formMethod: method,
97
+ formAction: url.href,
98
+ formData: body,
99
+ actionResult: result.data
100
+ };
101
+ await Promise.all([...revalidators].filter((task) => task !== main).map((task) => task.revalidate(args)));
102
+ }
87
103
  if (!signal.aborted) {
88
104
  submission = void 0;
89
105
  emit("idle");
@@ -6,6 +6,8 @@
6
6
  export type Params = Record<string, string>;
7
7
  /** Request, path parameters, and cancellation signal passed to a data handler. */
8
8
  export interface DataArgs {
9
+ /** Why this handler ran; independent of when browser history is committed. */
10
+ reason?: "initial" | "navigation" | "revalidation" | "fetcher" | "action";
9
11
  request: Request;
10
12
  params: Params;
11
13
  signal: AbortSignal;
@@ -1,7 +1,12 @@
1
1
  /** Browser mounting and hydration for Preact applications. @module */
2
2
  /** @jsxImportSource preact */
3
+ import { type DeploymentOptions } from "../client/index.ts";
3
4
  import { type Router } from "../core/index.ts";
4
5
  import type { AppRoute, RootModule } from "./route-types.ts";
5
6
  export * from "../client/index.ts";
6
7
  export { configureApplication } from "../core/environment.ts";
7
- export declare function mountApplication(routes: AppRoute[], rootModule?: RootModule, hash?: boolean): Promise<Router>;
8
+ export interface MountApplicationOptions {
9
+ hash?: boolean;
10
+ deployment?: DeploymentOptions | null;
11
+ }
12
+ export declare function mountApplication(routes: AppRoute[], rootModule?: RootModule, options?: boolean | MountApplicationOptions): Promise<Router>;
@@ -6,7 +6,7 @@ import { connectNavigation } from "../core/navigation.js";
6
6
  import { attachRootData } from "./root-data.js";
7
7
  import { RouterApplication, prepareRoutes } from "./router.js";
8
8
  import { deserialize } from "../client/deserialize.js";
9
- import { watchDeployment } from "../client/deployment.js";
9
+ import { recoverDeployment, watchDeployment } from "../client/deployment.js";
10
10
  import { serverRoute } from "../client/index.js";
11
11
  import { flushSync } from "preact/compat";
12
12
  import { jsx } from "preact/jsx-runtime";
@@ -14,20 +14,31 @@ import { hydrate, render } from "preact";
14
14
  //#region .generated/preact/client.tsx
15
15
  /** Browser mounting and hydration for Preact applications. @module */
16
16
  /** @jsxImportSource preact */
17
- async function mountApplication(routes, rootModule, hash = false) {
17
+ async function mountApplication(routes, rootModule, options = false) {
18
+ const { hash = false, deployment = null } = typeof options === "boolean" ? { hash: options } : options;
18
19
  const initialURL = new URL(hash ? location.hash.slice(1) || "/" : location.href, location.origin);
19
- await (matchRoutes(routes, initialURL.pathname)?.route)?.preload?.();
20
+ await (matchRoutes(routes, initialURL.pathname)?.route)?.preload?.().catch(async (error) => {
21
+ await recoverDeployment(deployment, initialURL);
22
+ throw error;
23
+ });
20
24
  routes = prepareRoutes(routes);
21
25
  const element = document.getElementById("smolter-data");
22
- const hydration = element?.textContent ? JSON.parse(element.textContent) : void 0;
26
+ const hydration = element?.textContent ? JSON.parse(element.textContent, (_key, value) => {
27
+ if (!value || typeof value !== "object" || !("__smolterRejected" in value)) return value;
28
+ const error = Object.assign(new Error(value.__smolterRejected.message), value.__smolterRejected);
29
+ const rejected = Promise.reject(error);
30
+ rejected.catch(() => {});
31
+ return rejected;
32
+ }) : void 0;
23
33
  if (hydration?.snapshot.error) hydration.snapshot.error = Object.assign(new Error(hydration.snapshot.error.message), hydration.snapshot.error);
24
34
  const router = createDataRouter(routes, {
25
35
  browser: !hash,
26
36
  url: hash ? new URL(location.hash.slice(1) || "/", location.origin).href : location.href,
27
37
  ...hydration && { hydrationData: hydration.snapshot }
28
38
  });
29
- retainRoute(router, flushSync);
39
+ watchDeployment(router, deployment);
30
40
  if (hash) connectHashHistory(router);
41
+ retainRoute(router, flushSync);
31
42
  connectNavigation(router);
32
43
  const rootStore = attachRootData(router, hydration?.root, rootModule);
33
44
  const props = {
@@ -35,7 +46,8 @@ async function mountApplication(routes, rootModule, hash = false) {
35
46
  router,
36
47
  root: hydration?.root,
37
48
  rootModule,
38
- rootStore
49
+ rootStore,
50
+ rootError: hydration?.rootError
39
51
  };
40
52
  if (hydration) {
41
53
  hydrate(/* @__PURE__ */ jsx(RouterApplication, {
@@ -49,20 +61,27 @@ async function mountApplication(routes, rootModule, hash = false) {
49
61
  return router;
50
62
  }
51
63
  function connectHashHistory(router) {
64
+ const read = router.getSnapshot;
52
65
  const navigate = router.navigate;
53
66
  router.navigate = async (url, options) => {
54
67
  const target = new URL(url, router.getSnapshot().url);
55
- const hash = `#${target.pathname}${target.search}`;
68
+ const hash = `#${target.pathname}${target.search}${target.hash}`;
56
69
  history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
57
70
  await navigate(url, options);
58
71
  };
72
+ const unsubscribe = router.subscribe(() => {
73
+ const target = new URL(read().url);
74
+ const hash = `#${target.pathname}${target.search}${target.hash}`;
75
+ if (location.hash !== hash) history.replaceState(history.state, "", hash);
76
+ });
59
77
  const onHashChange = () => void navigate(location.hash.slice(1) || "/");
60
78
  window.addEventListener("hashchange", onHashChange);
61
79
  const dispose = router.dispose;
62
80
  router.dispose = () => {
81
+ unsubscribe();
63
82
  window.removeEventListener("hashchange", onHashChange);
64
83
  dispose();
65
84
  };
66
85
  }
67
86
  //#endregion
68
- export { configureApplication, deserialize, mountApplication, serverRoute, watchDeployment };
87
+ export { configureApplication, deserialize, mountApplication, recoverDeployment, serverRoute, watchDeployment };
@@ -44,8 +44,9 @@ function Links() {
44
44
  return /* @__PURE__ */ jsx(Fragment$1, { children: rootModule?.links?.().map((props, index) => /* @__PURE__ */ jsx("link", { ...props }, index)) });
45
45
  }
46
46
  function Scripts({ nonce }) {
47
- const { root, router, entry } = useApplication();
47
+ const { root, rootError, router, entry, streaming } = useApplication();
48
48
  const [json] = useState(() => {
49
+ if (streaming) return "";
49
50
  const current = router.getSnapshot();
50
51
  const snapshot = {
51
52
  ...current,
@@ -57,7 +58,8 @@ function Scripts({ nonce }) {
57
58
  };
58
59
  return serializeData({
59
60
  root,
60
- snapshot
61
+ snapshot,
62
+ ...rootError && { rootError }
61
63
  });
62
64
  });
63
65
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
@@ -3,6 +3,7 @@ import { type ComponentType } from "preact";
3
3
  import type { AppRoute } from "./route-types.ts";
4
4
  export interface LazyRouteOptions {
5
5
  path: string;
6
+ dataEndpoint?: string;
6
7
  id?: string;
7
8
  loader?: boolean;
8
9
  action?: boolean;
@@ -16,7 +16,8 @@ function lazyRoute(options, importer) {
16
16
  pending ??= importer().then((loaded) => {
17
17
  module = serverRoute(options.path, loaded, {
18
18
  loader: options.loader,
19
- action: options.action
19
+ action: options.action,
20
+ dataEndpoint: options.dataEndpoint
20
21
  });
21
22
  }).catch((error) => {
22
23
  pending = void 0;
@@ -4,6 +4,8 @@ export interface ApplicationContext {
4
4
  router: Router;
5
5
  routes: AppRoute[];
6
6
  root: unknown;
7
+ rootError?: boolean;
8
+ streaming?: boolean;
7
9
  rootModule?: RootModule;
8
10
  hydration?: Hydration;
9
11
  entry?: string;
@@ -47,6 +47,7 @@ export interface RootModule {
47
47
  shouldRevalidate?: (args: RevalidationArgs) => boolean;
48
48
  }
49
49
  export interface Hydration {
50
+ rootError?: boolean;
50
51
  root: unknown;
51
52
  snapshot: Snapshot;
52
53
  }
@@ -13,4 +13,4 @@ export interface ApplicationProps extends ApplicationContext {
13
13
  rootStore?: ReturnType<typeof createRootStore>;
14
14
  }
15
15
  export declare function RouterApplication(props: ApplicationProps): import("preact").JSX.Element;
16
- export declare function prepareRoutes(routes: AppRoute[]): AppRoute[];
16
+ export declare function prepareRoutes(routes: AppRoute[], preserveDeferred?: boolean): AppRoute[];
@@ -21,7 +21,7 @@ function RouterApplication(props) {
21
21
  const root = useSyncExternalStore(rootStore.subscribe, rootStore.getSnapshot);
22
22
  const snapshot = useSyncExternalStore(props.router.subscribe, props.router.getSnapshot);
23
23
  const route = props.routes.find((item) => item.path === snapshot.path);
24
- const Root = snapshot.error && !route?.ErrorBoundary ? props.rootModule?.ErrorBoundary ?? Outlet : props.rootModule?.default ?? Outlet;
24
+ const Root = snapshot.error && (props.rootError || !route?.ErrorBoundary) ? props.rootModule?.ErrorBoundary ?? Outlet : props.rootModule?.default ?? Outlet;
25
25
  const Layout = props.rootModule?.Layout;
26
26
  const children = /* @__PURE__ */ jsx(Root, {});
27
27
  return /* @__PURE__ */ jsx(RouterProvider, {
@@ -35,7 +35,7 @@ function RouterApplication(props) {
35
35
  })
36
36
  });
37
37
  }
38
- function prepareRoutes(routes) {
38
+ function prepareRoutes(routes, preserveDeferred = false) {
39
39
  return routes.map((route) => {
40
40
  const wrapped = {
41
41
  ...route,
@@ -54,7 +54,7 @@ function prepareRoutes(routes) {
54
54
  ]) {
55
55
  const handler = route[key];
56
56
  if (!handler) continue;
57
- wrapped[key] = (args) => Promise.resolve().then(() => handler(args)).then(normalizeResult, normalizeResult);
57
+ wrapped[key] = (args) => Promise.resolve().then(() => handler(args)).then((value) => normalizeResult(value, !preserveDeferred), (error) => normalizeResult(error, !preserveDeferred));
58
58
  }
59
59
  return wrapped;
60
60
  });
@@ -1,4 +1,7 @@
1
1
  /** @jsxImportSource preact */
2
2
  import type { AppRoute, RootModule } from "../preact/route-types.ts";
3
- export declare function renderApplication(request: Request, routes: AppRoute[], rootModule: RootModule, entry: string): Promise<Response>;
3
+ export interface RenderApplicationOptions {
4
+ nonce?: string;
5
+ }
6
+ export declare function renderApplication(request: Request, routes: AppRoute[], rootModule: RootModule, entry: string, options?: RenderApplicationOptions): Promise<Response>;
4
7
  export declare function dispatchRoute(request: Request, routes: AppRoute[], document?: boolean): Promise<Response>;
@@ -1,13 +1,16 @@
1
+ import { safeWrapAsync } from "../core/errors.js";
1
2
  import { matchRoutes } from "../core/match.js";
2
3
  import { createDataRouter } from "../core/router.js";
3
4
  import { normalizeResult, resolveData } from "../core/responses.js";
4
5
  import { RouterApplication, prepareRoutes } from "../preact/router.js";
6
+ import { privateResponse } from "./private-response.js";
5
7
  import { loadRouter } from "./index.js";
8
+ import { resolveHydration, streamDocument } from "./stream-document.js";
6
9
  import { jsx } from "preact/jsx-runtime";
7
- import { renderToStringAsync } from "preact-render-to-string";
10
+ import { renderToReadableStream } from "preact-render-to-string/stream";
8
11
  //#region .generated/server/application.tsx
9
12
  /** @jsxImportSource preact */
10
- async function renderApplication(request, routes, rootModule, entry) {
13
+ async function renderApplication(request, routes, rootModule, entry, options = {}) {
11
14
  routes = routes.map((route) => ({
12
15
  ...route,
13
16
  path: route.routePath ?? route.path
@@ -21,16 +24,16 @@ async function renderApplication(request, routes, rootModule, entry) {
21
24
  if (actionResponse.headers.has("Location") || !actionResponse.headers.get("Content-Type")?.includes("application/json")) return actionResponse;
22
25
  actionData = await normalizeResult(await actionResponse.json());
23
26
  }
24
- const root = await resolveData(await rootModule.loader?.({
27
+ const rootResult = safeWrapAsync(() => Promise.resolve().then(() => rootModule.loader?.({
25
28
  request,
26
29
  params: {},
27
30
  signal: request.signal
28
- }));
31
+ })).then((value) => normalizeResult(value, false), (error) => error instanceof Response ? normalizeResult(error, false) : error instanceof Error ? error : new Error(String(error), { cause: error })));
29
32
  let redirectResponse;
30
33
  const prepared = prepareRoutes(routes.map((route) => ({
31
34
  ...route,
32
35
  ErrorBoundary: route.ErrorBoundary ?? rootModule.ErrorBoundary
33
- }))).map((route) => {
36
+ })), true).map((route) => {
34
37
  if (!route.loader) return route;
35
38
  const loader = route.loader;
36
39
  return {
@@ -42,41 +45,68 @@ async function renderApplication(request, routes, rootModule, entry) {
42
45
  }
43
46
  };
44
47
  });
45
- let router = await loadRouter(prepared, request);
48
+ const [rootState, loadedRouter] = await Promise.all([rootResult, loadRouter(prepared, request)]);
49
+ let router = loadedRouter;
50
+ const rootError = rootState.error ?? (rootState.data instanceof Error ? rootState.data : void 0);
51
+ const root = rootError ? void 0 : rootState.data;
52
+ if (root instanceof Response && root.headers.has("Location")) {
53
+ router.dispose();
54
+ return root;
55
+ }
46
56
  if (redirectResponse) {
47
57
  router.dispose();
48
58
  return redirectResponse;
49
59
  }
50
- if (actionResponse) {
60
+ if (actionResponse || rootError) {
51
61
  const hydrationData = {
52
62
  ...router.getSnapshot(),
53
63
  actionData: actionData instanceof Error ? void 0 : actionData,
54
- ...actionData instanceof Error && { error: actionData }
64
+ ...actionData instanceof Error && { error: actionData },
65
+ ...rootError && { error: rootError }
55
66
  };
56
67
  router.dispose();
57
68
  router = createDataRouter(router.routes, { hydrationData });
58
69
  }
59
70
  const snapshot = router.getSnapshot();
60
- const html = await renderToStringAsync(/* @__PURE__ */ jsx(RouterApplication, {
71
+ const stream = renderToReadableStream(/* @__PURE__ */ jsx(RouterApplication, {
61
72
  router,
62
73
  routes,
63
74
  root,
64
75
  rootModule,
65
76
  entry,
77
+ rootError: Boolean(rootError),
78
+ streaming: true,
66
79
  document: true
67
- }));
68
- router.dispose();
80
+ }), options);
81
+ const hydration = resolveHydration({
82
+ root,
83
+ snapshot: {
84
+ ...snapshot,
85
+ error: snapshot.error && {
86
+ ...snapshot.error,
87
+ name: snapshot.error.name,
88
+ message: snapshot.error.message
89
+ }
90
+ },
91
+ ...rootError && { rootError: true }
92
+ });
93
+ stream.allReady.catch(() => {});
69
94
  const headers = new Headers(actionResponse?.headers);
70
95
  headers.delete("Content-Length");
71
96
  headers.delete("Content-Encoding");
72
97
  headers.set("Content-Type", "text/html; charset=utf-8");
73
98
  headers.set("Cache-Control", "no-store");
74
- return new Response(`<!doctype html>${html}`, {
99
+ return new Response(streamDocument(stream, {
100
+ hydration,
101
+ signal: request.signal,
102
+ nonce: options.nonce,
103
+ dispose: () => router.dispose()
104
+ }), {
75
105
  status: snapshot.error ? Number("status" in snapshot.error ? snapshot.error.status : 500) : actionResponse?.status ?? 200,
76
106
  headers
77
107
  });
78
108
  }
79
- async function dispatchRoute(request, routes, document = false) {
109
+ async function dispatch(request, routes, document = false) {
80
110
  const match = matchRoutes(routes.map((route) => ({
81
111
  ...route,
82
112
  path: route.routePath ?? route.path
@@ -115,5 +145,9 @@ async function dispatchRoute(request, routes, document = false) {
115
145
  }
116
146
  return Response.json(await normalizeResult(value) ?? null);
117
147
  }
148
+ async function dispatchRoute(request, routes, document = false) {
149
+ const response = await dispatch(request, routes, document);
150
+ return document ? response : privateResponse(response);
151
+ }
118
152
  //#endregion
119
153
  export { dispatchRoute, renderApplication };
@@ -8,6 +8,8 @@ export interface RequestHandlerOptions {
8
8
  build: ServerBuild;
9
9
  /** Browser entry URL resolved from Vite's production manifest. */
10
10
  entry: string;
11
+ /** Must match the browser and Vite transport endpoint. */
12
+ dataEndpoint?: string;
11
13
  }
12
14
  export interface ServerIncomingMessage extends IncomingMessage {
13
15
  /** Express supplies this using the application's trust proxy policy. */
@@ -16,4 +18,4 @@ export interface ServerIncomingMessage extends IncomingMessage {
16
18
  }
17
19
  export type NextHandler = (error?: unknown) => void;
18
20
  /** Node/Express middleware; mount after metrics, tracing and static assets. */
19
- export declare function createRequestHandler({ build, entry, }: RequestHandlerOptions): (incoming: ServerIncomingMessage, outgoing: ServerResponse, next: NextHandler) => Promise<void>;
21
+ export declare function createRequestHandler({ build, entry, dataEndpoint, }: RequestHandlerOptions): (incoming: ServerIncomingMessage, outgoing: ServerResponse, next: NextHandler) => Promise<void>;
@@ -1,11 +1,13 @@
1
+ import { privateResponse } from "./private-response.js";
1
2
  import { send, webRequest } from "./http.js";
2
3
  //#region .generated/server/handler.ts
3
4
  /** Node/Express middleware; mount after metrics, tracing and static assets. */
4
- function createRequestHandler({ build, entry }) {
5
+ function createRequestHandler({ build, entry, dataEndpoint = "/_smolter" }) {
5
6
  return (incoming, outgoing, next) => {
6
7
  const handle = async () => {
7
8
  const url = new URL(incoming.originalUrl ?? incoming.url ?? "/", "http://local");
8
- const dataRequest = url.pathname === "/_smolter";
9
+ const dataRequest = url.pathname === dataEndpoint;
10
+ if (dataRequest) outgoing.setHeader("Cache-Control", "private, no-store");
9
11
  const target = dataRequest ? url.searchParams.get("url") : void 0;
10
12
  if (dataRequest && (!target?.startsWith("/") || new URL(target, "http://local").origin !== "http://local")) {
11
13
  await send(new Response("Invalid route URL", { status: 400 }), outgoing);
@@ -13,7 +15,7 @@ function createRequestHandler({ build, entry }) {
13
15
  }
14
16
  const request = await webRequest(incoming, outgoing, target ?? void 0);
15
17
  const response = dataRequest ? await build.handleDataRequest(request) : await build.default(request, entry);
16
- await send(response, outgoing);
18
+ await send(dataRequest ? privateResponse(response) : response, outgoing);
17
19
  };
18
20
  return handle().catch(next);
19
21
  };
@@ -1,4 +1,8 @@
1
1
  import type { ServerResponse } from "node:http";
2
2
  import type { ServerIncomingMessage } from "./handler.ts";
3
3
  export declare function webRequest(incoming: ServerIncomingMessage, outgoing: ServerResponse, target?: string): Promise<Request>;
4
- export declare function send(response: Response, outgoing: ServerResponse): Promise<void>;
4
+ interface StreamingResponse extends ServerResponse {
5
+ flush?: () => void;
6
+ }
7
+ export declare function send(response: Response, outgoing: StreamingResponse): Promise<void>;
8
+ export {};
@@ -1,3 +1,5 @@
1
+ import { Readable, Transform } from "node:stream";
2
+ import { pipeline } from "node:stream/promises";
1
3
  //#region .generated/server/http.ts
2
4
  async function webRequest(incoming, outgoing, target) {
3
5
  const origin = `${incoming.protocol ?? "http"}://${incoming.headers.host}`;
@@ -21,7 +23,14 @@ async function send(response, outgoing) {
21
23
  });
22
24
  const cookies = response.headers.getSetCookie();
23
25
  if (cookies.length) outgoing.setHeader("set-cookie", cookies.map((cookie) => process.env.NODE_ENV === "production" ? cookie : cookie.replace(/; Secure/gi, "")));
24
- outgoing.end(new Uint8Array(await response.arrayBuffer()));
26
+ if (!response.body) {
27
+ outgoing.end();
28
+ return;
29
+ }
30
+ await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, _encoding, done) {
31
+ done(null, chunk);
32
+ outgoing.flush?.();
33
+ } }), outgoing);
25
34
  }
26
35
  //#endregion
27
36
  export { send, webRequest };
@@ -6,5 +6,5 @@ import { type Route, type Router } from "../core/index.ts";
6
6
  /** Create one router per server request, await this, then render the provider. */
7
7
  export declare function loadRouter(routes: readonly Route[], input: string | Request): Promise<Router>;
8
8
  export { serialize } from "../core/index.ts";
9
- /** Dispatch an HTTP data request to a registered server loader or action. */
9
+ /** Dispatch a server data request with an explicit private cache policy. */
10
10
  export declare function handleRequest(routes: readonly Route[], request: Request): Promise<Response>;
@@ -2,6 +2,7 @@ import { safeWrapAsync } from "../core/errors.js";
2
2
  import { matchRoutes } from "../core/match.js";
3
3
  import { createDataRouter } from "../core/router.js";
4
4
  import { serialize } from "../core/serialize.js";
5
+ import { privateResponse } from "./private-response.js";
5
6
  //#region .generated/server/index.ts
6
7
  /**
7
8
  * Request-scoped server rendering and HTTP data dispatch.
@@ -46,7 +47,7 @@ async function loadRouter(routes, input) {
46
47
  return router;
47
48
  }
48
49
  /** Dispatch an HTTP data request to a registered server loader or action. */
49
- async function handleRequest(routes, request) {
50
+ async function dispatchRequest(routes, request) {
50
51
  const match = matchRoutes(routes, new URL(request.url).pathname);
51
52
  const reading = request.method === "GET";
52
53
  if (!match) return Response.json({ error: "No route" }, { status: 404 });
@@ -67,10 +68,19 @@ async function handleRequest(routes, request) {
67
68
  if (error) return Response.json({ error: error.message }, { status: 500 });
68
69
  if (result.data instanceof Response) {
69
70
  const location = result.data.headers.get("Location");
70
- if (location) return new Response(null, { headers: { "X-Smolter-Redirect": location } });
71
+ if (location) {
72
+ const headers = new Headers(result.data.headers);
73
+ headers.delete("Location");
74
+ headers.set("X-Smolter-Redirect", location);
75
+ return new Response(null, { headers });
76
+ }
71
77
  return result.data;
72
78
  }
73
79
  return Response.json(result.data ?? null, { headers: { "Cache-Control": "no-store" } });
74
80
  }
81
+ /** Dispatch a server data request with an explicit private cache policy. */
82
+ async function handleRequest(routes, request) {
83
+ return privateResponse(await dispatchRequest(routes, request));
84
+ }
75
85
  //#endregion
76
86
  export { handleRequest, loadRouter, serialize };
@@ -1,3 +1,3 @@
1
1
  /** Preact server rendering and HTTP data dispatch. @module */
2
- export { dispatchRoute, renderApplication, } from "./application.tsx";
2
+ export { dispatchRoute, type RenderApplicationOptions, renderApplication, } from "./application.tsx";
3
3
  export * from "./index.ts";
@@ -0,0 +1,2 @@
1
+ /** Transport data must not be cached by shared intermediaries or the browser. */
2
+ export declare function privateResponse(response: Response): Response;
@@ -0,0 +1,13 @@
1
+ //#region .generated/server/private-response.ts
2
+ /** Transport data must not be cached by shared intermediaries or the browser. */
3
+ function privateResponse(response) {
4
+ const headers = new Headers(response.headers);
5
+ headers.set("Cache-Control", "private, no-store");
6
+ return new Response(response.body, {
7
+ status: response.status,
8
+ statusText: response.statusText,
9
+ headers
10
+ });
11
+ }
12
+ //#endregion
13
+ export { privateResponse };
@@ -0,0 +1,9 @@
1
+ export declare function resolveHydration(value: unknown): Promise<unknown>;
2
+ export interface StreamDocumentOptions {
3
+ hydration: Promise<unknown>;
4
+ nonce?: string;
5
+ signal: AbortSignal;
6
+ dispose: () => void;
7
+ }
8
+ /** Stream the shell immediately, then finalize the JSON snapshot before module scripts execute. */
9
+ export declare function streamDocument(source: ReadableStream<Uint8Array>, options: StreamDocumentOptions): ReadableStream<Uint8Array>;
@@ -0,0 +1,78 @@
1
+ import { serializeData } from "../core/serialize.js";
2
+ //#region .generated/server/stream-document.ts
3
+ async function resolveHydration(value) {
4
+ if (value instanceof Promise) return value.then(resolveHydration, (error) => ({ __smolterRejected: { message: error instanceof Error ? error.message : String(error) } }));
5
+ if (Array.isArray(value)) return Promise.all(value.map(resolveHydration));
6
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
7
+ const entries = await Promise.all(Object.entries(value).map(async ([key, field]) => [key, await resolveHydration(field)]));
8
+ return Object.fromEntries(entries);
9
+ }
10
+ return value;
11
+ }
12
+ /** Stream the shell immediately, then finalize the JSON snapshot before module scripts execute. */
13
+ function streamDocument(source, options) {
14
+ const reader = source.getReader();
15
+ const encoder = new TextEncoder();
16
+ const decoder = new TextDecoder();
17
+ let suffix = "";
18
+ let first = true;
19
+ let cancelled = false;
20
+ let finish;
21
+ const interrupted = new Promise((resolve) => {
22
+ finish = resolve;
23
+ });
24
+ const abort = () => {
25
+ finish();
26
+ reader.cancel().catch(() => {});
27
+ };
28
+ options.signal.addEventListener("abort", abort, { once: true });
29
+ if (options.signal.aborted) abort();
30
+ function dispose() {
31
+ options.signal.removeEventListener("abort", abort);
32
+ options.dispose();
33
+ }
34
+ return new ReadableStream({
35
+ async pull(controller) {
36
+ const result = await reader.read().catch((error) => ({ error }));
37
+ if ("error" in result) {
38
+ dispose();
39
+ controller.error(result.error);
40
+ return;
41
+ }
42
+ if (result.done) {
43
+ if (!options.signal.aborted) {
44
+ const value = await Promise.race([options.hydration, interrupted]);
45
+ if (cancelled) return;
46
+ if (options.signal.aborted) {
47
+ dispose();
48
+ controller.close();
49
+ return;
50
+ }
51
+ const hydration = serializeData(value);
52
+ const nonce = options.nonce ? ` nonce="${options.nonce.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;")}"` : "";
53
+ controller.enqueue(encoder.encode(`<script type="application/json"${nonce}>${hydration}<\/script><script${nonce}>(function(){var script=document.currentScript,payload=script.previousElementSibling,data=document.getElementById("smolter-data");if(data)data.textContent=payload.textContent;payload.remove();script.remove()})()<\/script>${suffix}`));
54
+ }
55
+ dispose();
56
+ controller.close();
57
+ return;
58
+ }
59
+ let chunk = decoder.decode(result.value, { stream: true });
60
+ if (first && !/^<!doctype/i.test(chunk)) chunk = `<!doctype html>${chunk}`;
61
+ first = false;
62
+ const closing = chunk.lastIndexOf("</body>");
63
+ if (closing !== -1) {
64
+ suffix = chunk.slice(closing);
65
+ chunk = chunk.slice(0, closing);
66
+ }
67
+ controller.enqueue(encoder.encode(chunk));
68
+ },
69
+ async cancel() {
70
+ cancelled = true;
71
+ finish();
72
+ dispose();
73
+ await reader.cancel();
74
+ }
75
+ });
76
+ }
77
+ //#endregion
78
+ export { resolveHydration, streamDocument };
@@ -1,2 +1,2 @@
1
1
  import { type Plugin } from "vite";
2
- export declare function lazyFiles(directory: string): Plugin;
2
+ export declare function lazyFiles(directory: string, dataEndpoint?: string): Plugin;