@weftui/router 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +26 -4
- package/dist/client/index.js +225 -1
- package/dist/{compile-C0JShTTR.d.ts → compile-HOyeyWRy.d.ts} +47 -1
- package/dist/href-CSRbOQov.js +165 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -1
- package/dist/outlet-CXgDkheJ.js +467 -0
- package/dist/{outlet-BBEjKu3z.d.ts → outlet-DbqTgkXa.d.ts} +1 -1
- package/dist/server/index.d.ts +43 -8
- package/dist/server/index.js +212 -1
- package/package.json +5 -4
- package/dist/href-Dl30XcF4.js +0 -1
- package/dist/outlet-C7p8KXmO.js +0 -0
package/dist/server/index.js
CHANGED
|
@@ -1 +1,212 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import { c as RouterNotFound, n as outletNode, r as Router, u as isRouterNotFound } from "../outlet-CXgDkheJ.js";
|
|
2
|
+
import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpServer, HttpServerResponse } from "@effect/platform";
|
|
3
|
+
import { Cause, Effect, Exit, Layer, Option, Schema, Scope, Stream, Subscribable } from "effect";
|
|
4
|
+
import { AppRpcClientTag } from "@weftui/core";
|
|
5
|
+
import { SuspenseFailureHandlerTag, renderToHydratableShell, renderToStringHydratable } from "@weftui/dom/server";
|
|
6
|
+
import { RpcSerialization, RpcServer, RpcTest } from "@effect/rpc";
|
|
7
|
+
//#region src/server/router-server.ts
|
|
8
|
+
let RouterServer;
|
|
9
|
+
(function(_RouterServer) {
|
|
10
|
+
/** Path the in-process rpc web handler claims; mirrors `RouterLive`'s client URL. */
|
|
11
|
+
const RPC_PATH = "/_eui/rpc";
|
|
12
|
+
/** `text/html` response options at a given status. */
|
|
13
|
+
function htmlResponse(html, status) {
|
|
14
|
+
return HttpServerResponse.text(`<!DOCTYPE html>\n${html}`, {
|
|
15
|
+
status,
|
|
16
|
+
contentType: "text/html; charset=utf-8"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Builds the fixed per-request `Router` from an already-resolved match; `navigate` is a no-op on the server. */
|
|
20
|
+
function serverRouter(matched) {
|
|
21
|
+
const idle = { _tag: "Idle" };
|
|
22
|
+
return Router.of({
|
|
23
|
+
currentMatch: Subscribable.make({
|
|
24
|
+
get: Effect.succeed(matched),
|
|
25
|
+
changes: Stream.make(matched)
|
|
26
|
+
}),
|
|
27
|
+
navigate: () => Effect.void,
|
|
28
|
+
httpApiClient: Option.none(),
|
|
29
|
+
navigating: Subscribable.make({
|
|
30
|
+
get: Effect.succeed(idle),
|
|
31
|
+
changes: Stream.make(idle)
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* In-process {@link AppRpcClientTag} Layer over the app's handler Layer
|
|
37
|
+
* ({@link RpcTest.makeClient}, flat, no protocol/serialization). SSR
|
|
38
|
+
* `Boundary.rpc` resolution calls `call(tag, payload())` against this — the rpc
|
|
39
|
+
* runs in-process, never over the network.
|
|
40
|
+
*
|
|
41
|
+
* The render path requires the tag unconditionally, so with no `rpc` configured
|
|
42
|
+
* a stub is provided whose `call` fails descriptively — a `Boundary.rpc` in an
|
|
43
|
+
* rpc-less app surfaces the misconfiguration instead of dying opaquely.
|
|
44
|
+
*/
|
|
45
|
+
function appRpcClientLayer(rpc) {
|
|
46
|
+
if (rpc === void 0) return Layer.succeed(AppRpcClientTag, AppRpcClientTag.of({ call: (tag) => Effect.fail(/* @__PURE__ */ new Error(`Boundary.rpc "${tag}" cannot resolve: no \`rpc\` option was passed to RouterServer`)) }));
|
|
47
|
+
return Layer.scoped(AppRpcClientTag, Effect.map(RpcTest.makeClient(rpc.group, { flatten: true }), (flat) => AppRpcClientTag.of({ call: (tag, payload) => flat(tag, payload) }))).pipe(Layer.provide(rpc.handlers));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Renders the document shell — with `app` spliced via `Router.Outlet` — to a
|
|
51
|
+
* hydratable HTML string. The whole tree (shell + every route/layout leaf) drains
|
|
52
|
+
* in this one `renderToStringHydratable` context, so the app-wide `options.context`
|
|
53
|
+
* Layer provided here reaches the leaves too (the render-time provide seam). No
|
|
54
|
+
* context ⇒ `Layer.empty`, a no-op.
|
|
55
|
+
*/
|
|
56
|
+
function renderDocument(options, app, router) {
|
|
57
|
+
return renderToStringHydratable(Effect.provideService(options.document({}), Router.Outlet, app)).pipe(Effect.provideService(Router, router), Effect.provide(appRpcClientLayer(options.rpc)), Effect.provide(options.context ?? Layer.empty));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Renders the configured `notFound` page **directly** in the shell (no nested
|
|
61
|
+
* outlet, no reactive-region markers) at `status`. Mirrors the client's internal
|
|
62
|
+
* not-found boundary fallback — which replaces the whole outlet subtree — so the
|
|
63
|
+
* page-raised-404 HTML aligns for hydration.
|
|
64
|
+
*/
|
|
65
|
+
function renderNotFoundDirect(def, options, url, status) {
|
|
66
|
+
const router = serverRouter({
|
|
67
|
+
_tag: "NotFound",
|
|
68
|
+
url
|
|
69
|
+
});
|
|
70
|
+
return renderDocument(options, def.compiled.notFound(), router).pipe(Effect.map((html) => htmlResponse(html, status)));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Renders the no-match case: the bare {@link outletNode} with a `NotFound` match,
|
|
74
|
+
* so the `notFound` page renders **inside** the level-0 reactive region (markers
|
|
75
|
+
* present) — matching what the client outlet produces for an unmatched URL — at
|
|
76
|
+
* HTTP 404.
|
|
77
|
+
*/
|
|
78
|
+
function renderNoMatch(def, options, url) {
|
|
79
|
+
const router = serverRouter({
|
|
80
|
+
_tag: "NotFound",
|
|
81
|
+
url
|
|
82
|
+
});
|
|
83
|
+
return renderDocument(options, outletNode(def), router).pipe(Effect.map((html) => htmlResponse(html, 404)));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Renders one matched leaf: the bare {@link outletNode} with the platform-decoded
|
|
87
|
+
* match, replying `text/html` at 200. A page that raises `RouterNotFound` is
|
|
88
|
+
* caught here (the server omits `RouterApp`'s boundary so the failure surfaces)
|
|
89
|
+
* and re-rendered as the not-found page at 404 via {@link renderNotFoundDirect}.
|
|
90
|
+
*/
|
|
91
|
+
function renderLeaf(def, options, matched) {
|
|
92
|
+
const router = serverRouter(matched);
|
|
93
|
+
return renderDocument(options, outletNode(def), router).pipe(Effect.map((html) => htmlResponse(html, 200)), Effect.catchIf(isRouterNotFound, () => renderNotFoundDirect(def, options, matched.url, 404)));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The streaming pass's {@link SuspenseFailureHandlerTag} (SW1 late-404 row):
|
|
97
|
+
* a `RouterNotFound` escaping `Boundary.suspend` children after the shell has
|
|
98
|
+
* flushed is substituted with the router's `notFound` page plus a
|
|
99
|
+
* client-injected `<meta name="robots" content="noindex">` (Next.js soft-404
|
|
100
|
+
* parity). Any other cause keeps the dom swallow default (AC-ST8).
|
|
101
|
+
*
|
|
102
|
+
* The substitute also carries the `Schema`-encoded `RouterNotFound` as
|
|
103
|
+
* `failureReplay` (SW8), so the patch is the failure-replay variant
|
|
104
|
+
* (`streaming-shell.specs.md` AC-FH7) and a later `hydrate` replays the
|
|
105
|
+
* failure into `RouterApp`'s boundary instead of mismatching.
|
|
106
|
+
*/
|
|
107
|
+
function notFoundSuspenseHandler(def) {
|
|
108
|
+
return { handle: (cause) => {
|
|
109
|
+
const failure = Cause.failureOption(cause);
|
|
110
|
+
return Option.isSome(failure) && isRouterNotFound(failure.value) ? Option.some({
|
|
111
|
+
content: def.compiled.notFound(),
|
|
112
|
+
markNoindex: true,
|
|
113
|
+
failureReplay: Schema.encodeSync(RouterNotFound)(failure.value)
|
|
114
|
+
}) : Option.none();
|
|
115
|
+
} };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Streaming counterpart of {@link renderLeaf} (SW1 … SW6): renders the
|
|
119
|
+
* document via the dom shell-split API, decides the status off the buffered
|
|
120
|
+
* shell, then streams `<!DOCTYPE html>\n` + shell as the first chunk and the
|
|
121
|
+
* Suspense patches after it. A `RouterNotFound` raised during the shell walk
|
|
122
|
+
* is caught (nothing flushed yet) and re-rendered buffered at 404.
|
|
123
|
+
*/
|
|
124
|
+
function renderLeafStreaming(def, options, matched) {
|
|
125
|
+
const router = serverRouter(matched);
|
|
126
|
+
const app = outletNode(def);
|
|
127
|
+
return Effect.gen(function* () {
|
|
128
|
+
const scope = yield* Scope.make();
|
|
129
|
+
const { shell, patches } = yield* renderToHydratableShell(Effect.provideService(options.document({}), Router.Outlet, app)).pipe(Effect.provideService(Router, router), Effect.provideService(SuspenseFailureHandlerTag, notFoundSuspenseHandler(def)), Effect.provide(appRpcClientLayer(options.rpc)), Effect.provide(options.context ?? Layer.empty), Scope.extend(scope), Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
|
|
130
|
+
const body = Stream.make(`<!DOCTYPE html>\n${shell}`).pipe(Stream.concat(patches), Stream.ensuring(Scope.close(scope, Exit.void)), Stream.encodeText);
|
|
131
|
+
return HttpServerResponse.stream(body, {
|
|
132
|
+
status: 200,
|
|
133
|
+
contentType: "text/html; charset=utf-8"
|
|
134
|
+
});
|
|
135
|
+
}).pipe(Effect.catchIf(isRouterNotFound, () => renderNotFoundDirect(def, options, matched.url, 404)));
|
|
136
|
+
}
|
|
137
|
+
/** Memoized platform web handlers, keyed by `(def, document)`. */
|
|
138
|
+
const handlerCache = /* @__PURE__ */ new WeakMap();
|
|
139
|
+
/** Streaming handlers are memoized separately from the buffered ones. */
|
|
140
|
+
const streamingHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
141
|
+
/**
|
|
142
|
+
* Builds (and memoizes) the platform `(Request) => Promise<Response>` handler for
|
|
143
|
+
* `def`. Dispatch runs through a **server-local** `HttpApi`: `def.httpApi`
|
|
144
|
+
* (pristine — the client and the spec read it) extended with a second `"fallback"`
|
|
145
|
+
* group holding one catch-all `"*"` endpoint. Platform owns matching: a request
|
|
146
|
+
* routes to the specific leaf endpoint, or — when nothing matches — to the
|
|
147
|
+
* catch-all, which renders the configured not-found page at 404. (Platform's own
|
|
148
|
+
* unmatched path resolves a default empty 404 before any response hook can rewrite
|
|
149
|
+
* it, so the catch-all is the route that keeps no-match rendering ours.)
|
|
150
|
+
*/
|
|
151
|
+
function webHandlerWith(def, options, cache, leafRenderer) {
|
|
152
|
+
const perDef = cache.get(def) ?? /* @__PURE__ */ new WeakMap();
|
|
153
|
+
cache.set(def, perDef);
|
|
154
|
+
const cached = perDef.get(options.document);
|
|
155
|
+
if (cached !== void 0) return cached;
|
|
156
|
+
const leaves = def.compiled.leaves;
|
|
157
|
+
const builder = HttpApiBuilder;
|
|
158
|
+
const fallbackGroup = HttpApiGroup.make("fallback").add(HttpApiEndpoint.get("catchAll", "*").addSuccess(Schema.String));
|
|
159
|
+
const api = def.httpApi.add(fallbackGroup);
|
|
160
|
+
const pagesLayer = builder.group(api, "pages", (handlers) => leaves.reduce((h, leaf) => h.handle(leaf.id, (request) => leafRenderer(def, options, {
|
|
161
|
+
_tag: "Matched",
|
|
162
|
+
leaf,
|
|
163
|
+
path: request.path,
|
|
164
|
+
query: request.urlParams,
|
|
165
|
+
url: request.request.url
|
|
166
|
+
})), handlers));
|
|
167
|
+
const fallbackLayer = builder.group(api, "fallback", (handlers) => handlers.handle("catchAll", (request) => renderNoMatch(def, options, request.request.url)));
|
|
168
|
+
const apiLayer = builder.api(api).pipe(Layer.provide(Layer.mergeAll(pagesLayer, fallbackLayer)));
|
|
169
|
+
const { handler: pageHandler } = HttpApiBuilder.toWebHandler(Layer.mergeAll(apiLayer, HttpServer.layerContext));
|
|
170
|
+
const rpc = options.rpc;
|
|
171
|
+
let handler = pageHandler;
|
|
172
|
+
if (rpc !== void 0) {
|
|
173
|
+
const { handler: rpcHandler } = RpcServer.toWebHandler(rpc.group, { layer: Layer.mergeAll(rpc.handlers, RpcSerialization.layerJson) });
|
|
174
|
+
handler = (request) => new URL(request.url).pathname === RPC_PATH ? rpcHandler(request) : pageHandler(request);
|
|
175
|
+
}
|
|
176
|
+
perDef.set(options.document, handler);
|
|
177
|
+
return handler;
|
|
178
|
+
}
|
|
179
|
+
/** The buffered platform web handler (S2a). */
|
|
180
|
+
function webHandler(def, options) {
|
|
181
|
+
return webHandlerWith(def, options, handlerCache, renderLeaf);
|
|
182
|
+
}
|
|
183
|
+
/** Coerces a possibly-relative URL/path into an absolute URL for a synthetic `Request`. */
|
|
184
|
+
function absoluteUrl(url) {
|
|
185
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
186
|
+
return `http://localhost${url.startsWith("/") ? url : `/${url}`}`;
|
|
187
|
+
}
|
|
188
|
+
function render(def, options) {
|
|
189
|
+
const opts = options;
|
|
190
|
+
return Effect.tryPromise({
|
|
191
|
+
try: async () => {
|
|
192
|
+
const response = await webHandler(def, opts)(new Request(absoluteUrl(opts.url)));
|
|
193
|
+
return {
|
|
194
|
+
html: await response.text(),
|
|
195
|
+
status: response.status
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
catch: (error) => error instanceof Error ? error : new Error(String(error))
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
_RouterServer.render = render;
|
|
202
|
+
function toWebHandler(def, options) {
|
|
203
|
+
return webHandler(def, options);
|
|
204
|
+
}
|
|
205
|
+
_RouterServer.toWebHandler = toWebHandler;
|
|
206
|
+
function toStreamingWebHandler(def, options) {
|
|
207
|
+
return webHandlerWith(def, options, streamingHandlerCache, renderLeafStreaming);
|
|
208
|
+
}
|
|
209
|
+
_RouterServer.toStreamingWebHandler = toStreamingWebHandler;
|
|
210
|
+
})(RouterServer || (RouterServer = {}));
|
|
211
|
+
//#endregion
|
|
212
|
+
export { RouterServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weftui/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Universal nested router for Weft",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Stef van Wijchen",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@effect/platform": "^0.96.1",
|
|
34
34
|
"@effect/rpc": "^0.75.1",
|
|
35
|
-
"@weftui/core": "0.
|
|
36
|
-
"@weftui/dom": "0.
|
|
35
|
+
"@weftui/core": "0.23.0",
|
|
36
|
+
"@weftui/dom": "0.23.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/jsdom": "^28.0.3",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"jsdom": "^29.1.1",
|
|
43
43
|
"tsx": "^4.22.4",
|
|
44
44
|
"typescript": "^6.0.3",
|
|
45
|
-
"vite
|
|
45
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
|
|
46
|
+
"vite-plus": "0.2.2"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
49
|
"effect": "^3.21"
|
package/dist/href-Dl30XcF4.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./outlet-C7p8KXmO.js";import{Either as t,Option as n,Schema as r}from"effect";function i(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function a(e){return e.split(`/`).filter(e=>e.startsWith(`:`)).map(e=>e.slice(1))}function o(e){return e.split(`/`).filter(e=>e.startsWith(`:`)).length}function s(e){let t=e.split(`/`).filter(e=>e.length>0).map(e=>e.startsWith(`:`)?`([^/]+)`:i(e)).join(`/`);return RegExp(t.length===0?`^/?$`:`^/${t}/?$`)}const c=r.Struct({}),l=new WeakMap;function u(e){let t=l.get(e);if(t!==void 0)return t;let r=new Map;for(let t of e.compiled.leaves)r.set(t.id,t);let i=e.httpApi.groups.pages?.endpoints??{},u=[];for(let e of Object.values(i)){let t=r.get(e.name);t!==void 0&&u.push({leaf:t,regex:s(e.path),paramNames:a(e.path),pathSchema:n.getOrElse(e.pathSchema,()=>c),querySchema:n.getOrElse(e.urlParamsSchema,()=>c)})}return u.sort((e,t)=>{let n=o(e.leaf.fullPathPattern)-o(t.leaf.fullPathPattern);return n===0?t.leaf.fullPathPattern.length-e.leaf.fullPathPattern.length:n}),l.set(e,u),u}function d(e){let t=e.indexOf(`#`),n=t===-1?e:e.slice(0,t),r=n.indexOf(`?`),i=r===-1?n:n.slice(0,r),a=r===-1?``:n.slice(r+1),o=i.length===0?`/`:i;return o.startsWith(`/`)||(o=`/${o}`),o.length>1&&o.endsWith(`/`)&&(o=o.slice(0,-1)),{path:o,search:a}}function f(e){let t={};if(e.length===0)return t;for(let[n,r]of new URLSearchParams(e))t[n]=r;return t}function p(e,n){let i=u(e),{path:a,search:o}=d(n),s=o.length===0?a:`${a}?${o}`;for(let e of i){let n=e.regex.exec(a);if(n===null)continue;let i={};e.paramNames.forEach((e,t)=>{let r=n[t+1];r!==void 0&&(i[e]=decodeURIComponent(r))});let c=r.decodeUnknownEither(e.pathSchema)(i);if(t.isLeft(c))continue;let l=r.decodeUnknownEither(e.querySchema)(f(o));if(!t.isLeft(l))return{_tag:`Matched`,leaf:e.leaf,path:c.right,query:l.right,url:s}}return{_tag:`NotFound`,url:s}}function m(t,...n){let i=e.get(t);if(i===void 0)throw Error(`href: route has not been compiled. Seal the tree with Router.router() before calling href().`);let{path:a={},query:o={}}=n[0]??{},s=r.encodeUnknownSync(i.pathSchema)(a),c=i.fullPathPattern.replace(/:([A-Za-z0-9_]+)/g,(e,t)=>encodeURIComponent(String(s[t]))),l=r.encodeUnknownSync(i.querySchema)(o),u=new URLSearchParams;for(let e of Object.keys(l).sort()){let t=l[e];t!=null&&u.append(e,String(t))}let d=u.toString();return d.length>0&&(c=`${c}?${d}`),c}export{u as n,p as r,m as t};
|
package/dist/outlet-C7p8KXmO.js
DELETED
|
Binary file
|