@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/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
|
+
};
|