@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/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
+ '&': '&amp;',
22
+ '<': '&lt;',
23
+ '>': '&gt;',
24
+ '"': '&quot;',
25
+ "'": '&#39;'
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
+ }