@webtypen/webframez-react 0.0.1

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/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # webframez-react
2
+
3
+ React Server Components (RSC) extension for `@webtypen/webframez-core`.
4
+
5
+ `webframez-react` provides:
6
+ - seamless `webframez-core` integration via `initWebframezReact(Route)`
7
+ - file-based routing for `pages/**/index.tsx`
8
+ - layout/error handling with `RouteChildren`
9
+ - client-side navigation (`Link`, `Redirect`, `useRouter`) and cookies (`useCookie`)
10
+ - server-rendered initial HTML plus RSC streaming
11
+
12
+ ## Requirements
13
+
14
+ - Node.js `>= 20`
15
+ - `@webtypen/webframez-core`
16
+ - React Experimental (matching the package peer dependencies)
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm i @webtypen/webframez-react @webtypen/webframez-core react react-dom react-server-dom-webpack
22
+ ```
23
+
24
+ ## Quick Start with webframez-core
25
+
26
+ ```ts
27
+ // server.ts
28
+ import { WebApplication, Route } from "@webtypen/webframez-core";
29
+ import { initWebframezReact } from "@webtypen/webframez-react/webframez-core";
30
+
31
+ initWebframezReact(Route);
32
+
33
+ Route.renderReact("/react", {
34
+ distRootDir: `${process.cwd()}/dist`,
35
+ });
36
+
37
+ WebApplication.boot();
38
+ ```
39
+
40
+ Notes:
41
+ - `"/react"` is automatically registered as a catch-all route (`/react/*`).
42
+ - `basePath`, `assetsPrefix`, `rscPath`, and `clientScriptUrl` are derived automatically from the mount path.
43
+
44
+ ## Page Structure (File-Based Routing)
45
+
46
+ Example:
47
+
48
+ ```txt
49
+ pages/
50
+ layout.tsx
51
+ errors.tsx
52
+ index.tsx
53
+ about/index.tsx
54
+ accounts/[username]/index.tsx
55
+ ```
56
+
57
+ `pages/layout.tsx`:
58
+
59
+ ```tsx
60
+ "use server";
61
+
62
+ import React from "react";
63
+ import { RouteChildren } from "webframez-react/router";
64
+
65
+ export default function Layout() {
66
+ return (
67
+ <main>
68
+ <nav>...</nav>
69
+ <RouteChildren />
70
+ </main>
71
+ );
72
+ }
73
+ ```
74
+
75
+ `RouteChildren` marks where the currently resolved page should be rendered.
76
+
77
+ ## Error Handling with `abort()`
78
+
79
+ Every server page gets `abort()` via `RouteContext`.
80
+
81
+ ```tsx
82
+ "use server";
83
+
84
+ import type { PageProps } from "webframez-react/types";
85
+
86
+ export default function AccountPage({ params, abort }: PageProps) {
87
+ if (params.username !== "jane") {
88
+ abort({
89
+ status: 404,
90
+ message: `Account \"${params.username}\" not found`,
91
+ payload: { attemptedUsername: params.username },
92
+ });
93
+ }
94
+
95
+ return <section>...</section>;
96
+ }
97
+ ```
98
+
99
+ Behavior:
100
+ - default without options: `404` + `"Page not found"`
101
+ - rendered through `pages/errors.tsx` (same behavior as unmatched routes)
102
+ - `pathname` is provided automatically by context
103
+ - optional `payload` is forwarded to `errors.tsx`
104
+
105
+ ## Client Entry
106
+
107
+ ```tsx
108
+ // src/client.tsx
109
+ import { mountWebframezClient } from "webframez-react/client";
110
+
111
+ mountWebframezClient();
112
+ ```
113
+
114
+ Optional:
115
+
116
+ ```tsx
117
+ mountWebframezClient({
118
+ rootId: "root",
119
+ rscEndpoint: "/react/rsc",
120
+ });
121
+ ```
122
+
123
+ ## Navigation
124
+
125
+ ```tsx
126
+ "use client";
127
+
128
+ import React from "react";
129
+ import { Link, Redirect } from "webframez-react/navigation";
130
+
131
+ export function Nav() {
132
+ return (
133
+ <>
134
+ <Link to="/">Home</Link>
135
+ <Link to="/about">About</Link>
136
+ </>
137
+ );
138
+ }
139
+
140
+ export function Guard({ loggedIn }: { loggedIn: boolean }) {
141
+ if (!loggedIn) {
142
+ return <Redirect to="/login" replace />;
143
+ }
144
+
145
+ return null;
146
+ }
147
+ ```
148
+
149
+ Note:
150
+ - `Link` and `Redirect` automatically use the basename from `Route.renderReact()`.
151
+ - You can override it per usage via `basename`.
152
+
153
+ ## Router and Cookies in Client Components
154
+
155
+ ```tsx
156
+ "use client";
157
+
158
+ import React from "react";
159
+ import { useCookie, useRouter } from "webframez-react/client";
160
+
161
+ export default function LoginAction() {
162
+ const cookie = useCookie();
163
+ const router = useRouter();
164
+
165
+ return (
166
+ <button
167
+ onClick={() => {
168
+ cookie.set("logged_in", "1", { path: "/react", sameSite: "Lax" });
169
+ router.refresh();
170
+ }}
171
+ >
172
+ Login
173
+ </button>
174
+ );
175
+ }
176
+ ```
177
+
178
+ ## Public Entrypoints
179
+
180
+ - `webframez-react`
181
+ - `createNodeRequestHandler`, `createFileRouter`, `createHTMLShell`, `sendRSC`, `createRSCHandler`
182
+ - `webframez-react/webframez-core`
183
+ - `initWebframezReact`
184
+ - `webframez-react/router`
185
+ - `RouteChildren`
186
+ - `webframez-react/client`
187
+ - `mountWebframezClient`, `useRouter`, `useCookie`
188
+ - `webframez-react/navigation`
189
+ - `Link`, `Redirect`
190
+ - `webframez-react/types`
191
+ - all public types (`RouteContext`, `PageProps`, `ErrorPageProps`, ...)
192
+
193
+ ## package.json Scripts
194
+
195
+ Example scripts for a `webframez-react` app:
196
+
197
+ ```json
198
+ {
199
+ "scripts": {
200
+ "build:server": "tsc -p tsconfig.server.json",
201
+ "build:client": "webpack --config webpack.client.cjs",
202
+ "build": "npm run build:server && npm run build:client",
203
+ "start": "node --conditions react-server start-server.cjs",
204
+ "watch:server": "tsc -p tsconfig.server.json --watch --preserveWatchOutput",
205
+ "watch:client": "webpack --config webpack.client.cjs --watch",
206
+ "serve:watch": "node --watch --conditions react-server start-server.cjs",
207
+ "dev": "sh -c 'npm run watch:server & npm run watch:client & npm run serve:watch & wait'"
208
+ }
209
+ }
210
+ ```
211
+
212
+ Notes:
213
+ - `build` compiles server output (`pages`, `server.ts`) and client output (`client.tsx` bundle + RSC manifests).
214
+ - `start` runs the built app in React Server mode.
215
+ - `dev` enables watch mode for TypeScript and webpack and restarts Node automatically on server output changes.
216
+
217
+ ## Build
218
+
219
+ ```bash
220
+ npm run build
221
+ ```
222
+
223
+ Watch mode:
224
+
225
+ ```bash
226
+ npm run build:watch
227
+ ```
@@ -0,0 +1 @@
1
+ var _=Object.create;var d=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,U=Object.prototype.hasOwnProperty;var b=(e,n)=>{for(var t in n)d(e,t,{get:n[t],enumerable:!0})},g=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of O(n))!U.call(e,r)&&r!==t&&d(e,r,{get:()=>n[r],enumerable:!(o=k(n,r))||o.enumerable});return e};var A=(e,n,t)=>(t=e!=null?_(S(e)):{},g(n||!e||!e.__esModule?d(t,"default",{value:e,enumerable:!0}):t,e)),L=e=>g(d({},"__esModule",{value:!0}),e);var B={};b(B,{mountWebframezClient:()=>W,useCookie:()=>T,useRouter:()=>F});module.exports=L(B);var a=A(require("react"),1),x=require("react-dom/client"),v=require("react-server-dom-webpack/client"),i=require("react/jsx-runtime"),$={push:()=>{},replace:()=>{},refresh:()=>{}},l=typeof a.default.createContext=="function"?a.default.createContext(null):null,E="__WEBFRAMEZ_ROUTER__";function I(){return typeof window>"u"?null:window[E]??null}function P(e){typeof window>"u"||(window[E]=e)}function w(){let e={},n=typeof document>"u"?"":document.cookie;if(!n||n.trim()==="")return e;for(let t of n.split(";")){let o=t.trim();if(!o)continue;let r=o.indexOf("="),c=r>=0?o.slice(0,r).trim():o,u=r>=0?o.slice(r+1):"";c&&(e[c]=decodeURIComponent(u))}return e}function h(e,n,t={}){let o=[`${e}=${encodeURIComponent(n)}`];return t.path&&o.push(`Path=${t.path}`),t.domain&&o.push(`Domain=${t.domain}`),typeof t.maxAge=="number"&&o.push(`Max-Age=${Math.floor(t.maxAge)}`),t.expires&&o.push(`Expires=${t.expires.toUTCString()}`),t.sameSite&&o.push(`SameSite=${t.sameSite}`),t.secure&&o.push("Secure"),o.join("; ")}function T(){return a.default.useMemo(()=>({all:()=>w(),get:e=>w()[e],set:(e,n,t)=>{typeof document>"u"||(document.cookie=h(e,n,t))},remove:(e,n)=>{typeof document>"u"||(document.cookie=h(e,"",{...n??{},maxAge:0}))}}),[])}function F(){let e=l?a.default.useContext(l):null;if(!e){let n=I();if(n)return n;if(typeof window>"u")return $;throw new Error("useRouter must be used inside mountWebframezClient()")}return e}function C({active:e}){return(0,i.jsx)("div",{style:{position:"fixed",top:0,left:0,width:"100%",height:3,zIndex:9999,transformOrigin:"left",transform:e?"scaleX(1)":"scaleX(0)",transition:"transform 180ms ease",background:"linear-gradient(90deg, #0ea5e9, #22c55e)"}})}function M(e){return function(){let[t,o]=(0,a.useState)(null),[r,c]=(0,a.useState)(!1);async function u(s,f="push"){c(!0);try{let y=await(0,v.createFromFetch)(fetch(`${e}?path=${encodeURIComponent(s.pathname)}&search=${encodeURIComponent(s.search)}`,{headers:{Accept:"text/x-component"}}));o(y);let R=`${s.pathname}${s.search}`;f==="replace"?history.replaceState(null,"",R):f==="push"&&history.pushState(null,"",R)}catch(m){console.error("[webframez-react] Failed to render route",m),o((0,i.jsx)("p",{children:"Failed to load route."}))}finally{c(!1)}}(0,a.useEffect)(()=>{let s=()=>{u(new URL(window.location.href),"none")};return window.addEventListener("popstate",s),u(new URL(window.location.href),"none"),()=>{window.removeEventListener("popstate",s)}},[]);let p=a.default.useMemo(()=>({push:s=>{u(new URL(s,window.location.origin),"push")},replace:s=>{u(new URL(s,window.location.origin),"replace")},refresh:()=>{u(new URL(window.location.href),"none")}}),[]);return P(p),l?(0,i.jsxs)(l.Provider,{value:p,children:[(0,i.jsx)(C,{active:r}),t??(0,i.jsx)("p",{style:{padding:24},children:"Loading..."})]}):(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(C,{active:r}),t??(0,i.jsx)("p",{style:{padding:24},children:"Loading..."})]})}}function W(e={}){let n=e.rootId??"root",t=document.getElementById(n);if(!t)throw new Error(`Missing #${n} element`);let o=typeof window<"u"&&window.__RSC_ENDPOINT,r=e.rscEndpoint??o??"/rsc",c=M(r),u=(0,x.createRoot)(t);return u.render((0,i.jsx)(c,{})),u}
@@ -0,0 +1,32 @@
1
+ import type { Root } from "react-dom/client";
2
+
3
+ export type ClientOptions = {
4
+ rootId?: string;
5
+ rscEndpoint?: string;
6
+ };
7
+
8
+ export type RouterClient = {
9
+ push: (href: string) => void;
10
+ replace: (href: string) => void;
11
+ refresh: () => void;
12
+ };
13
+
14
+ export type CookieOptions = {
15
+ path?: string;
16
+ domain?: string;
17
+ secure?: boolean;
18
+ sameSite?: "Strict" | "Lax" | "None";
19
+ maxAge?: number;
20
+ expires?: Date;
21
+ };
22
+
23
+ export type CookieClient = {
24
+ all: () => Record<string, string>;
25
+ get: (name: string) => string | undefined;
26
+ set: (name: string, value: string, options?: CookieOptions) => void;
27
+ remove: (name: string, options?: Omit<CookieOptions, "maxAge" | "expires">) => void;
28
+ };
29
+
30
+ export function mountWebframezClient(options?: ClientOptions): Root;
31
+ export function useRouter(): RouterClient;
32
+ export function useCookie(): CookieClient;
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ import c,{useEffect as E,useState as R}from"react";import{createRoot as y}from"react-dom/client";import{createFromFetch as _}from"react-server-dom-webpack/client";import{Fragment as b,jsx as u,jsxs as C}from"react/jsx-runtime";var k={push:()=>{},replace:()=>{},refresh:()=>{}},d=typeof c.createContext=="function"?c.createContext(null):null,x="__WEBFRAMEZ_ROUTER__";function O(){return typeof window>"u"?null:window[x]??null}function S(e){typeof window>"u"||(window[x]=e)}function g(){let e={},o=typeof document>"u"?"":document.cookie;if(!o||o.trim()==="")return e;for(let t of o.split(";")){let n=t.trim();if(!n)continue;let s=n.indexOf("="),a=s>=0?n.slice(0,s).trim():n,i=s>=0?n.slice(s+1):"";a&&(e[a]=decodeURIComponent(i))}return e}function w(e,o,t={}){let n=[`${e}=${encodeURIComponent(o)}`];return t.path&&n.push(`Path=${t.path}`),t.domain&&n.push(`Domain=${t.domain}`),typeof t.maxAge=="number"&&n.push(`Max-Age=${Math.floor(t.maxAge)}`),t.expires&&n.push(`Expires=${t.expires.toUTCString()}`),t.sameSite&&n.push(`SameSite=${t.sameSite}`),t.secure&&n.push("Secure"),n.join("; ")}function I(){return c.useMemo(()=>({all:()=>g(),get:e=>g()[e],set:(e,o,t)=>{typeof document>"u"||(document.cookie=w(e,o,t))},remove:(e,o)=>{typeof document>"u"||(document.cookie=w(e,"",{...o??{},maxAge:0}))}}),[])}function P(){let e=d?c.useContext(d):null;if(!e){let o=O();if(o)return o;if(typeof window>"u")return k;throw new Error("useRouter must be used inside mountWebframezClient()")}return e}function h({active:e}){return u("div",{style:{position:"fixed",top:0,left:0,width:"100%",height:3,zIndex:9999,transformOrigin:"left",transform:e?"scaleX(1)":"scaleX(0)",transition:"transform 180ms ease",background:"linear-gradient(90deg, #0ea5e9, #22c55e)"}})}function U(e){return function(){let[t,n]=R(null),[s,a]=R(!1);async function i(r,p="push"){a(!0);try{let v=await _(fetch(`${e}?path=${encodeURIComponent(r.pathname)}&search=${encodeURIComponent(r.search)}`,{headers:{Accept:"text/x-component"}}));n(v);let m=`${r.pathname}${r.search}`;p==="replace"?history.replaceState(null,"",m):p==="push"&&history.pushState(null,"",m)}catch(f){console.error("[webframez-react] Failed to render route",f),n(u("p",{children:"Failed to load route."}))}finally{a(!1)}}E(()=>{let r=()=>{i(new URL(window.location.href),"none")};return window.addEventListener("popstate",r),i(new URL(window.location.href),"none"),()=>{window.removeEventListener("popstate",r)}},[]);let l=c.useMemo(()=>({push:r=>{i(new URL(r,window.location.origin),"push")},replace:r=>{i(new URL(r,window.location.origin),"replace")},refresh:()=>{i(new URL(window.location.href),"none")}}),[]);return S(l),d?C(d.Provider,{value:l,children:[u(h,{active:s}),t??u("p",{style:{padding:24},children:"Loading..."})]}):C(b,{children:[u(h,{active:s}),t??u("p",{style:{padding:24},children:"Loading..."})]})}}function T(e={}){let o=e.rootId??"root",t=document.getElementById(o);if(!t)throw new Error(`Missing #${o} element`);let n=typeof window<"u"&&window.__RSC_ENDPOINT,s=e.rscEndpoint??n??"/rsc",a=U(s),i=y(t);return i.render(u(a,{})),i}export{T as mountWebframezClient,I as useCookie,P as useRouter};