@flexireact/core 3.0.2 → 3.0.4
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 +3 -3
- package/dist/cli/index.js +14 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/build/index.js +689 -0
- package/dist/core/build/index.js.map +1 -0
- package/dist/core/config.js +86 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/start-dev.js +3092 -0
- package/dist/core/start-dev.js.map +1 -0
- package/dist/core/start-prod.js +3092 -0
- package/dist/core/start-prod.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,3092 @@
|
|
|
1
|
+
// core/server/index.ts
|
|
2
|
+
import http from "http";
|
|
3
|
+
import fs8 from "fs";
|
|
4
|
+
import path7 from "path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL as pathToFileURL4 } from "url";
|
|
6
|
+
|
|
7
|
+
// core/config.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
var defaultConfig = {
|
|
12
|
+
// Directories
|
|
13
|
+
pagesDir: "pages",
|
|
14
|
+
layoutsDir: "layouts",
|
|
15
|
+
publicDir: "public",
|
|
16
|
+
outDir: ".flexi",
|
|
17
|
+
// Build options
|
|
18
|
+
build: {
|
|
19
|
+
target: "es2022",
|
|
20
|
+
minify: true,
|
|
21
|
+
sourcemap: true,
|
|
22
|
+
splitting: true
|
|
23
|
+
},
|
|
24
|
+
// Server options
|
|
25
|
+
server: {
|
|
26
|
+
port: 3e3,
|
|
27
|
+
host: "localhost"
|
|
28
|
+
},
|
|
29
|
+
// SSG options
|
|
30
|
+
ssg: {
|
|
31
|
+
enabled: false,
|
|
32
|
+
paths: []
|
|
33
|
+
},
|
|
34
|
+
// Islands (partial hydration)
|
|
35
|
+
islands: {
|
|
36
|
+
enabled: true
|
|
37
|
+
},
|
|
38
|
+
// RSC options
|
|
39
|
+
rsc: {
|
|
40
|
+
enabled: true
|
|
41
|
+
},
|
|
42
|
+
// Plugins
|
|
43
|
+
plugins: [],
|
|
44
|
+
// Styles (CSS files to include)
|
|
45
|
+
styles: [],
|
|
46
|
+
// Scripts (JS files to include)
|
|
47
|
+
scripts: [],
|
|
48
|
+
// Favicon path
|
|
49
|
+
favicon: null
|
|
50
|
+
};
|
|
51
|
+
async function loadConfig(projectRoot) {
|
|
52
|
+
const configPathTs = path.join(projectRoot, "flexireact.config.ts");
|
|
53
|
+
const configPathJs = path.join(projectRoot, "flexireact.config.js");
|
|
54
|
+
const configPath = fs.existsSync(configPathTs) ? configPathTs : configPathJs;
|
|
55
|
+
let userConfig = {};
|
|
56
|
+
if (fs.existsSync(configPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
59
|
+
const module = await import(`${configUrl}?t=${Date.now()}`);
|
|
60
|
+
userConfig = module.default || module;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("Warning: Failed to load flexireact config:", error.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return deepMerge(defaultConfig, userConfig);
|
|
66
|
+
}
|
|
67
|
+
function deepMerge(target, source) {
|
|
68
|
+
const result = { ...target };
|
|
69
|
+
for (const key in source) {
|
|
70
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
71
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
72
|
+
} else {
|
|
73
|
+
result[key] = source[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function resolvePaths(config, projectRoot) {
|
|
79
|
+
return {
|
|
80
|
+
...config,
|
|
81
|
+
pagesDir: path.resolve(projectRoot, config.pagesDir),
|
|
82
|
+
layoutsDir: path.resolve(projectRoot, config.layoutsDir),
|
|
83
|
+
publicDir: path.resolve(projectRoot, config.publicDir),
|
|
84
|
+
outDir: path.resolve(projectRoot, config.outDir)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// core/router/index.ts
|
|
89
|
+
import fs3 from "fs";
|
|
90
|
+
import path2 from "path";
|
|
91
|
+
|
|
92
|
+
// core/utils.ts
|
|
93
|
+
import fs2 from "fs";
|
|
94
|
+
function escapeHtml(str) {
|
|
95
|
+
const htmlEntities = {
|
|
96
|
+
"&": "&",
|
|
97
|
+
"<": "<",
|
|
98
|
+
">": ">",
|
|
99
|
+
'"': """,
|
|
100
|
+
"'": "'"
|
|
101
|
+
};
|
|
102
|
+
return String(str).replace(/[&<>"']/g, (char) => htmlEntities[char]);
|
|
103
|
+
}
|
|
104
|
+
function isServerComponent(filePath) {
|
|
105
|
+
try {
|
|
106
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
107
|
+
const firstLine = content.split("\n")[0].trim();
|
|
108
|
+
return firstLine === "'use server'" || firstLine === '"use server"';
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isClientComponent(filePath) {
|
|
114
|
+
try {
|
|
115
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
116
|
+
const firstLine = content.split("\n")[0].trim();
|
|
117
|
+
return firstLine === "'use client'" || firstLine === '"use client"';
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function isIsland(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
125
|
+
const firstLine = content.split("\n")[0].trim();
|
|
126
|
+
return firstLine === "'use island'" || firstLine === '"use island"';
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// core/router/index.ts
|
|
133
|
+
var RouteType = {
|
|
134
|
+
PAGE: "page",
|
|
135
|
+
API: "api",
|
|
136
|
+
LAYOUT: "layout",
|
|
137
|
+
LOADING: "loading",
|
|
138
|
+
ERROR: "error",
|
|
139
|
+
NOT_FOUND: "not-found"
|
|
140
|
+
};
|
|
141
|
+
function buildRouteTree(pagesDir, layoutsDir, appDir = null, routesDir = null) {
|
|
142
|
+
const projectRoot = path2.dirname(pagesDir);
|
|
143
|
+
const routes = {
|
|
144
|
+
pages: [],
|
|
145
|
+
api: [],
|
|
146
|
+
layouts: /* @__PURE__ */ new Map(),
|
|
147
|
+
tree: {},
|
|
148
|
+
appRoutes: [],
|
|
149
|
+
// Next.js style app router routes
|
|
150
|
+
flexiRoutes: []
|
|
151
|
+
// FlexiReact v2 routes/ directory
|
|
152
|
+
};
|
|
153
|
+
const routesDirPath = routesDir || path2.join(projectRoot, "routes");
|
|
154
|
+
if (fs3.existsSync(routesDirPath)) {
|
|
155
|
+
scanRoutesDirectory(routesDirPath, routesDirPath, routes);
|
|
156
|
+
}
|
|
157
|
+
const appDirPath = appDir || path2.join(projectRoot, "app");
|
|
158
|
+
if (fs3.existsSync(appDirPath)) {
|
|
159
|
+
scanAppDirectory(appDirPath, appDirPath, routes);
|
|
160
|
+
}
|
|
161
|
+
if (fs3.existsSync(pagesDir)) {
|
|
162
|
+
scanDirectory(pagesDir, pagesDir, routes);
|
|
163
|
+
}
|
|
164
|
+
if (fs3.existsSync(layoutsDir)) {
|
|
165
|
+
scanLayouts(layoutsDir, routes.layouts);
|
|
166
|
+
}
|
|
167
|
+
const rootLayoutPath = path2.join(appDirPath, "layout.tsx");
|
|
168
|
+
const rootLayoutPathJs = path2.join(appDirPath, "layout.jsx");
|
|
169
|
+
if (fs3.existsSync(rootLayoutPath)) {
|
|
170
|
+
routes.rootLayout = rootLayoutPath;
|
|
171
|
+
} else if (fs3.existsSync(rootLayoutPathJs)) {
|
|
172
|
+
routes.rootLayout = rootLayoutPathJs;
|
|
173
|
+
}
|
|
174
|
+
routes.tree = buildTree([...routes.flexiRoutes, ...routes.appRoutes, ...routes.pages]);
|
|
175
|
+
return routes;
|
|
176
|
+
}
|
|
177
|
+
function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null, parentMiddleware = null) {
|
|
178
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
179
|
+
let layoutFile = null;
|
|
180
|
+
let loadingFile = null;
|
|
181
|
+
let errorFile = null;
|
|
182
|
+
let middlewareFile = null;
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (entry.isFile()) {
|
|
185
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, "");
|
|
186
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
187
|
+
const ext = path2.extname(entry.name);
|
|
188
|
+
if (name === "layout") layoutFile = fullPath;
|
|
189
|
+
if (name === "loading") loadingFile = fullPath;
|
|
190
|
+
if (name === "error") errorFile = fullPath;
|
|
191
|
+
if (name === "_middleware" || name === "middleware") middlewareFile = fullPath;
|
|
192
|
+
if (["layout", "loading", "error", "not-found", "_middleware", "middleware"].includes(name)) continue;
|
|
193
|
+
if (![".tsx", ".jsx", ".ts", ".js"].includes(ext)) continue;
|
|
194
|
+
const relativePath = path2.relative(baseDir, currentDir);
|
|
195
|
+
const isApiRoute = relativePath.startsWith("api") || relativePath.startsWith("api/");
|
|
196
|
+
if (isApiRoute && [".ts", ".js"].includes(ext)) {
|
|
197
|
+
const apiPath = "/" + [...parentSegments, name === "index" ? "" : name].filter(Boolean).join("/");
|
|
198
|
+
routes.api.push({
|
|
199
|
+
type: RouteType.API,
|
|
200
|
+
path: apiPath.replace(/\/+/g, "/") || "/",
|
|
201
|
+
filePath: fullPath,
|
|
202
|
+
pattern: createRoutePattern(apiPath),
|
|
203
|
+
segments: [...parentSegments, name === "index" ? "" : name].filter(Boolean)
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if ([".tsx", ".jsx"].includes(ext)) {
|
|
208
|
+
let routePath;
|
|
209
|
+
if (name === "home" && parentSegments.length === 0) {
|
|
210
|
+
routePath = "/";
|
|
211
|
+
} else if (name === "index") {
|
|
212
|
+
routePath = "/" + parentSegments.join("/") || "/";
|
|
213
|
+
} else if (name.startsWith("[") && name.endsWith("]")) {
|
|
214
|
+
const paramName = name.slice(1, -1);
|
|
215
|
+
if (paramName.startsWith("...")) {
|
|
216
|
+
routePath = "/" + [...parentSegments, "*" + paramName.slice(3)].join("/");
|
|
217
|
+
} else {
|
|
218
|
+
routePath = "/" + [...parentSegments, ":" + paramName].join("/");
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
routePath = "/" + [...parentSegments, name].join("/");
|
|
222
|
+
}
|
|
223
|
+
routes.flexiRoutes.push({
|
|
224
|
+
type: RouteType.PAGE,
|
|
225
|
+
path: routePath.replace(/\/+/g, "/"),
|
|
226
|
+
filePath: fullPath,
|
|
227
|
+
pattern: createRoutePattern(routePath),
|
|
228
|
+
segments: routePath.split("/").filter(Boolean),
|
|
229
|
+
layout: layoutFile || parentLayout,
|
|
230
|
+
loading: loadingFile,
|
|
231
|
+
error: errorFile,
|
|
232
|
+
middleware: middlewareFile || parentMiddleware,
|
|
233
|
+
isFlexiRouter: true,
|
|
234
|
+
isServerComponent: isServerComponent(fullPath),
|
|
235
|
+
isClientComponent: isClientComponent(fullPath),
|
|
236
|
+
isIsland: isIsland(fullPath)
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (entry.isDirectory()) {
|
|
243
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
244
|
+
const dirName = entry.name;
|
|
245
|
+
if (dirName.startsWith("_") || dirName.startsWith(".")) continue;
|
|
246
|
+
const isGroup = dirName.startsWith("(") && dirName.endsWith(")");
|
|
247
|
+
let segmentName = dirName;
|
|
248
|
+
if (dirName.startsWith("[") && dirName.endsWith("]")) {
|
|
249
|
+
const paramName = dirName.slice(1, -1);
|
|
250
|
+
if (paramName.startsWith("...")) {
|
|
251
|
+
segmentName = "*" + paramName.slice(3);
|
|
252
|
+
} else {
|
|
253
|
+
segmentName = ":" + paramName;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
|
|
257
|
+
const newLayout = layoutFile || parentLayout;
|
|
258
|
+
const newMiddleware = middlewareFile || parentMiddleware;
|
|
259
|
+
scanRoutesDirectory(baseDir, fullPath, routes, newSegments, newLayout, newMiddleware);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null, parentMiddleware = null) {
|
|
264
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
265
|
+
const specialFiles = {
|
|
266
|
+
page: null,
|
|
267
|
+
layout: null,
|
|
268
|
+
loading: null,
|
|
269
|
+
error: null,
|
|
270
|
+
notFound: null,
|
|
271
|
+
template: null,
|
|
272
|
+
middleware: null
|
|
273
|
+
};
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (entry.isFile()) {
|
|
276
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, "");
|
|
277
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
278
|
+
if (name === "page") specialFiles.page = fullPath;
|
|
279
|
+
if (name === "layout") specialFiles.layout = fullPath;
|
|
280
|
+
if (name === "loading") specialFiles.loading = fullPath;
|
|
281
|
+
if (name === "error") specialFiles.error = fullPath;
|
|
282
|
+
if (name === "not-found") specialFiles.notFound = fullPath;
|
|
283
|
+
if (name === "template") specialFiles.template = fullPath;
|
|
284
|
+
if (name === "middleware" || name === "_middleware") specialFiles.middleware = fullPath;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (specialFiles.page) {
|
|
288
|
+
const routePath = "/" + parentSegments.join("/") || "/";
|
|
289
|
+
routes.appRoutes.push({
|
|
290
|
+
type: RouteType.PAGE,
|
|
291
|
+
path: routePath.replace(/\/+/g, "/"),
|
|
292
|
+
filePath: specialFiles.page,
|
|
293
|
+
pattern: createRoutePattern(routePath),
|
|
294
|
+
segments: parentSegments,
|
|
295
|
+
layout: specialFiles.layout || parentLayout,
|
|
296
|
+
loading: specialFiles.loading,
|
|
297
|
+
error: specialFiles.error,
|
|
298
|
+
notFound: specialFiles.notFound,
|
|
299
|
+
template: specialFiles.template,
|
|
300
|
+
middleware: specialFiles.middleware || parentMiddleware,
|
|
301
|
+
isAppRouter: true,
|
|
302
|
+
isServerComponent: isServerComponent(specialFiles.page),
|
|
303
|
+
isClientComponent: isClientComponent(specialFiles.page),
|
|
304
|
+
isIsland: isIsland(specialFiles.page)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
310
|
+
const isGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
|
|
311
|
+
let segmentName = entry.name;
|
|
312
|
+
if (entry.name.startsWith("[") && entry.name.endsWith("]")) {
|
|
313
|
+
segmentName = ":" + entry.name.slice(1, -1);
|
|
314
|
+
if (entry.name.startsWith("[...")) {
|
|
315
|
+
segmentName = "*" + entry.name.slice(4, -1);
|
|
316
|
+
}
|
|
317
|
+
if (entry.name.startsWith("[[...")) {
|
|
318
|
+
segmentName = "*" + entry.name.slice(5, -2);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
|
|
322
|
+
const newLayout = specialFiles.layout || parentLayout;
|
|
323
|
+
const newMiddleware = specialFiles.middleware || parentMiddleware;
|
|
324
|
+
scanAppDirectory(baseDir, fullPath, routes, newSegments, newLayout, newMiddleware);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function scanDirectory(baseDir, currentDir, routes, parentSegments = []) {
|
|
329
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
330
|
+
const specialFiles = {
|
|
331
|
+
layout: null,
|
|
332
|
+
loading: null,
|
|
333
|
+
error: null,
|
|
334
|
+
notFound: null
|
|
335
|
+
};
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
if (entry.isFile()) {
|
|
338
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, "");
|
|
339
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
340
|
+
if (name === "layout") specialFiles.layout = fullPath;
|
|
341
|
+
if (name === "loading") specialFiles.loading = fullPath;
|
|
342
|
+
if (name === "error") specialFiles.error = fullPath;
|
|
343
|
+
if (name === "not-found" || name === "404") specialFiles.notFound = fullPath;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
348
|
+
const relativePath = path2.relative(baseDir, fullPath);
|
|
349
|
+
if (entry.isDirectory()) {
|
|
350
|
+
const isGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
|
|
351
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, entry.name];
|
|
352
|
+
scanDirectory(baseDir, fullPath, routes, newSegments);
|
|
353
|
+
} else if (entry.isFile()) {
|
|
354
|
+
const ext = path2.extname(entry.name);
|
|
355
|
+
const baseName = path2.basename(entry.name, ext);
|
|
356
|
+
if (["layout", "loading", "error", "not-found", "404"].includes(baseName)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if ([".jsx", ".js", ".tsx", ".ts"].includes(ext)) {
|
|
360
|
+
const isApi = relativePath.startsWith("api" + path2.sep) || relativePath.startsWith("api/");
|
|
361
|
+
if (isApi && [".js", ".ts"].includes(ext)) {
|
|
362
|
+
routes.api.push(createRoute(fullPath, baseDir, specialFiles, RouteType.API));
|
|
363
|
+
} else if (!isApi && [".jsx", ".tsx"].includes(ext)) {
|
|
364
|
+
routes.pages.push(createRoute(fullPath, baseDir, specialFiles, RouteType.PAGE));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function createRoute(filePath, baseDir, specialFiles, type) {
|
|
371
|
+
const relativePath = path2.relative(baseDir, filePath);
|
|
372
|
+
const routePath = filePathToRoute(relativePath);
|
|
373
|
+
return {
|
|
374
|
+
type,
|
|
375
|
+
path: routePath,
|
|
376
|
+
filePath,
|
|
377
|
+
pattern: createRoutePattern(routePath),
|
|
378
|
+
segments: routePath.split("/").filter(Boolean),
|
|
379
|
+
layout: specialFiles.layout,
|
|
380
|
+
loading: specialFiles.loading,
|
|
381
|
+
error: specialFiles.error,
|
|
382
|
+
notFound: specialFiles.notFound,
|
|
383
|
+
isServerComponent: isServerComponent(filePath),
|
|
384
|
+
isClientComponent: isClientComponent(filePath),
|
|
385
|
+
isIsland: isIsland(filePath)
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function scanLayouts(layoutsDir, layoutsMap) {
|
|
389
|
+
const entries = fs3.readdirSync(layoutsDir, { withFileTypes: true });
|
|
390
|
+
for (const entry of entries) {
|
|
391
|
+
if (entry.isFile() && /\.(jsx|tsx)$/.test(entry.name)) {
|
|
392
|
+
const name = entry.name.replace(/\.(jsx|tsx)$/, "");
|
|
393
|
+
layoutsMap.set(name, path2.join(layoutsDir, entry.name));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function filePathToRoute(filePath) {
|
|
398
|
+
let route = filePath.replace(/\\/g, "/");
|
|
399
|
+
route = route.replace(/\.(jsx|js|tsx|ts)$/, "");
|
|
400
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
|
|
401
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
402
|
+
if (route.endsWith("/index")) {
|
|
403
|
+
route = route.slice(0, -6) || "/";
|
|
404
|
+
} else if (route === "index") {
|
|
405
|
+
route = "/";
|
|
406
|
+
}
|
|
407
|
+
route = route.replace(/\/?\([^)]+\)\/?/g, "/");
|
|
408
|
+
if (!route.startsWith("/")) {
|
|
409
|
+
route = "/" + route;
|
|
410
|
+
}
|
|
411
|
+
route = route.replace(/\/+/g, "/");
|
|
412
|
+
return route;
|
|
413
|
+
}
|
|
414
|
+
function createRoutePattern(routePath) {
|
|
415
|
+
let pattern = routePath.replace(/\*[^/]*/g, "(.*)").replace(/:[^/]+/g, "([^/]+)").replace(/\//g, "\\/");
|
|
416
|
+
return new RegExp(`^${pattern}$`);
|
|
417
|
+
}
|
|
418
|
+
function buildTree(routes) {
|
|
419
|
+
const tree = { children: {}, routes: [] };
|
|
420
|
+
for (const route of routes) {
|
|
421
|
+
let current = tree;
|
|
422
|
+
for (const segment of route.segments) {
|
|
423
|
+
if (!current.children[segment]) {
|
|
424
|
+
current.children[segment] = { children: {}, routes: [] };
|
|
425
|
+
}
|
|
426
|
+
current = current.children[segment];
|
|
427
|
+
}
|
|
428
|
+
current.routes.push(route);
|
|
429
|
+
}
|
|
430
|
+
return tree;
|
|
431
|
+
}
|
|
432
|
+
function matchRoute(urlPath, routes) {
|
|
433
|
+
const normalizedPath = urlPath === "" ? "/" : urlPath.split("?")[0];
|
|
434
|
+
for (const route of routes) {
|
|
435
|
+
const match = normalizedPath.match(route.pattern);
|
|
436
|
+
if (match) {
|
|
437
|
+
const params = extractParams(route.path, match);
|
|
438
|
+
return { ...route, params };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
function extractParams(routePath, match) {
|
|
444
|
+
const params = {};
|
|
445
|
+
const paramNames = [];
|
|
446
|
+
const paramRegex = /:([^/]+)|\*([^/]*)/g;
|
|
447
|
+
let paramMatch;
|
|
448
|
+
while ((paramMatch = paramRegex.exec(routePath)) !== null) {
|
|
449
|
+
paramNames.push(paramMatch[1] || paramMatch[2] || "splat");
|
|
450
|
+
}
|
|
451
|
+
paramNames.forEach((name, index) => {
|
|
452
|
+
params[name] = match[index + 1];
|
|
453
|
+
});
|
|
454
|
+
return params;
|
|
455
|
+
}
|
|
456
|
+
function findRouteLayouts(route, layoutsMap) {
|
|
457
|
+
const layouts = [];
|
|
458
|
+
let currentPath = "";
|
|
459
|
+
for (const segment of route.segments) {
|
|
460
|
+
currentPath += "/" + segment;
|
|
461
|
+
const layoutName = segment;
|
|
462
|
+
if (layoutsMap.has(layoutName)) {
|
|
463
|
+
layouts.push({
|
|
464
|
+
name: layoutName,
|
|
465
|
+
filePath: layoutsMap.get(layoutName)
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (route.layout) {
|
|
470
|
+
layouts.push({
|
|
471
|
+
name: "route",
|
|
472
|
+
filePath: route.layout
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (layoutsMap.has("root")) {
|
|
476
|
+
layouts.unshift({
|
|
477
|
+
name: "root",
|
|
478
|
+
filePath: layoutsMap.get("root")
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return layouts;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// core/render/index.ts
|
|
485
|
+
import React from "react";
|
|
486
|
+
import { renderToString, renderToPipeableStream } from "react-dom/server";
|
|
487
|
+
async function renderPage(options) {
|
|
488
|
+
const {
|
|
489
|
+
Component,
|
|
490
|
+
props = {},
|
|
491
|
+
layouts = [],
|
|
492
|
+
loading = null,
|
|
493
|
+
error = null,
|
|
494
|
+
islands = [],
|
|
495
|
+
title = "FlexiReact App",
|
|
496
|
+
meta = {},
|
|
497
|
+
scripts = [],
|
|
498
|
+
styles = [],
|
|
499
|
+
favicon = null,
|
|
500
|
+
isSSG = false,
|
|
501
|
+
route = "/",
|
|
502
|
+
needsHydration = false
|
|
503
|
+
} = options;
|
|
504
|
+
const renderStart = Date.now();
|
|
505
|
+
try {
|
|
506
|
+
let element = React.createElement(Component, props);
|
|
507
|
+
if (error) {
|
|
508
|
+
element = React.createElement(ErrorBoundaryWrapper, {
|
|
509
|
+
fallback: error,
|
|
510
|
+
children: element
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (loading) {
|
|
514
|
+
element = React.createElement(React.Suspense, {
|
|
515
|
+
fallback: React.createElement(loading),
|
|
516
|
+
children: element
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
for (const layout of [...layouts].reverse()) {
|
|
520
|
+
if (layout.Component) {
|
|
521
|
+
const LayoutComponent = layout.Component;
|
|
522
|
+
element = React.createElement(LayoutComponent, {
|
|
523
|
+
...layout.props
|
|
524
|
+
}, element);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const content = renderToString(element);
|
|
528
|
+
const renderTime = Date.now() - renderStart;
|
|
529
|
+
const islandScripts = generateIslandScripts(islands);
|
|
530
|
+
return buildHtmlDocument({
|
|
531
|
+
content,
|
|
532
|
+
title,
|
|
533
|
+
meta,
|
|
534
|
+
scripts: [...scripts, ...islandScripts],
|
|
535
|
+
styles,
|
|
536
|
+
favicon,
|
|
537
|
+
props,
|
|
538
|
+
isSSG,
|
|
539
|
+
renderTime,
|
|
540
|
+
route,
|
|
541
|
+
isClientComponent: needsHydration
|
|
542
|
+
});
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.error("Render Error:", err);
|
|
545
|
+
throw err;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
var ErrorBoundaryWrapper = class extends React.Component {
|
|
549
|
+
constructor(props) {
|
|
550
|
+
super(props);
|
|
551
|
+
this.state = { hasError: false, error: null };
|
|
552
|
+
}
|
|
553
|
+
static getDerivedStateFromError(error) {
|
|
554
|
+
return { hasError: true, error };
|
|
555
|
+
}
|
|
556
|
+
render() {
|
|
557
|
+
if (this.state.hasError) {
|
|
558
|
+
const FallbackComponent = this.props.fallback;
|
|
559
|
+
return React.createElement(FallbackComponent, { error: this.state.error });
|
|
560
|
+
}
|
|
561
|
+
return this.props.children;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
function generateIslandScripts(islands) {
|
|
565
|
+
if (!islands.length) return [];
|
|
566
|
+
const scripts = [];
|
|
567
|
+
for (const island of islands) {
|
|
568
|
+
scripts.push({
|
|
569
|
+
type: "module",
|
|
570
|
+
content: `
|
|
571
|
+
import { hydrateIsland } from '/_flexi/client.js';
|
|
572
|
+
import ${island.name} from '${island.clientPath}';
|
|
573
|
+
hydrateIsland('${island.id}', ${island.name}, ${JSON.stringify(island.props)});
|
|
574
|
+
`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return scripts;
|
|
578
|
+
}
|
|
579
|
+
function generateDevToolbar(options = {}) {
|
|
580
|
+
const {
|
|
581
|
+
renderTime = 0,
|
|
582
|
+
pageType = "SSR",
|
|
583
|
+
route = "/",
|
|
584
|
+
hasError = false,
|
|
585
|
+
isHydrated = false,
|
|
586
|
+
errorMessage = null,
|
|
587
|
+
componentName = null
|
|
588
|
+
} = options;
|
|
589
|
+
const timeColor = renderTime < 50 ? "#00FF9C" : renderTime < 200 ? "#fbbf24" : "#ef4444";
|
|
590
|
+
const timeLabel = renderTime < 50 ? "Fast" : renderTime < 200 ? "OK" : "Slow";
|
|
591
|
+
return `
|
|
592
|
+
<!-- FlexiReact v2 Dev Toolbar -->
|
|
593
|
+
<div id="flexi-dev-toolbar" class="flexi-dev-collapsed">
|
|
594
|
+
<style>
|
|
595
|
+
#flexi-dev-toolbar {
|
|
596
|
+
position: fixed;
|
|
597
|
+
bottom: 16px;
|
|
598
|
+
left: 16px;
|
|
599
|
+
z-index: 99999;
|
|
600
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
601
|
+
font-size: 13px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* Main Button */
|
|
605
|
+
.flexi-dev-trigger {
|
|
606
|
+
display: flex;
|
|
607
|
+
align-items: center;
|
|
608
|
+
gap: 8px;
|
|
609
|
+
padding: 8px 12px;
|
|
610
|
+
background: rgba(10, 10, 10, 0.95);
|
|
611
|
+
backdrop-filter: blur(20px);
|
|
612
|
+
border: 1px solid rgba(0, 255, 156, 0.2);
|
|
613
|
+
border-radius: 10px;
|
|
614
|
+
color: #fafafa;
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
617
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.flexi-dev-trigger:hover {
|
|
621
|
+
border-color: rgba(0, 255, 156, 0.5);
|
|
622
|
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.6), 0 0 20px rgba(0, 255, 156, 0.15);
|
|
623
|
+
transform: translateY(-2px);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.flexi-dev-trigger.has-error {
|
|
627
|
+
border-color: rgba(239, 68, 68, 0.5);
|
|
628
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 15px rgba(239, 68, 68, 0.2);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.flexi-dev-logo {
|
|
632
|
+
width: 20px;
|
|
633
|
+
height: 20px;
|
|
634
|
+
background: linear-gradient(135deg, #00FF9C, #00D68F);
|
|
635
|
+
border-radius: 5px;
|
|
636
|
+
display: flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
justify-content: center;
|
|
639
|
+
font-weight: 800;
|
|
640
|
+
font-size: 11px;
|
|
641
|
+
color: #000;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.flexi-dev-trigger.has-error .flexi-dev-logo {
|
|
645
|
+
background: linear-gradient(135deg, #ef4444, #dc2626);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.flexi-dev-indicator {
|
|
649
|
+
display: flex;
|
|
650
|
+
align-items: center;
|
|
651
|
+
gap: 6px;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.flexi-dev-dot {
|
|
655
|
+
width: 6px;
|
|
656
|
+
height: 6px;
|
|
657
|
+
border-radius: 50%;
|
|
658
|
+
background: #00FF9C;
|
|
659
|
+
box-shadow: 0 0 8px rgba(0, 255, 156, 0.6);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.flexi-dev-dot.error {
|
|
663
|
+
background: #ef4444;
|
|
664
|
+
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
|
665
|
+
animation: errorPulse 1s infinite;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
@keyframes errorPulse {
|
|
669
|
+
0%, 100% { opacity: 1; }
|
|
670
|
+
50% { opacity: 0.4; }
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.flexi-dev-time {
|
|
674
|
+
font-size: 11px;
|
|
675
|
+
font-weight: 600;
|
|
676
|
+
color: ${timeColor};
|
|
677
|
+
font-variant-numeric: tabular-nums;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* Panel */
|
|
681
|
+
.flexi-dev-panel {
|
|
682
|
+
position: absolute;
|
|
683
|
+
bottom: calc(100% + 8px);
|
|
684
|
+
left: 0;
|
|
685
|
+
min-width: 340px;
|
|
686
|
+
background: rgba(10, 10, 10, 0.98);
|
|
687
|
+
backdrop-filter: blur(20px);
|
|
688
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
689
|
+
border-radius: 16px;
|
|
690
|
+
opacity: 0;
|
|
691
|
+
visibility: hidden;
|
|
692
|
+
transform: translateY(8px) scale(0.96);
|
|
693
|
+
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
694
|
+
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
|
|
695
|
+
overflow: hidden;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#flexi-dev-toolbar.flexi-dev-open .flexi-dev-panel {
|
|
699
|
+
opacity: 1;
|
|
700
|
+
visibility: visible;
|
|
701
|
+
transform: translateY(0) scale(1);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/* Header */
|
|
705
|
+
.flexi-dev-header {
|
|
706
|
+
display: flex;
|
|
707
|
+
align-items: center;
|
|
708
|
+
gap: 10px;
|
|
709
|
+
padding: 14px 16px;
|
|
710
|
+
background: linear-gradient(135deg, rgba(0, 255, 156, 0.08), rgba(0, 214, 143, 0.04));
|
|
711
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.flexi-dev-header-logo {
|
|
715
|
+
width: 26px;
|
|
716
|
+
height: 26px;
|
|
717
|
+
background: linear-gradient(135deg, #00FF9C, #00D68F);
|
|
718
|
+
border-radius: 7px;
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
justify-content: center;
|
|
722
|
+
font-weight: 800;
|
|
723
|
+
font-size: 13px;
|
|
724
|
+
color: #000;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.flexi-dev-header-info {
|
|
728
|
+
flex: 1;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.flexi-dev-header-title {
|
|
732
|
+
font-weight: 700;
|
|
733
|
+
font-size: 14px;
|
|
734
|
+
color: #fafafa;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.flexi-dev-header-subtitle {
|
|
738
|
+
font-size: 11px;
|
|
739
|
+
color: #52525b;
|
|
740
|
+
margin-top: 1px;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.flexi-dev-close {
|
|
744
|
+
width: 24px;
|
|
745
|
+
height: 24px;
|
|
746
|
+
display: flex;
|
|
747
|
+
align-items: center;
|
|
748
|
+
justify-content: center;
|
|
749
|
+
background: rgba(255, 255, 255, 0.05);
|
|
750
|
+
border: none;
|
|
751
|
+
border-radius: 6px;
|
|
752
|
+
color: #71717a;
|
|
753
|
+
cursor: pointer;
|
|
754
|
+
transition: all 0.15s;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.flexi-dev-close:hover {
|
|
758
|
+
background: rgba(255, 255, 255, 0.1);
|
|
759
|
+
color: #fafafa;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/* Content */
|
|
763
|
+
.flexi-dev-content {
|
|
764
|
+
padding: 12px 16px;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.flexi-dev-section {
|
|
768
|
+
margin-bottom: 12px;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.flexi-dev-section:last-child {
|
|
772
|
+
margin-bottom: 0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.flexi-dev-section-title {
|
|
776
|
+
font-size: 10px;
|
|
777
|
+
font-weight: 600;
|
|
778
|
+
color: #52525b;
|
|
779
|
+
text-transform: uppercase;
|
|
780
|
+
letter-spacing: 0.5px;
|
|
781
|
+
margin-bottom: 8px;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.flexi-dev-grid {
|
|
785
|
+
display: grid;
|
|
786
|
+
grid-template-columns: 1fr 1fr;
|
|
787
|
+
gap: 8px;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.flexi-dev-stat {
|
|
791
|
+
background: rgba(255, 255, 255, 0.03);
|
|
792
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
793
|
+
border-radius: 10px;
|
|
794
|
+
padding: 10px 12px;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.flexi-dev-stat-label {
|
|
798
|
+
font-size: 10px;
|
|
799
|
+
color: #52525b;
|
|
800
|
+
margin-bottom: 4px;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.flexi-dev-stat-value {
|
|
804
|
+
font-size: 14px;
|
|
805
|
+
font-weight: 600;
|
|
806
|
+
color: #fafafa;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.flexi-dev-stat-value.success { color: #00FF9C; }
|
|
810
|
+
.flexi-dev-stat-value.warning { color: #fbbf24; }
|
|
811
|
+
.flexi-dev-stat-value.error { color: #ef4444; }
|
|
812
|
+
|
|
813
|
+
/* Badges */
|
|
814
|
+
.flexi-dev-badge {
|
|
815
|
+
display: inline-flex;
|
|
816
|
+
align-items: center;
|
|
817
|
+
padding: 3px 8px;
|
|
818
|
+
border-radius: 5px;
|
|
819
|
+
font-size: 10px;
|
|
820
|
+
font-weight: 700;
|
|
821
|
+
letter-spacing: 0.3px;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.flexi-dev-badge.ssr { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
|
|
825
|
+
.flexi-dev-badge.ssg { background: rgba(0, 255, 156, 0.15); color: #00FF9C; }
|
|
826
|
+
.flexi-dev-badge.csr { background: rgba(6, 182, 212, 0.15); color: #06b6d4; }
|
|
827
|
+
.flexi-dev-badge.isr { background: rgba(139, 92, 246, 0.15); color: #a78bfa; }
|
|
828
|
+
|
|
829
|
+
/* Error Display */
|
|
830
|
+
.flexi-dev-error {
|
|
831
|
+
background: rgba(239, 68, 68, 0.1);
|
|
832
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
833
|
+
border-radius: 10px;
|
|
834
|
+
padding: 12px;
|
|
835
|
+
margin-top: 8px;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.flexi-dev-error-title {
|
|
839
|
+
display: flex;
|
|
840
|
+
align-items: center;
|
|
841
|
+
gap: 6px;
|
|
842
|
+
font-size: 11px;
|
|
843
|
+
font-weight: 600;
|
|
844
|
+
color: #ef4444;
|
|
845
|
+
margin-bottom: 6px;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.flexi-dev-error-message {
|
|
849
|
+
font-size: 12px;
|
|
850
|
+
color: #fca5a5;
|
|
851
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
852
|
+
word-break: break-word;
|
|
853
|
+
line-height: 1.5;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/* Footer */
|
|
857
|
+
.flexi-dev-footer {
|
|
858
|
+
display: flex;
|
|
859
|
+
gap: 6px;
|
|
860
|
+
padding: 12px 16px;
|
|
861
|
+
background: rgba(0, 0, 0, 0.3);
|
|
862
|
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.flexi-dev-action {
|
|
866
|
+
flex: 1;
|
|
867
|
+
padding: 8px;
|
|
868
|
+
background: rgba(255, 255, 255, 0.03);
|
|
869
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
870
|
+
border-radius: 8px;
|
|
871
|
+
color: #71717a;
|
|
872
|
+
font-size: 11px;
|
|
873
|
+
font-weight: 500;
|
|
874
|
+
text-decoration: none;
|
|
875
|
+
text-align: center;
|
|
876
|
+
cursor: pointer;
|
|
877
|
+
transition: all 0.15s;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.flexi-dev-action:hover {
|
|
881
|
+
background: rgba(0, 255, 156, 0.1);
|
|
882
|
+
border-color: rgba(0, 255, 156, 0.2);
|
|
883
|
+
color: #00FF9C;
|
|
884
|
+
}
|
|
885
|
+
</style>
|
|
886
|
+
|
|
887
|
+
<button class="flexi-dev-trigger ${hasError ? "has-error" : ""}" onclick="this.parentElement.classList.toggle('flexi-dev-open')">
|
|
888
|
+
<div class="flexi-dev-logo">F</div>
|
|
889
|
+
<div class="flexi-dev-indicator">
|
|
890
|
+
<div class="flexi-dev-dot ${hasError ? "error" : ""}"></div>
|
|
891
|
+
<span class="flexi-dev-time">${renderTime}ms</span>
|
|
892
|
+
</div>
|
|
893
|
+
</button>
|
|
894
|
+
|
|
895
|
+
<div class="flexi-dev-panel">
|
|
896
|
+
<div class="flexi-dev-header">
|
|
897
|
+
<div class="flexi-dev-header-logo">F</div>
|
|
898
|
+
<div class="flexi-dev-header-info">
|
|
899
|
+
<div class="flexi-dev-header-title">FlexiReact</div>
|
|
900
|
+
<div class="flexi-dev-header-subtitle">v2.0.0 \u2022 Development</div>
|
|
901
|
+
</div>
|
|
902
|
+
<button class="flexi-dev-close" onclick="this.closest('#flexi-dev-toolbar').classList.remove('flexi-dev-open')">\u2715</button>
|
|
903
|
+
</div>
|
|
904
|
+
|
|
905
|
+
<div class="flexi-dev-content">
|
|
906
|
+
<div class="flexi-dev-section">
|
|
907
|
+
<div class="flexi-dev-section-title">Page Info</div>
|
|
908
|
+
<div class="flexi-dev-grid">
|
|
909
|
+
<div class="flexi-dev-stat">
|
|
910
|
+
<div class="flexi-dev-stat-label">Route</div>
|
|
911
|
+
<div class="flexi-dev-stat-value">${route}</div>
|
|
912
|
+
</div>
|
|
913
|
+
<div class="flexi-dev-stat">
|
|
914
|
+
<div class="flexi-dev-stat-label">Type</div>
|
|
915
|
+
<div class="flexi-dev-stat-value"><span class="flexi-dev-badge ${pageType.toLowerCase()}">${pageType}</span></div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
|
|
920
|
+
<div class="flexi-dev-section">
|
|
921
|
+
<div class="flexi-dev-section-title">Performance</div>
|
|
922
|
+
<div class="flexi-dev-grid">
|
|
923
|
+
<div class="flexi-dev-stat">
|
|
924
|
+
<div class="flexi-dev-stat-label">Render Time</div>
|
|
925
|
+
<div class="flexi-dev-stat-value ${renderTime < 50 ? "success" : renderTime < 200 ? "warning" : "error"}">${renderTime}ms <small style="color:#52525b">${timeLabel}</small></div>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="flexi-dev-stat">
|
|
928
|
+
<div class="flexi-dev-stat-label">Hydration</div>
|
|
929
|
+
<div class="flexi-dev-stat-value" id="flexi-hydration-status">${isHydrated ? "\u2713 Client" : "\u25CB Server"}</div>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
|
|
934
|
+
${hasError && errorMessage ? `
|
|
935
|
+
<div class="flexi-dev-error">
|
|
936
|
+
<div class="flexi-dev-error-title">
|
|
937
|
+
<span>\u26A0</span> Runtime Error
|
|
938
|
+
</div>
|
|
939
|
+
<div class="flexi-dev-error-message">${errorMessage}</div>
|
|
940
|
+
</div>
|
|
941
|
+
` : ""}
|
|
942
|
+
</div>
|
|
943
|
+
|
|
944
|
+
<div class="flexi-dev-footer">
|
|
945
|
+
<a href="/_flexi/routes" class="flexi-dev-action">Routes</a>
|
|
946
|
+
<button class="flexi-dev-action" onclick="location.reload()">Refresh</button>
|
|
947
|
+
<a href="https://github.com/flexireact/flexireact" target="_blank" class="flexi-dev-action">Docs \u2197</a>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
|
|
952
|
+
<script>
|
|
953
|
+
// FlexiReact v2 DevTools
|
|
954
|
+
window.__FLEXI_DEV__ = {
|
|
955
|
+
version: '2.0.0',
|
|
956
|
+
renderTime: ${renderTime},
|
|
957
|
+
pageType: '${pageType}',
|
|
958
|
+
route: '${route}',
|
|
959
|
+
hydrated: false,
|
|
960
|
+
errors: []
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// Track hydration
|
|
964
|
+
const root = document.getElementById('root');
|
|
965
|
+
if (root) {
|
|
966
|
+
const observer = new MutationObserver(() => {
|
|
967
|
+
if (root.children.length > 0) {
|
|
968
|
+
window.__FLEXI_DEV__.hydrated = true;
|
|
969
|
+
const el = document.getElementById('flexi-hydration-status');
|
|
970
|
+
if (el) el.textContent = '\u2713 Client';
|
|
971
|
+
observer.disconnect();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Capture runtime errors
|
|
978
|
+
window.addEventListener('error', (e) => {
|
|
979
|
+
window.__FLEXI_DEV__.errors.push({
|
|
980
|
+
message: e.message,
|
|
981
|
+
file: e.filename,
|
|
982
|
+
line: e.lineno,
|
|
983
|
+
col: e.colno
|
|
984
|
+
});
|
|
985
|
+
console.error('%c[FlexiReact Error]', 'color: #ef4444; font-weight: bold;', e.message);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// Console branding
|
|
989
|
+
console.log(
|
|
990
|
+
'%c \u26A1 FlexiReact v2 %c ${pageType} %c ${renderTime}ms ',
|
|
991
|
+
'background: #00FF9C; color: #000; font-weight: bold; padding: 2px 6px; border-radius: 4px 0 0 4px;',
|
|
992
|
+
'background: #1e1e1e; color: #fafafa; padding: 2px 6px;',
|
|
993
|
+
'background: ${timeColor}20; color: ${timeColor}; padding: 2px 6px; border-radius: 0 4px 4px 0;'
|
|
994
|
+
);
|
|
995
|
+
</script>
|
|
996
|
+
`;
|
|
997
|
+
}
|
|
998
|
+
function buildHtmlDocument(options) {
|
|
999
|
+
const {
|
|
1000
|
+
content,
|
|
1001
|
+
title,
|
|
1002
|
+
meta = {},
|
|
1003
|
+
scripts = [],
|
|
1004
|
+
styles = [],
|
|
1005
|
+
props = {},
|
|
1006
|
+
isSSG = false,
|
|
1007
|
+
renderTime = 0,
|
|
1008
|
+
route = "/",
|
|
1009
|
+
isClientComponent: isClientComponent2 = false,
|
|
1010
|
+
favicon = null
|
|
1011
|
+
} = options;
|
|
1012
|
+
const metaTags = Object.entries(meta).map(([name, content2]) => {
|
|
1013
|
+
if (name.startsWith("og:")) {
|
|
1014
|
+
return `<meta property="${escapeHtml(name)}" content="${escapeHtml(content2)}">`;
|
|
1015
|
+
}
|
|
1016
|
+
return `<meta name="${escapeHtml(name)}" content="${escapeHtml(content2)}">`;
|
|
1017
|
+
}).join("\n ");
|
|
1018
|
+
const styleTags = styles.map((style) => {
|
|
1019
|
+
if (typeof style === "string") {
|
|
1020
|
+
return `<link rel="stylesheet" href="${escapeHtml(style)}">`;
|
|
1021
|
+
}
|
|
1022
|
+
return `<style>${style.content}</style>`;
|
|
1023
|
+
}).join("\n ");
|
|
1024
|
+
const scriptTags = scripts.map((script) => {
|
|
1025
|
+
if (typeof script === "string") {
|
|
1026
|
+
return `<script src="${escapeHtml(script)}"></script>`;
|
|
1027
|
+
}
|
|
1028
|
+
const type = script.type ? ` type="${script.type}"` : "";
|
|
1029
|
+
if (script.src) {
|
|
1030
|
+
return `<script${type} src="${escapeHtml(script.src)}"></script>`;
|
|
1031
|
+
}
|
|
1032
|
+
return `<script${type}>${script.content}</script>`;
|
|
1033
|
+
}).join("\n ");
|
|
1034
|
+
const faviconSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#00FF9C"/><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" fill="#000" font-family="system-ui" font-weight="bold" font-size="16">F</text></svg>`);
|
|
1035
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
1036
|
+
const pageType = isSSG ? "SSG" : isClientComponent2 ? "CSR" : "SSR";
|
|
1037
|
+
const devToolbar = isDev ? generateDevToolbar({
|
|
1038
|
+
renderTime,
|
|
1039
|
+
pageType,
|
|
1040
|
+
route,
|
|
1041
|
+
isHydrated: isClientComponent2
|
|
1042
|
+
}) : "";
|
|
1043
|
+
const faviconLink = favicon ? `<link rel="icon" href="${escapeHtml(favicon)}">` : `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${faviconSvg}">`;
|
|
1044
|
+
return `<!DOCTYPE html>
|
|
1045
|
+
<html lang="en" class="dark">
|
|
1046
|
+
<head>
|
|
1047
|
+
<meta charset="UTF-8">
|
|
1048
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1049
|
+
<title>${escapeHtml(title)}</title>
|
|
1050
|
+
${faviconLink}
|
|
1051
|
+
${metaTags}
|
|
1052
|
+
<style>
|
|
1053
|
+
:root { --flexi-bg: #0f172a; --flexi-fg: #f8fafc; }
|
|
1054
|
+
html, body { background-color: #0f172a; color: #f8fafc; min-height: 100vh; margin: 0; }
|
|
1055
|
+
</style>
|
|
1056
|
+
${styleTags}
|
|
1057
|
+
<script>
|
|
1058
|
+
(function() {
|
|
1059
|
+
var theme = localStorage.getItem('theme');
|
|
1060
|
+
if (theme === 'light') {
|
|
1061
|
+
document.documentElement.classList.remove('dark');
|
|
1062
|
+
document.documentElement.style.backgroundColor = '#ffffff';
|
|
1063
|
+
document.documentElement.style.color = '#0f172a';
|
|
1064
|
+
document.body.style.backgroundColor = '#ffffff';
|
|
1065
|
+
document.body.style.color = '#0f172a';
|
|
1066
|
+
}
|
|
1067
|
+
})();
|
|
1068
|
+
</script>
|
|
1069
|
+
</head>
|
|
1070
|
+
<body>
|
|
1071
|
+
<div id="root">${content}</div>
|
|
1072
|
+
<script>
|
|
1073
|
+
window.__FLEXI_DATA__ = ${JSON.stringify({ props, isSSG })};
|
|
1074
|
+
</script>
|
|
1075
|
+
${scriptTags}
|
|
1076
|
+
${devToolbar}
|
|
1077
|
+
</body>
|
|
1078
|
+
</html>`;
|
|
1079
|
+
}
|
|
1080
|
+
function renderError(statusCode, message, stack = null) {
|
|
1081
|
+
const showStack = process.env.NODE_ENV !== "production" && stack;
|
|
1082
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
1083
|
+
const errorDetails = parseErrorStack(stack);
|
|
1084
|
+
const errorMessages = {
|
|
1085
|
+
404: { title: "Page Not Found", icon: "search", color: "#00FF9C", desc: "The page you're looking for doesn't exist or has been moved." },
|
|
1086
|
+
500: { title: "Server Error", icon: "alert", color: "#ef4444", desc: "Something went wrong on our end." },
|
|
1087
|
+
403: { title: "Forbidden", icon: "lock", color: "#f59e0b", desc: "You don't have permission to access this resource." },
|
|
1088
|
+
401: { title: "Unauthorized", icon: "key", color: "#8b5cf6", desc: "Please log in to access this page." }
|
|
1089
|
+
};
|
|
1090
|
+
const errorInfo = errorMessages[statusCode] || { title: "Error", icon: "alert", color: "#ef4444", desc: message };
|
|
1091
|
+
const errorFramesHtml = showStack && errorDetails?.frames?.length > 0 ? errorDetails.frames.slice(0, 5).map((frame, i) => `
|
|
1092
|
+
<div class="error-frame ${i === 0 ? "error-frame-first" : ""}">
|
|
1093
|
+
<div class="error-frame-fn">${escapeHtml(frame.fn)}</div>
|
|
1094
|
+
<div class="error-frame-loc">${escapeHtml(frame.file)}:${frame.line}:${frame.col}</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
`).join("") : "";
|
|
1097
|
+
return `<!DOCTYPE html>
|
|
1098
|
+
<html lang="en">
|
|
1099
|
+
<head>
|
|
1100
|
+
<meta charset="UTF-8">
|
|
1101
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1102
|
+
<title>${statusCode} - ${errorInfo.title} | FlexiReact</title>
|
|
1103
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%2300FF9C'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='central' text-anchor='middle' fill='%23000' font-family='system-ui' font-weight='bold' font-size='16'%3EF%3C/text%3E%3C/svg%3E">
|
|
1104
|
+
<style>
|
|
1105
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1106
|
+
|
|
1107
|
+
body {
|
|
1108
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1109
|
+
min-height: 100vh;
|
|
1110
|
+
background: #0a0a0a;
|
|
1111
|
+
color: #fafafa;
|
|
1112
|
+
display: flex;
|
|
1113
|
+
align-items: center;
|
|
1114
|
+
justify-content: center;
|
|
1115
|
+
overflow-x: hidden;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/* Animated background */
|
|
1119
|
+
.bg-pattern {
|
|
1120
|
+
position: fixed;
|
|
1121
|
+
inset: 0;
|
|
1122
|
+
background-image:
|
|
1123
|
+
radial-gradient(circle at 25% 25%, rgba(0, 255, 156, 0.03) 0%, transparent 50%),
|
|
1124
|
+
radial-gradient(circle at 75% 75%, rgba(99, 102, 241, 0.03) 0%, transparent 50%);
|
|
1125
|
+
pointer-events: none;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
.bg-grid {
|
|
1129
|
+
position: fixed;
|
|
1130
|
+
inset: 0;
|
|
1131
|
+
background-image:
|
|
1132
|
+
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
|
1133
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
|
1134
|
+
background-size: 64px 64px;
|
|
1135
|
+
pointer-events: none;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.container {
|
|
1139
|
+
position: relative;
|
|
1140
|
+
width: 100%;
|
|
1141
|
+
max-width: ${showStack ? "800px" : "500px"};
|
|
1142
|
+
padding: 2rem;
|
|
1143
|
+
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
@keyframes fadeIn {
|
|
1147
|
+
from { opacity: 0; transform: translateY(30px); }
|
|
1148
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/* Error Card */
|
|
1152
|
+
.error-card {
|
|
1153
|
+
background: linear-gradient(145deg, rgba(23, 23, 23, 0.9), rgba(10, 10, 10, 0.95));
|
|
1154
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
1155
|
+
border-radius: 24px;
|
|
1156
|
+
padding: 3rem;
|
|
1157
|
+
text-align: center;
|
|
1158
|
+
backdrop-filter: blur(20px);
|
|
1159
|
+
box-shadow:
|
|
1160
|
+
0 0 0 1px rgba(255, 255, 255, 0.05),
|
|
1161
|
+
0 20px 50px -20px rgba(0, 0, 0, 0.5),
|
|
1162
|
+
0 0 100px -50px ${errorInfo.color}40;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/* Logo */
|
|
1166
|
+
.logo {
|
|
1167
|
+
width: 56px;
|
|
1168
|
+
height: 56px;
|
|
1169
|
+
background: linear-gradient(135deg, #00FF9C, #00D68F);
|
|
1170
|
+
border-radius: 14px;
|
|
1171
|
+
display: flex;
|
|
1172
|
+
align-items: center;
|
|
1173
|
+
justify-content: center;
|
|
1174
|
+
margin: 0 auto 2rem;
|
|
1175
|
+
font-weight: 800;
|
|
1176
|
+
font-size: 24px;
|
|
1177
|
+
color: #000;
|
|
1178
|
+
box-shadow: 0 0 30px rgba(0, 255, 156, 0.3);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/* Error Code */
|
|
1182
|
+
.error-code {
|
|
1183
|
+
font-size: 7rem;
|
|
1184
|
+
font-weight: 800;
|
|
1185
|
+
line-height: 1;
|
|
1186
|
+
background: linear-gradient(135deg, ${errorInfo.color}, ${errorInfo.color}99);
|
|
1187
|
+
-webkit-background-clip: text;
|
|
1188
|
+
-webkit-text-fill-color: transparent;
|
|
1189
|
+
background-clip: text;
|
|
1190
|
+
margin-bottom: 0.5rem;
|
|
1191
|
+
letter-spacing: -4px;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.error-title {
|
|
1195
|
+
font-size: 1.5rem;
|
|
1196
|
+
font-weight: 600;
|
|
1197
|
+
color: #fafafa;
|
|
1198
|
+
margin-bottom: 0.75rem;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.error-desc {
|
|
1202
|
+
font-size: 1rem;
|
|
1203
|
+
color: #71717a;
|
|
1204
|
+
line-height: 1.6;
|
|
1205
|
+
margin-bottom: 2rem;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/* Buttons */
|
|
1209
|
+
.buttons {
|
|
1210
|
+
display: flex;
|
|
1211
|
+
gap: 12px;
|
|
1212
|
+
justify-content: center;
|
|
1213
|
+
flex-wrap: wrap;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.btn {
|
|
1217
|
+
display: inline-flex;
|
|
1218
|
+
align-items: center;
|
|
1219
|
+
gap: 8px;
|
|
1220
|
+
padding: 12px 24px;
|
|
1221
|
+
border-radius: 12px;
|
|
1222
|
+
font-weight: 600;
|
|
1223
|
+
font-size: 14px;
|
|
1224
|
+
text-decoration: none;
|
|
1225
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1226
|
+
border: none;
|
|
1227
|
+
cursor: pointer;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.btn-primary {
|
|
1231
|
+
background: #00FF9C;
|
|
1232
|
+
color: #000;
|
|
1233
|
+
box-shadow: 0 0 20px rgba(0, 255, 156, 0.3);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.btn-primary:hover {
|
|
1237
|
+
transform: translateY(-2px);
|
|
1238
|
+
box-shadow: 0 0 30px rgba(0, 255, 156, 0.5);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.btn-secondary {
|
|
1242
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1243
|
+
color: #a1a1aa;
|
|
1244
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.btn-secondary:hover {
|
|
1248
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1249
|
+
color: #fafafa;
|
|
1250
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/* Error Stack (Dev Mode) */
|
|
1254
|
+
.error-stack {
|
|
1255
|
+
margin-top: 2rem;
|
|
1256
|
+
text-align: left;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.error-stack-header {
|
|
1260
|
+
display: flex;
|
|
1261
|
+
align-items: center;
|
|
1262
|
+
gap: 8px;
|
|
1263
|
+
margin-bottom: 12px;
|
|
1264
|
+
font-size: 12px;
|
|
1265
|
+
font-weight: 600;
|
|
1266
|
+
color: #ef4444;
|
|
1267
|
+
text-transform: uppercase;
|
|
1268
|
+
letter-spacing: 0.5px;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.error-stack-header::before {
|
|
1272
|
+
content: '';
|
|
1273
|
+
width: 8px;
|
|
1274
|
+
height: 8px;
|
|
1275
|
+
background: #ef4444;
|
|
1276
|
+
border-radius: 50%;
|
|
1277
|
+
animation: pulse 2s infinite;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
@keyframes pulse {
|
|
1281
|
+
0%, 100% { opacity: 1; }
|
|
1282
|
+
50% { opacity: 0.5; }
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.error-message {
|
|
1286
|
+
background: rgba(239, 68, 68, 0.1);
|
|
1287
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
1288
|
+
border-radius: 12px;
|
|
1289
|
+
padding: 16px;
|
|
1290
|
+
margin-bottom: 12px;
|
|
1291
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
1292
|
+
font-size: 13px;
|
|
1293
|
+
color: #fca5a5;
|
|
1294
|
+
word-break: break-word;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.error-frames {
|
|
1298
|
+
background: rgba(0, 0, 0, 0.4);
|
|
1299
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
1300
|
+
border-radius: 12px;
|
|
1301
|
+
overflow: hidden;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.error-frame {
|
|
1305
|
+
padding: 12px 16px;
|
|
1306
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
1307
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
1308
|
+
font-size: 12px;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.error-frame:last-child {
|
|
1312
|
+
border-bottom: none;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.error-frame-first {
|
|
1316
|
+
background: rgba(239, 68, 68, 0.05);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.error-frame-fn {
|
|
1320
|
+
color: #fafafa;
|
|
1321
|
+
font-weight: 500;
|
|
1322
|
+
margin-bottom: 4px;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.error-frame-loc {
|
|
1326
|
+
color: #52525b;
|
|
1327
|
+
font-size: 11px;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/* Dev Badge */
|
|
1331
|
+
.dev-badge {
|
|
1332
|
+
position: fixed;
|
|
1333
|
+
bottom: 20px;
|
|
1334
|
+
right: 20px;
|
|
1335
|
+
display: flex;
|
|
1336
|
+
align-items: center;
|
|
1337
|
+
gap: 8px;
|
|
1338
|
+
padding: 8px 14px;
|
|
1339
|
+
background: rgba(0, 0, 0, 0.8);
|
|
1340
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
1341
|
+
border-radius: 10px;
|
|
1342
|
+
font-size: 12px;
|
|
1343
|
+
font-weight: 500;
|
|
1344
|
+
color: #71717a;
|
|
1345
|
+
backdrop-filter: blur(10px);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.dev-badge-dot {
|
|
1349
|
+
width: 8px;
|
|
1350
|
+
height: 8px;
|
|
1351
|
+
background: #00FF9C;
|
|
1352
|
+
border-radius: 50%;
|
|
1353
|
+
box-shadow: 0 0 10px rgba(0, 255, 156, 0.5);
|
|
1354
|
+
}
|
|
1355
|
+
</style>
|
|
1356
|
+
</head>
|
|
1357
|
+
<body>
|
|
1358
|
+
<div class="bg-pattern"></div>
|
|
1359
|
+
<div class="bg-grid"></div>
|
|
1360
|
+
|
|
1361
|
+
<div class="container">
|
|
1362
|
+
<div class="error-card">
|
|
1363
|
+
<div class="logo">F</div>
|
|
1364
|
+
|
|
1365
|
+
<div class="error-code">${statusCode}</div>
|
|
1366
|
+
<h1 class="error-title">${errorInfo.title}</h1>
|
|
1367
|
+
<p class="error-desc">${errorInfo.desc}</p>
|
|
1368
|
+
|
|
1369
|
+
<div class="buttons">
|
|
1370
|
+
<a href="/" class="btn btn-primary">
|
|
1371
|
+
\u2190 Back to Home
|
|
1372
|
+
</a>
|
|
1373
|
+
<a href="javascript:history.back()" class="btn btn-secondary">
|
|
1374
|
+
Go Back
|
|
1375
|
+
</a>
|
|
1376
|
+
</div>
|
|
1377
|
+
|
|
1378
|
+
${showStack ? `
|
|
1379
|
+
<div class="error-stack">
|
|
1380
|
+
<div class="error-stack-header">Error Details</div>
|
|
1381
|
+
<div class="error-message">${escapeHtml(message)}</div>
|
|
1382
|
+
${errorFramesHtml ? `<div class="error-frames">${errorFramesHtml}</div>` : ""}
|
|
1383
|
+
</div>
|
|
1384
|
+
` : ""}
|
|
1385
|
+
</div>
|
|
1386
|
+
</div>
|
|
1387
|
+
|
|
1388
|
+
${isDev ? `
|
|
1389
|
+
<div class="dev-badge">
|
|
1390
|
+
<div class="dev-badge-dot"></div>
|
|
1391
|
+
FlexiReact v2
|
|
1392
|
+
</div>
|
|
1393
|
+
` : ""}
|
|
1394
|
+
</body>
|
|
1395
|
+
</html>`;
|
|
1396
|
+
}
|
|
1397
|
+
function parseErrorStack(stack) {
|
|
1398
|
+
if (!stack) return null;
|
|
1399
|
+
const lines = stack.split("\n");
|
|
1400
|
+
const parsed = {
|
|
1401
|
+
message: lines[0] || "",
|
|
1402
|
+
frames: []
|
|
1403
|
+
};
|
|
1404
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1405
|
+
const line = lines[i].trim();
|
|
1406
|
+
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/) || line.match(/at\s+(.+?):(\d+):(\d+)/);
|
|
1407
|
+
if (match) {
|
|
1408
|
+
parsed.frames.push({
|
|
1409
|
+
fn: match[1] || "anonymous",
|
|
1410
|
+
file: match[2] || match[1],
|
|
1411
|
+
line: match[3] || match[2],
|
|
1412
|
+
col: match[4] || match[3]
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return parsed;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// core/middleware/index.ts
|
|
1420
|
+
import fs4 from "fs";
|
|
1421
|
+
import path3 from "path";
|
|
1422
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1423
|
+
var MiddlewareRequest = class {
|
|
1424
|
+
raw;
|
|
1425
|
+
method;
|
|
1426
|
+
url;
|
|
1427
|
+
headers;
|
|
1428
|
+
pathname;
|
|
1429
|
+
searchParams;
|
|
1430
|
+
query;
|
|
1431
|
+
cookies;
|
|
1432
|
+
constructor(req) {
|
|
1433
|
+
this.raw = req;
|
|
1434
|
+
this.method = req.method;
|
|
1435
|
+
this.url = req.url;
|
|
1436
|
+
this.headers = new Map(Object.entries(req.headers || {}));
|
|
1437
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
1438
|
+
this.pathname = parsedUrl.pathname;
|
|
1439
|
+
this.searchParams = parsedUrl.searchParams;
|
|
1440
|
+
this.query = Object.fromEntries(parsedUrl.searchParams);
|
|
1441
|
+
this.cookies = this._parseCookies(req.headers.cookie || "");
|
|
1442
|
+
}
|
|
1443
|
+
_parseCookies(cookieHeader) {
|
|
1444
|
+
const cookies2 = /* @__PURE__ */ new Map();
|
|
1445
|
+
if (!cookieHeader) return cookies2;
|
|
1446
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
1447
|
+
const [name, ...rest] = cookie.split("=");
|
|
1448
|
+
if (name) {
|
|
1449
|
+
cookies2.set(name.trim(), rest.join("=").trim());
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
return cookies2;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Get a header value
|
|
1456
|
+
*/
|
|
1457
|
+
header(name) {
|
|
1458
|
+
return this.headers.get(name.toLowerCase());
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Get a cookie value
|
|
1462
|
+
*/
|
|
1463
|
+
cookie(name) {
|
|
1464
|
+
return this.cookies.get(name);
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Check if request matches a path pattern
|
|
1468
|
+
*/
|
|
1469
|
+
matches(pattern) {
|
|
1470
|
+
return matchPath(this.pathname, pattern);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
async function loadMiddleware(projectRoot) {
|
|
1474
|
+
const middlewarePath = path3.join(projectRoot, "middleware.js");
|
|
1475
|
+
if (!fs4.existsSync(middlewarePath)) {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
try {
|
|
1479
|
+
const url = pathToFileURL2(middlewarePath).href;
|
|
1480
|
+
const module = await import(`${url}?t=${Date.now()}`);
|
|
1481
|
+
return {
|
|
1482
|
+
handler: module.default,
|
|
1483
|
+
config: module.config || {}
|
|
1484
|
+
};
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
console.error("Failed to load middleware:", error);
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async function runMiddleware(req, res, middleware) {
|
|
1491
|
+
if (!middleware) {
|
|
1492
|
+
return { continue: true };
|
|
1493
|
+
}
|
|
1494
|
+
const { handler, config } = middleware;
|
|
1495
|
+
const request = new MiddlewareRequest(req);
|
|
1496
|
+
if (config.matcher) {
|
|
1497
|
+
const patterns = Array.isArray(config.matcher) ? config.matcher : [config.matcher];
|
|
1498
|
+
const matches = patterns.some((pattern) => matchPath(request.pathname, pattern));
|
|
1499
|
+
if (!matches) {
|
|
1500
|
+
return { continue: true };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
const response = await handler(request);
|
|
1505
|
+
if (!response || response.type === "next") {
|
|
1506
|
+
if (response?.headers) {
|
|
1507
|
+
for (const [key, value] of response.headers) {
|
|
1508
|
+
res.setHeader(key, value);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return { continue: true };
|
|
1512
|
+
}
|
|
1513
|
+
if (response.type === "redirect") {
|
|
1514
|
+
res.writeHead(response.status, { Location: response.url });
|
|
1515
|
+
res.end();
|
|
1516
|
+
return { continue: false };
|
|
1517
|
+
}
|
|
1518
|
+
if (response.type === "rewrite") {
|
|
1519
|
+
req.url = response.url;
|
|
1520
|
+
return { continue: true, rewritten: true };
|
|
1521
|
+
}
|
|
1522
|
+
if (response.type === "response") {
|
|
1523
|
+
for (const [key, value] of response.headers) {
|
|
1524
|
+
res.setHeader(key, value);
|
|
1525
|
+
}
|
|
1526
|
+
res.writeHead(response.status);
|
|
1527
|
+
res.end(response.body);
|
|
1528
|
+
return { continue: false };
|
|
1529
|
+
}
|
|
1530
|
+
return { continue: true };
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
console.error("Middleware error:", error);
|
|
1533
|
+
return { continue: true, error };
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function matchPath(pathname, pattern) {
|
|
1537
|
+
let regex = pattern.replace(/\*/g, ".*").replace(/:path\*/g, ".*").replace(/:(\w+)/g, "[^/]+");
|
|
1538
|
+
regex = `^${regex}$`;
|
|
1539
|
+
return new RegExp(regex).test(pathname);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// core/plugins/index.ts
|
|
1543
|
+
import fs5 from "fs";
|
|
1544
|
+
import path4 from "path";
|
|
1545
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
1546
|
+
var PluginHooks = {
|
|
1547
|
+
// Server lifecycle
|
|
1548
|
+
SERVER_START: "onServerStart",
|
|
1549
|
+
SERVER_STOP: "onServerStop",
|
|
1550
|
+
// Request lifecycle
|
|
1551
|
+
REQUEST: "onRequest",
|
|
1552
|
+
RESPONSE: "onResponse",
|
|
1553
|
+
// Render lifecycle
|
|
1554
|
+
BEFORE_RENDER: "onBeforeRender",
|
|
1555
|
+
AFTER_RENDER: "onAfterRender",
|
|
1556
|
+
// Build lifecycle
|
|
1557
|
+
BUILD_START: "onBuildStart",
|
|
1558
|
+
BUILD_END: "onBuildEnd",
|
|
1559
|
+
// Route lifecycle
|
|
1560
|
+
ROUTES_LOADED: "onRoutesLoaded",
|
|
1561
|
+
// Config
|
|
1562
|
+
CONFIG: "onConfig",
|
|
1563
|
+
ESBUILD_CONFIG: "esbuildConfig"
|
|
1564
|
+
};
|
|
1565
|
+
var PluginManager = class {
|
|
1566
|
+
plugins;
|
|
1567
|
+
hooks;
|
|
1568
|
+
constructor() {
|
|
1569
|
+
this.plugins = [];
|
|
1570
|
+
this.hooks = /* @__PURE__ */ new Map();
|
|
1571
|
+
for (const hook of Object.values(PluginHooks)) {
|
|
1572
|
+
this.hooks.set(hook, []);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Registers a plugin
|
|
1577
|
+
*/
|
|
1578
|
+
register(plugin) {
|
|
1579
|
+
if (!plugin.name) {
|
|
1580
|
+
throw new Error("Plugin must have a name");
|
|
1581
|
+
}
|
|
1582
|
+
if (this.plugins.find((p) => p.name === plugin.name)) {
|
|
1583
|
+
console.warn(`Plugin "${plugin.name}" is already registered`);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
this.plugins.push(plugin);
|
|
1587
|
+
for (const [hookName, handlers] of this.hooks) {
|
|
1588
|
+
if (typeof plugin[hookName] === "function") {
|
|
1589
|
+
handlers.push({
|
|
1590
|
+
plugin: plugin.name,
|
|
1591
|
+
handler: plugin[hookName].bind(plugin)
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
console.log(` \u2713 Plugin loaded: ${plugin.name}`);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Unregisters a plugin
|
|
1599
|
+
*/
|
|
1600
|
+
unregister(pluginName) {
|
|
1601
|
+
const index = this.plugins.findIndex((p) => p.name === pluginName);
|
|
1602
|
+
if (index === -1) return;
|
|
1603
|
+
this.plugins.splice(index, 1);
|
|
1604
|
+
for (const handlers of this.hooks.values()) {
|
|
1605
|
+
const hookIndex = handlers.findIndex((h) => h.plugin === pluginName);
|
|
1606
|
+
if (hookIndex !== -1) {
|
|
1607
|
+
handlers.splice(hookIndex, 1);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Runs a hook with all registered handlers
|
|
1613
|
+
*/
|
|
1614
|
+
async runHook(hookName, ...args) {
|
|
1615
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
1616
|
+
const results = [];
|
|
1617
|
+
for (const { plugin, handler } of handlers) {
|
|
1618
|
+
try {
|
|
1619
|
+
const result = await handler(...args);
|
|
1620
|
+
results.push({ plugin, result });
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
console.error(`Plugin "${plugin}" error in ${hookName}:`, error);
|
|
1623
|
+
results.push({ plugin, error });
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return results;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Runs a hook that can modify a value (waterfall)
|
|
1630
|
+
*/
|
|
1631
|
+
async runWaterfallHook(hookName, initialValue, ...args) {
|
|
1632
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
1633
|
+
let value = initialValue;
|
|
1634
|
+
for (const { plugin, handler } of handlers) {
|
|
1635
|
+
try {
|
|
1636
|
+
const result = await handler(value, ...args);
|
|
1637
|
+
if (result !== void 0) {
|
|
1638
|
+
value = result;
|
|
1639
|
+
}
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
console.error(`Plugin "${plugin}" error in ${hookName}:`, error);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return value;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Checks if any plugin handles a hook
|
|
1648
|
+
*/
|
|
1649
|
+
hasHook(hookName) {
|
|
1650
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
1651
|
+
return handlers.length > 0;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Gets all registered plugins
|
|
1655
|
+
*/
|
|
1656
|
+
getPlugins() {
|
|
1657
|
+
return [...this.plugins];
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
var pluginManager = new PluginManager();
|
|
1661
|
+
async function loadPlugins(projectRoot, config) {
|
|
1662
|
+
console.log("\n\u{1F4E6} Loading plugins...\n");
|
|
1663
|
+
const pluginPath = path4.join(projectRoot, "flexireact.plugin.js");
|
|
1664
|
+
if (fs5.existsSync(pluginPath)) {
|
|
1665
|
+
try {
|
|
1666
|
+
const url = pathToFileURL3(pluginPath).href;
|
|
1667
|
+
const module = await import(`${url}?t=${Date.now()}`);
|
|
1668
|
+
const plugin = module.default;
|
|
1669
|
+
if (plugin) {
|
|
1670
|
+
pluginManager.register(plugin);
|
|
1671
|
+
}
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
console.error("Failed to load flexireact.plugin.js:", error);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
1677
|
+
for (const pluginConfig of config.plugins) {
|
|
1678
|
+
try {
|
|
1679
|
+
if (typeof pluginConfig === "string") {
|
|
1680
|
+
const module = await import(pluginConfig);
|
|
1681
|
+
pluginManager.register(module.default);
|
|
1682
|
+
} else if (typeof pluginConfig === "object") {
|
|
1683
|
+
pluginManager.register(pluginConfig);
|
|
1684
|
+
} else if (typeof pluginConfig === "function") {
|
|
1685
|
+
pluginManager.register(pluginConfig());
|
|
1686
|
+
}
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
console.error(`Failed to load plugin:`, error);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
console.log(`
|
|
1693
|
+
Total plugins: ${pluginManager.getPlugins().length}
|
|
1694
|
+
`);
|
|
1695
|
+
return pluginManager;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// core/islands/index.ts
|
|
1699
|
+
import React2 from "react";
|
|
1700
|
+
import { renderToString as renderToString2 } from "react-dom/server";
|
|
1701
|
+
var islandRegistry = /* @__PURE__ */ new Map();
|
|
1702
|
+
function getRegisteredIslands() {
|
|
1703
|
+
const islands = Array.from(islandRegistry.values());
|
|
1704
|
+
islandRegistry.clear();
|
|
1705
|
+
return islands;
|
|
1706
|
+
}
|
|
1707
|
+
var LoadStrategy = {
|
|
1708
|
+
// Hydrate immediately when page loads
|
|
1709
|
+
IMMEDIATE: "immediate",
|
|
1710
|
+
// Hydrate when island becomes visible
|
|
1711
|
+
VISIBLE: "visible",
|
|
1712
|
+
// Hydrate when user interacts with the page
|
|
1713
|
+
IDLE: "idle",
|
|
1714
|
+
// Hydrate on specific media query
|
|
1715
|
+
MEDIA: "media"
|
|
1716
|
+
};
|
|
1717
|
+
function generateAdvancedHydrationScript(islands) {
|
|
1718
|
+
if (!islands.length) return "";
|
|
1719
|
+
const islandData = islands.map((island) => ({
|
|
1720
|
+
id: island.id,
|
|
1721
|
+
name: island.name,
|
|
1722
|
+
path: island.clientPath,
|
|
1723
|
+
props: island.props,
|
|
1724
|
+
strategy: island.strategy || LoadStrategy.IMMEDIATE,
|
|
1725
|
+
media: island.media
|
|
1726
|
+
}));
|
|
1727
|
+
return `
|
|
1728
|
+
<script type="module">
|
|
1729
|
+
const islands = ${JSON.stringify(islandData)};
|
|
1730
|
+
|
|
1731
|
+
async function hydrateIsland(island) {
|
|
1732
|
+
const element = document.querySelector(\`[data-island="\${island.id}"]\`);
|
|
1733
|
+
if (!element || element.hasAttribute('data-hydrated')) return;
|
|
1734
|
+
|
|
1735
|
+
try {
|
|
1736
|
+
const { hydrateRoot } = await import('/_flexi/react-dom-client.js');
|
|
1737
|
+
const React = await import('/_flexi/react.js');
|
|
1738
|
+
const module = await import(island.path);
|
|
1739
|
+
const Component = module.default;
|
|
1740
|
+
|
|
1741
|
+
hydrateRoot(element, React.createElement(Component, island.props));
|
|
1742
|
+
element.setAttribute('data-hydrated', 'true');
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
console.error(\`Failed to hydrate island \${island.name}:\`, error);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Intersection Observer for visible strategy
|
|
1749
|
+
const visibleObserver = new IntersectionObserver((entries) => {
|
|
1750
|
+
entries.forEach(entry => {
|
|
1751
|
+
if (entry.isIntersecting) {
|
|
1752
|
+
const id = entry.target.getAttribute('data-island');
|
|
1753
|
+
const island = islands.find(i => i.id === id);
|
|
1754
|
+
if (island) {
|
|
1755
|
+
hydrateIsland(island);
|
|
1756
|
+
visibleObserver.unobserve(entry.target);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
}, { rootMargin: '50px' });
|
|
1761
|
+
|
|
1762
|
+
// Process islands based on strategy
|
|
1763
|
+
function processIslands() {
|
|
1764
|
+
for (const island of islands) {
|
|
1765
|
+
const element = document.querySelector(\`[data-island="\${island.id}"]\`);
|
|
1766
|
+
if (!element) continue;
|
|
1767
|
+
|
|
1768
|
+
switch (island.strategy) {
|
|
1769
|
+
case 'immediate':
|
|
1770
|
+
hydrateIsland(island);
|
|
1771
|
+
break;
|
|
1772
|
+
case 'visible':
|
|
1773
|
+
visibleObserver.observe(element);
|
|
1774
|
+
break;
|
|
1775
|
+
case 'idle':
|
|
1776
|
+
requestIdleCallback(() => hydrateIsland(island));
|
|
1777
|
+
break;
|
|
1778
|
+
case 'media':
|
|
1779
|
+
if (island.media && window.matchMedia(island.media).matches) {
|
|
1780
|
+
hydrateIsland(island);
|
|
1781
|
+
}
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (document.readyState === 'loading') {
|
|
1788
|
+
document.addEventListener('DOMContentLoaded', processIslands);
|
|
1789
|
+
} else {
|
|
1790
|
+
processIslands();
|
|
1791
|
+
}
|
|
1792
|
+
</script>`;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// core/context.ts
|
|
1796
|
+
import React3 from "react";
|
|
1797
|
+
var RequestContext = React3.createContext(null);
|
|
1798
|
+
var RouteContext = React3.createContext(null);
|
|
1799
|
+
var LayoutContext = React3.createContext(null);
|
|
1800
|
+
function createRequestContext(req, res, params = {}, query = {}) {
|
|
1801
|
+
return {
|
|
1802
|
+
req,
|
|
1803
|
+
res,
|
|
1804
|
+
params,
|
|
1805
|
+
query,
|
|
1806
|
+
url: req.url,
|
|
1807
|
+
method: req.method,
|
|
1808
|
+
headers: req.headers,
|
|
1809
|
+
cookies: parseCookies(req.headers.cookie || "")
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
function parseCookies(cookieHeader) {
|
|
1813
|
+
const cookies2 = {};
|
|
1814
|
+
if (!cookieHeader) return cookies2;
|
|
1815
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
1816
|
+
const [name, ...rest] = cookie.split("=");
|
|
1817
|
+
if (name) {
|
|
1818
|
+
cookies2[name.trim()] = rest.join("=").trim();
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
return cookies2;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// core/logger.ts
|
|
1825
|
+
var colors = {
|
|
1826
|
+
reset: "\x1B[0m",
|
|
1827
|
+
bold: "\x1B[1m",
|
|
1828
|
+
dim: "\x1B[2m",
|
|
1829
|
+
italic: "\x1B[3m",
|
|
1830
|
+
underline: "\x1B[4m",
|
|
1831
|
+
// Text colors
|
|
1832
|
+
black: "\x1B[30m",
|
|
1833
|
+
red: "\x1B[31m",
|
|
1834
|
+
green: "\x1B[32m",
|
|
1835
|
+
yellow: "\x1B[33m",
|
|
1836
|
+
blue: "\x1B[34m",
|
|
1837
|
+
magenta: "\x1B[35m",
|
|
1838
|
+
cyan: "\x1B[36m",
|
|
1839
|
+
white: "\x1B[37m",
|
|
1840
|
+
gray: "\x1B[90m",
|
|
1841
|
+
// Bright colors
|
|
1842
|
+
brightRed: "\x1B[91m",
|
|
1843
|
+
brightGreen: "\x1B[92m",
|
|
1844
|
+
brightYellow: "\x1B[93m",
|
|
1845
|
+
brightBlue: "\x1B[94m",
|
|
1846
|
+
brightMagenta: "\x1B[95m",
|
|
1847
|
+
brightCyan: "\x1B[96m",
|
|
1848
|
+
brightWhite: "\x1B[97m",
|
|
1849
|
+
// Background colors
|
|
1850
|
+
bgRed: "\x1B[41m",
|
|
1851
|
+
bgGreen: "\x1B[42m",
|
|
1852
|
+
bgYellow: "\x1B[43m",
|
|
1853
|
+
bgBlue: "\x1B[44m",
|
|
1854
|
+
bgMagenta: "\x1B[45m",
|
|
1855
|
+
bgCyan: "\x1B[46m"
|
|
1856
|
+
};
|
|
1857
|
+
var c = colors;
|
|
1858
|
+
function getStatusColor(status) {
|
|
1859
|
+
if (status >= 500) return c.red;
|
|
1860
|
+
if (status >= 400) return c.yellow;
|
|
1861
|
+
if (status >= 300) return c.cyan;
|
|
1862
|
+
if (status >= 200) return c.green;
|
|
1863
|
+
return c.white;
|
|
1864
|
+
}
|
|
1865
|
+
function getMethodColor(method) {
|
|
1866
|
+
const methodColors = {
|
|
1867
|
+
GET: c.brightGreen,
|
|
1868
|
+
POST: c.brightBlue,
|
|
1869
|
+
PUT: c.brightYellow,
|
|
1870
|
+
PATCH: c.brightMagenta,
|
|
1871
|
+
DELETE: c.brightRed,
|
|
1872
|
+
OPTIONS: c.gray,
|
|
1873
|
+
HEAD: c.gray
|
|
1874
|
+
};
|
|
1875
|
+
return methodColors[method] || c.white;
|
|
1876
|
+
}
|
|
1877
|
+
function formatTime(ms) {
|
|
1878
|
+
if (ms < 1) return `${c.gray}<1ms${c.reset}`;
|
|
1879
|
+
if (ms < 100) return `${c.green}${ms}ms${c.reset}`;
|
|
1880
|
+
if (ms < 500) return `${c.yellow}${ms}ms${c.reset}`;
|
|
1881
|
+
return `${c.red}${ms}ms${c.reset}`;
|
|
1882
|
+
}
|
|
1883
|
+
var LOGO = `
|
|
1884
|
+
${c.green} \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E${c.reset}
|
|
1885
|
+
${c.green} \u2502${c.reset} ${c.green}\u2502${c.reset}
|
|
1886
|
+
${c.green} \u2502${c.reset} ${c.brightGreen}\u26A1${c.reset} ${c.bold}${c.white}F L E X I R E A C T${c.reset} ${c.dim}v1.0.0${c.reset} ${c.green}\u2502${c.reset}
|
|
1887
|
+
${c.green} \u2502${c.reset} ${c.green}\u2502${c.reset}
|
|
1888
|
+
${c.green} \u2502${c.reset} ${c.dim}The Modern React Framework${c.reset} ${c.green}\u2502${c.reset}
|
|
1889
|
+
${c.green} \u2502${c.reset} ${c.green}\u2502${c.reset}
|
|
1890
|
+
${c.green} \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F${c.reset}
|
|
1891
|
+
`;
|
|
1892
|
+
var MINI_LOGO = `${c.brightGreen}\u26A1${c.reset} ${c.bold}FlexiReact${c.reset}`;
|
|
1893
|
+
var READY_MSG = ` ${c.green}\u25B2${c.reset} ${c.bold}Ready${c.reset} in`;
|
|
1894
|
+
var logger = {
|
|
1895
|
+
// Show startup logo
|
|
1896
|
+
logo() {
|
|
1897
|
+
console.log(LOGO);
|
|
1898
|
+
},
|
|
1899
|
+
// Server started - Next.js style
|
|
1900
|
+
serverStart(config, startTime = Date.now()) {
|
|
1901
|
+
const { port, host, mode, pagesDir, islands, rsc } = config;
|
|
1902
|
+
const elapsed = Date.now() - startTime;
|
|
1903
|
+
console.log("");
|
|
1904
|
+
console.log(` ${c.green}\u25B2${c.reset} ${c.bold}Ready${c.reset} in ${c.cyan}${elapsed}ms${c.reset}`);
|
|
1905
|
+
console.log("");
|
|
1906
|
+
console.log(` ${c.dim}\u250C${c.reset} ${c.bold}Local:${c.reset} ${c.cyan}http://${host}:${port}${c.reset}`);
|
|
1907
|
+
console.log(` ${c.dim}\u251C${c.reset} ${c.bold}Environment:${c.reset} ${mode === "development" ? `${c.yellow}development${c.reset}` : `${c.green}production${c.reset}`}`);
|
|
1908
|
+
if (islands) {
|
|
1909
|
+
console.log(` ${c.dim}\u251C${c.reset} ${c.bold}Islands:${c.reset} ${c.green}enabled${c.reset}`);
|
|
1910
|
+
}
|
|
1911
|
+
if (rsc) {
|
|
1912
|
+
console.log(` ${c.dim}\u251C${c.reset} ${c.bold}RSC:${c.reset} ${c.green}enabled${c.reset}`);
|
|
1913
|
+
}
|
|
1914
|
+
console.log(` ${c.dim}\u2514${c.reset} ${c.bold}Pages:${c.reset} ${c.dim}${pagesDir}${c.reset}`);
|
|
1915
|
+
console.log("");
|
|
1916
|
+
},
|
|
1917
|
+
// HTTP request log - Compact single line like Next.js
|
|
1918
|
+
request(method, path8, status, time, extra = {}) {
|
|
1919
|
+
const methodColor = getMethodColor(method);
|
|
1920
|
+
const statusColor = getStatusColor(status);
|
|
1921
|
+
const timeStr = formatTime(time);
|
|
1922
|
+
let badge = "";
|
|
1923
|
+
if (extra.type === "static" || extra.type === "ssg") {
|
|
1924
|
+
badge = `${c.dim}\u25CB${c.reset}`;
|
|
1925
|
+
} else if (extra.type === "dynamic" || extra.type === "ssr") {
|
|
1926
|
+
badge = `${c.magenta}\u0192${c.reset}`;
|
|
1927
|
+
} else if (extra.type === "api") {
|
|
1928
|
+
badge = `${c.blue}\u03BB${c.reset}`;
|
|
1929
|
+
} else if (extra.type === "asset") {
|
|
1930
|
+
badge = `${c.dim}\u25E6${c.reset}`;
|
|
1931
|
+
} else {
|
|
1932
|
+
badge = `${c.magenta}\u0192${c.reset}`;
|
|
1933
|
+
}
|
|
1934
|
+
const statusStr = `${statusColor}${status}${c.reset}`;
|
|
1935
|
+
const methodStr = `${methodColor}${method}${c.reset}`;
|
|
1936
|
+
console.log(` ${badge} ${methodStr} ${path8} ${statusStr} ${c.dim}in${c.reset} ${timeStr}`);
|
|
1937
|
+
},
|
|
1938
|
+
// Info message
|
|
1939
|
+
info(msg) {
|
|
1940
|
+
console.log(` ${c.cyan}\u2139${c.reset} ${msg}`);
|
|
1941
|
+
},
|
|
1942
|
+
// Success message
|
|
1943
|
+
success(msg) {
|
|
1944
|
+
console.log(` ${c.green}\u2713${c.reset} ${msg}`);
|
|
1945
|
+
},
|
|
1946
|
+
// Warning message
|
|
1947
|
+
warn(msg) {
|
|
1948
|
+
console.log(` ${c.yellow}\u26A0${c.reset} ${c.yellow}${msg}${c.reset}`);
|
|
1949
|
+
},
|
|
1950
|
+
// Error message
|
|
1951
|
+
error(msg, err = null) {
|
|
1952
|
+
console.log(` ${c.red}\u2717${c.reset} ${c.red}${msg}${c.reset}`);
|
|
1953
|
+
if (err && err.stack) {
|
|
1954
|
+
const stack = err.stack.split("\n").slice(1, 4).join("\n");
|
|
1955
|
+
console.log(`${c.dim}${stack}${c.reset}`);
|
|
1956
|
+
}
|
|
1957
|
+
},
|
|
1958
|
+
// Compilation message
|
|
1959
|
+
compile(file, time) {
|
|
1960
|
+
console.log(` ${c.magenta}\u25C9${c.reset} Compiled ${c.cyan}${file}${c.reset} ${c.dim}(${time}ms)${c.reset}`);
|
|
1961
|
+
},
|
|
1962
|
+
// Hot reload
|
|
1963
|
+
hmr(file) {
|
|
1964
|
+
console.log(` ${c.yellow}\u21BB${c.reset} HMR update: ${c.cyan}${file}${c.reset}`);
|
|
1965
|
+
},
|
|
1966
|
+
// Plugin loaded
|
|
1967
|
+
plugin(name) {
|
|
1968
|
+
console.log(` ${c.blue}\u2B21${c.reset} Plugin: ${c.cyan}${name}${c.reset}`);
|
|
1969
|
+
},
|
|
1970
|
+
// Route info
|
|
1971
|
+
route(path8, type) {
|
|
1972
|
+
const typeColors = {
|
|
1973
|
+
static: c.green,
|
|
1974
|
+
dynamic: c.yellow,
|
|
1975
|
+
api: c.blue
|
|
1976
|
+
};
|
|
1977
|
+
const color = typeColors[type] || c.white;
|
|
1978
|
+
console.log(` ${c.dim}\u251C\u2500${c.reset} ${path8} ${color}[${type}]${c.reset}`);
|
|
1979
|
+
},
|
|
1980
|
+
// Divider
|
|
1981
|
+
divider() {
|
|
1982
|
+
console.log(`${c.dim} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}`);
|
|
1983
|
+
},
|
|
1984
|
+
// Blank line
|
|
1985
|
+
blank() {
|
|
1986
|
+
console.log("");
|
|
1987
|
+
},
|
|
1988
|
+
// Port in use error with solution
|
|
1989
|
+
portInUse(port) {
|
|
1990
|
+
console.log(`
|
|
1991
|
+
${c.red} \u2717 Port ${port} is already in use${c.reset}
|
|
1992
|
+
|
|
1993
|
+
${c.dim}Try one of these solutions:${c.reset}
|
|
1994
|
+
|
|
1995
|
+
${c.yellow}1.${c.reset} Kill the process using the port:
|
|
1996
|
+
${c.cyan}npx kill-port ${port}${c.reset}
|
|
1997
|
+
|
|
1998
|
+
${c.yellow}2.${c.reset} Use a different port in ${c.cyan}flexireact.config.js${c.reset}:
|
|
1999
|
+
${c.dim}server: { port: 3001 }${c.reset}
|
|
2000
|
+
|
|
2001
|
+
${c.yellow}3.${c.reset} Set PORT environment variable:
|
|
2002
|
+
${c.cyan}PORT=3001 npm run dev${c.reset}
|
|
2003
|
+
`);
|
|
2004
|
+
},
|
|
2005
|
+
// Build info
|
|
2006
|
+
build(stats) {
|
|
2007
|
+
console.log(`
|
|
2008
|
+
${c.green}\u2713${c.reset} Build complete!
|
|
2009
|
+
|
|
2010
|
+
${c.dim}\u251C\u2500${c.reset} Pages: ${c.cyan}${stats.pages}${c.reset}
|
|
2011
|
+
${c.dim}\u251C\u2500${c.reset} API: ${c.cyan}${stats.api}${c.reset}
|
|
2012
|
+
${c.dim}\u251C\u2500${c.reset} Assets: ${c.cyan}${stats.assets}${c.reset}
|
|
2013
|
+
${c.dim}\u2514\u2500${c.reset} Time: ${c.green}${stats.time}ms${c.reset}
|
|
2014
|
+
`);
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
// core/helpers.ts
|
|
2019
|
+
var RedirectError = class extends Error {
|
|
2020
|
+
url;
|
|
2021
|
+
statusCode;
|
|
2022
|
+
type = "redirect";
|
|
2023
|
+
constructor(url, statusCode = 307) {
|
|
2024
|
+
super(`Redirect to ${url}`);
|
|
2025
|
+
this.name = "RedirectError";
|
|
2026
|
+
this.url = url;
|
|
2027
|
+
this.statusCode = statusCode;
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
var NotFoundError = class extends Error {
|
|
2031
|
+
type = "notFound";
|
|
2032
|
+
constructor(message = "Page not found") {
|
|
2033
|
+
super(message);
|
|
2034
|
+
this.name = "NotFoundError";
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
function redirect(url, type = "replace") {
|
|
2038
|
+
const statusCode = type === "permanent" ? 308 : 307;
|
|
2039
|
+
throw new RedirectError(url, statusCode);
|
|
2040
|
+
}
|
|
2041
|
+
function notFound(message) {
|
|
2042
|
+
throw new NotFoundError(message);
|
|
2043
|
+
}
|
|
2044
|
+
var cookies = {
|
|
2045
|
+
/**
|
|
2046
|
+
* Parse cookies from a cookie header string
|
|
2047
|
+
*/
|
|
2048
|
+
parse(cookieHeader) {
|
|
2049
|
+
const cookies2 = {};
|
|
2050
|
+
if (!cookieHeader) return cookies2;
|
|
2051
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
2052
|
+
const [name, ...rest] = cookie.split("=");
|
|
2053
|
+
if (name) {
|
|
2054
|
+
const value = rest.join("=");
|
|
2055
|
+
cookies2[name.trim()] = decodeURIComponent(value.trim());
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
return cookies2;
|
|
2059
|
+
},
|
|
2060
|
+
/**
|
|
2061
|
+
* Get a cookie value from request headers
|
|
2062
|
+
*/
|
|
2063
|
+
get(request, name) {
|
|
2064
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
2065
|
+
const parsed = this.parse(cookieHeader);
|
|
2066
|
+
return parsed[name];
|
|
2067
|
+
},
|
|
2068
|
+
/**
|
|
2069
|
+
* Get all cookies from request
|
|
2070
|
+
*/
|
|
2071
|
+
getAll(request) {
|
|
2072
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
2073
|
+
return this.parse(cookieHeader);
|
|
2074
|
+
},
|
|
2075
|
+
/**
|
|
2076
|
+
* Serialize a cookie for Set-Cookie header
|
|
2077
|
+
*/
|
|
2078
|
+
serialize(name, value, options = {}) {
|
|
2079
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
2080
|
+
if (options.maxAge !== void 0) {
|
|
2081
|
+
cookie += `; Max-Age=${options.maxAge}`;
|
|
2082
|
+
}
|
|
2083
|
+
if (options.expires) {
|
|
2084
|
+
cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
2085
|
+
}
|
|
2086
|
+
if (options.path) {
|
|
2087
|
+
cookie += `; Path=${options.path}`;
|
|
2088
|
+
}
|
|
2089
|
+
if (options.domain) {
|
|
2090
|
+
cookie += `; Domain=${options.domain}`;
|
|
2091
|
+
}
|
|
2092
|
+
if (options.secure) {
|
|
2093
|
+
cookie += "; Secure";
|
|
2094
|
+
}
|
|
2095
|
+
if (options.httpOnly) {
|
|
2096
|
+
cookie += "; HttpOnly";
|
|
2097
|
+
}
|
|
2098
|
+
if (options.sameSite) {
|
|
2099
|
+
cookie += `; SameSite=${options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
|
|
2100
|
+
}
|
|
2101
|
+
return cookie;
|
|
2102
|
+
},
|
|
2103
|
+
/**
|
|
2104
|
+
* Create a Set-Cookie header value
|
|
2105
|
+
*/
|
|
2106
|
+
set(name, value, options = {}) {
|
|
2107
|
+
return this.serialize(name, value, {
|
|
2108
|
+
path: "/",
|
|
2109
|
+
httpOnly: true,
|
|
2110
|
+
secure: process.env.NODE_ENV === "production",
|
|
2111
|
+
sameSite: "lax",
|
|
2112
|
+
...options
|
|
2113
|
+
});
|
|
2114
|
+
},
|
|
2115
|
+
/**
|
|
2116
|
+
* Create a cookie deletion header
|
|
2117
|
+
*/
|
|
2118
|
+
delete(name, options = {}) {
|
|
2119
|
+
return this.serialize(name, "", {
|
|
2120
|
+
...options,
|
|
2121
|
+
path: "/",
|
|
2122
|
+
maxAge: 0
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
var headers = {
|
|
2127
|
+
/**
|
|
2128
|
+
* Create a new Headers object with common defaults
|
|
2129
|
+
*/
|
|
2130
|
+
create(init) {
|
|
2131
|
+
return new Headers(init);
|
|
2132
|
+
},
|
|
2133
|
+
/**
|
|
2134
|
+
* Get a header value from request
|
|
2135
|
+
*/
|
|
2136
|
+
get(request, name) {
|
|
2137
|
+
return request.headers.get(name);
|
|
2138
|
+
},
|
|
2139
|
+
/**
|
|
2140
|
+
* Get all headers as an object
|
|
2141
|
+
*/
|
|
2142
|
+
getAll(request) {
|
|
2143
|
+
const result = {};
|
|
2144
|
+
request.headers.forEach((value, key) => {
|
|
2145
|
+
result[key] = value;
|
|
2146
|
+
});
|
|
2147
|
+
return result;
|
|
2148
|
+
},
|
|
2149
|
+
/**
|
|
2150
|
+
* Check if request has a specific header
|
|
2151
|
+
*/
|
|
2152
|
+
has(request, name) {
|
|
2153
|
+
return request.headers.has(name);
|
|
2154
|
+
},
|
|
2155
|
+
/**
|
|
2156
|
+
* Get content type from request
|
|
2157
|
+
*/
|
|
2158
|
+
contentType(request) {
|
|
2159
|
+
return request.headers.get("content-type");
|
|
2160
|
+
},
|
|
2161
|
+
/**
|
|
2162
|
+
* Check if request accepts JSON
|
|
2163
|
+
*/
|
|
2164
|
+
acceptsJson(request) {
|
|
2165
|
+
const accept = request.headers.get("accept") || "";
|
|
2166
|
+
return accept.includes("application/json") || accept.includes("*/*");
|
|
2167
|
+
},
|
|
2168
|
+
/**
|
|
2169
|
+
* Check if request is AJAX/fetch
|
|
2170
|
+
*/
|
|
2171
|
+
isAjax(request) {
|
|
2172
|
+
return request.headers.get("x-requested-with") === "XMLHttpRequest" || this.acceptsJson(request);
|
|
2173
|
+
},
|
|
2174
|
+
/**
|
|
2175
|
+
* Get authorization header
|
|
2176
|
+
*/
|
|
2177
|
+
authorization(request) {
|
|
2178
|
+
const auth = request.headers.get("authorization");
|
|
2179
|
+
if (!auth) return null;
|
|
2180
|
+
const [type, ...rest] = auth.split(" ");
|
|
2181
|
+
return {
|
|
2182
|
+
type: type.toLowerCase(),
|
|
2183
|
+
credentials: rest.join(" ")
|
|
2184
|
+
};
|
|
2185
|
+
},
|
|
2186
|
+
/**
|
|
2187
|
+
* Get bearer token from authorization header
|
|
2188
|
+
*/
|
|
2189
|
+
bearerToken(request) {
|
|
2190
|
+
const auth = this.authorization(request);
|
|
2191
|
+
if (auth?.type === "bearer") {
|
|
2192
|
+
return auth.credentials;
|
|
2193
|
+
}
|
|
2194
|
+
return null;
|
|
2195
|
+
},
|
|
2196
|
+
/**
|
|
2197
|
+
* Common security headers
|
|
2198
|
+
*/
|
|
2199
|
+
security() {
|
|
2200
|
+
return {
|
|
2201
|
+
"X-Content-Type-Options": "nosniff",
|
|
2202
|
+
"X-Frame-Options": "DENY",
|
|
2203
|
+
"X-XSS-Protection": "1; mode=block",
|
|
2204
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
2205
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
|
|
2206
|
+
};
|
|
2207
|
+
},
|
|
2208
|
+
/**
|
|
2209
|
+
* CORS headers
|
|
2210
|
+
*/
|
|
2211
|
+
cors(options = {}) {
|
|
2212
|
+
const {
|
|
2213
|
+
origin = "*",
|
|
2214
|
+
methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
2215
|
+
headers: allowHeaders = ["Content-Type", "Authorization"],
|
|
2216
|
+
credentials = false,
|
|
2217
|
+
maxAge = 86400
|
|
2218
|
+
} = options;
|
|
2219
|
+
const corsHeaders = {
|
|
2220
|
+
"Access-Control-Allow-Origin": origin,
|
|
2221
|
+
"Access-Control-Allow-Methods": methods.join(", "),
|
|
2222
|
+
"Access-Control-Allow-Headers": allowHeaders.join(", "),
|
|
2223
|
+
"Access-Control-Max-Age": String(maxAge)
|
|
2224
|
+
};
|
|
2225
|
+
if (credentials) {
|
|
2226
|
+
corsHeaders["Access-Control-Allow-Credentials"] = "true";
|
|
2227
|
+
}
|
|
2228
|
+
return corsHeaders;
|
|
2229
|
+
},
|
|
2230
|
+
/**
|
|
2231
|
+
* Cache control headers
|
|
2232
|
+
*/
|
|
2233
|
+
cache(options = {}) {
|
|
2234
|
+
if (options.noStore) {
|
|
2235
|
+
return { "Cache-Control": "no-store, no-cache, must-revalidate" };
|
|
2236
|
+
}
|
|
2237
|
+
const directives = [];
|
|
2238
|
+
if (options.private) {
|
|
2239
|
+
directives.push("private");
|
|
2240
|
+
} else {
|
|
2241
|
+
directives.push("public");
|
|
2242
|
+
}
|
|
2243
|
+
if (options.maxAge !== void 0) {
|
|
2244
|
+
directives.push(`max-age=${options.maxAge}`);
|
|
2245
|
+
}
|
|
2246
|
+
if (options.sMaxAge !== void 0) {
|
|
2247
|
+
directives.push(`s-maxage=${options.sMaxAge}`);
|
|
2248
|
+
}
|
|
2249
|
+
if (options.staleWhileRevalidate !== void 0) {
|
|
2250
|
+
directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
|
|
2251
|
+
}
|
|
2252
|
+
return { "Cache-Control": directives.join(", ") };
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
|
|
2256
|
+
// core/actions/index.ts
|
|
2257
|
+
globalThis.__FLEXI_ACTIONS__ = globalThis.__FLEXI_ACTIONS__ || {};
|
|
2258
|
+
globalThis.__FLEXI_ACTION_CONTEXT__ = null;
|
|
2259
|
+
async function executeAction(actionId, args, context) {
|
|
2260
|
+
const action = globalThis.__FLEXI_ACTIONS__[actionId];
|
|
2261
|
+
if (!action) {
|
|
2262
|
+
return {
|
|
2263
|
+
success: false,
|
|
2264
|
+
error: `Server action not found: ${actionId}`
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
const actionContext = {
|
|
2268
|
+
request: context?.request || new Request("http://localhost"),
|
|
2269
|
+
cookies,
|
|
2270
|
+
headers,
|
|
2271
|
+
redirect,
|
|
2272
|
+
notFound
|
|
2273
|
+
};
|
|
2274
|
+
globalThis.__FLEXI_ACTION_CONTEXT__ = actionContext;
|
|
2275
|
+
try {
|
|
2276
|
+
const result = await action(...args);
|
|
2277
|
+
return {
|
|
2278
|
+
success: true,
|
|
2279
|
+
data: result
|
|
2280
|
+
};
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
if (error instanceof RedirectError) {
|
|
2283
|
+
return {
|
|
2284
|
+
success: true,
|
|
2285
|
+
redirect: error.url
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
if (error instanceof NotFoundError) {
|
|
2289
|
+
return {
|
|
2290
|
+
success: false,
|
|
2291
|
+
error: "Not found"
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
return {
|
|
2295
|
+
success: false,
|
|
2296
|
+
error: error.message || "Action failed"
|
|
2297
|
+
};
|
|
2298
|
+
} finally {
|
|
2299
|
+
globalThis.__FLEXI_ACTION_CONTEXT__ = null;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function deserializeArgs(args) {
|
|
2303
|
+
return args.map((arg) => {
|
|
2304
|
+
if (arg && typeof arg === "object") {
|
|
2305
|
+
if (arg.$$type === "FormData") {
|
|
2306
|
+
const formData = new FormData();
|
|
2307
|
+
for (const [key, value] of Object.entries(arg.data)) {
|
|
2308
|
+
if (Array.isArray(value)) {
|
|
2309
|
+
value.forEach((v) => formData.append(key, v));
|
|
2310
|
+
} else {
|
|
2311
|
+
formData.append(key, value);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return formData;
|
|
2315
|
+
}
|
|
2316
|
+
if (arg.$$type === "Date") {
|
|
2317
|
+
return new Date(arg.value);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return arg;
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// core/image/index.ts
|
|
2325
|
+
import React4 from "react";
|
|
2326
|
+
import path5 from "path";
|
|
2327
|
+
import fs6 from "fs";
|
|
2328
|
+
import crypto from "crypto";
|
|
2329
|
+
var defaultImageConfig = {
|
|
2330
|
+
domains: [],
|
|
2331
|
+
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
2332
|
+
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
2333
|
+
formats: ["webp", "avif"],
|
|
2334
|
+
minimumCacheTTL: 60 * 60 * 24 * 30,
|
|
2335
|
+
// 30 days
|
|
2336
|
+
dangerouslyAllowSVG: false,
|
|
2337
|
+
quality: 75,
|
|
2338
|
+
cacheDir: ".flexi/image-cache"
|
|
2339
|
+
};
|
|
2340
|
+
function generateSrcSet(src, widths, quality = 75) {
|
|
2341
|
+
return widths.map((w) => `/_flexi/image?url=${encodeURIComponent(src)}&w=${w}&q=${quality} ${w}w`).join(", ");
|
|
2342
|
+
}
|
|
2343
|
+
function generateSizes(sizes) {
|
|
2344
|
+
if (sizes) return sizes;
|
|
2345
|
+
return "100vw";
|
|
2346
|
+
}
|
|
2347
|
+
async function handleImageOptimization(req, res, config = {}) {
|
|
2348
|
+
const fullConfig = { ...defaultImageConfig, ...config };
|
|
2349
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2350
|
+
const imageUrl = url.searchParams.get("url");
|
|
2351
|
+
const width = parseInt(url.searchParams.get("w") || "0", 10);
|
|
2352
|
+
const quality = parseInt(url.searchParams.get("q") || String(fullConfig.quality), 10);
|
|
2353
|
+
const format = url.searchParams.get("f");
|
|
2354
|
+
if (!imageUrl) {
|
|
2355
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2356
|
+
res.end("Missing url parameter");
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
try {
|
|
2360
|
+
let imageBuffer;
|
|
2361
|
+
let contentType;
|
|
2362
|
+
if (imageUrl.startsWith("http")) {
|
|
2363
|
+
const response = await fetch(imageUrl);
|
|
2364
|
+
if (!response.ok) throw new Error("Failed to fetch image");
|
|
2365
|
+
imageBuffer = Buffer.from(await response.arrayBuffer());
|
|
2366
|
+
contentType = response.headers.get("content-type") || "image/jpeg";
|
|
2367
|
+
} else {
|
|
2368
|
+
const imagePath = path5.join(process.cwd(), "public", imageUrl);
|
|
2369
|
+
if (!fs6.existsSync(imagePath)) {
|
|
2370
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2371
|
+
res.end("Image not found");
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
imageBuffer = fs6.readFileSync(imagePath);
|
|
2375
|
+
contentType = getContentType(imagePath);
|
|
2376
|
+
}
|
|
2377
|
+
const cacheKey = crypto.createHash("md5").update(`${imageUrl}-${width}-${quality}-${format}`).digest("hex");
|
|
2378
|
+
const cacheDir = path5.join(process.cwd(), fullConfig.cacheDir);
|
|
2379
|
+
const cachePath = path5.join(cacheDir, `${cacheKey}.${format || "webp"}`);
|
|
2380
|
+
if (fs6.existsSync(cachePath)) {
|
|
2381
|
+
const cachedImage = fs6.readFileSync(cachePath);
|
|
2382
|
+
res.writeHead(200, {
|
|
2383
|
+
"Content-Type": `image/${format || "webp"}`,
|
|
2384
|
+
"Cache-Control": `public, max-age=${fullConfig.minimumCacheTTL}`,
|
|
2385
|
+
"X-Flexi-Image-Cache": "HIT"
|
|
2386
|
+
});
|
|
2387
|
+
res.end(cachedImage);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
res.writeHead(200, {
|
|
2391
|
+
"Content-Type": contentType,
|
|
2392
|
+
"Cache-Control": `public, max-age=${fullConfig.minimumCacheTTL}`,
|
|
2393
|
+
"X-Flexi-Image-Cache": "MISS"
|
|
2394
|
+
});
|
|
2395
|
+
res.end(imageBuffer);
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
console.error("Image optimization error:", error);
|
|
2398
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2399
|
+
res.end("Image optimization failed");
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function getContentType(filePath) {
|
|
2403
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
2404
|
+
const types = {
|
|
2405
|
+
".jpg": "image/jpeg",
|
|
2406
|
+
".jpeg": "image/jpeg",
|
|
2407
|
+
".png": "image/png",
|
|
2408
|
+
".gif": "image/gif",
|
|
2409
|
+
".webp": "image/webp",
|
|
2410
|
+
".avif": "image/avif",
|
|
2411
|
+
".svg": "image/svg+xml",
|
|
2412
|
+
".ico": "image/x-icon"
|
|
2413
|
+
};
|
|
2414
|
+
return types[ext] || "application/octet-stream";
|
|
2415
|
+
}
|
|
2416
|
+
function createImageComponent(config = {}) {
|
|
2417
|
+
const fullConfig = { ...defaultImageConfig, ...config };
|
|
2418
|
+
return function Image2(props) {
|
|
2419
|
+
const {
|
|
2420
|
+
src,
|
|
2421
|
+
alt,
|
|
2422
|
+
width,
|
|
2423
|
+
height,
|
|
2424
|
+
fill = false,
|
|
2425
|
+
sizes,
|
|
2426
|
+
quality = fullConfig.quality,
|
|
2427
|
+
priority = false,
|
|
2428
|
+
placeholder = "empty",
|
|
2429
|
+
blurDataURL,
|
|
2430
|
+
loading,
|
|
2431
|
+
className = "",
|
|
2432
|
+
style = {},
|
|
2433
|
+
unoptimized = false,
|
|
2434
|
+
...rest
|
|
2435
|
+
} = props;
|
|
2436
|
+
const loadingAttr = priority ? "eager" : loading || "lazy";
|
|
2437
|
+
const optimizedSrc = unoptimized ? src : `/_flexi/image?url=${encodeURIComponent(src)}&w=${width || 1920}&q=${quality}`;
|
|
2438
|
+
const allSizes = [...fullConfig.imageSizes, ...fullConfig.deviceSizes].sort((a, b) => a - b);
|
|
2439
|
+
const relevantSizes = width ? allSizes.filter((s) => s <= width * 2) : allSizes;
|
|
2440
|
+
const srcSet = unoptimized ? void 0 : generateSrcSet(src, relevantSizes, quality);
|
|
2441
|
+
const imgStyle = {
|
|
2442
|
+
...style,
|
|
2443
|
+
...fill ? {
|
|
2444
|
+
position: "absolute",
|
|
2445
|
+
top: 0,
|
|
2446
|
+
left: 0,
|
|
2447
|
+
width: "100%",
|
|
2448
|
+
height: "100%",
|
|
2449
|
+
objectFit: "cover"
|
|
2450
|
+
} : {}
|
|
2451
|
+
};
|
|
2452
|
+
const wrapperStyle = fill ? {
|
|
2453
|
+
position: "relative",
|
|
2454
|
+
width: "100%",
|
|
2455
|
+
height: "100%"
|
|
2456
|
+
} : {};
|
|
2457
|
+
const placeholderStyle = placeholder === "blur" ? {
|
|
2458
|
+
backgroundImage: `url(${blurDataURL || generateBlurPlaceholderSync()})`,
|
|
2459
|
+
backgroundSize: "cover",
|
|
2460
|
+
backgroundPosition: "center",
|
|
2461
|
+
filter: "blur(20px)",
|
|
2462
|
+
transform: "scale(1.1)"
|
|
2463
|
+
} : {};
|
|
2464
|
+
const imgElement = React4.createElement("img", {
|
|
2465
|
+
src: optimizedSrc,
|
|
2466
|
+
alt,
|
|
2467
|
+
width: fill ? void 0 : width,
|
|
2468
|
+
height: fill ? void 0 : height,
|
|
2469
|
+
loading: loadingAttr,
|
|
2470
|
+
decoding: "async",
|
|
2471
|
+
srcSet,
|
|
2472
|
+
sizes: generateSizes(sizes),
|
|
2473
|
+
className: `flexi-image ${className}`.trim(),
|
|
2474
|
+
style: imgStyle,
|
|
2475
|
+
fetchPriority: priority ? "high" : void 0,
|
|
2476
|
+
...rest
|
|
2477
|
+
});
|
|
2478
|
+
if (fill || placeholder === "blur") {
|
|
2479
|
+
return React4.createElement("div", {
|
|
2480
|
+
className: "flexi-image-wrapper",
|
|
2481
|
+
style: { ...wrapperStyle, ...placeholderStyle }
|
|
2482
|
+
}, imgElement);
|
|
2483
|
+
}
|
|
2484
|
+
return imgElement;
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
function generateBlurPlaceholderSync() {
|
|
2488
|
+
return `data:image/svg+xml;base64,${Buffer.from(
|
|
2489
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 5">
|
|
2490
|
+
<filter id="b" color-interpolation-filters="sRGB">
|
|
2491
|
+
<feGaussianBlur stdDeviation="1"/>
|
|
2492
|
+
</filter>
|
|
2493
|
+
<rect width="100%" height="100%" fill="#1a1a1a"/>
|
|
2494
|
+
</svg>`
|
|
2495
|
+
).toString("base64")}`;
|
|
2496
|
+
}
|
|
2497
|
+
var Image = createImageComponent();
|
|
2498
|
+
|
|
2499
|
+
// core/font/index.ts
|
|
2500
|
+
import fs7 from "fs";
|
|
2501
|
+
import path6 from "path";
|
|
2502
|
+
import crypto2 from "crypto";
|
|
2503
|
+
var GOOGLE_FONTS_API = "https://fonts.googleapis.com/css2";
|
|
2504
|
+
async function handleFontRequest(req, res) {
|
|
2505
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2506
|
+
const pathParts = url.pathname.split("/");
|
|
2507
|
+
const fontFamily = decodeURIComponent(pathParts[pathParts.length - 1] || "");
|
|
2508
|
+
const weight = url.searchParams.get("weight") || "400";
|
|
2509
|
+
const style = url.searchParams.get("style") || "normal";
|
|
2510
|
+
if (!fontFamily) {
|
|
2511
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2512
|
+
res.end("Missing font family");
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
try {
|
|
2516
|
+
const cacheDir = path6.join(process.cwd(), ".flexi", "font-cache");
|
|
2517
|
+
const cacheKey = crypto2.createHash("md5").update(`${fontFamily}-${weight}-${style}`).digest("hex");
|
|
2518
|
+
const cachePath = path6.join(cacheDir, `${cacheKey}.woff2`);
|
|
2519
|
+
if (fs7.existsSync(cachePath)) {
|
|
2520
|
+
const fontData = fs7.readFileSync(cachePath);
|
|
2521
|
+
res.writeHead(200, {
|
|
2522
|
+
"Content-Type": "font/woff2",
|
|
2523
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
2524
|
+
"X-Flexi-Font-Cache": "HIT"
|
|
2525
|
+
});
|
|
2526
|
+
res.end(fontData);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
const googleUrl = `${GOOGLE_FONTS_API}?family=${encodeURIComponent(fontFamily)}:wght@${weight}&display=swap`;
|
|
2530
|
+
const cssResponse = await fetch(googleUrl, {
|
|
2531
|
+
headers: {
|
|
2532
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
if (!cssResponse.ok) {
|
|
2536
|
+
throw new Error("Failed to fetch font CSS");
|
|
2537
|
+
}
|
|
2538
|
+
const css = await cssResponse.text();
|
|
2539
|
+
const woff2Match = css.match(/url\((https:\/\/fonts\.gstatic\.com[^)]+\.woff2)\)/);
|
|
2540
|
+
if (!woff2Match) {
|
|
2541
|
+
throw new Error("Could not find woff2 URL");
|
|
2542
|
+
}
|
|
2543
|
+
const fontResponse = await fetch(woff2Match[1]);
|
|
2544
|
+
if (!fontResponse.ok) {
|
|
2545
|
+
throw new Error("Failed to fetch font file");
|
|
2546
|
+
}
|
|
2547
|
+
const fontBuffer = Buffer.from(await fontResponse.arrayBuffer());
|
|
2548
|
+
if (!fs7.existsSync(cacheDir)) {
|
|
2549
|
+
fs7.mkdirSync(cacheDir, { recursive: true });
|
|
2550
|
+
}
|
|
2551
|
+
fs7.writeFileSync(cachePath, fontBuffer);
|
|
2552
|
+
res.writeHead(200, {
|
|
2553
|
+
"Content-Type": "font/woff2",
|
|
2554
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
2555
|
+
"X-Flexi-Font-Cache": "MISS"
|
|
2556
|
+
});
|
|
2557
|
+
res.end(fontBuffer);
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
console.error("Font loading error:", error);
|
|
2560
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2561
|
+
res.end("Font loading failed");
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// core/server/index.ts
|
|
2566
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
2567
|
+
var __dirname2 = path7.dirname(__filename2);
|
|
2568
|
+
var MIME_TYPES = {
|
|
2569
|
+
".html": "text/html",
|
|
2570
|
+
".css": "text/css",
|
|
2571
|
+
".js": "application/javascript",
|
|
2572
|
+
".mjs": "application/javascript",
|
|
2573
|
+
".json": "application/json",
|
|
2574
|
+
".png": "image/png",
|
|
2575
|
+
".jpg": "image/jpeg",
|
|
2576
|
+
".jpeg": "image/jpeg",
|
|
2577
|
+
".gif": "image/gif",
|
|
2578
|
+
".svg": "image/svg+xml",
|
|
2579
|
+
".ico": "image/x-icon",
|
|
2580
|
+
".woff": "font/woff",
|
|
2581
|
+
".woff2": "font/woff2",
|
|
2582
|
+
".ttf": "font/ttf",
|
|
2583
|
+
".webp": "image/webp",
|
|
2584
|
+
".mp4": "video/mp4",
|
|
2585
|
+
".webm": "video/webm"
|
|
2586
|
+
};
|
|
2587
|
+
async function createServer(options = {}) {
|
|
2588
|
+
const serverStartTime = Date.now();
|
|
2589
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
2590
|
+
const isDev = options.mode === "development";
|
|
2591
|
+
logger.logo();
|
|
2592
|
+
const rawConfig = await loadConfig(projectRoot);
|
|
2593
|
+
const config = resolvePaths(rawConfig, projectRoot);
|
|
2594
|
+
await loadPlugins(projectRoot, config);
|
|
2595
|
+
await pluginManager.runHook(PluginHooks.CONFIG, config);
|
|
2596
|
+
const middleware = await loadMiddleware(projectRoot);
|
|
2597
|
+
let routes = buildRouteTree(config.pagesDir, config.layoutsDir);
|
|
2598
|
+
await pluginManager.runHook(PluginHooks.ROUTES_LOADED, routes);
|
|
2599
|
+
const loadModule = createModuleLoader(isDev);
|
|
2600
|
+
const server = http.createServer(async (req, res) => {
|
|
2601
|
+
const startTime = Date.now();
|
|
2602
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
2603
|
+
const pathname = url.pathname;
|
|
2604
|
+
try {
|
|
2605
|
+
await pluginManager.runHook(PluginHooks.REQUEST, req, res);
|
|
2606
|
+
const middlewareResult = await runMiddleware(req, res, middleware);
|
|
2607
|
+
if (!middlewareResult.continue) {
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const effectivePath = middlewareResult.rewritten ? new URL(req.url, `http://${req.headers.host}`).pathname : pathname;
|
|
2611
|
+
if (await serveStaticFile(res, config.publicDir, effectivePath)) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
if (!isDev && effectivePath.startsWith("/_flexi/")) {
|
|
2615
|
+
const assetPath = path7.join(config.outDir, "client", effectivePath.slice(8));
|
|
2616
|
+
if (await serveStaticFile(res, path7.dirname(assetPath), path7.basename(assetPath))) {
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (effectivePath.startsWith("/_flexi/component/")) {
|
|
2621
|
+
const componentName = effectivePath.slice(18).replace(".js", "");
|
|
2622
|
+
return await serveClientComponent(res, config.pagesDir, componentName);
|
|
2623
|
+
}
|
|
2624
|
+
if (effectivePath === "/_flexi/action" && req.method === "POST") {
|
|
2625
|
+
return await handleServerAction(req, res);
|
|
2626
|
+
}
|
|
2627
|
+
if (effectivePath.startsWith("/_flexi/image")) {
|
|
2628
|
+
return await handleImageOptimization(req, res, config.images || {});
|
|
2629
|
+
}
|
|
2630
|
+
if (effectivePath.startsWith("/_flexi/font")) {
|
|
2631
|
+
return await handleFontRequest(req, res);
|
|
2632
|
+
}
|
|
2633
|
+
if (isDev) {
|
|
2634
|
+
routes = buildRouteTree(config.pagesDir, config.layoutsDir);
|
|
2635
|
+
}
|
|
2636
|
+
const apiRoute = matchRoute(effectivePath, routes.api);
|
|
2637
|
+
if (apiRoute) {
|
|
2638
|
+
return await handleApiRoute(req, res, apiRoute, loadModule);
|
|
2639
|
+
}
|
|
2640
|
+
const flexiRoute = matchRoute(effectivePath, routes.flexiRoutes || []);
|
|
2641
|
+
if (flexiRoute) {
|
|
2642
|
+
return await handlePageRoute(req, res, flexiRoute, routes, config, loadModule, url);
|
|
2643
|
+
}
|
|
2644
|
+
const appRoute = matchRoute(effectivePath, routes.appRoutes || []);
|
|
2645
|
+
if (appRoute) {
|
|
2646
|
+
return await handlePageRoute(req, res, appRoute, routes, config, loadModule, url);
|
|
2647
|
+
}
|
|
2648
|
+
const pageRoute = matchRoute(effectivePath, routes.pages);
|
|
2649
|
+
if (pageRoute) {
|
|
2650
|
+
return await handlePageRoute(req, res, pageRoute, routes, config, loadModule, url);
|
|
2651
|
+
}
|
|
2652
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
2653
|
+
res.end(renderError(404, "Page not found"));
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
if (error instanceof RedirectError) {
|
|
2656
|
+
res.writeHead(error.statusCode, { "Location": error.url });
|
|
2657
|
+
res.end();
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (error instanceof NotFoundError) {
|
|
2661
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
2662
|
+
res.end(renderError(404, error.message));
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
console.error("Server Error:", error);
|
|
2666
|
+
if (!res.headersSent) {
|
|
2667
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2668
|
+
res.end(renderError(500, error.message, isDev ? error.stack : null));
|
|
2669
|
+
}
|
|
2670
|
+
} finally {
|
|
2671
|
+
const duration = Date.now() - startTime;
|
|
2672
|
+
if (isDev) {
|
|
2673
|
+
const routeType = pathname.startsWith("/api/") ? "api" : pathname.startsWith("/_flexi/") ? "asset" : pathname.match(/\.(js|css|png|jpg|svg|ico)$/) ? "asset" : "dynamic";
|
|
2674
|
+
logger.request(req.method, pathname, res.statusCode, duration, { type: routeType });
|
|
2675
|
+
}
|
|
2676
|
+
await pluginManager.runHook(PluginHooks.RESPONSE, req, res, duration);
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
const port = process.env.PORT || options.port || config.server.port;
|
|
2680
|
+
const host = options.host || config.server.host;
|
|
2681
|
+
return new Promise((resolve, reject) => {
|
|
2682
|
+
server.on("error", (err) => {
|
|
2683
|
+
if (err.code === "EADDRINUSE") {
|
|
2684
|
+
logger.portInUse(port);
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
} else {
|
|
2687
|
+
logger.error("Server error", err);
|
|
2688
|
+
reject(err);
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
server.listen(port, host, async () => {
|
|
2692
|
+
logger.serverStart({
|
|
2693
|
+
port,
|
|
2694
|
+
host,
|
|
2695
|
+
mode: isDev ? "development" : "production",
|
|
2696
|
+
pagesDir: config.pagesDir,
|
|
2697
|
+
islands: config.islands?.enabled,
|
|
2698
|
+
rsc: config.rsc?.enabled
|
|
2699
|
+
}, serverStartTime);
|
|
2700
|
+
await pluginManager.runHook(PluginHooks.SERVER_START, server);
|
|
2701
|
+
resolve(server);
|
|
2702
|
+
});
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
function createModuleLoader(isDev) {
|
|
2706
|
+
return async (filePath) => {
|
|
2707
|
+
const url = pathToFileURL4(filePath).href;
|
|
2708
|
+
const cacheBuster = isDev ? `?t=${Date.now()}` : "";
|
|
2709
|
+
return import(`${url}${cacheBuster}`);
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
async function serveStaticFile(res, baseDir, pathname) {
|
|
2713
|
+
const safePath = path7.normalize(pathname).replace(/^(\.\.[\/\\])+/, "");
|
|
2714
|
+
const filePath = path7.join(baseDir, safePath);
|
|
2715
|
+
if (!filePath.startsWith(baseDir) || !fs8.existsSync(filePath)) {
|
|
2716
|
+
return false;
|
|
2717
|
+
}
|
|
2718
|
+
const stat = fs8.statSync(filePath);
|
|
2719
|
+
if (!stat.isFile()) {
|
|
2720
|
+
return false;
|
|
2721
|
+
}
|
|
2722
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
2723
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2724
|
+
res.writeHead(200, {
|
|
2725
|
+
"Content-Type": contentType,
|
|
2726
|
+
"Content-Length": stat.size,
|
|
2727
|
+
"Cache-Control": "public, max-age=31536000"
|
|
2728
|
+
});
|
|
2729
|
+
fs8.createReadStream(filePath).pipe(res);
|
|
2730
|
+
return true;
|
|
2731
|
+
}
|
|
2732
|
+
async function handleApiRoute(req, res, route, loadModule) {
|
|
2733
|
+
try {
|
|
2734
|
+
const module = await loadModule(route.filePath);
|
|
2735
|
+
const method = req.method.toLowerCase();
|
|
2736
|
+
const body = await parseBody(req);
|
|
2737
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2738
|
+
const query = Object.fromEntries(url.searchParams);
|
|
2739
|
+
const enhancedReq = {
|
|
2740
|
+
...req,
|
|
2741
|
+
body,
|
|
2742
|
+
query,
|
|
2743
|
+
params: route.params,
|
|
2744
|
+
method: req.method
|
|
2745
|
+
};
|
|
2746
|
+
const enhancedRes = createApiResponse(res);
|
|
2747
|
+
const handler = module[method] || module[method.toUpperCase()] || module.default;
|
|
2748
|
+
if (!handler) {
|
|
2749
|
+
enhancedRes.status(405).json({ error: "Method not allowed" });
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
await handler(enhancedReq, enhancedRes);
|
|
2753
|
+
} catch (error) {
|
|
2754
|
+
console.error("API Error:", error);
|
|
2755
|
+
if (!res.headersSent) {
|
|
2756
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2757
|
+
res.end(JSON.stringify({ error: "Internal Server Error" }));
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async function handleServerAction(req, res) {
|
|
2762
|
+
try {
|
|
2763
|
+
const body = await parseBody(req);
|
|
2764
|
+
const { actionId, args } = body;
|
|
2765
|
+
if (!actionId) {
|
|
2766
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2767
|
+
res.end(JSON.stringify({ success: false, error: "Missing actionId" }));
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const deserializedArgs = deserializeArgs(args || []);
|
|
2771
|
+
const result = await executeAction(actionId, deserializedArgs, {
|
|
2772
|
+
request: new Request(`http://${req.headers.host}${req.url}`, {
|
|
2773
|
+
method: req.method,
|
|
2774
|
+
headers: req.headers
|
|
2775
|
+
})
|
|
2776
|
+
});
|
|
2777
|
+
res.writeHead(200, {
|
|
2778
|
+
"Content-Type": "application/json",
|
|
2779
|
+
"X-Flexi-Action": actionId
|
|
2780
|
+
});
|
|
2781
|
+
res.end(JSON.stringify(result));
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
console.error("Server Action Error:", error);
|
|
2784
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2785
|
+
res.end(JSON.stringify({
|
|
2786
|
+
success: false,
|
|
2787
|
+
error: error.message || "Action execution failed"
|
|
2788
|
+
}));
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
function createApiResponse(res) {
|
|
2792
|
+
return {
|
|
2793
|
+
_res: res,
|
|
2794
|
+
_status: 200,
|
|
2795
|
+
_headers: {},
|
|
2796
|
+
status(code) {
|
|
2797
|
+
this._status = code;
|
|
2798
|
+
return this;
|
|
2799
|
+
},
|
|
2800
|
+
setHeader(name, value) {
|
|
2801
|
+
this._headers[name] = value;
|
|
2802
|
+
return this;
|
|
2803
|
+
},
|
|
2804
|
+
json(data) {
|
|
2805
|
+
this._headers["Content-Type"] = "application/json";
|
|
2806
|
+
this._send(JSON.stringify(data));
|
|
2807
|
+
},
|
|
2808
|
+
send(data) {
|
|
2809
|
+
if (typeof data === "object") {
|
|
2810
|
+
this.json(data);
|
|
2811
|
+
} else {
|
|
2812
|
+
this._headers["Content-Type"] = this._headers["Content-Type"] || "text/plain";
|
|
2813
|
+
this._send(String(data));
|
|
2814
|
+
}
|
|
2815
|
+
},
|
|
2816
|
+
html(data) {
|
|
2817
|
+
this._headers["Content-Type"] = "text/html";
|
|
2818
|
+
this._send(data);
|
|
2819
|
+
},
|
|
2820
|
+
redirect(url, status = 302) {
|
|
2821
|
+
this._status = status;
|
|
2822
|
+
this._headers["Location"] = url;
|
|
2823
|
+
this._send("");
|
|
2824
|
+
},
|
|
2825
|
+
_send(body) {
|
|
2826
|
+
if (!this._res.headersSent) {
|
|
2827
|
+
this._res.writeHead(this._status, this._headers);
|
|
2828
|
+
this._res.end(body);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
async function handlePageRoute(req, res, route, routes, config, loadModule, url) {
|
|
2834
|
+
try {
|
|
2835
|
+
if (route.middleware) {
|
|
2836
|
+
try {
|
|
2837
|
+
const middlewareModule = await loadModule(route.middleware);
|
|
2838
|
+
const middlewareFn = middlewareModule.default || middlewareModule.middleware;
|
|
2839
|
+
if (typeof middlewareFn === "function") {
|
|
2840
|
+
const result = await middlewareFn(req, res, { route, params: route.params });
|
|
2841
|
+
if (result?.redirect) {
|
|
2842
|
+
res.writeHead(result.statusCode || 307, { "Location": result.redirect });
|
|
2843
|
+
res.end();
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
if (result?.rewrite) {
|
|
2847
|
+
req.url = result.rewrite;
|
|
2848
|
+
}
|
|
2849
|
+
if (result === false || result?.stop) {
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
} catch (middlewareError) {
|
|
2854
|
+
console.error("Route middleware error:", middlewareError.message);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
const pageModule = await loadModule(route.filePath);
|
|
2858
|
+
const Component = pageModule.default;
|
|
2859
|
+
if (!Component) {
|
|
2860
|
+
throw new Error(`No default export in ${route.filePath}`);
|
|
2861
|
+
}
|
|
2862
|
+
const query = Object.fromEntries(url.searchParams);
|
|
2863
|
+
const context = createRequestContext(req, res, route.params, query);
|
|
2864
|
+
let props = { params: route.params, query };
|
|
2865
|
+
if (pageModule.getServerSideProps) {
|
|
2866
|
+
const result = await pageModule.getServerSideProps({
|
|
2867
|
+
params: route.params,
|
|
2868
|
+
query,
|
|
2869
|
+
req,
|
|
2870
|
+
res
|
|
2871
|
+
});
|
|
2872
|
+
if (result.redirect) {
|
|
2873
|
+
res.writeHead(result.redirect.statusCode || 302, {
|
|
2874
|
+
Location: result.redirect.destination
|
|
2875
|
+
});
|
|
2876
|
+
res.end();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (result.notFound) {
|
|
2880
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
2881
|
+
res.end(renderError(404, "Page not found"));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
props = { ...props, ...result.props };
|
|
2885
|
+
}
|
|
2886
|
+
if (pageModule.getStaticProps) {
|
|
2887
|
+
const result = await pageModule.getStaticProps({ params: route.params });
|
|
2888
|
+
if (result.notFound) {
|
|
2889
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
2890
|
+
res.end(renderError(404, "Page not found"));
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
props = { ...props, ...result.props };
|
|
2894
|
+
}
|
|
2895
|
+
const layouts = [];
|
|
2896
|
+
try {
|
|
2897
|
+
const layoutConfigs = findRouteLayouts(route, routes.layouts);
|
|
2898
|
+
for (const layoutConfig of layoutConfigs) {
|
|
2899
|
+
if (layoutConfig.filePath) {
|
|
2900
|
+
const layoutModule = await loadModule(layoutConfig.filePath);
|
|
2901
|
+
if (layoutModule.default) {
|
|
2902
|
+
layouts.push({
|
|
2903
|
+
Component: layoutModule.default,
|
|
2904
|
+
props: {}
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
} catch (layoutError) {
|
|
2910
|
+
console.warn("Layout loading skipped:", layoutError.message);
|
|
2911
|
+
}
|
|
2912
|
+
let LoadingComponent = null;
|
|
2913
|
+
if (route.loading) {
|
|
2914
|
+
const loadingModule = await loadModule(route.loading);
|
|
2915
|
+
LoadingComponent = loadingModule.default;
|
|
2916
|
+
}
|
|
2917
|
+
let ErrorComponent = null;
|
|
2918
|
+
if (route.error) {
|
|
2919
|
+
const errorModule = await loadModule(route.error);
|
|
2920
|
+
ErrorComponent = errorModule.default;
|
|
2921
|
+
}
|
|
2922
|
+
props = await pluginManager.runWaterfallHook(
|
|
2923
|
+
PluginHooks.BEFORE_RENDER,
|
|
2924
|
+
props,
|
|
2925
|
+
{ route, Component }
|
|
2926
|
+
);
|
|
2927
|
+
const isClientComponent2 = route.isClientComponent || pageModule.__isClient || typeof pageModule.default === "function" && pageModule.default.toString().includes("useState");
|
|
2928
|
+
let html = await renderPage({
|
|
2929
|
+
Component,
|
|
2930
|
+
props,
|
|
2931
|
+
layouts,
|
|
2932
|
+
loading: LoadingComponent,
|
|
2933
|
+
error: ErrorComponent,
|
|
2934
|
+
islands: getRegisteredIslands(),
|
|
2935
|
+
title: pageModule.title || pageModule.metadata?.title || "FlexiReact App",
|
|
2936
|
+
meta: pageModule.metadata || {},
|
|
2937
|
+
styles: config.styles || [],
|
|
2938
|
+
scripts: config.scripts || [],
|
|
2939
|
+
favicon: config.favicon || null,
|
|
2940
|
+
needsHydration: isClientComponent2,
|
|
2941
|
+
componentPath: route.filePath,
|
|
2942
|
+
route: route.path || url.pathname,
|
|
2943
|
+
isSSG: !!pageModule.getStaticProps
|
|
2944
|
+
});
|
|
2945
|
+
const islands = getRegisteredIslands();
|
|
2946
|
+
if (islands.length > 0 && config.islands.enabled) {
|
|
2947
|
+
const hydrationScript = generateAdvancedHydrationScript(islands);
|
|
2948
|
+
html = html.replace("</body>", `${hydrationScript}</body>`);
|
|
2949
|
+
}
|
|
2950
|
+
if (isClientComponent2) {
|
|
2951
|
+
const hydrationScript = generateClientHydrationScript(route.filePath, props);
|
|
2952
|
+
html = html.replace("</body>", `${hydrationScript}</body>`);
|
|
2953
|
+
}
|
|
2954
|
+
html = await pluginManager.runWaterfallHook(
|
|
2955
|
+
PluginHooks.AFTER_RENDER,
|
|
2956
|
+
html,
|
|
2957
|
+
{ route, Component, props }
|
|
2958
|
+
);
|
|
2959
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2960
|
+
res.end(html);
|
|
2961
|
+
} catch (error) {
|
|
2962
|
+
console.error("Page Render Error:", error);
|
|
2963
|
+
throw error;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
async function serveClientComponent(res, pagesDir, componentName) {
|
|
2967
|
+
const { transformSync } = await import("esbuild");
|
|
2968
|
+
const cleanName = componentName.replace(/\.(tsx|jsx|ts|js)\.js$/, "").replace(/\.js$/, "");
|
|
2969
|
+
const possiblePaths = [
|
|
2970
|
+
path7.join(pagesDir, `${cleanName}.tsx`),
|
|
2971
|
+
path7.join(pagesDir, `${cleanName}.ts`),
|
|
2972
|
+
path7.join(pagesDir, `${cleanName}.jsx`),
|
|
2973
|
+
path7.join(pagesDir, `${cleanName}.js`)
|
|
2974
|
+
];
|
|
2975
|
+
let componentPath = null;
|
|
2976
|
+
for (const p of possiblePaths) {
|
|
2977
|
+
if (fs8.existsSync(p)) {
|
|
2978
|
+
componentPath = p;
|
|
2979
|
+
break;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
if (!componentPath) {
|
|
2983
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2984
|
+
res.end(`Component not found: ${cleanName}`);
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
const ext = path7.extname(componentPath);
|
|
2988
|
+
const loader = ext === ".tsx" ? "tsx" : ext === ".ts" ? "ts" : "jsx";
|
|
2989
|
+
try {
|
|
2990
|
+
let source = fs8.readFileSync(componentPath, "utf-8");
|
|
2991
|
+
source = source.replace(/^['"]use (client|server|island)['"];?\s*/m, "");
|
|
2992
|
+
const result = transformSync(source, {
|
|
2993
|
+
loader,
|
|
2994
|
+
format: "esm",
|
|
2995
|
+
jsx: "transform",
|
|
2996
|
+
jsxFactory: "React.createElement",
|
|
2997
|
+
jsxFragment: "React.Fragment",
|
|
2998
|
+
target: "es2020",
|
|
2999
|
+
// Replace React imports with global
|
|
3000
|
+
banner: `
|
|
3001
|
+
const React = window.React;
|
|
3002
|
+
const useState = window.useState;
|
|
3003
|
+
const useEffect = window.useEffect;
|
|
3004
|
+
const useCallback = window.useCallback;
|
|
3005
|
+
const useMemo = window.useMemo;
|
|
3006
|
+
const useRef = window.useRef;
|
|
3007
|
+
`
|
|
3008
|
+
});
|
|
3009
|
+
let code = result.code;
|
|
3010
|
+
code = code.replace(/import\s+React\s+from\s+['"]react['"];?\s*/g, "");
|
|
3011
|
+
code = code.replace(/import\s+\{[^}]+\}\s+from\s+['"]react['"];?\s*/g, "");
|
|
3012
|
+
code = code.replace(/import\s+React\s*,\s*\{[^}]+\}\s+from\s+['"]react['"];?\s*/g, "");
|
|
3013
|
+
res.writeHead(200, {
|
|
3014
|
+
"Content-Type": "application/javascript",
|
|
3015
|
+
"Cache-Control": "no-cache"
|
|
3016
|
+
});
|
|
3017
|
+
res.end(code);
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
console.error("Error serving client component:", error);
|
|
3020
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
3021
|
+
res.end("Error compiling component");
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
function generateClientHydrationScript(componentPath, props) {
|
|
3025
|
+
const ext = path7.extname(componentPath);
|
|
3026
|
+
const componentName = path7.basename(componentPath, ext);
|
|
3027
|
+
return `
|
|
3028
|
+
<script type="module">
|
|
3029
|
+
// FlexiReact Client Hydration
|
|
3030
|
+
(async function() {
|
|
3031
|
+
try {
|
|
3032
|
+
const React = await import('https://esm.sh/react@18.3.1');
|
|
3033
|
+
const ReactDOM = await import('https://esm.sh/react-dom@18.3.1/client');
|
|
3034
|
+
|
|
3035
|
+
// Make React available globally for the component
|
|
3036
|
+
window.React = React.default || React;
|
|
3037
|
+
window.useState = React.useState;
|
|
3038
|
+
window.useEffect = React.useEffect;
|
|
3039
|
+
window.useCallback = React.useCallback;
|
|
3040
|
+
window.useMemo = React.useMemo;
|
|
3041
|
+
window.useRef = React.useRef;
|
|
3042
|
+
|
|
3043
|
+
// Fetch the component code
|
|
3044
|
+
const response = await fetch('/_flexi/component/${componentName}.js');
|
|
3045
|
+
const code = await response.text();
|
|
3046
|
+
|
|
3047
|
+
// Create and import the module
|
|
3048
|
+
const blob = new Blob([code], { type: 'application/javascript' });
|
|
3049
|
+
const moduleUrl = URL.createObjectURL(blob);
|
|
3050
|
+
const module = await import(moduleUrl);
|
|
3051
|
+
|
|
3052
|
+
const Component = module.default;
|
|
3053
|
+
const props = ${JSON.stringify(props)};
|
|
3054
|
+
|
|
3055
|
+
// Hydrate the root
|
|
3056
|
+
const root = document.getElementById('root');
|
|
3057
|
+
ReactDOM.hydrateRoot(root, window.React.createElement(Component, props));
|
|
3058
|
+
|
|
3059
|
+
console.log('\u26A1 FlexiReact: Component hydrated successfully');
|
|
3060
|
+
} catch (error) {
|
|
3061
|
+
console.error('\u26A1 FlexiReact: Hydration failed', error);
|
|
3062
|
+
}
|
|
3063
|
+
})();
|
|
3064
|
+
</script>`;
|
|
3065
|
+
}
|
|
3066
|
+
async function parseBody(req) {
|
|
3067
|
+
return new Promise((resolve) => {
|
|
3068
|
+
const contentType = req.headers["content-type"] || "";
|
|
3069
|
+
let body = "";
|
|
3070
|
+
req.on("data", (chunk) => {
|
|
3071
|
+
body += chunk.toString();
|
|
3072
|
+
});
|
|
3073
|
+
req.on("end", () => {
|
|
3074
|
+
try {
|
|
3075
|
+
if (contentType.includes("application/json") && body) {
|
|
3076
|
+
resolve(JSON.parse(body));
|
|
3077
|
+
} else if (contentType.includes("application/x-www-form-urlencoded") && body) {
|
|
3078
|
+
resolve(Object.fromEntries(new URLSearchParams(body)));
|
|
3079
|
+
} else {
|
|
3080
|
+
resolve(body || null);
|
|
3081
|
+
}
|
|
3082
|
+
} catch {
|
|
3083
|
+
resolve(body);
|
|
3084
|
+
}
|
|
3085
|
+
});
|
|
3086
|
+
req.on("error", () => resolve(null));
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// core/start-prod.ts
|
|
3091
|
+
createServer({ mode: "production" });
|
|
3092
|
+
//# sourceMappingURL=start-prod.js.map
|