@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/server.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { buildRoutes, matchRoute } from './router.js';
|
|
8
|
+
import { render, renderError } from './render.js';
|
|
9
|
+
import { handleApiRoute } from './api.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// Configuration
|
|
15
|
+
const PORT = process.env.PORT || 3000;
|
|
16
|
+
const HOST = process.env.HOST || 'localhost';
|
|
17
|
+
|
|
18
|
+
// Determine the project root (where the user's app is)
|
|
19
|
+
const PROJECT_ROOT = process.cwd();
|
|
20
|
+
const PAGES_DIR = path.join(PROJECT_ROOT, 'pages');
|
|
21
|
+
|
|
22
|
+
// MIME types for static files
|
|
23
|
+
const MIME_TYPES = {
|
|
24
|
+
'.html': 'text/html',
|
|
25
|
+
'.css': 'text/css',
|
|
26
|
+
'.js': 'application/javascript',
|
|
27
|
+
'.json': 'application/json',
|
|
28
|
+
'.png': 'image/png',
|
|
29
|
+
'.jpg': 'image/jpeg',
|
|
30
|
+
'.jpeg': 'image/jpeg',
|
|
31
|
+
'.gif': 'image/gif',
|
|
32
|
+
'.svg': 'image/svg+xml',
|
|
33
|
+
'.ico': 'image/x-icon',
|
|
34
|
+
'.woff': 'font/woff',
|
|
35
|
+
'.woff2': 'font/woff2',
|
|
36
|
+
'.ttf': 'font/ttf'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates and starts the FlexiReact dev server
|
|
41
|
+
*/
|
|
42
|
+
export function createServer(options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
port = PORT,
|
|
45
|
+
host = HOST,
|
|
46
|
+
pagesDir = PAGES_DIR
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const server = http.createServer(async (req, res) => {
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Parse URL
|
|
54
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
55
|
+
const pathname = url.pathname;
|
|
56
|
+
|
|
57
|
+
// Log request
|
|
58
|
+
console.log(`${req.method} ${pathname}`);
|
|
59
|
+
|
|
60
|
+
// Try to serve static files from public directory
|
|
61
|
+
const publicPath = path.join(PROJECT_ROOT, 'public', pathname);
|
|
62
|
+
if (fs.existsSync(publicPath) && fs.statSync(publicPath).isFile()) {
|
|
63
|
+
return serveStaticFile(res, publicPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build routes (rebuild on each request for hot reload)
|
|
67
|
+
const routes = buildRoutes(pagesDir);
|
|
68
|
+
|
|
69
|
+
// Check for API routes first
|
|
70
|
+
const apiRoute = matchRoute(pathname, routes.api);
|
|
71
|
+
if (apiRoute) {
|
|
72
|
+
return await handleApiRoute(req, res, apiRoute);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for page routes
|
|
76
|
+
const pageRoute = matchRoute(pathname, routes.pages);
|
|
77
|
+
if (pageRoute) {
|
|
78
|
+
return await handlePageRoute(req, res, pageRoute);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 404 Not Found
|
|
82
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
83
|
+
res.end(renderError(404, 'Page not found'));
|
|
84
|
+
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Server Error:', error);
|
|
87
|
+
|
|
88
|
+
if (!res.headersSent) {
|
|
89
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
90
|
+
res.end(renderError(500, process.env.NODE_ENV === 'development'
|
|
91
|
+
? error.message
|
|
92
|
+
: 'Internal Server Error'));
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
const duration = Date.now() - startTime;
|
|
96
|
+
console.log(` └─ ${res.statusCode} (${duration}ms)`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.listen(port, host, () => {
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(' ⚡ FlexiReact Dev Server');
|
|
103
|
+
console.log(' ─────────────────────────');
|
|
104
|
+
console.log(` → Local: http://${host}:${port}`);
|
|
105
|
+
console.log(` → Pages: ${pagesDir}`);
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(' Ready for requests...');
|
|
108
|
+
console.log('');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return server;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Handles page route requests with SSR
|
|
116
|
+
*/
|
|
117
|
+
async function handlePageRoute(req, res, route) {
|
|
118
|
+
try {
|
|
119
|
+
// Import the page component with cache busting for hot reload
|
|
120
|
+
const modulePath = `file://${route.filePath.replace(/\\/g, '/')}?t=${Date.now()}`;
|
|
121
|
+
const pageModule = await import(modulePath);
|
|
122
|
+
|
|
123
|
+
// Get the component (default export)
|
|
124
|
+
const Component = pageModule.default;
|
|
125
|
+
|
|
126
|
+
if (!Component) {
|
|
127
|
+
throw new Error(`No default export found in ${route.filePath}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get page props if getServerSideProps exists
|
|
131
|
+
let props = { params: route.params };
|
|
132
|
+
|
|
133
|
+
if (pageModule.getServerSideProps) {
|
|
134
|
+
const context = {
|
|
135
|
+
params: route.params,
|
|
136
|
+
req,
|
|
137
|
+
res,
|
|
138
|
+
query: Object.fromEntries(new URL(req.url, `http://${req.headers.host}`).searchParams)
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await pageModule.getServerSideProps(context);
|
|
142
|
+
|
|
143
|
+
if (result.redirect) {
|
|
144
|
+
res.writeHead(result.redirect.statusCode || 302, {
|
|
145
|
+
Location: result.redirect.destination
|
|
146
|
+
});
|
|
147
|
+
res.end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.notFound) {
|
|
152
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
153
|
+
res.end(renderError(404, 'Page not found'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
props = { ...props, ...result.props };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Get page title if defined
|
|
161
|
+
const title = pageModule.title || 'FlexiReact App';
|
|
162
|
+
|
|
163
|
+
// Render the page
|
|
164
|
+
const html = render(Component, props, { title });
|
|
165
|
+
|
|
166
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
167
|
+
res.end(html);
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Page Render Error:', error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Serves static files
|
|
177
|
+
*/
|
|
178
|
+
function serveStaticFile(res, filePath) {
|
|
179
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
180
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
181
|
+
|
|
182
|
+
const content = fs.readFileSync(filePath);
|
|
183
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
184
|
+
res.end(content);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Auto-start server when run directly
|
|
188
|
+
const scriptPath = process.argv[1];
|
|
189
|
+
|
|
190
|
+
// Check if running directly (handles symlinks on Windows)
|
|
191
|
+
const isDirectRun = scriptPath && scriptPath.endsWith('server.js');
|
|
192
|
+
|
|
193
|
+
if (isDirectRun) {
|
|
194
|
+
createServer();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default createServer;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Static Site Generation (SSG)
|
|
3
|
+
*
|
|
4
|
+
* SSG pre-renders pages at build time, generating static HTML files.
|
|
5
|
+
* This provides the fastest possible page loads and enables CDN caching.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* - Export getStaticProps() from a page to fetch data at build time
|
|
9
|
+
* - Export getStaticPaths() for dynamic routes to specify which paths to pre-render
|
|
10
|
+
* - Pages without these exports are rendered as static HTML
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { pathToFileURL } from 'url';
|
|
16
|
+
import { renderPage } from '../render/index.js';
|
|
17
|
+
import { ensureDir, cleanDir } from '../utils.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SSG Build Result
|
|
21
|
+
*/
|
|
22
|
+
export class SSGResult {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.pages = [];
|
|
25
|
+
this.errors = [];
|
|
26
|
+
this.duration = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
addPage(path, file, size) {
|
|
30
|
+
this.pages.push({ path, file, size });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addError(path, error) {
|
|
34
|
+
this.errors.push({ path, error: error.message });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get success() {
|
|
38
|
+
return this.errors.length === 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get totalSize() {
|
|
42
|
+
return this.pages.reduce((sum, p) => sum + p.size, 0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generates static pages for all routes
|
|
48
|
+
*/
|
|
49
|
+
export async function generateStaticSite(options) {
|
|
50
|
+
const {
|
|
51
|
+
routes,
|
|
52
|
+
outDir,
|
|
53
|
+
config,
|
|
54
|
+
loadModule
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
const result = new SSGResult();
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
|
|
60
|
+
// Clean and create output directory
|
|
61
|
+
const staticDir = path.join(outDir, 'static');
|
|
62
|
+
cleanDir(staticDir);
|
|
63
|
+
|
|
64
|
+
console.log('\n📦 Generating static pages...\n');
|
|
65
|
+
|
|
66
|
+
for (const route of routes) {
|
|
67
|
+
try {
|
|
68
|
+
await generateRoutePage(route, staticDir, loadModule, result, config);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(` ✗ ${route.path}: ${error.message}`);
|
|
71
|
+
result.addError(route.path, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
result.duration = Date.now() - startTime;
|
|
76
|
+
|
|
77
|
+
// Generate summary
|
|
78
|
+
console.log('\n' + '─'.repeat(50));
|
|
79
|
+
console.log(` Generated ${result.pages.length} pages in ${result.duration}ms`);
|
|
80
|
+
if (result.errors.length > 0) {
|
|
81
|
+
console.log(` ${result.errors.length} errors occurred`);
|
|
82
|
+
}
|
|
83
|
+
console.log('─'.repeat(50) + '\n');
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generates a single route's static page(s)
|
|
90
|
+
*/
|
|
91
|
+
async function generateRoutePage(route, outDir, loadModule, result, config) {
|
|
92
|
+
const module = await loadModule(route.filePath);
|
|
93
|
+
const Component = module.default;
|
|
94
|
+
|
|
95
|
+
if (!Component) {
|
|
96
|
+
throw new Error(`No default export in ${route.filePath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for getStaticPaths (dynamic routes)
|
|
100
|
+
let paths = [{ params: {} }];
|
|
101
|
+
|
|
102
|
+
if (route.path.includes(':') || route.path.includes('*')) {
|
|
103
|
+
if (!module.getStaticPaths) {
|
|
104
|
+
console.log(` ⊘ ${route.path} (dynamic, no getStaticPaths)`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const staticPaths = await module.getStaticPaths();
|
|
109
|
+
paths = staticPaths.paths || [];
|
|
110
|
+
|
|
111
|
+
if (staticPaths.fallback === false && paths.length === 0) {
|
|
112
|
+
console.log(` ⊘ ${route.path} (no paths to generate)`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Generate page for each path
|
|
118
|
+
for (const pathConfig of paths) {
|
|
119
|
+
const params = pathConfig.params || {};
|
|
120
|
+
const actualPath = substituteParams(route.path, params);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Get static props
|
|
124
|
+
let props = { params };
|
|
125
|
+
|
|
126
|
+
if (module.getStaticProps) {
|
|
127
|
+
const staticProps = await module.getStaticProps({ params });
|
|
128
|
+
|
|
129
|
+
if (staticProps.notFound) {
|
|
130
|
+
console.log(` ⊘ ${actualPath} (notFound)`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (staticProps.redirect) {
|
|
135
|
+
// Generate redirect HTML
|
|
136
|
+
await generateRedirectPage(actualPath, staticProps.redirect, outDir, result);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
props = { ...props, ...staticProps.props };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Render the page
|
|
144
|
+
const html = await renderPage({
|
|
145
|
+
Component,
|
|
146
|
+
props,
|
|
147
|
+
title: module.title || module.metadata?.title || 'FlexiReact App',
|
|
148
|
+
meta: module.metadata || {},
|
|
149
|
+
isSSG: true
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Write to file
|
|
153
|
+
const filePath = getOutputPath(actualPath, outDir);
|
|
154
|
+
ensureDir(path.dirname(filePath));
|
|
155
|
+
fs.writeFileSync(filePath, html);
|
|
156
|
+
|
|
157
|
+
const size = Buffer.byteLength(html, 'utf8');
|
|
158
|
+
result.addPage(actualPath, filePath, size);
|
|
159
|
+
|
|
160
|
+
console.log(` ✓ ${actualPath} (${formatSize(size)})`);
|
|
161
|
+
|
|
162
|
+
} catch (error) {
|
|
163
|
+
result.addError(actualPath, error);
|
|
164
|
+
console.error(` ✗ ${actualPath}: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generates a redirect page
|
|
171
|
+
*/
|
|
172
|
+
async function generateRedirectPage(fromPath, redirect, outDir, result) {
|
|
173
|
+
const { destination, permanent = false } = redirect;
|
|
174
|
+
const statusCode = permanent ? 301 : 302;
|
|
175
|
+
|
|
176
|
+
const html = `<!DOCTYPE html>
|
|
177
|
+
<html>
|
|
178
|
+
<head>
|
|
179
|
+
<meta charset="utf-8">
|
|
180
|
+
<meta http-equiv="refresh" content="0;url=${destination}">
|
|
181
|
+
<link rel="canonical" href="${destination}">
|
|
182
|
+
<title>Redirecting...</title>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<p>Redirecting to <a href="${destination}">${destination}</a></p>
|
|
186
|
+
<script>window.location.href = "${destination}";</script>
|
|
187
|
+
</body>
|
|
188
|
+
</html>`;
|
|
189
|
+
|
|
190
|
+
const filePath = getOutputPath(fromPath, outDir);
|
|
191
|
+
ensureDir(path.dirname(filePath));
|
|
192
|
+
fs.writeFileSync(filePath, html);
|
|
193
|
+
|
|
194
|
+
const size = Buffer.byteLength(html, 'utf8');
|
|
195
|
+
result.addPage(fromPath, filePath, size);
|
|
196
|
+
|
|
197
|
+
console.log(` ↪ ${fromPath} → ${destination}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Substitutes route params into path
|
|
202
|
+
*/
|
|
203
|
+
function substituteParams(routePath, params) {
|
|
204
|
+
let result = routePath;
|
|
205
|
+
|
|
206
|
+
for (const [key, value] of Object.entries(params)) {
|
|
207
|
+
result = result.replace(`:${key}`, value);
|
|
208
|
+
result = result.replace(`*${key}`, value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Gets the output file path for a route
|
|
216
|
+
*/
|
|
217
|
+
function getOutputPath(routePath, outDir) {
|
|
218
|
+
if (routePath === '/') {
|
|
219
|
+
return path.join(outDir, 'index.html');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Remove leading slash and add index.html
|
|
223
|
+
const cleanPath = routePath.replace(/^\//, '');
|
|
224
|
+
return path.join(outDir, cleanPath, 'index.html');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Formats file size
|
|
229
|
+
*/
|
|
230
|
+
function formatSize(bytes) {
|
|
231
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
232
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
233
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Incremental Static Regeneration (ISR) support
|
|
238
|
+
* Allows pages to be regenerated after a specified interval
|
|
239
|
+
*/
|
|
240
|
+
export class ISRManager {
|
|
241
|
+
constructor(options = {}) {
|
|
242
|
+
this.cache = new Map();
|
|
243
|
+
this.revalidating = new Set();
|
|
244
|
+
this.defaultRevalidate = options.defaultRevalidate || 60; // seconds
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Gets a cached page or regenerates it
|
|
249
|
+
*/
|
|
250
|
+
async getPage(routePath, generator) {
|
|
251
|
+
const cached = this.cache.get(routePath);
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
|
|
254
|
+
if (cached) {
|
|
255
|
+
// Check if revalidation is needed
|
|
256
|
+
if (cached.revalidateAfter && now > cached.revalidateAfter) {
|
|
257
|
+
// Trigger background revalidation
|
|
258
|
+
this.revalidateInBackground(routePath, generator);
|
|
259
|
+
}
|
|
260
|
+
return cached.html;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Generate fresh page
|
|
264
|
+
const result = await generator();
|
|
265
|
+
this.cache.set(routePath, {
|
|
266
|
+
html: result.html,
|
|
267
|
+
generatedAt: now,
|
|
268
|
+
revalidateAfter: result.revalidate
|
|
269
|
+
? now + (result.revalidate * 1000)
|
|
270
|
+
: null
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return result.html;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Revalidates a page in the background
|
|
278
|
+
*/
|
|
279
|
+
async revalidateInBackground(routePath, generator) {
|
|
280
|
+
if (this.revalidating.has(routePath)) return;
|
|
281
|
+
|
|
282
|
+
this.revalidating.add(routePath);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await generator();
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
|
|
288
|
+
this.cache.set(routePath, {
|
|
289
|
+
html: result.html,
|
|
290
|
+
generatedAt: now,
|
|
291
|
+
revalidateAfter: result.revalidate
|
|
292
|
+
? now + (result.revalidate * 1000)
|
|
293
|
+
: null
|
|
294
|
+
});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error(`ISR revalidation failed for ${routePath}:`, error);
|
|
297
|
+
} finally {
|
|
298
|
+
this.revalidating.delete(routePath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Invalidates a cached page
|
|
304
|
+
*/
|
|
305
|
+
invalidate(routePath) {
|
|
306
|
+
this.cache.delete(routePath);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Clears all cached pages
|
|
311
|
+
*/
|
|
312
|
+
clear() {
|
|
313
|
+
this.cache.clear();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export default {
|
|
318
|
+
generateStaticSite,
|
|
319
|
+
SSGResult,
|
|
320
|
+
ISRManager
|
|
321
|
+
};
|
package/core/utils.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Utility Functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a unique hash for cache busting
|
|
11
|
+
*/
|
|
12
|
+
export function generateHash(content) {
|
|
13
|
+
return crypto.createHash('md5').update(content).digest('hex').slice(0, 8);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Escapes HTML special characters
|
|
18
|
+
*/
|
|
19
|
+
export function escapeHtml(str) {
|
|
20
|
+
const htmlEntities = {
|
|
21
|
+
'&': '&',
|
|
22
|
+
'<': '<',
|
|
23
|
+
'>': '>',
|
|
24
|
+
'"': '"',
|
|
25
|
+
"'": '''
|
|
26
|
+
};
|
|
27
|
+
return String(str).replace(/[&<>"']/g, char => htmlEntities[char]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Recursively finds all files matching a pattern
|
|
32
|
+
*/
|
|
33
|
+
export function findFiles(dir, pattern, files = []) {
|
|
34
|
+
if (!fs.existsSync(dir)) return files;
|
|
35
|
+
|
|
36
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const fullPath = path.join(dir, entry.name);
|
|
40
|
+
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
findFiles(fullPath, pattern, files);
|
|
43
|
+
} else if (pattern.test(entry.name)) {
|
|
44
|
+
files.push(fullPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ensures a directory exists
|
|
53
|
+
*/
|
|
54
|
+
export function ensureDir(dir) {
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cleans a directory
|
|
62
|
+
*/
|
|
63
|
+
export function cleanDir(dir) {
|
|
64
|
+
if (fs.existsSync(dir)) {
|
|
65
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Copies a directory recursively
|
|
72
|
+
*/
|
|
73
|
+
export function copyDir(src, dest) {
|
|
74
|
+
ensureDir(dest);
|
|
75
|
+
|
|
76
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const srcPath = path.join(src, entry.name);
|
|
80
|
+
const destPath = path.join(dest, entry.name);
|
|
81
|
+
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
copyDir(srcPath, destPath);
|
|
84
|
+
} else {
|
|
85
|
+
fs.copyFileSync(srcPath, destPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Debounce function for file watching
|
|
92
|
+
*/
|
|
93
|
+
export function debounce(fn, delay) {
|
|
94
|
+
let timeout;
|
|
95
|
+
return (...args) => {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
timeout = setTimeout(() => fn(...args), delay);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Formats bytes to human readable string
|
|
103
|
+
*/
|
|
104
|
+
export function formatBytes(bytes) {
|
|
105
|
+
if (bytes === 0) return '0 B';
|
|
106
|
+
const k = 1024;
|
|
107
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
108
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
109
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Formats milliseconds to human readable string
|
|
114
|
+
*/
|
|
115
|
+
export function formatTime(ms) {
|
|
116
|
+
if (ms < 1000) return `${ms}ms`;
|
|
117
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Creates a deferred promise
|
|
122
|
+
*/
|
|
123
|
+
export function createDeferred() {
|
|
124
|
+
let resolve, reject;
|
|
125
|
+
const promise = new Promise((res, rej) => {
|
|
126
|
+
resolve = res;
|
|
127
|
+
reject = rej;
|
|
128
|
+
});
|
|
129
|
+
return { promise, resolve, reject };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Sleep utility
|
|
134
|
+
*/
|
|
135
|
+
export function sleep(ms) {
|
|
136
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a file is a server component (has 'use server' directive)
|
|
141
|
+
*/
|
|
142
|
+
export function isServerComponent(filePath) {
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
145
|
+
const firstLine = content.split('\n')[0].trim();
|
|
146
|
+
return firstLine === "'use server'" || firstLine === '"use server"';
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if a file is a client component (has 'use client' directive)
|
|
154
|
+
*/
|
|
155
|
+
export function isClientComponent(filePath) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
158
|
+
const firstLine = content.split('\n')[0].trim();
|
|
159
|
+
return firstLine === "'use client'" || firstLine === '"use client"';
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a component is an island (has 'use island' directive)
|
|
167
|
+
*/
|
|
168
|
+
export function isIsland(filePath) {
|
|
169
|
+
try {
|
|
170
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
171
|
+
const firstLine = content.split('\n')[0].trim();
|
|
172
|
+
return firstLine === "'use island'" || firstLine === '"use island"';
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|