@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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Plugin System
|
|
3
|
+
*
|
|
4
|
+
* Plugins can extend FlexiReact's functionality by hooking into various lifecycle events.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* Create a flexireact.plugin.js file:
|
|
8
|
+
*
|
|
9
|
+
* export default {
|
|
10
|
+
* name: 'my-plugin',
|
|
11
|
+
*
|
|
12
|
+
* // Called when the server starts
|
|
13
|
+
* onServerStart(server) {},
|
|
14
|
+
*
|
|
15
|
+
* // Called before each request
|
|
16
|
+
* onRequest(req, res) {},
|
|
17
|
+
*
|
|
18
|
+
* // Called before rendering a page
|
|
19
|
+
* onBeforeRender(page, props) {},
|
|
20
|
+
*
|
|
21
|
+
* // Called after rendering a page
|
|
22
|
+
* onAfterRender(html, page) {},
|
|
23
|
+
*
|
|
24
|
+
* // Called during build
|
|
25
|
+
* onBuild(config) {},
|
|
26
|
+
*
|
|
27
|
+
* // Modify esbuild config
|
|
28
|
+
* esbuildConfig(config) {},
|
|
29
|
+
* };
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
import path from 'path';
|
|
34
|
+
import { pathToFileURL } from 'url';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Plugin lifecycle hooks
|
|
38
|
+
*/
|
|
39
|
+
export const PluginHooks = {
|
|
40
|
+
// Server lifecycle
|
|
41
|
+
SERVER_START: 'onServerStart',
|
|
42
|
+
SERVER_STOP: 'onServerStop',
|
|
43
|
+
|
|
44
|
+
// Request lifecycle
|
|
45
|
+
REQUEST: 'onRequest',
|
|
46
|
+
RESPONSE: 'onResponse',
|
|
47
|
+
|
|
48
|
+
// Render lifecycle
|
|
49
|
+
BEFORE_RENDER: 'onBeforeRender',
|
|
50
|
+
AFTER_RENDER: 'onAfterRender',
|
|
51
|
+
|
|
52
|
+
// Build lifecycle
|
|
53
|
+
BUILD_START: 'onBuildStart',
|
|
54
|
+
BUILD_END: 'onBuildEnd',
|
|
55
|
+
|
|
56
|
+
// Route lifecycle
|
|
57
|
+
ROUTES_LOADED: 'onRoutesLoaded',
|
|
58
|
+
|
|
59
|
+
// Config
|
|
60
|
+
CONFIG: 'onConfig',
|
|
61
|
+
ESBUILD_CONFIG: 'esbuildConfig'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Plugin manager class
|
|
66
|
+
*/
|
|
67
|
+
export class PluginManager {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.plugins = [];
|
|
70
|
+
this.hooks = new Map();
|
|
71
|
+
|
|
72
|
+
// Initialize hook arrays
|
|
73
|
+
for (const hook of Object.values(PluginHooks)) {
|
|
74
|
+
this.hooks.set(hook, []);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Registers a plugin
|
|
80
|
+
*/
|
|
81
|
+
register(plugin) {
|
|
82
|
+
if (!plugin.name) {
|
|
83
|
+
throw new Error('Plugin must have a name');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for duplicate
|
|
87
|
+
if (this.plugins.find(p => p.name === plugin.name)) {
|
|
88
|
+
console.warn(`Plugin "${plugin.name}" is already registered`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.plugins.push(plugin);
|
|
93
|
+
|
|
94
|
+
// Register hooks
|
|
95
|
+
for (const [hookName, handlers] of this.hooks) {
|
|
96
|
+
if (typeof plugin[hookName] === 'function') {
|
|
97
|
+
handlers.push({
|
|
98
|
+
plugin: plugin.name,
|
|
99
|
+
handler: plugin[hookName].bind(plugin)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(` ✓ Plugin loaded: ${plugin.name}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Unregisters a plugin
|
|
109
|
+
*/
|
|
110
|
+
unregister(pluginName) {
|
|
111
|
+
const index = this.plugins.findIndex(p => p.name === pluginName);
|
|
112
|
+
if (index === -1) return;
|
|
113
|
+
|
|
114
|
+
this.plugins.splice(index, 1);
|
|
115
|
+
|
|
116
|
+
// Remove hooks
|
|
117
|
+
for (const handlers of this.hooks.values()) {
|
|
118
|
+
const hookIndex = handlers.findIndex(h => h.plugin === pluginName);
|
|
119
|
+
if (hookIndex !== -1) {
|
|
120
|
+
handlers.splice(hookIndex, 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Runs a hook with all registered handlers
|
|
127
|
+
*/
|
|
128
|
+
async runHook(hookName, ...args) {
|
|
129
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
130
|
+
const results = [];
|
|
131
|
+
|
|
132
|
+
for (const { plugin, handler } of handlers) {
|
|
133
|
+
try {
|
|
134
|
+
const result = await handler(...args);
|
|
135
|
+
results.push({ plugin, result });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`Plugin "${plugin}" error in ${hookName}:`, error);
|
|
138
|
+
results.push({ plugin, error });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Runs a hook that can modify a value (waterfall)
|
|
147
|
+
*/
|
|
148
|
+
async runWaterfallHook(hookName, initialValue, ...args) {
|
|
149
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
150
|
+
let value = initialValue;
|
|
151
|
+
|
|
152
|
+
for (const { plugin, handler } of handlers) {
|
|
153
|
+
try {
|
|
154
|
+
const result = await handler(value, ...args);
|
|
155
|
+
if (result !== undefined) {
|
|
156
|
+
value = result;
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(`Plugin "${plugin}" error in ${hookName}:`, error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Checks if any plugin handles a hook
|
|
168
|
+
*/
|
|
169
|
+
hasHook(hookName) {
|
|
170
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
171
|
+
return handlers.length > 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets all registered plugins
|
|
176
|
+
*/
|
|
177
|
+
getPlugins() {
|
|
178
|
+
return [...this.plugins];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Global plugin manager instance
|
|
183
|
+
export const pluginManager = new PluginManager();
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Loads plugins from project and config
|
|
187
|
+
*/
|
|
188
|
+
export async function loadPlugins(projectRoot, config) {
|
|
189
|
+
console.log('\n📦 Loading plugins...\n');
|
|
190
|
+
|
|
191
|
+
// Load from flexireact.plugin.js
|
|
192
|
+
const pluginPath = path.join(projectRoot, 'flexireact.plugin.js');
|
|
193
|
+
|
|
194
|
+
if (fs.existsSync(pluginPath)) {
|
|
195
|
+
try {
|
|
196
|
+
const url = pathToFileURL(pluginPath).href;
|
|
197
|
+
const module = await import(`${url}?t=${Date.now()}`);
|
|
198
|
+
const plugin = module.default;
|
|
199
|
+
|
|
200
|
+
if (plugin) {
|
|
201
|
+
pluginManager.register(plugin);
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Failed to load flexireact.plugin.js:', error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Load plugins from config
|
|
209
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
210
|
+
for (const pluginConfig of config.plugins) {
|
|
211
|
+
try {
|
|
212
|
+
if (typeof pluginConfig === 'string') {
|
|
213
|
+
// Load from node_modules
|
|
214
|
+
const module = await import(pluginConfig);
|
|
215
|
+
pluginManager.register(module.default);
|
|
216
|
+
} else if (typeof pluginConfig === 'object') {
|
|
217
|
+
// Inline plugin
|
|
218
|
+
pluginManager.register(pluginConfig);
|
|
219
|
+
} else if (typeof pluginConfig === 'function') {
|
|
220
|
+
// Plugin factory
|
|
221
|
+
pluginManager.register(pluginConfig());
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`Failed to load plugin:`, error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`\n Total plugins: ${pluginManager.getPlugins().length}\n`);
|
|
230
|
+
|
|
231
|
+
return pluginManager;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Creates a plugin
|
|
236
|
+
*/
|
|
237
|
+
export function definePlugin(options) {
|
|
238
|
+
return {
|
|
239
|
+
name: options.name || 'unnamed-plugin',
|
|
240
|
+
...options
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Built-in plugins
|
|
246
|
+
*/
|
|
247
|
+
export const builtinPlugins = {
|
|
248
|
+
/**
|
|
249
|
+
* Analytics plugin
|
|
250
|
+
*/
|
|
251
|
+
analytics(options = {}) {
|
|
252
|
+
const { trackingId } = options;
|
|
253
|
+
|
|
254
|
+
return definePlugin({
|
|
255
|
+
name: 'flexi-analytics',
|
|
256
|
+
|
|
257
|
+
onAfterRender(html) {
|
|
258
|
+
if (!trackingId) return html;
|
|
259
|
+
|
|
260
|
+
const script = `
|
|
261
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=${trackingId}"></script>
|
|
262
|
+
<script>
|
|
263
|
+
window.dataLayer = window.dataLayer || [];
|
|
264
|
+
function gtag(){dataLayer.push(arguments);}
|
|
265
|
+
gtag('js', new Date());
|
|
266
|
+
gtag('config', '${trackingId}');
|
|
267
|
+
</script>
|
|
268
|
+
`;
|
|
269
|
+
|
|
270
|
+
return html.replace('</head>', `${script}</head>`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* PWA plugin
|
|
277
|
+
*/
|
|
278
|
+
pwa(options = {}) {
|
|
279
|
+
const { manifest = '/manifest.json', serviceWorker = '/sw.js' } = options;
|
|
280
|
+
|
|
281
|
+
return definePlugin({
|
|
282
|
+
name: 'flexi-pwa',
|
|
283
|
+
|
|
284
|
+
onAfterRender(html) {
|
|
285
|
+
const tags = `
|
|
286
|
+
<link rel="manifest" href="${manifest}">
|
|
287
|
+
<script>
|
|
288
|
+
if ('serviceWorker' in navigator) {
|
|
289
|
+
navigator.serviceWorker.register('${serviceWorker}');
|
|
290
|
+
}
|
|
291
|
+
</script>
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
return html.replace('</head>', `${tags}</head>`);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* SEO plugin
|
|
301
|
+
*/
|
|
302
|
+
seo(options = {}) {
|
|
303
|
+
const { defaultTitle, titleTemplate = '%s', defaultDescription } = options;
|
|
304
|
+
|
|
305
|
+
return definePlugin({
|
|
306
|
+
name: 'flexi-seo',
|
|
307
|
+
|
|
308
|
+
onBeforeRender(page, props) {
|
|
309
|
+
const title = props.title || page.title || defaultTitle;
|
|
310
|
+
const description = props.description || page.description || defaultDescription;
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
...props,
|
|
314
|
+
_seo: {
|
|
315
|
+
title: titleTemplate.replace('%s', title),
|
|
316
|
+
description
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Compression plugin (for production)
|
|
325
|
+
*/
|
|
326
|
+
compression() {
|
|
327
|
+
return definePlugin({
|
|
328
|
+
name: 'flexi-compression',
|
|
329
|
+
|
|
330
|
+
onResponse(req, res, html) {
|
|
331
|
+
// Note: Actual compression would require zlib
|
|
332
|
+
// This is a placeholder for the concept
|
|
333
|
+
res.setHeader('Content-Encoding', 'identity');
|
|
334
|
+
return html;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Security headers plugin
|
|
341
|
+
*/
|
|
342
|
+
securityHeaders(options = {}) {
|
|
343
|
+
const headers = {
|
|
344
|
+
'X-Content-Type-Options': 'nosniff',
|
|
345
|
+
'X-Frame-Options': 'DENY',
|
|
346
|
+
'X-XSS-Protection': '1; mode=block',
|
|
347
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
348
|
+
...options.headers
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return definePlugin({
|
|
352
|
+
name: 'flexi-security',
|
|
353
|
+
|
|
354
|
+
onRequest(req, res) {
|
|
355
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
356
|
+
res.setHeader(key, value);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export default {
|
|
364
|
+
PluginManager,
|
|
365
|
+
PluginHooks,
|
|
366
|
+
pluginManager,
|
|
367
|
+
loadPlugins,
|
|
368
|
+
definePlugin,
|
|
369
|
+
builtinPlugins
|
|
370
|
+
};
|