mastercontroller 1.2.12 → 1.2.14
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/.claude/settings.local.json +12 -0
- package/MasterAction.js +297 -73
- package/MasterControl.js +112 -19
- package/MasterHtml.js +101 -14
- package/MasterRouter.js +281 -66
- package/MasterTemplate.js +96 -3
- package/README.md +0 -44
- package/error/ErrorBoundary.js +353 -0
- package/error/HydrationMismatch.js +265 -0
- package/error/MasterBackendErrorHandler.js +769 -0
- package/{MasterError.js → error/MasterError.js} +2 -2
- package/error/MasterErrorHandler.js +487 -0
- package/error/MasterErrorLogger.js +360 -0
- package/error/MasterErrorMiddleware.js +407 -0
- package/error/SSRErrorHandler.js +273 -0
- package/monitoring/MasterCache.js +400 -0
- package/monitoring/MasterMemoryMonitor.js +188 -0
- package/monitoring/MasterProfiler.js +409 -0
- package/monitoring/PerformanceMonitor.js +233 -0
- package/package.json +3 -3
- package/security/CSPConfig.js +319 -0
- package/security/EventHandlerValidator.js +464 -0
- package/security/MasterSanitizer.js +429 -0
- package/security/MasterValidator.js +546 -0
- package/security/SecurityMiddleware.js +486 -0
- package/security/SessionSecurity.js +416 -0
- package/ssr/hydration-client.js +93 -0
- package/ssr/runtime-ssr.cjs +553 -0
- package/ssr/ssr-shims.js +73 -0
- package/examples/FileServingExample.js +0 -88
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// Vanilla Web Components SSR runtime using LinkeDOM
|
|
2
|
+
// - Executes connectedCallback() and child component upgrades on the server
|
|
3
|
+
// - No Enhance templates, no hardcoded per-component renderers
|
|
4
|
+
// - Returns full serialized HTML document string
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// const compileWebComponentsHTML = require('./ssr/runtime-ssr.cjs');
|
|
8
|
+
// const htmlOut = await compileWebComponentsHTML(inputHTML);
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const vm = require('vm');
|
|
13
|
+
const moduleCache = new Map();
|
|
14
|
+
|
|
15
|
+
// Error handling and monitoring
|
|
16
|
+
const { MasterControllerError, findSimilarStrings } = require('../error/MasterErrorHandler');
|
|
17
|
+
const { safeRenderComponent, validateSSRComponent, wrapConnectedCallback } = require('../error/SSRErrorHandler');
|
|
18
|
+
const { monitor } = require('../monitoring/PerformanceMonitor');
|
|
19
|
+
const { logger } = require('../error/MasterErrorLogger');
|
|
20
|
+
|
|
21
|
+
// Security - Sanitization and validation
|
|
22
|
+
const { sanitizer, sanitizeTemplateHTML, sanitizeProps } = require('../security/MasterSanitizer');
|
|
23
|
+
const { validateEventAttribute } = require('../security/EventHandlerValidator');
|
|
24
|
+
|
|
25
|
+
// Performance - Caching and profiling
|
|
26
|
+
const { cache } = require('../monitoring/MasterCache');
|
|
27
|
+
const { profiler } = require('../monitoring/MasterProfiler');
|
|
28
|
+
|
|
29
|
+
// Track registered custom elements to detect duplicates
|
|
30
|
+
const registeredElements = new Map();
|
|
31
|
+
|
|
32
|
+
// Development mode check
|
|
33
|
+
const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate @event attributes in HTML to prevent code injection
|
|
37
|
+
*/
|
|
38
|
+
function validateEventAttributes(html) {
|
|
39
|
+
if (!html || typeof html !== 'string') return;
|
|
40
|
+
|
|
41
|
+
// Find all @event attributes
|
|
42
|
+
const eventAttrRegex = /@([a-z][a-z0-9-]*)\s*=\s*["']([^"']*)["']/gi;
|
|
43
|
+
let match;
|
|
44
|
+
|
|
45
|
+
while ((match = eventAttrRegex.exec(html)) !== null) {
|
|
46
|
+
const attrName = `@${match[1]}`;
|
|
47
|
+
const attrValue = match[2];
|
|
48
|
+
|
|
49
|
+
// Validate the event handler expression
|
|
50
|
+
const validation = validateEventAttribute(attrName, attrValue, {
|
|
51
|
+
source: 'SSR',
|
|
52
|
+
location: 'runtime-ssr.cjs'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!validation.valid) {
|
|
56
|
+
logger.error({
|
|
57
|
+
code: 'MC_SECURITY_INVALID_EVENT_ATTR',
|
|
58
|
+
message: `Invalid @event attribute detected during SSR`,
|
|
59
|
+
attribute: attrName,
|
|
60
|
+
value: attrValue,
|
|
61
|
+
error: validation.error
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// In development, throw error to prevent bad code
|
|
65
|
+
if (isDevelopment) {
|
|
66
|
+
throw new MasterControllerError({
|
|
67
|
+
code: 'MC_SECURITY_INVALID_EVENT_ATTR',
|
|
68
|
+
message: `Invalid @event attribute: ${attrName}="${attrValue}"`,
|
|
69
|
+
details: validation.error.message,
|
|
70
|
+
suggestions: [
|
|
71
|
+
'Use only this.methodName or this.methodName() syntax',
|
|
72
|
+
'Avoid eval, Function, or other code execution patterns',
|
|
73
|
+
'Check event handler documentation'
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = async function compileWebComponentsHTML(inputHTML, preloadModules = []) {
|
|
82
|
+
// Start performance monitoring
|
|
83
|
+
monitor.startSession();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Defer loading until called (avoid polluting global state early)
|
|
87
|
+
const { parseHTML } = require('linkedom');
|
|
88
|
+
|
|
89
|
+
// Create a clean server DOM realm
|
|
90
|
+
const { window, document } = parseHTML('<!doctype html><html><head></head><body></body></html>');
|
|
91
|
+
|
|
92
|
+
// Register globals for custom element definitions to bind to this realm
|
|
93
|
+
globalThis.window = window;
|
|
94
|
+
globalThis.document = document;
|
|
95
|
+
|
|
96
|
+
// Wrap customElements.define to detect duplicates and validate components
|
|
97
|
+
const originalDefine = window.customElements.define.bind(window.customElements);
|
|
98
|
+
window.customElements.define = function(name, constructor, options) {
|
|
99
|
+
// Check for duplicate registration
|
|
100
|
+
if (registeredElements.has(name)) {
|
|
101
|
+
const existingFile = registeredElements.get(name);
|
|
102
|
+
const currentStack = new Error().stack;
|
|
103
|
+
|
|
104
|
+
const error = new MasterControllerError({
|
|
105
|
+
code: 'MC_ERR_DUPLICATE_ELEMENT',
|
|
106
|
+
message: `Duplicate custom element registration attempted`,
|
|
107
|
+
component: name,
|
|
108
|
+
file: existingFile,
|
|
109
|
+
details: `Element "${name}" is already registered. This will cause a browser error.\n\nPossible solutions:\n1. Rename one of the elements\n2. Remove duplicate import\n3. Check if you meant to import the existing component`,
|
|
110
|
+
context: { currentStack }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (isDevelopment) {
|
|
114
|
+
console.warn(error.format());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.warn({
|
|
118
|
+
code: error.code,
|
|
119
|
+
message: error.message,
|
|
120
|
+
component: name,
|
|
121
|
+
file: existingFile
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Don't throw - let browser handle it naturally
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Track registration
|
|
129
|
+
const stack = new Error().stack;
|
|
130
|
+
const fileMatch = stack.match(/at .*\((.+?):\d+:\d+\)/);
|
|
131
|
+
const filePath = fileMatch ? fileMatch[1] : 'unknown';
|
|
132
|
+
registeredElements.set(name, filePath);
|
|
133
|
+
|
|
134
|
+
// Validate component for SSR
|
|
135
|
+
if (isDevelopment) {
|
|
136
|
+
validateSSRComponent(constructor, name, filePath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Call original define
|
|
140
|
+
return originalDefine(name, constructor, options);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
globalThis.customElements = window.customElements;
|
|
144
|
+
globalThis.HTMLElement = window.HTMLElement;
|
|
145
|
+
globalThis.Node = window.Node;
|
|
146
|
+
globalThis.Element = window.Element;
|
|
147
|
+
globalThis.MutationObserver = window.MutationObserver;
|
|
148
|
+
|
|
149
|
+
// Minimal stubs used by some components during SSR (noop on server)
|
|
150
|
+
if (typeof globalThis.requestAnimationFrame === 'undefined') {
|
|
151
|
+
globalThis.requestAnimationFrame = (cb) => (typeof cb === 'function' ? cb(0) : undefined);
|
|
152
|
+
}
|
|
153
|
+
if (typeof globalThis.cancelAnimationFrame === 'undefined') {
|
|
154
|
+
globalThis.cancelAnimationFrame = () => {};
|
|
155
|
+
}
|
|
156
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
157
|
+
globalThis.ResizeObserver = class { observe(){} unobserve(){} disconnect(){} };
|
|
158
|
+
}
|
|
159
|
+
if (typeof globalThis.getComputedStyle === 'undefined') {
|
|
160
|
+
globalThis.getComputedStyle = () => ({ getPropertyValue: () => '' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create a VM context bound to the LinkeDOM globals
|
|
164
|
+
const context = vm.createContext({
|
|
165
|
+
window,
|
|
166
|
+
document,
|
|
167
|
+
customElements: window.customElements,
|
|
168
|
+
HTMLElement: window.HTMLElement,
|
|
169
|
+
Node: window.Node,
|
|
170
|
+
Element: window.Element,
|
|
171
|
+
MutationObserver: window.MutationObserver,
|
|
172
|
+
console,
|
|
173
|
+
globalThis: window,
|
|
174
|
+
setTimeout: window.setTimeout.bind(window),
|
|
175
|
+
clearTimeout: window.clearTimeout.bind(window),
|
|
176
|
+
setInterval: window.setInterval.bind(window),
|
|
177
|
+
clearInterval: window.clearInterval.bind(window),
|
|
178
|
+
});
|
|
179
|
+
context.__loaderResolve = resolveFile;
|
|
180
|
+
context.__loaderLoad = (p) => loadModuleESMCompatNew(p, context);
|
|
181
|
+
|
|
182
|
+
// Resolve ESM file path with basic extension fallback
|
|
183
|
+
function resolveFile(from, spec) {
|
|
184
|
+
const base = spec.startsWith('.') ? path.resolve(path.dirname(from), spec) : null;
|
|
185
|
+
if (!base) return null;
|
|
186
|
+
const candidates = [
|
|
187
|
+
base,
|
|
188
|
+
base + '.js',
|
|
189
|
+
base + '.mjs',
|
|
190
|
+
path.join(base, 'index.js'),
|
|
191
|
+
path.join(base, 'index.mjs'),
|
|
192
|
+
];
|
|
193
|
+
for (const file of candidates) {
|
|
194
|
+
try { if (fs.existsSync(file)) return file; } catch (_) {}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ESM compatibility loader that returns exports and preserves imports
|
|
200
|
+
function loadModuleESMCompatNew(absPath, ctx) {
|
|
201
|
+
if (!absPath) return {};
|
|
202
|
+
if (moduleCache.has(absPath)) return moduleCache.get(absPath);
|
|
203
|
+
let code = '';
|
|
204
|
+
try { code = fs.readFileSync(absPath, 'utf8'); }
|
|
205
|
+
catch (e) { console.warn('[SSR] Read failed:', absPath, e && e.message); return {}; }
|
|
206
|
+
|
|
207
|
+
// Transform import statements into __requireESM() bindings
|
|
208
|
+
code = code
|
|
209
|
+
// import default from 'spec'
|
|
210
|
+
.replace(/^[ \t]*import\s+([A-Za-z_$][\w$]*)\s+from\s+['"]([^'"]+)['"];?\s*$/mg, 'const $1 = __requireESM("$2").default;')
|
|
211
|
+
// import * as ns from 'spec'
|
|
212
|
+
.replace(/^[ \t]*import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]([^'"]+)['"];?\s*$/mg, 'const $1 = __requireESM("$2");')
|
|
213
|
+
// import { a, b as c } from 'spec'
|
|
214
|
+
.replace(/^[ \t]*import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"];?\s*$/mg, (m, g1, spec) => {
|
|
215
|
+
const mapped = g1.split(',').map(s => s.trim()).filter(Boolean).map(pair => pair.replace(/\s+as\s+/i, ': ')).join(', ');
|
|
216
|
+
return `const { ${mapped} } = __requireESM("${spec}");`;
|
|
217
|
+
})
|
|
218
|
+
// bare import 'spec'
|
|
219
|
+
.replace(/^[ \t]*import\s+['"]([^'"]+)['"];?\s*$/mg, '__requireESM("$1");');
|
|
220
|
+
|
|
221
|
+
// Transform export-from
|
|
222
|
+
code = code
|
|
223
|
+
.replace(/^[ \t]*export\s+\*\s+from\s+['"]([^'"]+)['"];?\s*$/mg, (m, spec) => `Object.assign(__exports, __requireESM("${spec}"));`)
|
|
224
|
+
.replace(/^[ \t]*export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"];?\s*$/mg, (m, names, spec) => {
|
|
225
|
+
return `(function(){ const __m = __requireESM("${spec}"); ${names.split(',').map(s => s.trim()).filter(Boolean).map(pair => {
|
|
226
|
+
if (pair.includes(' as ')) { const [orig, alias] = pair.split(/\s+as\s+/i).map(x=>x.trim()); return `__exports.${alias} = __m.${orig};`; }
|
|
227
|
+
else { return `__exports.${pair} = __m.${pair};`; }
|
|
228
|
+
}).join(' ')} })();`;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Transform standalone export { ... } (not from another module)
|
|
232
|
+
// Must match multi-line patterns like: export {\n Foo,\n Bar\n};
|
|
233
|
+
code = code.replace(/export\s*\{([^}]+)\}\s*;?/g, (m, names) => {
|
|
234
|
+
// Extract names and attach them to __exports
|
|
235
|
+
const assignments = names.split(',').map(s => s.trim()).filter(Boolean).map(pair => {
|
|
236
|
+
if (pair.includes(' as ')) {
|
|
237
|
+
const [orig, alias] = pair.split(/\s+as\s+/i).map(x=>x.trim());
|
|
238
|
+
return `__exports.${alias} = ${orig};`;
|
|
239
|
+
} else {
|
|
240
|
+
return `__exports.${pair} = ${pair};`;
|
|
241
|
+
}
|
|
242
|
+
}).join(' ');
|
|
243
|
+
return `(function(){ ${assignments} })();`;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Transform export default
|
|
247
|
+
code = code.replace(/^[ \t]*export\s+default\s+/mg, '__exports.default = ');
|
|
248
|
+
|
|
249
|
+
// Track names of exported declarations to attach to __exports after evaluation
|
|
250
|
+
const exportDeclNames = [];
|
|
251
|
+
(code.match(/^[ \t]*export\s+(?:const|let|var|function|class|async\s+function)\s+([A-Za-z_$][\w$]*)/mg) || []).forEach(line => {
|
|
252
|
+
const name = line.replace(/^[ \t]*export\s+(?:const|let|var|function|class|async\s+function)\s+([A-Za-z_$][\w$]*).*/,'$1');
|
|
253
|
+
exportDeclNames.push(name);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Transform export declarations into plain declarations
|
|
257
|
+
// Note: These regexes need to capture and re-emit the full declaration
|
|
258
|
+
code = code
|
|
259
|
+
.replace(/^[ \t]*export\s+(const\s+[A-Za-z_$][\w$]*\s*=)/mg, '$1')
|
|
260
|
+
.replace(/^[ \t]*export\s+(let\s+[A-Za-z_$][\w$]*\s*=)/mg, '$1')
|
|
261
|
+
.replace(/^[ \t]*export\s+(var\s+[A-Za-z_$][\w$]*\s*=)/mg, '$1')
|
|
262
|
+
.replace(/^[ \t]*export\s+(function\s+[A-Za-z_$][\w$]*\s*\()/mg, '$1')
|
|
263
|
+
.replace(/^[ \t]*export\s+(class\s+[A-Za-z_$][\w$]*\s+)/mg, '$1')
|
|
264
|
+
.replace(/^[ \t]*export\s+(async\s+function\s+[A-Za-z_$][\w$]*\s*\()/mg, '$1');
|
|
265
|
+
|
|
266
|
+
// Wrap entire module in IIFE to prevent __exports collision between modules
|
|
267
|
+
const wrappedCode = `
|
|
268
|
+
(function() {
|
|
269
|
+
const __exports = {};
|
|
270
|
+
const __modulePath = ${JSON.stringify(absPath)};
|
|
271
|
+
function __requireESM(spec) {
|
|
272
|
+
const resolved = __loaderResolve(__modulePath, spec);
|
|
273
|
+
return __loaderLoad(resolved);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
${code}
|
|
277
|
+
|
|
278
|
+
${exportDeclNames.map(n => `if (typeof ${n} !== 'undefined') __exports.${n} = ${n};`).join('\n')}
|
|
279
|
+
return __exports;
|
|
280
|
+
})()
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
let result;
|
|
284
|
+
try {
|
|
285
|
+
result = vm.runInContext(wrappedCode, ctx, { filename: absPath });
|
|
286
|
+
} catch (e) {
|
|
287
|
+
const error = new MasterControllerError({
|
|
288
|
+
code: 'MC_ERR_MODULE_LOAD',
|
|
289
|
+
message: `Module execution failed: ${e.message}`,
|
|
290
|
+
file: absPath,
|
|
291
|
+
originalError: e
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (isDevelopment) {
|
|
295
|
+
console.error(error.format());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
logger.error({
|
|
299
|
+
code: error.code,
|
|
300
|
+
message: error.message,
|
|
301
|
+
file: absPath,
|
|
302
|
+
originalError: e
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
result = {};
|
|
306
|
+
}
|
|
307
|
+
moduleCache.set(absPath, result);
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Minimal ESM->CJS execution for side-effect modules (customElements.define)
|
|
312
|
+
function loadModuleESMCompat(absPath, ctx, visited = new Set()) {
|
|
313
|
+
if (!absPath || visited.has(absPath)) return;
|
|
314
|
+
visited.add(absPath);
|
|
315
|
+
let code = '';
|
|
316
|
+
try { code = fs.readFileSync(absPath, 'utf8'); }
|
|
317
|
+
catch (e) { console.warn('[SSR] Read failed:', absPath, e && e.message); return; }
|
|
318
|
+
|
|
319
|
+
// Collect relative imports and export-froms, then preload them recursively
|
|
320
|
+
const deps = new Set();
|
|
321
|
+
const reFrom = /^\s*import\s+[^'"]+\s+from\s+['"]([^'"]+)['"];?/mg;
|
|
322
|
+
const reBare = /^\s*import\s+['"]([^'"]+)['"];?/mg;
|
|
323
|
+
const reExportFrom = /^\s*export\s+(?:\*\s+from|{[^}]*}\s+from)\s+['"]([^'"]+)['"];?/mg;
|
|
324
|
+
let m;
|
|
325
|
+
while ((m = reFrom.exec(code))) deps.add(m[1]);
|
|
326
|
+
while ((m = reBare.exec(code))) deps.add(m[1]);
|
|
327
|
+
while ((m = reExportFrom.exec(code))) deps.add(m[1]);
|
|
328
|
+
|
|
329
|
+
for (const spec of deps) {
|
|
330
|
+
if (!spec.startsWith('.')) continue; // skip bare specifiers
|
|
331
|
+
const resolved = resolveFile(absPath, spec);
|
|
332
|
+
if (resolved) loadModuleESMCompat(resolved, ctx, visited);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Strip import/export syntax; keep side effects (customElements.define)
|
|
336
|
+
// - Convert `export { ... } from '...';` into side-effect import
|
|
337
|
+
code = code
|
|
338
|
+
.replace(/^\s*import\s+[^;]+;?\s*$/mg, '')
|
|
339
|
+
.replace(/^\s*export\s+default\s+/mg, '')
|
|
340
|
+
.replace(/^\s*export\s+{[^}]*}\s+from\s+['"][^'"]+['"];?\s*$/mg, (m) => {
|
|
341
|
+
const spec = m.replace(/^[\s\S]*from\s+['"]([^'"]+)['"].*$/m, '$1');
|
|
342
|
+
return `import '${spec}';`;
|
|
343
|
+
})
|
|
344
|
+
.replace(/^\s*export\s+{[^}]*};?\s*$/mg, '')
|
|
345
|
+
.replace(/^\s*export\s+(class|function)\s+/mg, '$1 ')
|
|
346
|
+
.replace(/^\s*export\s+(const|let|var)\s+/mg, '$1 ')
|
|
347
|
+
.replace(/^\s*export\s+\*\s+from\s+['"]([^'"]+)['"];?\s*$/mg, (m, spec) => {
|
|
348
|
+
return `import '${spec}';`;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
vm.runInContext(code, ctx, { filename: absPath });
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.warn('[SSR] Exec failed:', absPath, e && e.message);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 1) Load component libraries into this realm so customElements.define runs
|
|
359
|
+
// (Import shad components first, then any route-specific modules)
|
|
360
|
+
try {
|
|
361
|
+
const root = process.cwd();
|
|
362
|
+
const indexFile = path.resolve(root, 'app/assets/javascripts/shad-web-components/index.js');
|
|
363
|
+
loadModuleESMCompatNew(indexFile, context);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.warn('[SSR] Failed to import components index:', e && e.message);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (Array.isArray(preloadModules)) {
|
|
369
|
+
for (const mod of preloadModules) {
|
|
370
|
+
if (!mod) continue;
|
|
371
|
+
try {
|
|
372
|
+
const abs = path.isAbsolute(mod) ? mod : path.resolve(process.cwd(), String(mod));
|
|
373
|
+
loadModuleESMCompatNew(abs, context);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.warn('[SSR] Failed to preload module:', mod, e && e.message);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 2) Extract <head> and <body> content from the input HTML (fallback to full)
|
|
381
|
+
const headMatch = String(inputHTML).match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
382
|
+
const bodyMatch = String(inputHTML).match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
383
|
+
const headHTML = headMatch ? headMatch[1] : '';
|
|
384
|
+
const bodyHTML = bodyMatch ? bodyMatch[1] : String(inputHTML);
|
|
385
|
+
|
|
386
|
+
// Security: Sanitize HTML before injection (remove dangerous tags/attributes)
|
|
387
|
+
const sanitizedHeadHTML = sanitizeTemplateHTML(headHTML);
|
|
388
|
+
const sanitizedBodyHTML = sanitizeTemplateHTML(bodyHTML);
|
|
389
|
+
|
|
390
|
+
// Security: Validate @event attributes
|
|
391
|
+
validateEventAttributes(sanitizedBodyHTML);
|
|
392
|
+
|
|
393
|
+
// 3) Inject markup AFTER definitions exist to ensure upgrades + connectedCallback
|
|
394
|
+
document.head.innerHTML = sanitizedHeadHTML;
|
|
395
|
+
document.body.innerHTML = sanitizedBodyHTML;
|
|
396
|
+
|
|
397
|
+
// 4) Manually invoke connectedCallback on all custom elements to ensure render() executes
|
|
398
|
+
const allElements = document.querySelectorAll('*');
|
|
399
|
+
for (const el of allElements) {
|
|
400
|
+
if (el.tagName && el.tagName.includes('-') && typeof el.connectedCallback === 'function') {
|
|
401
|
+
const componentName = el.tagName.toLowerCase();
|
|
402
|
+
const filePath = registeredElements.get(componentName) || 'unknown';
|
|
403
|
+
|
|
404
|
+
// Start profiling
|
|
405
|
+
const profile = profiler.startComponentRender(componentName);
|
|
406
|
+
|
|
407
|
+
// Check cache first
|
|
408
|
+
const cachedHTML = cache.getCachedRender(componentName, {});
|
|
409
|
+
if (cachedHTML && !isDevelopment) {
|
|
410
|
+
el.innerHTML = cachedHTML;
|
|
411
|
+
profiler.endComponentRender(profile);
|
|
412
|
+
monitor.recordComponent(componentName, 0, filePath);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Use safe render component wrapper
|
|
417
|
+
const renderStart = Date.now();
|
|
418
|
+
try {
|
|
419
|
+
el.connectedCallback();
|
|
420
|
+
const renderTime = Date.now() - renderStart;
|
|
421
|
+
|
|
422
|
+
// Cache the render output (if cacheable)
|
|
423
|
+
if (el.innerHTML && el.innerHTML.length > 0) {
|
|
424
|
+
cache.cacheRender(componentName, {}, el.innerHTML);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// End profiling
|
|
428
|
+
profiler.endComponentRender(profile);
|
|
429
|
+
|
|
430
|
+
// Track performance
|
|
431
|
+
monitor.recordComponent(componentName, renderTime, filePath);
|
|
432
|
+
|
|
433
|
+
} catch (e) {
|
|
434
|
+
const renderTime = Date.now() - renderStart;
|
|
435
|
+
|
|
436
|
+
// End profiling
|
|
437
|
+
profiler.endComponentRender(profile);
|
|
438
|
+
|
|
439
|
+
monitor.recordComponent(componentName, renderTime, filePath);
|
|
440
|
+
|
|
441
|
+
const error = new MasterControllerError({
|
|
442
|
+
code: 'MC_ERR_COMPONENT_RENDER_FAILED',
|
|
443
|
+
message: `connectedCallback failed: ${e.message}`,
|
|
444
|
+
component: componentName,
|
|
445
|
+
file: filePath,
|
|
446
|
+
originalError: e
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (isDevelopment) {
|
|
450
|
+
console.error(error.format());
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
logger.error({
|
|
454
|
+
code: error.code,
|
|
455
|
+
message: error.message,
|
|
456
|
+
component: componentName,
|
|
457
|
+
file: filePath,
|
|
458
|
+
originalError: e
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Replace with error/fallback UI
|
|
462
|
+
if (isDevelopment) {
|
|
463
|
+
el.innerHTML = error.toHTML();
|
|
464
|
+
} else {
|
|
465
|
+
const { renderFallback } = require('../error/SSRErrorHandler');
|
|
466
|
+
el.innerHTML = renderFallback(componentName);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 5) Serialize by walking childNodes (fixes LinkedOM innerHTML caching issue)
|
|
473
|
+
// When components use appendChild(), innerHTML doesn't update, but childNodes does
|
|
474
|
+
function serializeNode(node) {
|
|
475
|
+
if (node.nodeType === 3) return node.textContent; // Text node
|
|
476
|
+
if (node.nodeType === 8) return `<!--${node.textContent}-->`; // Comment
|
|
477
|
+
if (node.nodeType !== 1) return ''; // Skip other types
|
|
478
|
+
|
|
479
|
+
// Element node - serialize opening tag, children, closing tag
|
|
480
|
+
const tag = node.tagName.toLowerCase();
|
|
481
|
+
let html = `<${tag}`;
|
|
482
|
+
|
|
483
|
+
// Add attributes
|
|
484
|
+
if (node.attributes) {
|
|
485
|
+
for (const attr of node.attributes) {
|
|
486
|
+
const value = attr.value.replace(/"/g, '"');
|
|
487
|
+
html += ` ${attr.name}="${value}"`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Self-closing tags
|
|
492
|
+
const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
493
|
+
if (voidElements.includes(tag)) {
|
|
494
|
+
return html + '>';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
html += '>';
|
|
498
|
+
|
|
499
|
+
// Serialize children by walking childNodes (NOT innerHTML)
|
|
500
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
501
|
+
for (const child of node.childNodes) {
|
|
502
|
+
html += serializeNode(child);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
html += `</${tag}>`;
|
|
507
|
+
return html;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Serialize the full document
|
|
511
|
+
const htmlElement = document.documentElement;
|
|
512
|
+
const finalHTML = '<!DOCTYPE html>' + serializeNode(htmlElement);
|
|
513
|
+
|
|
514
|
+
// End performance monitoring and generate report
|
|
515
|
+
const perfReport = monitor.endSession();
|
|
516
|
+
|
|
517
|
+
// Log performance metrics
|
|
518
|
+
if (isDevelopment && perfReport) {
|
|
519
|
+
logger.info({
|
|
520
|
+
code: 'MC_INFO_SSR_COMPLETE',
|
|
521
|
+
message: 'SSR completed successfully',
|
|
522
|
+
context: {
|
|
523
|
+
totalTime: perfReport.totalTime,
|
|
524
|
+
componentCount: perfReport.componentCount,
|
|
525
|
+
averageRenderTime: perfReport.averageRenderTime
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return finalHTML;
|
|
531
|
+
|
|
532
|
+
} catch (e) {
|
|
533
|
+
const error = new MasterControllerError({
|
|
534
|
+
code: 'MC_ERR_SSR_RUNTIME',
|
|
535
|
+
message: `SSR runtime failed: ${e.message}`,
|
|
536
|
+
originalError: e,
|
|
537
|
+
details: 'The SSR runtime encountered a fatal error. Falling back to original HTML.'
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (isDevelopment) {
|
|
541
|
+
console.error(error.format());
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
logger.error({
|
|
545
|
+
code: error.code,
|
|
546
|
+
message: error.message,
|
|
547
|
+
originalError: e
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Fallback: return original input
|
|
551
|
+
return String(inputHTML);
|
|
552
|
+
}
|
|
553
|
+
};
|
package/ssr/ssr-shims.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SSR Shims for Browser-only APIs
|
|
2
|
+
// This module provides no-op implementations of browser APIs that don't exist in Node.js
|
|
3
|
+
// Used during server-side rendering to prevent errors when components reference browser APIs
|
|
4
|
+
|
|
5
|
+
if (typeof globalThis.window === 'undefined') {
|
|
6
|
+
globalThis.window = globalThis;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof globalThis.document === 'undefined') {
|
|
10
|
+
// LinkeDOM document gets assigned elsewhere (MasterWebComponent.js)
|
|
11
|
+
// but we ensure a placeholder so code referencing window.document doesn't crash.
|
|
12
|
+
globalThis.document = { createElement: () => ({}) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 🧭 ResizeObserver
|
|
16
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
17
|
+
globalThis.ResizeObserver = class {
|
|
18
|
+
constructor() {}
|
|
19
|
+
observe() {}
|
|
20
|
+
unobserve() {}
|
|
21
|
+
disconnect() {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 👀 IntersectionObserver
|
|
26
|
+
if (typeof globalThis.IntersectionObserver === 'undefined') {
|
|
27
|
+
globalThis.IntersectionObserver = class {
|
|
28
|
+
constructor() {}
|
|
29
|
+
observe() {}
|
|
30
|
+
unobserve() {}
|
|
31
|
+
disconnect() {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 🖥️ MatchMedia
|
|
36
|
+
if (typeof globalThis.matchMedia === 'undefined') {
|
|
37
|
+
globalThis.matchMedia = () => ({
|
|
38
|
+
matches: false,
|
|
39
|
+
addListener() {},
|
|
40
|
+
removeListener() {},
|
|
41
|
+
addEventListener() {},
|
|
42
|
+
removeEventListener() {}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 🕒 requestAnimationFrame & cancelAnimationFrame
|
|
47
|
+
if (typeof globalThis.requestAnimationFrame === 'undefined') {
|
|
48
|
+
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
|
49
|
+
}
|
|
50
|
+
if (typeof globalThis.cancelAnimationFrame === 'undefined') {
|
|
51
|
+
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 💡 getComputedStyle
|
|
55
|
+
if (typeof globalThis.getComputedStyle === 'undefined') {
|
|
56
|
+
globalThis.getComputedStyle = () => ({
|
|
57
|
+
getPropertyValue: () => ''
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 🧍 window.navigator shim
|
|
62
|
+
if (typeof globalThis.navigator === 'undefined') {
|
|
63
|
+
globalThis.navigator = { userAgent: 'ssr' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 📜 Common window methods that should be no-ops during SSR
|
|
67
|
+
['scrollTo', 'scrollBy', 'alert', 'confirm', 'focus', 'blur'].forEach(fn => {
|
|
68
|
+
if (typeof globalThis[fn] === 'undefined') {
|
|
69
|
+
globalThis[fn] = () => {};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export default {};
|