voltjs-framework 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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * VoltJS Configuration Manager
3
+ *
4
+ * Handles configuration with:
5
+ * - Default values
6
+ * - Environment variable overrides
7
+ * - Config file loading (volt.config.js)
8
+ * - Nested key access with dot notation
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+
16
+ const DEFAULTS = {
17
+ env: process.env.NODE_ENV || 'development',
18
+ port: parseInt(process.env.PORT, 10) || 3000,
19
+ host: process.env.HOST || '0.0.0.0',
20
+
21
+ // Security defaults - ON by default
22
+ security: {
23
+ csrf: true,
24
+ xss: true,
25
+ cors: {
26
+ origin: '*',
27
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
28
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
29
+ credentials: true,
30
+ maxAge: 86400,
31
+ },
32
+ rateLimit: {
33
+ windowMs: 15 * 60 * 1000, // 15 minutes
34
+ max: 100, // 100 requests per window
35
+ },
36
+ headers: true,
37
+ hsts: true,
38
+ sanitize: true,
39
+ },
40
+
41
+ // Server defaults
42
+ server: {
43
+ maxBodySize: 10 * 1024 * 1024, // 10MB
44
+ timeout: 30000, // 30s
45
+ keepAlive: true,
46
+ compression: true,
47
+ },
48
+
49
+ // Logging
50
+ logging: {
51
+ requests: true,
52
+ level: 'info',
53
+ format: 'pretty',
54
+ file: null,
55
+ },
56
+
57
+ // Views
58
+ views: {
59
+ engine: 'volt',
60
+ dir: 'views',
61
+ cache: process.env.NODE_ENV === 'production',
62
+ },
63
+
64
+ // Database
65
+ database: {
66
+ driver: null,
67
+ host: 'localhost',
68
+ port: null,
69
+ name: null,
70
+ user: null,
71
+ password: null,
72
+ filename: null,
73
+ pool: { min: 2, max: 10 },
74
+ },
75
+
76
+ // Cache
77
+ cache: {
78
+ driver: 'memory',
79
+ ttl: 3600,
80
+ prefix: 'volt:',
81
+ },
82
+
83
+ // Mail
84
+ mail: {
85
+ driver: 'smtp',
86
+ host: null,
87
+ port: 587,
88
+ secure: false,
89
+ user: null,
90
+ password: null,
91
+ from: null,
92
+ },
93
+
94
+ // Session
95
+ session: {
96
+ secret: null,
97
+ name: 'volt.sid',
98
+ maxAge: 24 * 60 * 60 * 1000, // 24 hours
99
+ secure: false,
100
+ httpOnly: true,
101
+ sameSite: 'Lax',
102
+ },
103
+ };
104
+
105
+ class Config {
106
+ constructor(userConfig = {}) {
107
+ // Deep merge: defaults → config file → user config → env vars
108
+ this._config = this._deepMerge(DEFAULTS, this._loadConfigFile(), userConfig);
109
+ this._applyEnvOverrides();
110
+ }
111
+
112
+ /** Get config value using dot notation */
113
+ get(key, defaultValue = undefined) {
114
+ const keys = key.split('.');
115
+ let value = this._config;
116
+
117
+ for (const k of keys) {
118
+ if (value === undefined || value === null) return defaultValue;
119
+ value = value[k];
120
+ }
121
+
122
+ return value !== undefined ? value : defaultValue;
123
+ }
124
+
125
+ /** Set config value using dot notation */
126
+ set(key, value) {
127
+ const keys = key.split('.');
128
+ let target = this._config;
129
+
130
+ for (let i = 0; i < keys.length - 1; i++) {
131
+ if (!(keys[i] in target)) target[keys[i]] = {};
132
+ target = target[keys[i]];
133
+ }
134
+
135
+ target[keys[keys.length - 1]] = value;
136
+ return this;
137
+ }
138
+
139
+ /** Check if key exists */
140
+ has(key) {
141
+ return this.get(key) !== undefined;
142
+ }
143
+
144
+ /** Get all config */
145
+ all() {
146
+ return { ...this._config };
147
+ }
148
+
149
+ // ===== INTERNAL =====
150
+
151
+ _loadConfigFile() {
152
+ const configPaths = [
153
+ path.join(process.cwd(), 'volt.config.js'),
154
+ path.join(process.cwd(), 'volt.config.json'),
155
+ path.join(process.cwd(), '.voltrc'),
156
+ path.join(process.cwd(), '.voltrc.js'),
157
+ ];
158
+
159
+ for (const configPath of configPaths) {
160
+ try {
161
+ if (fs.existsSync(configPath)) {
162
+ return require(configPath);
163
+ }
164
+ } catch {
165
+ // Skip invalid config files
166
+ }
167
+ }
168
+
169
+ return {};
170
+ }
171
+
172
+ _applyEnvOverrides() {
173
+ // Map common ENV vars to config keys
174
+ const envMap = {
175
+ VOLT_PORT: 'port',
176
+ VOLT_HOST: 'host',
177
+ VOLT_ENV: 'env',
178
+ NODE_ENV: 'env',
179
+ DATABASE_URL: 'database.url',
180
+ DB_HOST: 'database.host',
181
+ DB_PORT: 'database.port',
182
+ DB_NAME: 'database.name',
183
+ DB_USER: 'database.user',
184
+ DB_PASSWORD: 'database.password',
185
+ MAIL_HOST: 'mail.host',
186
+ MAIL_PORT: 'mail.port',
187
+ MAIL_USER: 'mail.user',
188
+ MAIL_PASSWORD: 'mail.password',
189
+ MAIL_FROM: 'mail.from',
190
+ SESSION_SECRET: 'session.secret',
191
+ CACHE_DRIVER: 'cache.driver',
192
+ };
193
+
194
+ for (const [envKey, configKey] of Object.entries(envMap)) {
195
+ if (process.env[envKey] !== undefined) {
196
+ let value = process.env[envKey];
197
+ // Auto-convert numbers and booleans
198
+ if (value === 'true') value = true;
199
+ else if (value === 'false') value = false;
200
+ else if (/^\d+$/.test(value)) value = parseInt(value, 10);
201
+ this.set(configKey, value);
202
+ }
203
+ }
204
+ }
205
+
206
+ _deepMerge(target, ...sources) {
207
+ const result = { ...target };
208
+
209
+ for (const source of sources) {
210
+ if (!source || typeof source !== 'object') continue;
211
+
212
+ for (const key of Object.keys(source)) {
213
+ if (
214
+ source[key] &&
215
+ typeof source[key] === 'object' &&
216
+ !Array.isArray(source[key]) &&
217
+ result[key] &&
218
+ typeof result[key] === 'object' &&
219
+ !Array.isArray(result[key])
220
+ ) {
221
+ result[key] = this._deepMerge(result[key], source[key]);
222
+ } else {
223
+ result[key] = source[key];
224
+ }
225
+ }
226
+ }
227
+
228
+ return result;
229
+ }
230
+ }
231
+
232
+ module.exports = { Config };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * VoltJS Middleware Engine
3
+ *
4
+ * Supports global middleware, path-specific middleware, and named middleware.
5
+ * All middleware follows (req, res, next?) pattern.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ class Middleware {
11
+ constructor() {
12
+ this._global = [];
13
+ this._paths = new Map();
14
+ this._named = new Map();
15
+ }
16
+
17
+ /** Add global middleware */
18
+ addGlobal(fn) {
19
+ if (typeof fn !== 'function') {
20
+ throw new Error('Middleware must be a function');
21
+ }
22
+ this._global.push(fn);
23
+ return this;
24
+ }
25
+
26
+ /** Add path-specific middleware */
27
+ addPath(pathPattern, fns) {
28
+ const handlers = Array.isArray(fns) ? fns : [fns];
29
+ if (!this._paths.has(pathPattern)) {
30
+ this._paths.set(pathPattern, []);
31
+ }
32
+ this._paths.get(pathPattern).push(...handlers);
33
+ return this;
34
+ }
35
+
36
+ /** Register named middleware for reuse */
37
+ register(name, fn) {
38
+ this._named.set(name, fn);
39
+ return this;
40
+ }
41
+
42
+ /** Get named middleware */
43
+ get(name) {
44
+ return this._named.get(name);
45
+ }
46
+
47
+ /** Run global middleware chain */
48
+ async runGlobal(req, res) {
49
+ return this._runChain(this._global, req, res);
50
+ }
51
+
52
+ /** Run path-specific middleware */
53
+ async runPath(requestPath, req, res) {
54
+ const matchingMiddleware = [];
55
+
56
+ for (const [pathPattern, handlers] of this._paths) {
57
+ if (this._pathMatches(requestPath, pathPattern)) {
58
+ matchingMiddleware.push(...handlers);
59
+ }
60
+ }
61
+
62
+ if (matchingMiddleware.length === 0) return true;
63
+ return this._runChain(matchingMiddleware, req, res);
64
+ }
65
+
66
+ /** Execute middleware chain */
67
+ async _runChain(middlewares, req, res) {
68
+ for (const mw of middlewares) {
69
+ if (res.writableEnded) return false;
70
+
71
+ const result = await new Promise((resolve, reject) => {
72
+ try {
73
+ // Support both (req, res, next) and (req, res) patterns
74
+ if (mw.length >= 3) {
75
+ const ret = mw(req, res, (err) => {
76
+ if (err) reject(err);
77
+ else resolve(true);
78
+ });
79
+ // Handle async middleware
80
+ if (ret && typeof ret.then === 'function') {
81
+ ret.catch(reject);
82
+ }
83
+ } else {
84
+ const ret = mw(req, res);
85
+ if (ret && typeof ret.then === 'function') {
86
+ ret.then(r => resolve(r !== false)).catch(reject);
87
+ } else {
88
+ resolve(ret !== false);
89
+ }
90
+ }
91
+ } catch (err) {
92
+ reject(err);
93
+ }
94
+ });
95
+
96
+ if (result === false) return false;
97
+ }
98
+ return true;
99
+ }
100
+
101
+ /** Check if a request path matches a middleware path pattern */
102
+ _pathMatches(requestPath, pattern) {
103
+ if (pattern === '*' || pattern === '/') return true;
104
+
105
+ // Exact match
106
+ if (requestPath === pattern) return true;
107
+
108
+ // Prefix match (e.g., '/admin' matches '/admin/users')
109
+ if (requestPath.startsWith(pattern + '/')) return true;
110
+
111
+ // Wildcard pattern
112
+ if (pattern.includes('*')) {
113
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
114
+ return regex.test(requestPath);
115
+ }
116
+
117
+ return false;
118
+ }
119
+
120
+ /** Create composed middleware from multiple middleware functions */
121
+ static compose(...middlewares) {
122
+ return async (req, res) => {
123
+ for (const mw of middlewares.flat()) {
124
+ if (res.writableEnded) return false;
125
+ const result = typeof mw === 'function' ? await mw(req, res) : undefined;
126
+ if (result === false) return false;
127
+ }
128
+ return true;
129
+ };
130
+ }
131
+ }
132
+
133
+ module.exports = { Middleware };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * VoltJS Plugin Manager
3
+ *
4
+ * Extensible plugin system for adding functionality.
5
+ *
6
+ * @example
7
+ * // Creating a plugin
8
+ * const myPlugin = {
9
+ * name: 'my-plugin',
10
+ * version: '1.0.0',
11
+ * install(app, options) {
12
+ * app.get('/my-route', (req, res) => res.json({ plugin: true }));
13
+ * app.use((req, res) => { req.myPlugin = true; });
14
+ * }
15
+ * };
16
+ *
17
+ * app.use(myPlugin);
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ class PluginManager {
23
+ constructor(app) {
24
+ this._app = app;
25
+ this._plugins = new Map();
26
+ this._hooks = new Map();
27
+ }
28
+
29
+ /** Register a plugin */
30
+ register(plugin, options = {}) {
31
+ if (!plugin || !plugin.name) {
32
+ throw new Error('Plugin must have a name property');
33
+ }
34
+
35
+ if (this._plugins.has(plugin.name)) {
36
+ console.warn(`\x1b[33mWarning: Plugin "${plugin.name}" is already registered. Skipping.\x1b[0m`);
37
+ return this;
38
+ }
39
+
40
+ // Validate plugin
41
+ if (typeof plugin.install !== 'function') {
42
+ throw new Error(`Plugin "${plugin.name}" must have an install() method`);
43
+ }
44
+
45
+ // Install plugin
46
+ try {
47
+ plugin.install(this._app, options);
48
+ this._plugins.set(plugin.name, { plugin, options });
49
+ console.log(`\x1b[32m ✓ Plugin loaded: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}\x1b[0m`);
50
+ } catch (err) {
51
+ console.error(`\x1b[31m ✗ Plugin "${plugin.name}" failed to install: ${err.message}\x1b[0m`);
52
+ throw err;
53
+ }
54
+
55
+ return this;
56
+ }
57
+
58
+ /** Check if a plugin is registered */
59
+ has(name) {
60
+ return this._plugins.has(name);
61
+ }
62
+
63
+ /** Get a registered plugin */
64
+ get(name) {
65
+ const entry = this._plugins.get(name);
66
+ return entry ? entry.plugin : null;
67
+ }
68
+
69
+ /** List all registered plugins */
70
+ list() {
71
+ return Array.from(this._plugins.entries()).map(([name, { plugin }]) => ({
72
+ name,
73
+ version: plugin.version || '0.0.0',
74
+ }));
75
+ }
76
+
77
+ /** Unregister a plugin */
78
+ unregister(name) {
79
+ const entry = this._plugins.get(name);
80
+ if (entry && typeof entry.plugin.uninstall === 'function') {
81
+ entry.plugin.uninstall(this._app);
82
+ }
83
+ this._plugins.delete(name);
84
+ return this;
85
+ }
86
+ }
87
+
88
+ module.exports = { PluginManager };
@@ -0,0 +1,244 @@
1
+ /**
2
+ * VoltJS React Server-Side Renderer
3
+ *
4
+ * Renders React components to HTML on the server using ReactDOMServer.
5
+ * Supports:
6
+ * - renderToString (full SSR with hydration support)
7
+ * - renderToStaticMarkup (static HTML, no React client overhead)
8
+ * - Layout wrapping with <head>, styles, scripts
9
+ * - Passing initial props/state to client for hydration
10
+ * - Component registry for file-based component discovery
11
+ *
12
+ * @example
13
+ * const { ReactRenderer } = require('./react-renderer');
14
+ * const renderer = new ReactRenderer();
15
+ *
16
+ * // Register a component
17
+ * renderer.register('App', AppComponent);
18
+ *
19
+ * // Render to full HTML page
20
+ * const html = renderer.renderPage('App', { tasks: [...] }, {
21
+ * title: 'My App',
22
+ * styles: ['/css/app.css'],
23
+ * });
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const React = require('react');
29
+ const ReactDOMServer = require('react-dom/server');
30
+
31
+ class ReactRenderer {
32
+ constructor(options = {}) {
33
+ this._components = new Map();
34
+ this._layouts = new Map();
35
+ this._defaultLayout = options.layout || null;
36
+ this._staticMode = options.static || false; // true = renderToStaticMarkup
37
+ this._doctype = options.doctype !== false;
38
+ }
39
+
40
+ /**
41
+ * Register a React component by name
42
+ * @param {string} name - Component name
43
+ * @param {React.Component|Function} component - React component
44
+ */
45
+ register(name, component) {
46
+ this._components.set(name, component);
47
+ return this;
48
+ }
49
+
50
+ /**
51
+ * Register a layout component
52
+ * @param {string} name - Layout name
53
+ * @param {React.Component|Function} layout - Layout component that accepts { children, ...props }
54
+ */
55
+ layout(name, layout) {
56
+ this._layouts.set(name, layout);
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Set the default layout
62
+ * @param {string} name - Layout name
63
+ */
64
+ setDefaultLayout(name) {
65
+ this._defaultLayout = name;
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Render a React element to HTML string
71
+ * @param {React.Element} element - React element to render
72
+ * @returns {string} HTML string
73
+ */
74
+ renderToString(element) {
75
+ if (this._staticMode) {
76
+ return ReactDOMServer.renderToStaticMarkup(element);
77
+ }
78
+ return ReactDOMServer.renderToString(element);
79
+ }
80
+
81
+ /**
82
+ * Render a registered component by name
83
+ * @param {string} name - Component name
84
+ * @param {object} props - Props to pass to the component
85
+ * @returns {string} HTML string
86
+ */
87
+ render(name, props = {}) {
88
+ const Component = this._components.get(name);
89
+ if (!Component) {
90
+ throw new Error(`React component "${name}" not registered. Use renderer.register('${name}', Component) first.`);
91
+ }
92
+ const element = React.createElement(Component, props);
93
+ return this.renderToString(element);
94
+ }
95
+
96
+ /**
97
+ * Render a component directly (not registered)
98
+ * @param {React.Component|Function} Component - React component
99
+ * @param {object} props - Props
100
+ * @returns {string} HTML string
101
+ */
102
+ renderComponent(Component, props = {}) {
103
+ const element = React.createElement(Component, props);
104
+ return this.renderToString(element);
105
+ }
106
+
107
+ /**
108
+ * Render a full HTML page with a React component as body
109
+ * @param {string|React.Component|Function} componentOrName - Component name or component itself
110
+ * @param {object} props - Props for the component
111
+ * @param {object} pageOptions - Page-level options
112
+ * @param {string} pageOptions.title - Page title
113
+ * @param {string[]} pageOptions.styles - CSS file paths
114
+ * @param {string[]} pageOptions.scripts - JS file paths
115
+ * @param {string} pageOptions.layout - Layout name to use
116
+ * @param {object} pageOptions.meta - Meta tags { name: content }
117
+ * @param {string} pageOptions.lang - HTML lang attribute
118
+ * @param {boolean} pageOptions.hydrate - Include hydration script + initial state
119
+ * @param {string} pageOptions.containerId - Root container ID (default: 'root')
120
+ * @param {string} pageOptions.inlineStyles - Inline CSS string
121
+ * @param {string} pageOptions.headHtml - Extra HTML to inject in <head>
122
+ * @returns {string} Full HTML document
123
+ */
124
+ renderPage(componentOrName, props = {}, pageOptions = {}) {
125
+ const {
126
+ title = 'VoltJS App',
127
+ styles = [],
128
+ scripts = [],
129
+ layout = this._defaultLayout,
130
+ meta = {},
131
+ lang = 'en',
132
+ hydrate = false,
133
+ containerId = 'root',
134
+ inlineStyles = '',
135
+ headHtml = '',
136
+ } = pageOptions;
137
+
138
+ // Resolve the component
139
+ let Component;
140
+ let componentName;
141
+ if (typeof componentOrName === 'string') {
142
+ Component = this._components.get(componentOrName);
143
+ componentName = componentOrName;
144
+ if (!Component) {
145
+ throw new Error(`React component "${componentOrName}" not registered.`);
146
+ }
147
+ } else {
148
+ Component = componentOrName;
149
+ componentName = Component.displayName || Component.name || 'Component';
150
+ }
151
+
152
+ // Create the React element
153
+ let element = React.createElement(Component, props);
154
+
155
+ // Wrap in layout if specified
156
+ if (layout) {
157
+ const Layout = this._layouts.get(layout);
158
+ if (Layout) {
159
+ element = React.createElement(Layout, { ...props, title }, element);
160
+ }
161
+ }
162
+
163
+ // Render to HTML
164
+ const bodyHtml = this.renderToString(element);
165
+
166
+ // Build meta tags
167
+ const metaTags = Object.entries(meta)
168
+ .map(([name, content]) => ` <meta name="${name}" content="${content}">`)
169
+ .join('\n');
170
+
171
+ // Build style links
172
+ const styleLinks = styles
173
+ .map(href => ` <link rel="stylesheet" href="${href}">`)
174
+ .join('\n');
175
+
176
+ // Build script tags
177
+ const scriptTags = scripts
178
+ .map(src => ` <script src="${src}"></script>`)
179
+ .join('\n');
180
+
181
+ // Build hydration script (passes initial props to client React)
182
+ let hydrationScript = '';
183
+ if (hydrate) {
184
+ const safeProps = JSON.stringify(props)
185
+ .replace(/</g, '\\u003c')
186
+ .replace(/>/g, '\\u003e')
187
+ .replace(/&/g, '\\u0026');
188
+ hydrationScript = `
189
+ <script>
190
+ window.__VOLT_INITIAL_STATE__ = ${safeProps};
191
+ window.__VOLT_COMPONENT__ = "${componentName}";
192
+ </script>`;
193
+ }
194
+
195
+ // Build the full document
196
+ const doc = `${this._doctype ? '<!DOCTYPE html>\n' : ''}<html lang="${lang}">
197
+ <head>
198
+ <meta charset="UTF-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
200
+ <title>${this._escapeHtml(title)}</title>
201
+ ${metaTags}${styleLinks}${inlineStyles ? `\n <style>\n${inlineStyles}\n </style>` : ''}${headHtml ? `\n${headHtml}` : ''}
202
+ </head>
203
+ <body>
204
+ <div id="${containerId}">${bodyHtml}</div>${hydrationScript}
205
+ ${scriptTags}
206
+ </body>
207
+ </html>`;
208
+
209
+ return doc;
210
+ }
211
+
212
+ /**
213
+ * Render a React element to a stream (for large pages)
214
+ * @param {React.Element} element - React element
215
+ * @returns {NodeJS.ReadableStream}
216
+ */
217
+ renderToStream(element) {
218
+ if (ReactDOMServer.renderToPipeableStream) {
219
+ // React 18+
220
+ return new Promise((resolve, reject) => {
221
+ const { pipe, abort } = ReactDOMServer.renderToPipeableStream(element, {
222
+ onShellReady() { resolve({ pipe, abort }); },
223
+ onError(err) { reject(err); },
224
+ });
225
+ });
226
+ }
227
+ // Fallback: renderToNodeStream (deprecated but available)
228
+ if (ReactDOMServer.renderToNodeStream) {
229
+ return ReactDOMServer.renderToNodeStream(element);
230
+ }
231
+ throw new Error('Streaming SSR not available in this React version.');
232
+ }
233
+
234
+ /** Escape HTML entities */
235
+ _escapeHtml(str) {
236
+ return String(str)
237
+ .replace(/&/g, '&amp;')
238
+ .replace(/</g, '&lt;')
239
+ .replace(/>/g, '&gt;')
240
+ .replace(/"/g, '&quot;');
241
+ }
242
+ }
243
+
244
+ module.exports = { ReactRenderer };