@flexireact/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +549 -0
- package/cli/index.js +992 -0
- package/cli/index.ts +1129 -0
- package/core/api.js +143 -0
- package/core/build/index.js +357 -0
- package/core/cli/logger.js +347 -0
- package/core/client/hydration.js +137 -0
- package/core/client/index.js +8 -0
- package/core/client/islands.js +138 -0
- package/core/client/navigation.js +204 -0
- package/core/client/runtime.js +36 -0
- package/core/config.js +113 -0
- package/core/context.js +83 -0
- package/core/dev.js +47 -0
- package/core/index.js +76 -0
- package/core/islands/index.js +281 -0
- package/core/loader.js +111 -0
- package/core/logger.js +242 -0
- package/core/middleware/index.js +393 -0
- package/core/plugins/index.js +370 -0
- package/core/render/index.js +765 -0
- package/core/render.js +134 -0
- package/core/router/index.js +296 -0
- package/core/router.js +141 -0
- package/core/rsc/index.js +198 -0
- package/core/server/index.js +653 -0
- package/core/server.js +197 -0
- package/core/ssg/index.js +321 -0
- package/core/utils.js +176 -0
- package/package.json +73 -0
package/core/render.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { renderToString } from 'react-dom/server';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders a React component to a full HTML page
|
|
6
|
+
* @param {React.Component} Component - The React component to render
|
|
7
|
+
* @param {Object} props - Props to pass to the component
|
|
8
|
+
* @param {Object} options - Rendering options
|
|
9
|
+
* @returns {string} Complete HTML string
|
|
10
|
+
*/
|
|
11
|
+
export function render(Component, props = {}, options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
title = 'FlexiReact App',
|
|
14
|
+
scripts = [],
|
|
15
|
+
styles = []
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
// Render the component to string
|
|
19
|
+
const content = renderToString(React.createElement(Component, props));
|
|
20
|
+
|
|
21
|
+
// Build the HTML document
|
|
22
|
+
const html = `<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8">
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
27
|
+
<title>${escapeHtml(title)}</title>
|
|
28
|
+
${styles.map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('\n ')}
|
|
29
|
+
<style>
|
|
30
|
+
* {
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 0;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
}
|
|
35
|
+
body {
|
|
36
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
37
|
+
line-height: 1.6;
|
|
38
|
+
color: #333;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="root">${content}</div>
|
|
44
|
+
<script>
|
|
45
|
+
// FlexiReact client-side runtime
|
|
46
|
+
window.__FLEXIREACT_PROPS__ = ${JSON.stringify(props)};
|
|
47
|
+
</script>
|
|
48
|
+
${scripts.map(src => `<script src="${escapeHtml(src)}"></script>`).join('\n ')}
|
|
49
|
+
</body>
|
|
50
|
+
</html>`;
|
|
51
|
+
|
|
52
|
+
return html;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Renders an error page
|
|
57
|
+
* @param {number} statusCode - HTTP status code
|
|
58
|
+
* @param {string} message - Error message
|
|
59
|
+
* @returns {string} HTML error page
|
|
60
|
+
*/
|
|
61
|
+
export function renderError(statusCode, message) {
|
|
62
|
+
return `<!DOCTYPE html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="UTF-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
67
|
+
<title>Error ${statusCode} - FlexiReact</title>
|
|
68
|
+
<style>
|
|
69
|
+
* {
|
|
70
|
+
margin: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
}
|
|
74
|
+
body {
|
|
75
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
min-height: 100vh;
|
|
80
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
81
|
+
color: white;
|
|
82
|
+
}
|
|
83
|
+
.error-container {
|
|
84
|
+
text-align: center;
|
|
85
|
+
padding: 2rem;
|
|
86
|
+
}
|
|
87
|
+
.error-code {
|
|
88
|
+
font-size: 6rem;
|
|
89
|
+
font-weight: bold;
|
|
90
|
+
opacity: 0.8;
|
|
91
|
+
}
|
|
92
|
+
.error-message {
|
|
93
|
+
font-size: 1.5rem;
|
|
94
|
+
margin-top: 1rem;
|
|
95
|
+
opacity: 0.9;
|
|
96
|
+
}
|
|
97
|
+
.back-link {
|
|
98
|
+
display: inline-block;
|
|
99
|
+
margin-top: 2rem;
|
|
100
|
+
padding: 0.75rem 1.5rem;
|
|
101
|
+
background: rgba(255,255,255,0.2);
|
|
102
|
+
color: white;
|
|
103
|
+
text-decoration: none;
|
|
104
|
+
border-radius: 5px;
|
|
105
|
+
transition: background 0.3s;
|
|
106
|
+
}
|
|
107
|
+
.back-link:hover {
|
|
108
|
+
background: rgba(255,255,255,0.3);
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body>
|
|
113
|
+
<div class="error-container">
|
|
114
|
+
<div class="error-code">${statusCode}</div>
|
|
115
|
+
<div class="error-message">${escapeHtml(message)}</div>
|
|
116
|
+
<a href="/" class="back-link">← Back to Home</a>
|
|
117
|
+
</div>
|
|
118
|
+
</body>
|
|
119
|
+
</html>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Escapes HTML special characters
|
|
124
|
+
*/
|
|
125
|
+
function escapeHtml(str) {
|
|
126
|
+
const htmlEntities = {
|
|
127
|
+
'&': '&',
|
|
128
|
+
'<': '<',
|
|
129
|
+
'>': '>',
|
|
130
|
+
'"': '"',
|
|
131
|
+
"'": '''
|
|
132
|
+
};
|
|
133
|
+
return String(str).replace(/[&<>"']/g, char => htmlEntities[char]);
|
|
134
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Router v2
|
|
3
|
+
* Advanced file-based routing with nested routes, loading, and error boundaries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { isServerComponent, isClientComponent, isIsland } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Route types
|
|
12
|
+
*/
|
|
13
|
+
export const RouteType = {
|
|
14
|
+
PAGE: 'page',
|
|
15
|
+
API: 'api',
|
|
16
|
+
LAYOUT: 'layout',
|
|
17
|
+
LOADING: 'loading',
|
|
18
|
+
ERROR: 'error',
|
|
19
|
+
NOT_FOUND: 'not-found'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the complete route tree from pages directory
|
|
24
|
+
*/
|
|
25
|
+
export function buildRouteTree(pagesDir, layoutsDir) {
|
|
26
|
+
const routes = {
|
|
27
|
+
pages: [],
|
|
28
|
+
api: [],
|
|
29
|
+
layouts: new Map(),
|
|
30
|
+
tree: {}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(pagesDir)) {
|
|
34
|
+
return routes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Scan pages directory
|
|
38
|
+
scanDirectory(pagesDir, pagesDir, routes);
|
|
39
|
+
|
|
40
|
+
// Scan layouts directory
|
|
41
|
+
if (fs.existsSync(layoutsDir)) {
|
|
42
|
+
scanLayouts(layoutsDir, routes.layouts);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build route tree for nested routes
|
|
46
|
+
routes.tree = buildTree(routes.pages);
|
|
47
|
+
|
|
48
|
+
return routes;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scans directory recursively for route files
|
|
53
|
+
*/
|
|
54
|
+
function scanDirectory(baseDir, currentDir, routes, parentSegments = []) {
|
|
55
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
56
|
+
|
|
57
|
+
// First, find special files in current directory
|
|
58
|
+
const specialFiles = {
|
|
59
|
+
layout: null,
|
|
60
|
+
loading: null,
|
|
61
|
+
error: null,
|
|
62
|
+
notFound: null
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.isFile()) {
|
|
67
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, '');
|
|
68
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
69
|
+
|
|
70
|
+
if (name === 'layout') specialFiles.layout = fullPath;
|
|
71
|
+
if (name === 'loading') specialFiles.loading = fullPath;
|
|
72
|
+
if (name === 'error') specialFiles.error = fullPath;
|
|
73
|
+
if (name === 'not-found' || name === '404') specialFiles.notFound = fullPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
79
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
80
|
+
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
// Handle route groups (parentheses)
|
|
83
|
+
const isGroup = entry.name.startsWith('(') && entry.name.endsWith(')');
|
|
84
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, entry.name];
|
|
85
|
+
|
|
86
|
+
scanDirectory(baseDir, fullPath, routes, newSegments);
|
|
87
|
+
} else if (entry.isFile()) {
|
|
88
|
+
const ext = path.extname(entry.name);
|
|
89
|
+
const baseName = path.basename(entry.name, ext);
|
|
90
|
+
|
|
91
|
+
// Skip special files (already processed)
|
|
92
|
+
if (['layout', 'loading', 'error', 'not-found', '404'].includes(baseName)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (['.jsx', '.js', '.tsx', '.ts'].includes(ext)) {
|
|
97
|
+
const isApi = relativePath.startsWith('api' + path.sep) || relativePath.startsWith('api/');
|
|
98
|
+
|
|
99
|
+
if (isApi && ['.js', '.ts'].includes(ext)) {
|
|
100
|
+
routes.api.push(createRoute(fullPath, baseDir, specialFiles, RouteType.API));
|
|
101
|
+
} else if (!isApi && ['.jsx', '.tsx'].includes(ext)) {
|
|
102
|
+
routes.pages.push(createRoute(fullPath, baseDir, specialFiles, RouteType.PAGE));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates a route object from file path
|
|
111
|
+
*/
|
|
112
|
+
function createRoute(filePath, baseDir, specialFiles, type) {
|
|
113
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
114
|
+
const routePath = filePathToRoute(relativePath);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
type,
|
|
118
|
+
path: routePath,
|
|
119
|
+
filePath,
|
|
120
|
+
pattern: createRoutePattern(routePath),
|
|
121
|
+
segments: routePath.split('/').filter(Boolean),
|
|
122
|
+
layout: specialFiles.layout,
|
|
123
|
+
loading: specialFiles.loading,
|
|
124
|
+
error: specialFiles.error,
|
|
125
|
+
notFound: specialFiles.notFound,
|
|
126
|
+
isServerComponent: isServerComponent(filePath),
|
|
127
|
+
isClientComponent: isClientComponent(filePath),
|
|
128
|
+
isIsland: isIsland(filePath)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scans layouts directory
|
|
134
|
+
*/
|
|
135
|
+
function scanLayouts(layoutsDir, layoutsMap) {
|
|
136
|
+
const entries = fs.readdirSync(layoutsDir, { withFileTypes: true });
|
|
137
|
+
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (entry.isFile() && /\.(jsx|tsx)$/.test(entry.name)) {
|
|
140
|
+
const name = entry.name.replace(/\.(jsx|tsx)$/, '');
|
|
141
|
+
layoutsMap.set(name, path.join(layoutsDir, entry.name));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Converts file path to route path
|
|
148
|
+
*/
|
|
149
|
+
function filePathToRoute(filePath) {
|
|
150
|
+
let route = filePath.replace(/\\/g, '/');
|
|
151
|
+
|
|
152
|
+
// Remove extension
|
|
153
|
+
route = route.replace(/\.(jsx|js|tsx|ts)$/, '');
|
|
154
|
+
|
|
155
|
+
// Convert [param] to :param
|
|
156
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, '*$1'); // Catch-all [...slug]
|
|
157
|
+
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
158
|
+
|
|
159
|
+
// Handle index files
|
|
160
|
+
if (route.endsWith('/index')) {
|
|
161
|
+
route = route.slice(0, -6) || '/';
|
|
162
|
+
} else if (route === 'index') {
|
|
163
|
+
route = '/';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle route groups - remove (groupName) from path
|
|
167
|
+
route = route.replace(/\/?\([^)]+\)\/?/g, '/');
|
|
168
|
+
|
|
169
|
+
// Ensure leading slash and clean up
|
|
170
|
+
if (!route.startsWith('/')) {
|
|
171
|
+
route = '/' + route;
|
|
172
|
+
}
|
|
173
|
+
route = route.replace(/\/+/g, '/');
|
|
174
|
+
|
|
175
|
+
return route;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Creates regex pattern for route matching
|
|
180
|
+
*/
|
|
181
|
+
function createRoutePattern(routePath) {
|
|
182
|
+
let pattern = routePath
|
|
183
|
+
.replace(/\*[^/]*/g, '(.*)') // Catch-all
|
|
184
|
+
.replace(/:[^/]+/g, '([^/]+)') // Dynamic segments
|
|
185
|
+
.replace(/\//g, '\\/');
|
|
186
|
+
|
|
187
|
+
return new RegExp(`^${pattern}$`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds a tree structure for nested routes
|
|
192
|
+
*/
|
|
193
|
+
function buildTree(routes) {
|
|
194
|
+
const tree = { children: {}, routes: [] };
|
|
195
|
+
|
|
196
|
+
for (const route of routes) {
|
|
197
|
+
let current = tree;
|
|
198
|
+
|
|
199
|
+
for (const segment of route.segments) {
|
|
200
|
+
if (!current.children[segment]) {
|
|
201
|
+
current.children[segment] = { children: {}, routes: [] };
|
|
202
|
+
}
|
|
203
|
+
current = current.children[segment];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
current.routes.push(route);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return tree;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Matches URL path against routes
|
|
214
|
+
*/
|
|
215
|
+
export function matchRoute(urlPath, routes) {
|
|
216
|
+
const normalizedPath = urlPath === '' ? '/' : urlPath.split('?')[0];
|
|
217
|
+
|
|
218
|
+
for (const route of routes) {
|
|
219
|
+
const match = normalizedPath.match(route.pattern);
|
|
220
|
+
|
|
221
|
+
if (match) {
|
|
222
|
+
const params = extractParams(route.path, match);
|
|
223
|
+
return { ...route, params };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Extracts parameters from route match
|
|
232
|
+
*/
|
|
233
|
+
function extractParams(routePath, match) {
|
|
234
|
+
const params = {};
|
|
235
|
+
const paramNames = [];
|
|
236
|
+
|
|
237
|
+
// Extract param names from route path
|
|
238
|
+
const paramRegex = /:([^/]+)|\*([^/]*)/g;
|
|
239
|
+
let paramMatch;
|
|
240
|
+
|
|
241
|
+
while ((paramMatch = paramRegex.exec(routePath)) !== null) {
|
|
242
|
+
paramNames.push(paramMatch[1] || paramMatch[2] || 'splat');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
paramNames.forEach((name, index) => {
|
|
246
|
+
params[name] = match[index + 1];
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return params;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Finds all layouts that apply to a route
|
|
254
|
+
*/
|
|
255
|
+
export function findRouteLayouts(route, layoutsMap) {
|
|
256
|
+
const layouts = [];
|
|
257
|
+
|
|
258
|
+
// Check for segment-based layouts
|
|
259
|
+
let currentPath = '';
|
|
260
|
+
for (const segment of route.segments) {
|
|
261
|
+
currentPath += '/' + segment;
|
|
262
|
+
const layoutName = segment;
|
|
263
|
+
|
|
264
|
+
if (layoutsMap.has(layoutName)) {
|
|
265
|
+
layouts.push({
|
|
266
|
+
name: layoutName,
|
|
267
|
+
filePath: layoutsMap.get(layoutName)
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check for route-specific layout
|
|
273
|
+
if (route.layout) {
|
|
274
|
+
layouts.push({
|
|
275
|
+
name: 'route',
|
|
276
|
+
filePath: route.layout
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for root layout
|
|
281
|
+
if (layoutsMap.has('root')) {
|
|
282
|
+
layouts.unshift({
|
|
283
|
+
name: 'root',
|
|
284
|
+
filePath: layoutsMap.get('root')
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return layouts;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default {
|
|
292
|
+
buildRouteTree,
|
|
293
|
+
matchRoute,
|
|
294
|
+
findRouteLayouts,
|
|
295
|
+
RouteType
|
|
296
|
+
};
|
package/core/router.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scans the pages directory and builds a route map
|
|
6
|
+
* @param {string} pagesDir - Path to the pages directory
|
|
7
|
+
* @returns {Object} Route map with paths and file locations
|
|
8
|
+
*/
|
|
9
|
+
export function buildRoutes(pagesDir) {
|
|
10
|
+
const routes = {
|
|
11
|
+
pages: [],
|
|
12
|
+
api: []
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(pagesDir)) {
|
|
16
|
+
return routes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
scanDirectory(pagesDir, pagesDir, routes);
|
|
20
|
+
return routes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively scans a directory for route files
|
|
25
|
+
*/
|
|
26
|
+
function scanDirectory(baseDir, currentDir, routes) {
|
|
27
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
31
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
32
|
+
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
scanDirectory(baseDir, fullPath, routes);
|
|
35
|
+
} else if (entry.isFile()) {
|
|
36
|
+
const ext = path.extname(entry.name);
|
|
37
|
+
|
|
38
|
+
if (ext === '.jsx' || ext === '.js') {
|
|
39
|
+
const routePath = filePathToRoute(relativePath);
|
|
40
|
+
const isApi = relativePath.startsWith('api' + path.sep) || relativePath.startsWith('api/');
|
|
41
|
+
|
|
42
|
+
if (isApi && ext === '.js') {
|
|
43
|
+
routes.api.push({
|
|
44
|
+
path: routePath,
|
|
45
|
+
filePath: fullPath,
|
|
46
|
+
pattern: createRoutePattern(routePath)
|
|
47
|
+
});
|
|
48
|
+
} else if (!isApi && ext === '.jsx') {
|
|
49
|
+
routes.pages.push({
|
|
50
|
+
path: routePath,
|
|
51
|
+
filePath: fullPath,
|
|
52
|
+
pattern: createRoutePattern(routePath)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts a file path to a route path
|
|
62
|
+
* pages/index.jsx -> /
|
|
63
|
+
* pages/about.jsx -> /about
|
|
64
|
+
* pages/blog/[id].jsx -> /blog/:id
|
|
65
|
+
* pages/api/hello.js -> /api/hello
|
|
66
|
+
*/
|
|
67
|
+
function filePathToRoute(filePath) {
|
|
68
|
+
// Normalize path separators
|
|
69
|
+
let route = filePath.replace(/\\/g, '/');
|
|
70
|
+
|
|
71
|
+
// Remove extension
|
|
72
|
+
route = route.replace(/\.(jsx|js)$/, '');
|
|
73
|
+
|
|
74
|
+
// Convert [param] to :param
|
|
75
|
+
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
76
|
+
|
|
77
|
+
// Handle index files
|
|
78
|
+
if (route.endsWith('/index')) {
|
|
79
|
+
route = route.slice(0, -6) || '/';
|
|
80
|
+
} else if (route === 'index') {
|
|
81
|
+
route = '/';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ensure leading slash
|
|
85
|
+
if (!route.startsWith('/')) {
|
|
86
|
+
route = '/' + route;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return route;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a regex pattern for route matching
|
|
94
|
+
*/
|
|
95
|
+
function createRoutePattern(routePath) {
|
|
96
|
+
const pattern = routePath
|
|
97
|
+
.replace(/:[^/]+/g, '([^/]+)')
|
|
98
|
+
.replace(/\//g, '\\/');
|
|
99
|
+
|
|
100
|
+
return new RegExp(`^${pattern}$`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Matches a URL path against the route map
|
|
105
|
+
* @param {string} urlPath - The URL path to match
|
|
106
|
+
* @param {Array} routes - Array of route objects
|
|
107
|
+
* @returns {Object|null} Matched route with params or null
|
|
108
|
+
*/
|
|
109
|
+
export function matchRoute(urlPath, routes) {
|
|
110
|
+
// Normalize the URL path
|
|
111
|
+
const normalizedPath = urlPath === '' ? '/' : urlPath;
|
|
112
|
+
|
|
113
|
+
for (const route of routes) {
|
|
114
|
+
const match = normalizedPath.match(route.pattern);
|
|
115
|
+
|
|
116
|
+
if (match) {
|
|
117
|
+
// Extract params from the match
|
|
118
|
+
const params = {};
|
|
119
|
+
const paramNames = (route.path.match(/:[^/]+/g) || []).map(p => p.slice(1));
|
|
120
|
+
|
|
121
|
+
paramNames.forEach((name, index) => {
|
|
122
|
+
params[name] = match[index + 1];
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
...route,
|
|
127
|
+
params
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Invalidates the module cache for hot reload
|
|
137
|
+
*/
|
|
138
|
+
export function invalidateCache(filePath) {
|
|
139
|
+
// For ESM, we use query string cache busting
|
|
140
|
+
return `${filePath}?t=${Date.now()}`;
|
|
141
|
+
}
|