@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/api.js ADDED
@@ -0,0 +1,143 @@
1
+ import { URL } from 'url';
2
+
3
+ /**
4
+ * Handles API route requests
5
+ * @param {Object} req - HTTP request object
6
+ * @param {Object} res - HTTP response object
7
+ * @param {Object} route - Matched route object
8
+ */
9
+ export async function handleApiRoute(req, res, route) {
10
+ try {
11
+ // Import the API handler with cache busting for hot reload
12
+ const modulePath = `file://${route.filePath.replace(/\\/g, '/')}?t=${Date.now()}`;
13
+ const handler = await import(modulePath);
14
+
15
+ // Parse request body for POST/PUT/PATCH
16
+ const body = await parseBody(req);
17
+
18
+ // Parse query parameters
19
+ const url = new URL(req.url, `http://${req.headers.host}`);
20
+ const query = Object.fromEntries(url.searchParams);
21
+
22
+ // Create enhanced request object
23
+ const enhancedReq = {
24
+ ...req,
25
+ body,
26
+ query,
27
+ params: route.params,
28
+ method: req.method
29
+ };
30
+
31
+ // Create enhanced response object
32
+ const enhancedRes = createEnhancedResponse(res);
33
+
34
+ // Check for method-specific handlers
35
+ const method = req.method.toLowerCase();
36
+
37
+ if (handler[method]) {
38
+ // Method-specific handler (get, post, put, delete, etc.)
39
+ await handler[method](enhancedReq, enhancedRes);
40
+ } else if (handler.default) {
41
+ // Default handler
42
+ await handler.default(enhancedReq, enhancedRes);
43
+ } else {
44
+ // No handler found
45
+ enhancedRes.status(405).json({ error: 'Method not allowed' });
46
+ }
47
+ } catch (error) {
48
+ console.error('API Error:', error);
49
+
50
+ if (!res.headersSent) {
51
+ res.writeHead(500, { 'Content-Type': 'application/json' });
52
+ res.end(JSON.stringify({
53
+ error: 'Internal Server Error',
54
+ message: process.env.NODE_ENV === 'development' ? error.message : undefined
55
+ }));
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Parses the request body
62
+ */
63
+ async function parseBody(req) {
64
+ return new Promise((resolve, reject) => {
65
+ const contentType = req.headers['content-type'] || '';
66
+ let body = '';
67
+
68
+ req.on('data', chunk => {
69
+ body += chunk.toString();
70
+ });
71
+
72
+ req.on('end', () => {
73
+ try {
74
+ if (contentType.includes('application/json') && body) {
75
+ resolve(JSON.parse(body));
76
+ } else if (contentType.includes('application/x-www-form-urlencoded') && body) {
77
+ resolve(Object.fromEntries(new URLSearchParams(body)));
78
+ } else {
79
+ resolve(body || null);
80
+ }
81
+ } catch (error) {
82
+ resolve(body);
83
+ }
84
+ });
85
+
86
+ req.on('error', reject);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Creates an enhanced response object with helper methods
92
+ */
93
+ function createEnhancedResponse(res) {
94
+ const enhanced = {
95
+ _res: res,
96
+ _statusCode: 200,
97
+ _headers: {},
98
+
99
+ status(code) {
100
+ this._statusCode = code;
101
+ return this;
102
+ },
103
+
104
+ setHeader(name, value) {
105
+ this._headers[name] = value;
106
+ return this;
107
+ },
108
+
109
+ json(data) {
110
+ this._headers['Content-Type'] = 'application/json';
111
+ this._sendResponse(JSON.stringify(data));
112
+ },
113
+
114
+ send(data) {
115
+ if (typeof data === 'object') {
116
+ this.json(data);
117
+ } else {
118
+ this._headers['Content-Type'] = this._headers['Content-Type'] || 'text/plain';
119
+ this._sendResponse(String(data));
120
+ }
121
+ },
122
+
123
+ html(data) {
124
+ this._headers['Content-Type'] = 'text/html';
125
+ this._sendResponse(data);
126
+ },
127
+
128
+ redirect(url, statusCode = 302) {
129
+ this._statusCode = statusCode;
130
+ this._headers['Location'] = url;
131
+ this._sendResponse('');
132
+ },
133
+
134
+ _sendResponse(body) {
135
+ if (!this._res.headersSent) {
136
+ this._res.writeHead(this._statusCode, this._headers);
137
+ this._res.end(body);
138
+ }
139
+ }
140
+ };
141
+
142
+ return enhanced;
143
+ }
@@ -0,0 +1,357 @@
1
+ /**
2
+ * FlexiReact Build System
3
+ * Uses esbuild for fast bundling of client and server code
4
+ */
5
+
6
+ import * as esbuild from 'esbuild';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { findFiles, ensureDir, cleanDir, generateHash, isClientComponent, isIsland } from '../utils.js';
11
+ import { buildRouteTree } from '../router/index.js';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ /**
17
+ * Build configuration
18
+ */
19
+ export const BuildMode = {
20
+ DEVELOPMENT: 'development',
21
+ PRODUCTION: 'production'
22
+ };
23
+
24
+ /**
25
+ * Main build function
26
+ */
27
+ export async function build(options) {
28
+ const {
29
+ projectRoot,
30
+ config,
31
+ mode = BuildMode.PRODUCTION
32
+ } = options;
33
+
34
+ const startTime = Date.now();
35
+ const outDir = config.outDir;
36
+ const isDev = mode === BuildMode.DEVELOPMENT;
37
+
38
+ console.log('\n⚡ FlexiReact Build\n');
39
+ console.log(` Mode: ${mode}`);
40
+ console.log(` Output: ${outDir}\n`);
41
+
42
+ // Clean output directory
43
+ cleanDir(outDir);
44
+ ensureDir(path.join(outDir, 'client'));
45
+ ensureDir(path.join(outDir, 'server'));
46
+ ensureDir(path.join(outDir, 'static'));
47
+
48
+ // Build routes
49
+ const routes = buildRouteTree(config.pagesDir, config.layoutsDir);
50
+
51
+ // Find all client components and islands
52
+ const clientEntries = findClientEntries(config.pagesDir, config.layoutsDir);
53
+
54
+ // Build client bundle
55
+ console.log('📦 Building client bundle...');
56
+ const clientResult = await buildClient({
57
+ entries: clientEntries,
58
+ outDir: path.join(outDir, 'client'),
59
+ config,
60
+ isDev
61
+ });
62
+
63
+ // Build server bundle
64
+ console.log('📦 Building server bundle...');
65
+ const serverResult = await buildServer({
66
+ pagesDir: config.pagesDir,
67
+ layoutsDir: config.layoutsDir,
68
+ outDir: path.join(outDir, 'server'),
69
+ config,
70
+ isDev
71
+ });
72
+
73
+ // Copy public assets
74
+ console.log('📁 Copying public assets...');
75
+ await copyPublicAssets(config.publicDir, path.join(outDir, 'static'));
76
+
77
+ // Generate manifest
78
+ const manifest = generateManifest({
79
+ routes,
80
+ clientResult,
81
+ serverResult,
82
+ config
83
+ });
84
+
85
+ fs.writeFileSync(
86
+ path.join(outDir, 'manifest.json'),
87
+ JSON.stringify(manifest, null, 2)
88
+ );
89
+
90
+ const duration = Date.now() - startTime;
91
+
92
+ console.log('\n✨ Build complete!\n');
93
+ console.log(` Duration: ${duration}ms`);
94
+ console.log(` Client chunks: ${clientResult.outputs.length}`);
95
+ console.log(` Server modules: ${serverResult.outputs.length}`);
96
+ console.log('');
97
+
98
+ return {
99
+ success: true,
100
+ duration,
101
+ manifest,
102
+ clientResult,
103
+ serverResult
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Finds all client component entries
109
+ */
110
+ function findClientEntries(pagesDir, layoutsDir) {
111
+ const entries = [];
112
+ const dirs = [pagesDir, layoutsDir].filter(d => fs.existsSync(d));
113
+
114
+ for (const dir of dirs) {
115
+ const files = findFiles(dir, /\.(jsx|tsx)$/);
116
+
117
+ for (const file of files) {
118
+ if (isClientComponent(file) || isIsland(file)) {
119
+ entries.push(file);
120
+ }
121
+ }
122
+ }
123
+
124
+ return entries;
125
+ }
126
+
127
+ /**
128
+ * Builds client-side JavaScript
129
+ */
130
+ async function buildClient(options) {
131
+ const { entries, outDir, config, isDev } = options;
132
+
133
+ if (entries.length === 0) {
134
+ return { outputs: [] };
135
+ }
136
+
137
+ // Create entry points map
138
+ const entryPoints = {};
139
+ for (const entry of entries) {
140
+ const name = path.basename(entry, path.extname(entry));
141
+ const hash = generateHash(entry);
142
+ entryPoints[`${name}-${hash}`] = entry;
143
+ }
144
+
145
+ // Add runtime entry
146
+ const runtimePath = path.join(__dirname, '..', 'client', 'runtime.js');
147
+ if (fs.existsSync(runtimePath)) {
148
+ entryPoints['runtime'] = runtimePath;
149
+ }
150
+
151
+ try {
152
+ const result = await esbuild.build({
153
+ entryPoints,
154
+ bundle: true,
155
+ splitting: true,
156
+ format: 'esm',
157
+ outdir: outDir,
158
+ minify: !isDev && config.build.minify,
159
+ sourcemap: config.build.sourcemap,
160
+ target: config.build.target,
161
+ jsx: 'automatic',
162
+ jsxImportSource: 'react',
163
+ metafile: true,
164
+ external: [],
165
+ define: {
166
+ 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production')
167
+ },
168
+ loader: {
169
+ '.js': 'jsx',
170
+ '.jsx': 'jsx',
171
+ '.ts': 'tsx',
172
+ '.tsx': 'tsx'
173
+ }
174
+ });
175
+
176
+ const outputs = Object.keys(result.metafile.outputs).map(file => ({
177
+ file: path.basename(file),
178
+ size: result.metafile.outputs[file].bytes
179
+ }));
180
+
181
+ return { outputs, metafile: result.metafile };
182
+
183
+ } catch (error) {
184
+ console.error('Client build failed:', error);
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Builds server-side modules
191
+ */
192
+ async function buildServer(options) {
193
+ const { pagesDir, layoutsDir, outDir, config, isDev } = options;
194
+
195
+ const entries = [];
196
+
197
+ // Find all page and layout files
198
+ for (const dir of [pagesDir, layoutsDir]) {
199
+ if (fs.existsSync(dir)) {
200
+ entries.push(...findFiles(dir, /\.(jsx|tsx|js|ts)$/));
201
+ }
202
+ }
203
+
204
+ if (entries.length === 0) {
205
+ return { outputs: [] };
206
+ }
207
+
208
+ // Create entry points
209
+ const entryPoints = {};
210
+ for (const entry of entries) {
211
+ const relativePath = path.relative(pagesDir, entry);
212
+ const name = relativePath.replace(/[\/\\]/g, '_').replace(/\.(jsx|tsx|js|ts)$/, '');
213
+ entryPoints[name] = entry;
214
+ }
215
+
216
+ try {
217
+ const result = await esbuild.build({
218
+ entryPoints,
219
+ bundle: true,
220
+ format: 'esm',
221
+ platform: 'node',
222
+ outdir: outDir,
223
+ minify: false, // Keep server code readable
224
+ sourcemap: true,
225
+ target: 'node18',
226
+ jsx: 'automatic',
227
+ jsxImportSource: 'react',
228
+ metafile: true,
229
+ packages: 'external', // Don't bundle node_modules
230
+ loader: {
231
+ '.js': 'jsx',
232
+ '.jsx': 'jsx',
233
+ '.ts': 'tsx',
234
+ '.tsx': 'tsx'
235
+ }
236
+ });
237
+
238
+ const outputs = Object.keys(result.metafile.outputs).map(file => ({
239
+ file: path.basename(file),
240
+ size: result.metafile.outputs[file].bytes
241
+ }));
242
+
243
+ return { outputs, metafile: result.metafile };
244
+
245
+ } catch (error) {
246
+ console.error('Server build failed:', error);
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Copies public assets to output directory
253
+ */
254
+ async function copyPublicAssets(publicDir, outDir) {
255
+ if (!fs.existsSync(publicDir)) {
256
+ return;
257
+ }
258
+
259
+ const copyRecursive = (src, dest) => {
260
+ const entries = fs.readdirSync(src, { withFileTypes: true });
261
+
262
+ ensureDir(dest);
263
+
264
+ for (const entry of entries) {
265
+ const srcPath = path.join(src, entry.name);
266
+ const destPath = path.join(dest, entry.name);
267
+
268
+ if (entry.isDirectory()) {
269
+ copyRecursive(srcPath, destPath);
270
+ } else {
271
+ fs.copyFileSync(srcPath, destPath);
272
+ }
273
+ }
274
+ };
275
+
276
+ copyRecursive(publicDir, outDir);
277
+ }
278
+
279
+ /**
280
+ * Generates build manifest
281
+ */
282
+ function generateManifest(options) {
283
+ const { routes, clientResult, serverResult, config } = options;
284
+
285
+ return {
286
+ version: '2.0.0',
287
+ generatedAt: new Date().toISOString(),
288
+ routes: {
289
+ pages: routes.pages.map(r => ({
290
+ path: r.path,
291
+ file: r.filePath,
292
+ hasLayout: !!r.layout,
293
+ hasLoading: !!r.loading,
294
+ hasError: !!r.error
295
+ })),
296
+ api: routes.api.map(r => ({
297
+ path: r.path,
298
+ file: r.filePath
299
+ }))
300
+ },
301
+ client: {
302
+ chunks: clientResult.outputs || []
303
+ },
304
+ server: {
305
+ modules: serverResult.outputs || []
306
+ },
307
+ config: {
308
+ islands: config.islands.enabled,
309
+ rsc: config.rsc.enabled
310
+ }
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Development build with watch mode
316
+ */
317
+ export async function buildDev(options) {
318
+ const { projectRoot, config, onChange } = options;
319
+
320
+ const outDir = config.outDir;
321
+ ensureDir(outDir);
322
+
323
+ // Use esbuild's watch mode
324
+ const ctx = await esbuild.context({
325
+ entryPoints: findFiles(config.pagesDir, /\.(jsx|tsx)$/),
326
+ bundle: true,
327
+ format: 'esm',
328
+ outdir: path.join(outDir, 'dev'),
329
+ sourcemap: true,
330
+ jsx: 'automatic',
331
+ jsxImportSource: 'react',
332
+ loader: {
333
+ '.js': 'jsx',
334
+ '.jsx': 'jsx'
335
+ },
336
+ plugins: [{
337
+ name: 'flexi-watch',
338
+ setup(build) {
339
+ build.onEnd(result => {
340
+ if (result.errors.length === 0) {
341
+ onChange?.();
342
+ }
343
+ });
344
+ }
345
+ }]
346
+ });
347
+
348
+ await ctx.watch();
349
+
350
+ return ctx;
351
+ }
352
+
353
+ export default {
354
+ build,
355
+ buildDev,
356
+ BuildMode
357
+ };