@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 +227 -0
- package/dist/client.cjs +1 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +1 -0
- package/dist/http.cjs +704 -0
- package/dist/http.d.ts +16 -0
- package/dist/http.js +678 -0
- package/dist/index.cjs +894 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +857 -0
- package/dist/navigation.cjs +149 -0
- package/dist/navigation.d.ts +16 -0
- package/dist/navigation.js +125 -0
- package/dist/router.cjs +386 -0
- package/dist/router.d.ts +21 -0
- package/dist/router.js +355 -0
- package/dist/types.cjs +17 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +0 -0
- package/dist/webframez-core.cjs +852 -0
- package/dist/webframez-core.d.ts +19 -0
- package/dist/webframez-core.js +823 -0
- package/package.json +84 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/http.ts
|
|
10
|
+
import fs2 from "node:fs";
|
|
11
|
+
import path2 from "node:path";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
// src/server.ts
|
|
15
|
+
import { renderToPipeableStream } from "react-server-dom-webpack/server";
|
|
16
|
+
function defaultOnError(err) {
|
|
17
|
+
console.error("[webframez-react] RSC render error", err);
|
|
18
|
+
}
|
|
19
|
+
function createHTMLShell(options = {}) {
|
|
20
|
+
const {
|
|
21
|
+
title = "RSC App",
|
|
22
|
+
rscEndpoint = "/rsc",
|
|
23
|
+
clientScriptUrl = "/client.js",
|
|
24
|
+
headTags = "",
|
|
25
|
+
rootHtml = "",
|
|
26
|
+
basename = "",
|
|
27
|
+
liveReloadPath,
|
|
28
|
+
liveReloadServerId
|
|
29
|
+
} = options;
|
|
30
|
+
const liveReloadScript = liveReloadPath && liveReloadServerId ? `<script>
|
|
31
|
+
(() => {
|
|
32
|
+
const endpoint = ${JSON.stringify(liveReloadPath)};
|
|
33
|
+
let currentServerId = ${JSON.stringify(liveReloadServerId)};
|
|
34
|
+
|
|
35
|
+
const connect = () => {
|
|
36
|
+
const source = new EventSource(endpoint);
|
|
37
|
+
|
|
38
|
+
source.onmessage = (event) => {
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(event.data);
|
|
41
|
+
const nextServerId =
|
|
42
|
+
data && typeof data.serverId === "string" ? data.serverId : "";
|
|
43
|
+
|
|
44
|
+
if (nextServerId && nextServerId !== currentServerId) {
|
|
45
|
+
currentServerId = nextServerId;
|
|
46
|
+
window.location.reload();
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Ignore malformed payloads in dev mode.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
source.onerror = () => {
|
|
54
|
+
source.close();
|
|
55
|
+
setTimeout(connect, 350);
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
connect();
|
|
60
|
+
})();
|
|
61
|
+
</script>` : "";
|
|
62
|
+
return `<!doctype html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="utf-8" />
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
67
|
+
<title>${title}</title>
|
|
68
|
+
${headTags}
|
|
69
|
+
</head>
|
|
70
|
+
<body>
|
|
71
|
+
<div id="root">${rootHtml}</div>
|
|
72
|
+
<script>window.__RSC_ENDPOINT = "${rscEndpoint}";</script>
|
|
73
|
+
<script>window.__RSC_BASENAME = "${basename}";</script>
|
|
74
|
+
<script type="module" src="${clientScriptUrl}"></script>
|
|
75
|
+
${liveReloadScript}
|
|
76
|
+
</body>
|
|
77
|
+
</html>`;
|
|
78
|
+
}
|
|
79
|
+
function sendRSC(res, model, options = {}) {
|
|
80
|
+
const {
|
|
81
|
+
moduleMap = {},
|
|
82
|
+
onError = defaultOnError,
|
|
83
|
+
statusCode = 200,
|
|
84
|
+
contentType = "text/x-component"
|
|
85
|
+
} = options;
|
|
86
|
+
res.statusCode = statusCode;
|
|
87
|
+
res.setHeader("Content-Type", contentType);
|
|
88
|
+
const stream = renderToPipeableStream(model, moduleMap, { onError });
|
|
89
|
+
stream.pipe(res);
|
|
90
|
+
return stream;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/file-router.tsx
|
|
94
|
+
import fs from "node:fs";
|
|
95
|
+
import path from "node:path";
|
|
96
|
+
|
|
97
|
+
// src/router-runtime.tsx
|
|
98
|
+
import React from "react";
|
|
99
|
+
var ROUTE_CHILDREN_TAG = "webframez-route-children";
|
|
100
|
+
var ROUTE_CHILDREN_SENTINEL = "__webframezRouteChildren";
|
|
101
|
+
var ROUTE_CHILDREN_DISPLAY_NAME = "WebframezRouteChildren";
|
|
102
|
+
var RouteChildrenImpl = () => React.createElement(ROUTE_CHILDREN_TAG);
|
|
103
|
+
RouteChildrenImpl.displayName = ROUTE_CHILDREN_DISPLAY_NAME;
|
|
104
|
+
RouteChildrenImpl[ROUTE_CHILDREN_SENTINEL] = true;
|
|
105
|
+
var RouteChildren = RouteChildrenImpl;
|
|
106
|
+
function isRouteChildrenType(type) {
|
|
107
|
+
if (type === ROUTE_CHILDREN_TAG || type === RouteChildren) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (!type || typeof type !== "function" && typeof type !== "object") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const candidate = type;
|
|
115
|
+
return candidate[ROUTE_CHILDREN_SENTINEL] === true || candidate.displayName === ROUTE_CHILDREN_DISPLAY_NAME || candidate.name === "RouteChildren";
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function injectRouteChildren(node, routeChildren) {
|
|
121
|
+
if (node === null || node === void 0 || typeof node === "boolean") {
|
|
122
|
+
return node;
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(node)) {
|
|
125
|
+
let changed = false;
|
|
126
|
+
const next = node.map((child) => {
|
|
127
|
+
const injected = injectRouteChildren(child, routeChildren);
|
|
128
|
+
if (injected !== child) {
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
return injected;
|
|
132
|
+
});
|
|
133
|
+
return changed ? next : node;
|
|
134
|
+
}
|
|
135
|
+
if (!React.isValidElement(node)) {
|
|
136
|
+
return node;
|
|
137
|
+
}
|
|
138
|
+
if (isRouteChildrenType(node.type)) {
|
|
139
|
+
return routeChildren;
|
|
140
|
+
}
|
|
141
|
+
const props = node.props;
|
|
142
|
+
if (!("children" in props)) {
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
const nextChildren = injectRouteChildren(props.children, routeChildren);
|
|
146
|
+
if (nextChildren === props.children) {
|
|
147
|
+
return node;
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(nextChildren)) {
|
|
150
|
+
return React.cloneElement(node, void 0, ...nextChildren);
|
|
151
|
+
}
|
|
152
|
+
return React.cloneElement(node, void 0, nextChildren);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/file-router.tsx
|
|
156
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
157
|
+
function normalizePathname(pathname) {
|
|
158
|
+
const trimmed = pathname.replace(/\/+$/, "") || "/";
|
|
159
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
160
|
+
}
|
|
161
|
+
function splitSegments(pathname) {
|
|
162
|
+
const normalized = normalizePathname(pathname);
|
|
163
|
+
if (normalized === "/") {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return normalized.slice(1).split("/").filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
function walkFiles(dir) {
|
|
169
|
+
if (!fs.existsSync(dir)) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const result = [];
|
|
173
|
+
const stack = [dir];
|
|
174
|
+
while (stack.length > 0) {
|
|
175
|
+
const current = stack.pop();
|
|
176
|
+
if (!current) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
180
|
+
const fullPath = path.join(current, entry.name);
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
stack.push(fullPath);
|
|
183
|
+
} else if (entry.isFile()) {
|
|
184
|
+
result.push(fullPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
function readSearchParams(urlSearchParams) {
|
|
191
|
+
const output = {};
|
|
192
|
+
for (const key of urlSearchParams.keys()) {
|
|
193
|
+
const values = urlSearchParams.getAll(key);
|
|
194
|
+
output[key] = values.length <= 1 ? values[0] ?? "" : values;
|
|
195
|
+
}
|
|
196
|
+
return output;
|
|
197
|
+
}
|
|
198
|
+
function escapeHtml(value) {
|
|
199
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
200
|
+
}
|
|
201
|
+
function toRouteEntry(pagesDir, filePath) {
|
|
202
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
203
|
+
if (!normalized.endsWith("/index.js")) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const relativeDir = path.dirname(path.relative(pagesDir, filePath)).replace(/\\/g, "/");
|
|
207
|
+
const segments = relativeDir === "." ? [] : relativeDir.split("/").filter(Boolean);
|
|
208
|
+
const staticCount = segments.filter((segment) => !segment.startsWith("[")).length;
|
|
209
|
+
return {
|
|
210
|
+
filePath,
|
|
211
|
+
segments,
|
|
212
|
+
staticCount
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function matchRoute(entry, pathname) {
|
|
216
|
+
const targetSegments = splitSegments(pathname);
|
|
217
|
+
if (entry.segments.length !== targetSegments.length) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const params = {};
|
|
221
|
+
for (let index = 0; index < entry.segments.length; index += 1) {
|
|
222
|
+
const routeSegment = entry.segments[index];
|
|
223
|
+
const targetSegment = targetSegments[index];
|
|
224
|
+
if (routeSegment.startsWith("[") && routeSegment.endsWith("]")) {
|
|
225
|
+
const paramName = routeSegment.slice(1, -1);
|
|
226
|
+
if (!paramName) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
params[paramName] = decodeURIComponent(targetSegment);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (routeSegment !== targetSegment) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return params;
|
|
237
|
+
}
|
|
238
|
+
function mergeHead(...configs) {
|
|
239
|
+
const merged = {
|
|
240
|
+
meta: [],
|
|
241
|
+
links: []
|
|
242
|
+
};
|
|
243
|
+
for (const config of configs) {
|
|
244
|
+
if (!config) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (config.title) {
|
|
248
|
+
merged.title = config.title;
|
|
249
|
+
}
|
|
250
|
+
if (config.description) {
|
|
251
|
+
merged.description = config.description;
|
|
252
|
+
}
|
|
253
|
+
if (config.favicon) {
|
|
254
|
+
merged.favicon = config.favicon;
|
|
255
|
+
}
|
|
256
|
+
if (config.meta) {
|
|
257
|
+
merged.meta?.push(...config.meta);
|
|
258
|
+
}
|
|
259
|
+
if (config.links) {
|
|
260
|
+
merged.links?.push(...config.links);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return merged;
|
|
264
|
+
}
|
|
265
|
+
function renderHeadToString(head) {
|
|
266
|
+
const tags = [];
|
|
267
|
+
if (head.description) {
|
|
268
|
+
tags.push(
|
|
269
|
+
`<meta name="description" content="${escapeHtml(head.description)}" />`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (head.favicon) {
|
|
273
|
+
tags.push(`<link rel="icon" href="${escapeHtml(head.favicon)}" />`);
|
|
274
|
+
}
|
|
275
|
+
for (const meta of head.meta ?? []) {
|
|
276
|
+
const attrs = Object.entries(meta).filter(([, value]) => Boolean(value)).map(([key, value]) => `${key}="${escapeHtml(String(value))}"`).join(" ");
|
|
277
|
+
tags.push(`<meta ${attrs} />`);
|
|
278
|
+
}
|
|
279
|
+
for (const link of head.links ?? []) {
|
|
280
|
+
const attrs = Object.entries(link).filter(([, value]) => Boolean(value)).map(([key, value]) => `${key}="${escapeHtml(String(value))}"`).join(" ");
|
|
281
|
+
tags.push(`<link ${attrs} />`);
|
|
282
|
+
}
|
|
283
|
+
return tags.join("\n");
|
|
284
|
+
}
|
|
285
|
+
function resolveModule(modulePath) {
|
|
286
|
+
delete __require.cache[modulePath];
|
|
287
|
+
return __require(modulePath);
|
|
288
|
+
}
|
|
289
|
+
async function resolveHead(candidate, context) {
|
|
290
|
+
if (!candidate.Head) {
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
return candidate.Head(context);
|
|
294
|
+
}
|
|
295
|
+
function findBestMatch(entries, pathname) {
|
|
296
|
+
const matches = [];
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
const params = matchRoute(entry, pathname);
|
|
299
|
+
if (params) {
|
|
300
|
+
matches.push({ entry, params });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
matches.sort((a, b) => {
|
|
304
|
+
if (b.entry.staticCount !== a.entry.staticCount) {
|
|
305
|
+
return b.entry.staticCount - a.entry.staticCount;
|
|
306
|
+
}
|
|
307
|
+
return b.entry.segments.length - a.entry.segments.length;
|
|
308
|
+
});
|
|
309
|
+
return matches[0] ?? null;
|
|
310
|
+
}
|
|
311
|
+
function normalizeAbortStatus(value) {
|
|
312
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
313
|
+
return 404;
|
|
314
|
+
}
|
|
315
|
+
const normalized = Math.trunc(value);
|
|
316
|
+
if (normalized < 100 || normalized > 599) {
|
|
317
|
+
return 404;
|
|
318
|
+
}
|
|
319
|
+
return normalized;
|
|
320
|
+
}
|
|
321
|
+
function createAbort(options) {
|
|
322
|
+
return {
|
|
323
|
+
__webframezRouteAbort: true,
|
|
324
|
+
statusCode: normalizeAbortStatus(options?.status),
|
|
325
|
+
message: options?.message?.trim() || "Page not found",
|
|
326
|
+
payload: options?.payload
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function isRouteAbort(value) {
|
|
330
|
+
if (!value || typeof value !== "object") {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
return value.__webframezRouteAbort === true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function createFileRouter(options) {
|
|
340
|
+
const pagesDir = options.pagesDir;
|
|
341
|
+
const layoutPath = path.join(pagesDir, "layout.js");
|
|
342
|
+
const errorPath = path.join(pagesDir, "errors.js");
|
|
343
|
+
function buildRouteEntries() {
|
|
344
|
+
const files = walkFiles(pagesDir);
|
|
345
|
+
return files.map((filePath) => toRouteEntry(pagesDir, filePath)).filter((entry) => entry !== null).sort((a, b) => {
|
|
346
|
+
if (b.staticCount !== a.staticCount) {
|
|
347
|
+
return b.staticCount - a.staticCount;
|
|
348
|
+
}
|
|
349
|
+
return b.segments.length - a.segments.length;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async function renderError(context, statusCode, message, payload) {
|
|
353
|
+
const errorProps = {
|
|
354
|
+
...context,
|
|
355
|
+
statusCode,
|
|
356
|
+
message,
|
|
357
|
+
payload
|
|
358
|
+
};
|
|
359
|
+
const layoutModule = fs.existsSync(layoutPath) ? resolveModule(layoutPath) : null;
|
|
360
|
+
if (!fs.existsSync(errorPath)) {
|
|
361
|
+
const fallback = /* @__PURE__ */ jsxs("main", { style: { fontFamily: "system-ui, sans-serif", padding: 24 }, children: [
|
|
362
|
+
/* @__PURE__ */ jsx("h1", { children: statusCode }),
|
|
363
|
+
/* @__PURE__ */ jsx("p", { children: message })
|
|
364
|
+
] });
|
|
365
|
+
return {
|
|
366
|
+
statusCode,
|
|
367
|
+
model: fallback,
|
|
368
|
+
head: {
|
|
369
|
+
title: `${statusCode} - ${message}`
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const errorModule = resolveModule(errorPath);
|
|
374
|
+
const errorNode = errorModule.default(errorProps);
|
|
375
|
+
const layoutHead = layoutModule ? await resolveHead(layoutModule, context) : void 0;
|
|
376
|
+
const errorHead = await resolveHead(errorModule, errorProps);
|
|
377
|
+
const model = layoutModule ? injectRouteChildren(layoutModule.default(context), errorNode) : errorNode;
|
|
378
|
+
return {
|
|
379
|
+
statusCode,
|
|
380
|
+
model,
|
|
381
|
+
head: mergeHead(layoutHead, errorHead)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function resolve(input) {
|
|
385
|
+
const pathname = normalizePathname(input.pathname);
|
|
386
|
+
const contextBase = {
|
|
387
|
+
pathname,
|
|
388
|
+
params: {},
|
|
389
|
+
searchParams: input.searchParams,
|
|
390
|
+
cookies: input.cookies ?? {},
|
|
391
|
+
abort: (options2) => {
|
|
392
|
+
throw createAbort(options2);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
let activeContext = contextBase;
|
|
396
|
+
try {
|
|
397
|
+
const entries = buildRouteEntries();
|
|
398
|
+
const match = findBestMatch(entries, pathname);
|
|
399
|
+
if (!match) {
|
|
400
|
+
return renderError(contextBase, 404, "Page not found");
|
|
401
|
+
}
|
|
402
|
+
const context = {
|
|
403
|
+
...contextBase,
|
|
404
|
+
params: match.params
|
|
405
|
+
};
|
|
406
|
+
activeContext = context;
|
|
407
|
+
const pageModule = resolveModule(match.entry.filePath);
|
|
408
|
+
const layoutModule = fs.existsSync(layoutPath) ? resolveModule(layoutPath) : null;
|
|
409
|
+
const pageNode = pageModule.default(context);
|
|
410
|
+
const layoutHead = layoutModule ? await resolveHead(layoutModule, context) : void 0;
|
|
411
|
+
const pageHead = await resolveHead(pageModule, context);
|
|
412
|
+
const model = layoutModule ? injectRouteChildren(layoutModule.default(context), pageNode) : pageNode;
|
|
413
|
+
return {
|
|
414
|
+
statusCode: 200,
|
|
415
|
+
model,
|
|
416
|
+
head: mergeHead(layoutHead, pageHead)
|
|
417
|
+
};
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (isRouteAbort(error)) {
|
|
420
|
+
return renderError(activeContext, error.statusCode, error.message, error.payload);
|
|
421
|
+
}
|
|
422
|
+
console.error("[webframez-react] Failed to resolve route", error);
|
|
423
|
+
return renderError(contextBase, 500, "Internal server error");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
resolve,
|
|
428
|
+
readSearchParams
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function parseSearchParams(query) {
|
|
432
|
+
return readSearchParams(query);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/http.ts
|
|
436
|
+
function normalizeBasePath(basePath) {
|
|
437
|
+
if (!basePath || basePath === "/") {
|
|
438
|
+
return "";
|
|
439
|
+
}
|
|
440
|
+
const withLeadingSlash = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
441
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
442
|
+
}
|
|
443
|
+
function stripBasePath(pathname, basePath) {
|
|
444
|
+
if (!basePath) {
|
|
445
|
+
return pathname;
|
|
446
|
+
}
|
|
447
|
+
if (pathname === basePath) {
|
|
448
|
+
return "/";
|
|
449
|
+
}
|
|
450
|
+
if (pathname.startsWith(`${basePath}/`)) {
|
|
451
|
+
return pathname.slice(basePath.length) || "/";
|
|
452
|
+
}
|
|
453
|
+
return pathname;
|
|
454
|
+
}
|
|
455
|
+
function runNodeCommand(args) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
execFile(process.execPath, args, { timeout: 1e4, maxBuffer: 1024 * 1024 * 5 }, (error, stdout, stderr) => {
|
|
458
|
+
if (error) {
|
|
459
|
+
const out = stderr && stderr.trim() !== "" ? stderr : stdout;
|
|
460
|
+
reject(new Error(out || error.message));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
resolve({ stdout, stderr });
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async function renderInitialHtmlInWorker(options) {
|
|
468
|
+
const payload = Buffer.from(JSON.stringify(options), "utf8").toString("base64url");
|
|
469
|
+
const script = `
|
|
470
|
+
const path = require("node:path");
|
|
471
|
+
const input = JSON.parse(Buffer.from(process.argv[1], "base64url").toString("utf8"));
|
|
472
|
+
globalThis.__RSC_BASENAME = input.basename || "";
|
|
473
|
+
const { createFileRouter } = require("webframez-react/router");
|
|
474
|
+
const reactDomPkg = require.resolve("react-dom/package.json", {
|
|
475
|
+
paths: [process.cwd(), input.pagesDir]
|
|
476
|
+
});
|
|
477
|
+
const reactDomServer = require(path.join(path.dirname(reactDomPkg), "server.node.js"));
|
|
478
|
+
|
|
479
|
+
(async () => {
|
|
480
|
+
const router = createFileRouter({ pagesDir: input.pagesDir });
|
|
481
|
+
const resolved = await router.resolve({
|
|
482
|
+
pathname: input.pathname,
|
|
483
|
+
searchParams: input.searchParams || {},
|
|
484
|
+
cookies: input.cookies || {},
|
|
485
|
+
});
|
|
486
|
+
const html = reactDomServer.renderToString(resolved.model);
|
|
487
|
+
process.stdout.write(JSON.stringify({ html }));
|
|
488
|
+
})().catch((error) => {
|
|
489
|
+
process.stderr.write(error && (error.stack || String(error)) ? (error.stack || String(error)) : "Unknown SSR worker error");
|
|
490
|
+
process.exit(1);
|
|
491
|
+
});
|
|
492
|
+
`;
|
|
493
|
+
const { stdout } = await runNodeCommand(["-e", script, payload]);
|
|
494
|
+
const parsed = JSON.parse(stdout || "{}");
|
|
495
|
+
return parsed.html || "";
|
|
496
|
+
}
|
|
497
|
+
function withRequestBasename(basename, fn) {
|
|
498
|
+
const target = globalThis;
|
|
499
|
+
const previous = target.__RSC_BASENAME;
|
|
500
|
+
target.__RSC_BASENAME = basename;
|
|
501
|
+
const finish = () => {
|
|
502
|
+
target.__RSC_BASENAME = previous;
|
|
503
|
+
};
|
|
504
|
+
try {
|
|
505
|
+
const result = fn();
|
|
506
|
+
if (result && typeof result.then === "function") {
|
|
507
|
+
return result.finally(finish);
|
|
508
|
+
}
|
|
509
|
+
finish();
|
|
510
|
+
return result;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
finish();
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function parseCookies(rawCookieHeader) {
|
|
517
|
+
const raw = Array.isArray(rawCookieHeader) ? rawCookieHeader.join("; ") : rawCookieHeader ?? "";
|
|
518
|
+
const output = {};
|
|
519
|
+
if (!raw || raw.trim() === "") {
|
|
520
|
+
return output;
|
|
521
|
+
}
|
|
522
|
+
for (const pair of raw.split(";")) {
|
|
523
|
+
const part = pair.trim();
|
|
524
|
+
if (!part) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const eqIndex = part.indexOf("=");
|
|
528
|
+
const name = eqIndex >= 0 ? part.slice(0, eqIndex).trim() : part.trim();
|
|
529
|
+
const value = eqIndex >= 0 ? part.slice(eqIndex + 1).trim() : "";
|
|
530
|
+
if (!name) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
output[name] = decodeURIComponent(value);
|
|
534
|
+
}
|
|
535
|
+
return output;
|
|
536
|
+
}
|
|
537
|
+
function createNodeRequestHandler(options) {
|
|
538
|
+
const devServerId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
539
|
+
const distRootDir = path2.resolve(options.distRootDir);
|
|
540
|
+
const pagesDir = path2.resolve(options.pagesDir ?? path2.join(distRootDir, "pages"));
|
|
541
|
+
const manifestPath = path2.resolve(
|
|
542
|
+
options.manifestPath ?? path2.join(distRootDir, "react-client-manifest.json")
|
|
543
|
+
);
|
|
544
|
+
const assetsPrefix = options.assetsPrefix ?? "/assets/";
|
|
545
|
+
const rscPath = options.rscPath ?? "/rsc";
|
|
546
|
+
const clientScriptUrl = options.clientScriptUrl ?? "/assets/client.js";
|
|
547
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
548
|
+
const nodeEnv = process.env.NODE_ENV || "";
|
|
549
|
+
const runningInWatchMode = Array.isArray(process.execArgv) && process.execArgv.includes("--watch");
|
|
550
|
+
const liveReloadEnabled = options.liveReloadPath !== false && (nodeEnv === "development" || runningInWatchMode);
|
|
551
|
+
const liveReloadPath = !liveReloadEnabled ? "" : options.liveReloadPath ?? `${basePath || ""}/__webframez_live_reload`;
|
|
552
|
+
const liveReloadClients = /* @__PURE__ */ new Set();
|
|
553
|
+
const router = createFileRouter({ pagesDir });
|
|
554
|
+
const moduleMap = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
555
|
+
return async function handleRequest(req, res) {
|
|
556
|
+
if (!req.url) {
|
|
557
|
+
res.statusCode = 400;
|
|
558
|
+
res.end("Bad request");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const url = new URL(req.url, "http://localhost");
|
|
562
|
+
const requestCookies = parseCookies(req.headers.cookie);
|
|
563
|
+
if (liveReloadPath && url.pathname === liveReloadPath) {
|
|
564
|
+
const accept = String(req.headers.accept || "");
|
|
565
|
+
if (accept.includes("text/event-stream")) {
|
|
566
|
+
res.statusCode = 200;
|
|
567
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
568
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
569
|
+
res.setHeader("Connection", "keep-alive");
|
|
570
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
571
|
+
if (typeof res.flushHeaders === "function") {
|
|
572
|
+
res.flushHeaders();
|
|
573
|
+
}
|
|
574
|
+
res.write(`retry: 400
|
|
575
|
+
`);
|
|
576
|
+
res.write(`data: ${JSON.stringify({ serverId: devServerId })}
|
|
577
|
+
|
|
578
|
+
`);
|
|
579
|
+
liveReloadClients.add(res);
|
|
580
|
+
const heartbeat = setInterval(() => {
|
|
581
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
582
|
+
res.write(`: ping
|
|
583
|
+
|
|
584
|
+
`);
|
|
585
|
+
}
|
|
586
|
+
}, 15e3);
|
|
587
|
+
const cleanup = () => {
|
|
588
|
+
clearInterval(heartbeat);
|
|
589
|
+
liveReloadClients.delete(res);
|
|
590
|
+
};
|
|
591
|
+
req.on("close", cleanup);
|
|
592
|
+
res.on("close", cleanup);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
res.statusCode = 200;
|
|
596
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
597
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
598
|
+
res.end(JSON.stringify({ serverId: devServerId, clients: liveReloadClients.size }));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (url.pathname === rscPath) {
|
|
602
|
+
const pathname = stripBasePath(url.searchParams.get("path") || "/", basePath);
|
|
603
|
+
const search = new URLSearchParams(url.searchParams.get("search") || "");
|
|
604
|
+
const resolved2 = await withRequestBasename(
|
|
605
|
+
basePath,
|
|
606
|
+
() => router.resolve({
|
|
607
|
+
pathname,
|
|
608
|
+
searchParams: parseSearchParams(search),
|
|
609
|
+
cookies: requestCookies
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
sendRSC(res, resolved2.model, {
|
|
613
|
+
moduleMap,
|
|
614
|
+
statusCode: resolved2.statusCode
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (url.pathname.startsWith(assetsPrefix)) {
|
|
619
|
+
const relative = url.pathname.slice(assetsPrefix.length);
|
|
620
|
+
const filePath = path2.resolve(distRootDir, relative);
|
|
621
|
+
if (!filePath.startsWith(distRootDir)) {
|
|
622
|
+
res.statusCode = 400;
|
|
623
|
+
res.end("Invalid path");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const ext = path2.extname(filePath);
|
|
627
|
+
if (ext === ".js" || ext === ".mjs") {
|
|
628
|
+
res.setHeader("Content-Type", "text/javascript; charset=utf-8");
|
|
629
|
+
} else if (ext === ".json") {
|
|
630
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
631
|
+
}
|
|
632
|
+
const stream = fs2.createReadStream(filePath);
|
|
633
|
+
stream.on("error", () => {
|
|
634
|
+
res.statusCode = 404;
|
|
635
|
+
res.end("Not found");
|
|
636
|
+
});
|
|
637
|
+
stream.pipe(res);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const resolved = await withRequestBasename(
|
|
641
|
+
basePath,
|
|
642
|
+
() => router.resolve({
|
|
643
|
+
pathname: stripBasePath(url.pathname, basePath),
|
|
644
|
+
searchParams: parseSearchParams(url.searchParams),
|
|
645
|
+
cookies: requestCookies
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
let rootHtml = "";
|
|
649
|
+
try {
|
|
650
|
+
rootHtml = await renderInitialHtmlInWorker({
|
|
651
|
+
pagesDir,
|
|
652
|
+
pathname: stripBasePath(url.pathname, basePath),
|
|
653
|
+
searchParams: parseSearchParams(url.searchParams),
|
|
654
|
+
cookies: requestCookies,
|
|
655
|
+
basename: basePath
|
|
656
|
+
});
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error("[webframez-react] Failed to render initial HTML", error);
|
|
659
|
+
}
|
|
660
|
+
res.statusCode = resolved.statusCode;
|
|
661
|
+
res.setHeader("Content-Type", "text/html");
|
|
662
|
+
res.end(
|
|
663
|
+
createHTMLShell({
|
|
664
|
+
title: resolved.head.title || "Webframez React",
|
|
665
|
+
headTags: renderHeadToString(resolved.head),
|
|
666
|
+
clientScriptUrl,
|
|
667
|
+
rscEndpoint: rscPath,
|
|
668
|
+
rootHtml,
|
|
669
|
+
basename: basePath,
|
|
670
|
+
liveReloadPath: liveReloadPath || void 0,
|
|
671
|
+
liveReloadServerId: liveReloadPath ? devServerId : void 0
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/webframez-core.ts
|
|
678
|
+
function normalizeMountPath(path3) {
|
|
679
|
+
const trimmed = (path3 || "").trim();
|
|
680
|
+
let normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
681
|
+
if (normalized.endsWith("/**")) {
|
|
682
|
+
normalized = normalized.slice(0, -3);
|
|
683
|
+
} else if (normalized.endsWith("/*")) {
|
|
684
|
+
normalized = normalized.slice(0, -2);
|
|
685
|
+
}
|
|
686
|
+
while (normalized.length > 1 && normalized.endsWith("/")) {
|
|
687
|
+
normalized = normalized.slice(0, -1);
|
|
688
|
+
}
|
|
689
|
+
return normalized || "/";
|
|
690
|
+
}
|
|
691
|
+
function buildRenderDefaults(path3) {
|
|
692
|
+
const mountPath = normalizeMountPath(path3);
|
|
693
|
+
const routePath = mountPath === "/" ? "/*" : `${mountPath}/*`;
|
|
694
|
+
if (mountPath === "/") {
|
|
695
|
+
return {
|
|
696
|
+
routePath,
|
|
697
|
+
basePath: void 0,
|
|
698
|
+
assetsPrefix: void 0,
|
|
699
|
+
rscPath: void 0,
|
|
700
|
+
clientScriptUrl: void 0
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
routePath,
|
|
705
|
+
basePath: mountPath,
|
|
706
|
+
assetsPrefix: `${mountPath}/assets/`,
|
|
707
|
+
rscPath: `${mountPath}/rsc`,
|
|
708
|
+
clientScriptUrl: `${mountPath}/assets/client.js`
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
async function waitForResponseFinish(res) {
|
|
712
|
+
if (res.writableEnded || res.destroyed) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
await new Promise((resolve, reject) => {
|
|
716
|
+
const onFinish = () => {
|
|
717
|
+
cleanup();
|
|
718
|
+
resolve();
|
|
719
|
+
};
|
|
720
|
+
const onClose = () => {
|
|
721
|
+
cleanup();
|
|
722
|
+
resolve();
|
|
723
|
+
};
|
|
724
|
+
const onError = (error) => {
|
|
725
|
+
cleanup();
|
|
726
|
+
reject(error);
|
|
727
|
+
};
|
|
728
|
+
const cleanup = () => {
|
|
729
|
+
res.off("finish", onFinish);
|
|
730
|
+
res.off("close", onClose);
|
|
731
|
+
res.off("error", onError);
|
|
732
|
+
};
|
|
733
|
+
res.on("finish", onFinish);
|
|
734
|
+
res.on("close", onClose);
|
|
735
|
+
res.on("error", onError);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function normalizeMethods(method) {
|
|
739
|
+
if (!method) {
|
|
740
|
+
return ["GET"];
|
|
741
|
+
}
|
|
742
|
+
return Array.isArray(method) ? method : [method];
|
|
743
|
+
}
|
|
744
|
+
function registerByMethod(route, method, path3, component, routeOptions) {
|
|
745
|
+
if (method === "GET") {
|
|
746
|
+
route.get(path3, component, routeOptions);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (method === "POST") {
|
|
750
|
+
route.post(path3, component, routeOptions);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (method === "PUT") {
|
|
754
|
+
route.put(path3, component, routeOptions);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
route.delete(path3, component, routeOptions);
|
|
758
|
+
}
|
|
759
|
+
function registerRouteRenderer(route, methodName) {
|
|
760
|
+
route.extend(methodName, () => {
|
|
761
|
+
return (path3, options) => {
|
|
762
|
+
if (!options || !options.distRootDir) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`Route.${methodName} requires at least { distRootDir }`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const defaults = buildRenderDefaults(path3);
|
|
768
|
+
const {
|
|
769
|
+
method,
|
|
770
|
+
routeOptions,
|
|
771
|
+
basePath,
|
|
772
|
+
assetsPrefix,
|
|
773
|
+
rscPath,
|
|
774
|
+
clientScriptUrl,
|
|
775
|
+
...nodeHandlerOptions
|
|
776
|
+
} = options;
|
|
777
|
+
const handleNodeRequest = createNodeRequestHandler({
|
|
778
|
+
...nodeHandlerOptions,
|
|
779
|
+
basePath: basePath ?? defaults.basePath,
|
|
780
|
+
assetsPrefix: assetsPrefix ?? defaults.assetsPrefix,
|
|
781
|
+
rscPath: rscPath ?? defaults.rscPath,
|
|
782
|
+
clientScriptUrl: clientScriptUrl ?? defaults.clientScriptUrl
|
|
783
|
+
});
|
|
784
|
+
const methods = normalizeMethods(method);
|
|
785
|
+
for (const currentMethod of methods) {
|
|
786
|
+
registerByMethod(
|
|
787
|
+
route,
|
|
788
|
+
currentMethod,
|
|
789
|
+
defaults.routePath,
|
|
790
|
+
async (req, res) => {
|
|
791
|
+
if (!req.message || !res.res) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
`Route.${methodName} only supports node/http requests`
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
await handleNodeRequest(req.message, res.res);
|
|
797
|
+
await waitForResponseFinish(res.res);
|
|
798
|
+
},
|
|
799
|
+
routeOptions
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function initWebframezReact(route) {
|
|
806
|
+
if (!route || typeof route.extend !== "function") {
|
|
807
|
+
throw new Error(
|
|
808
|
+
"initWebframezReact requires a webframez-core Route facade"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (typeof route.renderReact !== "function") {
|
|
812
|
+
registerRouteRenderer(route, "renderReact");
|
|
813
|
+
}
|
|
814
|
+
if (typeof route.reactRender !== "function") {
|
|
815
|
+
registerRouteRenderer(route, "reactRender");
|
|
816
|
+
}
|
|
817
|
+
return route;
|
|
818
|
+
}
|
|
819
|
+
var setupWebframezCoreReactRoute = initWebframezReact;
|
|
820
|
+
export {
|
|
821
|
+
initWebframezReact,
|
|
822
|
+
setupWebframezCoreReactRoute
|
|
823
|
+
};
|