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