mastercontroller 1.2.11 → 1.2.13

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,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('../MasterErrorHandler');
17
+ const { safeRenderComponent, validateSSRComponent, wrapConnectedCallback } = require('./SSRErrorHandler');
18
+ const { monitor } = require('./PerformanceMonitor');
19
+ const { logger } = require('../MasterErrorLogger');
20
+
21
+ // Security - Sanitization and validation
22
+ const { sanitizer, sanitizeTemplateHTML, sanitizeProps } = require('../MasterSanitizer');
23
+ const { validateEventAttribute } = require('../EventHandlerValidator');
24
+
25
+ // Performance - Caching and profiling
26
+ const { cache } = require('../MasterCache');
27
+ const { profiler } = require('../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('./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, '&quot;');
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
+ };
@@ -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 {};