@kasperrt/smolter 0.3.0 → 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.
- package/.generated/client/deployment-check.ts +70 -0
- package/.generated/client/deployment.ts +124 -0
- package/.generated/client/index.ts +9 -1
- package/.generated/core/responses.ts +8 -3
- package/.generated/core/router.ts +32 -0
- package/.generated/core/types.ts +2 -0
- package/.generated/preact/client.tsx +48 -6
- package/.generated/preact/document.tsx +4 -2
- package/.generated/preact/lazy-route.ts +2 -0
- package/.generated/preact/root-context.ts +2 -0
- package/.generated/preact/route-types.ts +1 -0
- package/.generated/preact/router.tsx +9 -3
- package/.generated/server/application.tsx +83 -19
- package/.generated/server/handler.ts +8 -2
- package/.generated/server/http.ts +24 -2
- package/.generated/server/index.ts +17 -5
- package/.generated/server/preact.ts +1 -0
- package/.generated/server/private-response.ts +11 -0
- package/.generated/server/stream-document.ts +121 -0
- package/.generated/vite/application/lazy-files.ts +10 -2
- package/.generated/vite/application/vite.ts +9 -3
- package/.generated/vite/deployment.ts +46 -0
- package/.generated/vite/index.ts +15 -6
- package/.generated/vite/preact.ts +5 -0
- package/.generated/vite/react.ts +5 -0
- package/.generated/vite/transport.ts +4 -1
- package/CHANGELOG.md +19 -0
- package/README.md +79 -11
- package/dist/client/deployment-check.d.ts +6 -0
- package/dist/client/deployment-check.js +51 -0
- package/dist/client/deployment.d.ts +14 -0
- package/dist/client/deployment.js +74 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -2
- package/dist/core/responses.d.ts +1 -1
- package/dist/core/responses.js +3 -3
- package/dist/core/router.js +16 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/preact/client.d.ts +6 -1
- package/dist/preact/client.js +27 -7
- package/dist/preact/document.js +4 -2
- package/dist/preact/lazy-route.d.ts +1 -0
- package/dist/preact/lazy-route.js +2 -1
- package/dist/preact/root-context.d.ts +2 -0
- package/dist/preact/route-types.d.ts +1 -0
- package/dist/preact/router.d.ts +1 -1
- package/dist/preact/router.js +3 -3
- package/dist/server/application.d.ts +4 -1
- package/dist/server/application.js +47 -13
- package/dist/server/handler.d.ts +3 -1
- package/dist/server/handler.js +5 -3
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +10 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +12 -2
- package/dist/server/preact.d.ts +1 -1
- package/dist/server/private-response.d.ts +2 -0
- package/dist/server/private-response.js +13 -0
- package/dist/server/stream-document.d.ts +9 -0
- package/dist/server/stream-document.js +78 -0
- package/dist/vite/application/lazy-files.d.ts +1 -1
- package/dist/vite/application/lazy-files.js +6 -2
- package/dist/vite/application/vite.d.ts +2 -1
- package/dist/vite/application/vite.js +7 -4
- package/dist/vite/deployment.d.ts +8 -0
- package/dist/vite/deployment.js +36 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +15 -7
- package/dist/vite/preact.d.ts +1 -0
- package/dist/vite/preact.js +2 -1
- package/dist/vite/react.d.ts +1 -0
- package/dist/vite/react.js +2 -1
- package/dist/vite/transport.d.ts +1 -1
- package/dist/vite/transport.js +3 -2
- package/jsr.json +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region .generated/client/deployment-check.ts
|
|
2
|
+
function deploymentCheck(options) {
|
|
3
|
+
const { version, manifestURL, timeout = 2e3 } = options;
|
|
4
|
+
const manifest = new URL(manifestURL, location.href);
|
|
5
|
+
const key = `smolter:deployment:${manifest.href}:${version}`;
|
|
6
|
+
let stopped = false;
|
|
7
|
+
let pending;
|
|
8
|
+
let controller;
|
|
9
|
+
function changed() {
|
|
10
|
+
if (pending) return pending;
|
|
11
|
+
controller = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => controller?.abort(), timeout);
|
|
13
|
+
pending = fetch(manifest, {
|
|
14
|
+
cache: "no-store",
|
|
15
|
+
signal: controller.signal
|
|
16
|
+
}).then(async (response) => {
|
|
17
|
+
if (!response.ok) return false;
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
return typeof data === "object" && data !== null && "version" in data && typeof data.version === "string" && data.version.length > 0 && data.version !== version;
|
|
20
|
+
}).catch(() => false).finally(() => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
pending = void 0;
|
|
23
|
+
});
|
|
24
|
+
return pending;
|
|
25
|
+
}
|
|
26
|
+
async function reload(target) {
|
|
27
|
+
const allowed = await Promise.resolve().then(() => {
|
|
28
|
+
if (sessionStorage.getItem(key)) return false;
|
|
29
|
+
sessionStorage.setItem(key, "1");
|
|
30
|
+
return true;
|
|
31
|
+
}).catch(() => false);
|
|
32
|
+
if (allowed && !stopped) {
|
|
33
|
+
if (options?.reload) options.reload(target);
|
|
34
|
+
if (!options?.reload) {
|
|
35
|
+
if (target.href === location.href) location.reload();
|
|
36
|
+
if (target.href !== location.href) location.assign(target.href);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return allowed && !stopped;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
changed,
|
|
43
|
+
reload,
|
|
44
|
+
dispose() {
|
|
45
|
+
stopped = true;
|
|
46
|
+
controller?.abort();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { deploymentCheck };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NavigationRouter } from "../core/index.ts";
|
|
2
|
+
/** Build identity and the fresh, non-cacheable manifest URL emitted by deploymentPlugin. */
|
|
3
|
+
export interface DeploymentOptions {
|
|
4
|
+
version: string;
|
|
5
|
+
manifestURL: string;
|
|
6
|
+
/** Maximum time to wait for the update check before continuing navigation. */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Override document navigation, for example to ask about unsaved changes. */
|
|
9
|
+
reload?: (destination: URL) => void;
|
|
10
|
+
}
|
|
11
|
+
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
12
|
+
export declare function watchDeployment(router: NavigationRouter, options: DeploymentOptions | null): () => void;
|
|
13
|
+
/** Recover an initial lazy-import failure before a router has mounted. Returns whether a reload was requested. */
|
|
14
|
+
export declare function recoverDeployment(options: DeploymentOptions | null, destination?: URL): Promise<boolean>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { deploymentCheck } from "./deployment-check.js";
|
|
2
|
+
//#region .generated/client/deployment.ts
|
|
3
|
+
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
4
|
+
function watchDeployment(router, options) {
|
|
5
|
+
if (!options || typeof window === "undefined") return () => {};
|
|
6
|
+
const check = deploymentCheck(options);
|
|
7
|
+
const { changed, reload } = check;
|
|
8
|
+
const navigate = router.navigate;
|
|
9
|
+
const dispose = router.dispose;
|
|
10
|
+
let sequence = 0;
|
|
11
|
+
let stopped = false;
|
|
12
|
+
let replay = false;
|
|
13
|
+
let destination = new URL(location.href);
|
|
14
|
+
const guarded = async (href, operation) => {
|
|
15
|
+
const target = new URL(href, router.getSnapshot().url);
|
|
16
|
+
const currentURL = new URL(router.getSnapshot().url);
|
|
17
|
+
const current = ++sequence;
|
|
18
|
+
destination = target;
|
|
19
|
+
if (target.origin === location.origin && (target.pathname !== currentURL.pathname || target.search !== currentURL.search) && await changed()) {
|
|
20
|
+
if (current !== sequence || stopped || await reload(target)) return;
|
|
21
|
+
}
|
|
22
|
+
if (current === sequence && !stopped) await navigate(href, operation);
|
|
23
|
+
};
|
|
24
|
+
const pop = (event) => {
|
|
25
|
+
if (replay) return;
|
|
26
|
+
const target = new URL(location.href);
|
|
27
|
+
const previous = new URL(router.getSnapshot().url);
|
|
28
|
+
if (target.pathname === previous.pathname && target.search === previous.search) return;
|
|
29
|
+
event.stopImmediatePropagation();
|
|
30
|
+
const current = ++sequence;
|
|
31
|
+
destination = target;
|
|
32
|
+
changed().then(async (updated) => {
|
|
33
|
+
if (stopped || current !== sequence) return;
|
|
34
|
+
if (updated && await reload(target)) return;
|
|
35
|
+
replay = true;
|
|
36
|
+
window.dispatchEvent(new PopStateEvent("popstate", { state: event.state }));
|
|
37
|
+
replay = false;
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const preloadError = () => {
|
|
41
|
+
if (router.getSnapshot().state === "submitting") return;
|
|
42
|
+
const current = sequence;
|
|
43
|
+
const target = destination;
|
|
44
|
+
changed().then((updated) => {
|
|
45
|
+
if (updated && current === sequence && !stopped) return reload(target);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
function cleanup() {
|
|
49
|
+
stopped = true;
|
|
50
|
+
sequence++;
|
|
51
|
+
check.dispose();
|
|
52
|
+
window.removeEventListener("popstate", pop, true);
|
|
53
|
+
window.removeEventListener("vite:preloadError", preloadError);
|
|
54
|
+
if (router.navigate === guarded) router.navigate = navigate;
|
|
55
|
+
if (router.dispose === guardedDispose) router.dispose = dispose;
|
|
56
|
+
}
|
|
57
|
+
function guardedDispose() {
|
|
58
|
+
cleanup();
|
|
59
|
+
dispose();
|
|
60
|
+
}
|
|
61
|
+
router.navigate = guarded;
|
|
62
|
+
router.dispose = guardedDispose;
|
|
63
|
+
window.addEventListener("popstate", pop, true);
|
|
64
|
+
window.addEventListener("vite:preloadError", preloadError);
|
|
65
|
+
return cleanup;
|
|
66
|
+
}
|
|
67
|
+
/** Recover an initial lazy-import failure before a router has mounted. Returns whether a reload was requested. */
|
|
68
|
+
async function recoverDeployment(options, destination) {
|
|
69
|
+
if (!options || typeof window === "undefined") return false;
|
|
70
|
+
const check = deploymentCheck(options);
|
|
71
|
+
return check.changed().then((changed) => changed ? check.reload(destination ?? new URL(location.href)) : false).finally(check.dispose);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { recoverDeployment, watchDeployment };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface ClientRouteModule {
|
|
|
19
19
|
export interface ServerHandlers {
|
|
20
20
|
loader?: boolean;
|
|
21
21
|
action?: boolean;
|
|
22
|
+
/** Application-scoped HTTP transport path. Defaults to /_smolter. */
|
|
23
|
+
dataEndpoint?: string;
|
|
22
24
|
}
|
|
23
25
|
/** Browser transport for server routes. File-router builds generate this call. */
|
|
24
26
|
export declare function serverRoute<M extends RouteModule = RouteModule>(path: string, module: ClientRouteModule, handlers?: ServerHandlers): Route<M>;
|
|
27
|
+
export { type DeploymentOptions, recoverDeployment, watchDeployment, } from "./deployment.ts";
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deserialize } from "./deserialize.js";
|
|
2
|
+
import { recoverDeployment, watchDeployment } from "./deployment.js";
|
|
2
3
|
//#region .generated/client/index.ts
|
|
3
4
|
/** Browser transport for server routes. File-router builds generate this call. */
|
|
4
5
|
function serverRoute(path, module, handlers = {
|
|
@@ -7,7 +8,7 @@ function serverRoute(path, module, handlers = {
|
|
|
7
8
|
}) {
|
|
8
9
|
const remote = async ({ request, signal }) => {
|
|
9
10
|
const url = new URL(request.url);
|
|
10
|
-
const response = await fetch(
|
|
11
|
+
const response = await fetch(`${handlers.dataEndpoint ?? "/_smolter"}?url=${encodeURIComponent(url.pathname + url.search)}`, {
|
|
11
12
|
method: request.method,
|
|
12
13
|
signal,
|
|
13
14
|
...request.method !== "GET" && { body: await request.formData() }
|
|
@@ -43,4 +44,4 @@ function serverRoute(path, module, handlers = {
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
//#endregion
|
|
46
|
-
export { deserialize, serverRoute };
|
|
47
|
+
export { deserialize, recoverDeployment, serverRoute, watchDeployment };
|
package/dist/core/responses.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export interface DataResponse<T> {
|
|
|
11
11
|
export declare function data<T>(value: T, init?: ResponseInit): DataResponse<T>;
|
|
12
12
|
export declare function redirect(location: string, init?: number | ResponseInit): Response;
|
|
13
13
|
export declare function isRouteErrorResponse(value: unknown): value is RouteError;
|
|
14
|
-
export declare function normalizeResult(value: unknown): Promise<unknown>;
|
|
14
|
+
export declare function normalizeResult(value: unknown, resolveDeferred?: boolean): Promise<unknown>;
|
|
15
15
|
export declare function resolveData(value: unknown): Promise<unknown>;
|
package/dist/core/responses.js
CHANGED
|
@@ -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;
|
package/dist/core/router.js
CHANGED
|
@@ -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");
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/preact/client.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/preact/client.js
CHANGED
|
@@ -6,6 +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 { recoverDeployment, watchDeployment } from "../client/deployment.js";
|
|
9
10
|
import { serverRoute } from "../client/index.js";
|
|
10
11
|
import { flushSync } from "preact/compat";
|
|
11
12
|
import { jsx } from "preact/jsx-runtime";
|
|
@@ -13,20 +14,31 @@ import { hydrate, render } from "preact";
|
|
|
13
14
|
//#region .generated/preact/client.tsx
|
|
14
15
|
/** Browser mounting and hydration for Preact applications. @module */
|
|
15
16
|
/** @jsxImportSource preact */
|
|
16
|
-
async function mountApplication(routes, rootModule,
|
|
17
|
+
async function mountApplication(routes, rootModule, options = false) {
|
|
18
|
+
const { hash = false, deployment = null } = typeof options === "boolean" ? { hash: options } : options;
|
|
17
19
|
const initialURL = new URL(hash ? location.hash.slice(1) || "/" : location.href, location.origin);
|
|
18
|
-
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
|
+
});
|
|
19
24
|
routes = prepareRoutes(routes);
|
|
20
25
|
const element = document.getElementById("smolter-data");
|
|
21
|
-
const hydration = element?.textContent ? JSON.parse(element.textContent)
|
|
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;
|
|
22
33
|
if (hydration?.snapshot.error) hydration.snapshot.error = Object.assign(new Error(hydration.snapshot.error.message), hydration.snapshot.error);
|
|
23
34
|
const router = createDataRouter(routes, {
|
|
24
35
|
browser: !hash,
|
|
25
36
|
url: hash ? new URL(location.hash.slice(1) || "/", location.origin).href : location.href,
|
|
26
37
|
...hydration && { hydrationData: hydration.snapshot }
|
|
27
38
|
});
|
|
28
|
-
|
|
39
|
+
watchDeployment(router, deployment);
|
|
29
40
|
if (hash) connectHashHistory(router);
|
|
41
|
+
retainRoute(router, flushSync);
|
|
30
42
|
connectNavigation(router);
|
|
31
43
|
const rootStore = attachRootData(router, hydration?.root, rootModule);
|
|
32
44
|
const props = {
|
|
@@ -34,7 +46,8 @@ async function mountApplication(routes, rootModule, hash = false) {
|
|
|
34
46
|
router,
|
|
35
47
|
root: hydration?.root,
|
|
36
48
|
rootModule,
|
|
37
|
-
rootStore
|
|
49
|
+
rootStore,
|
|
50
|
+
rootError: hydration?.rootError
|
|
38
51
|
};
|
|
39
52
|
if (hydration) {
|
|
40
53
|
hydrate(/* @__PURE__ */ jsx(RouterApplication, {
|
|
@@ -48,20 +61,27 @@ async function mountApplication(routes, rootModule, hash = false) {
|
|
|
48
61
|
return router;
|
|
49
62
|
}
|
|
50
63
|
function connectHashHistory(router) {
|
|
64
|
+
const read = router.getSnapshot;
|
|
51
65
|
const navigate = router.navigate;
|
|
52
66
|
router.navigate = async (url, options) => {
|
|
53
67
|
const target = new URL(url, router.getSnapshot().url);
|
|
54
|
-
const hash = `#${target.pathname}${target.search}`;
|
|
68
|
+
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
55
69
|
history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
|
|
56
70
|
await navigate(url, options);
|
|
57
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
|
+
});
|
|
58
77
|
const onHashChange = () => void navigate(location.hash.slice(1) || "/");
|
|
59
78
|
window.addEventListener("hashchange", onHashChange);
|
|
60
79
|
const dispose = router.dispose;
|
|
61
80
|
router.dispose = () => {
|
|
81
|
+
unsubscribe();
|
|
62
82
|
window.removeEventListener("hashchange", onHashChange);
|
|
63
83
|
dispose();
|
|
64
84
|
};
|
|
65
85
|
}
|
|
66
86
|
//#endregion
|
|
67
|
-
export { configureApplication, deserialize, mountApplication, serverRoute };
|
|
87
|
+
export { configureApplication, deserialize, mountApplication, recoverDeployment, serverRoute, watchDeployment };
|
package/dist/preact/document.js
CHANGED
|
@@ -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: [
|
|
@@ -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;
|
package/dist/preact/router.d.ts
CHANGED
|
@@ -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[];
|
package/dist/preact/router.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 };
|
package/dist/server/handler.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/handler.js
CHANGED
|
@@ -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 ===
|
|
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
|
};
|
package/dist/server/http.d.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
+
interface StreamingResponse extends ServerResponse {
|
|
5
|
+
flush?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function send(response: Response, outgoing: StreamingResponse): Promise<void>;
|
|
8
|
+
export {};
|
package/dist/server/http.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|