agentgui 1.0.730 → 1.0.732

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,83 @@
1
+ import { app, BrowserWindow, dialog } from 'electron';
2
+ import { spawn } from 'child_process';
3
+ import { fileURLToPath } from 'url';
4
+ import path from 'path';
5
+ import http from 'http';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const ROOT = path.join(__dirname, '..');
9
+ const PORT = process.env.PORT || 3000;
10
+ const BASE_URL = (process.env.BASE_URL || '/gm').replace(/\/+$/, '');
11
+ const APP_URL = `http://localhost:${PORT}${BASE_URL}/`;
12
+
13
+ let serverProcess = null;
14
+ let mainWindow = null;
15
+
16
+ function startServer() {
17
+ serverProcess = spawn(process.execPath, ['server.js'], {
18
+ cwd: ROOT,
19
+ env: { ...process.env, PORT: String(PORT) },
20
+ stdio: ['ignore', 'pipe', 'pipe'],
21
+ });
22
+ serverProcess.stdout.on('data', d => process.stdout.write(`[server] ${d}`));
23
+ serverProcess.stderr.on('data', d => process.stderr.write(`[server] ${d}`));
24
+ serverProcess.on('exit', code => {
25
+ if (code !== 0 && mainWindow) {
26
+ dialog.showErrorBox('Server exited', `AgentGUI server exited with code ${code}`);
27
+ }
28
+ });
29
+ }
30
+
31
+ function pollReady(retries = 40) {
32
+ return new Promise((resolve, reject) => {
33
+ function attempt(n) {
34
+ http.get(APP_URL, res => {
35
+ if (res.statusCode === 200 || res.statusCode === 401) return resolve();
36
+ if (n <= 0) return reject(new Error(`Server not ready after polling (last status: ${res.statusCode})`));
37
+ setTimeout(() => attempt(n - 1), 500);
38
+ }).on('error', err => {
39
+ if (n <= 0) return reject(new Error(`Server not reachable: ${err.message}`));
40
+ setTimeout(() => attempt(n - 1), 500);
41
+ });
42
+ }
43
+ attempt(retries);
44
+ });
45
+ }
46
+
47
+ function createWindow() {
48
+ mainWindow = new BrowserWindow({
49
+ width: 1280,
50
+ height: 800,
51
+ title: 'AgentGUI',
52
+ webPreferences: {
53
+ contextIsolation: true,
54
+ nodeIntegration: false,
55
+ },
56
+ });
57
+ mainWindow.loadURL(APP_URL);
58
+ mainWindow.on('closed', () => { mainWindow = null; });
59
+ }
60
+
61
+ app.whenReady().then(async () => {
62
+ startServer();
63
+ try {
64
+ await pollReady();
65
+ } catch (e) {
66
+ dialog.showErrorBox('Startup failed', e.message);
67
+ app.quit();
68
+ return;
69
+ }
70
+ createWindow();
71
+ app.on('activate', () => { if (!mainWindow) createWindow(); });
72
+ });
73
+
74
+ app.on('window-all-closed', () => {
75
+ if (process.platform !== 'darwin') app.quit();
76
+ });
77
+
78
+ app.on('will-quit', () => {
79
+ if (serverProcess) {
80
+ serverProcess.kill('SIGTERM');
81
+ serverProcess = null;
82
+ }
83
+ });
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.730",
3
+ "version": "1.0.732",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
- "main": "server.js",
6
+ "main": "electron/main.js",
7
7
  "bin": {
8
8
  "agentgui": "./bin/gmgui.cjs"
9
9
  },
@@ -18,7 +18,9 @@
18
18
  "scripts": {
19
19
  "start": "node server.js",
20
20
  "dev": "node server.js --watch",
21
- "postinstall": "node scripts/patch-fsbrowse.js && node scripts/copy-vendor.js"
21
+ "postinstall": "node scripts/patch-fsbrowse.js && node scripts/copy-vendor.js",
22
+ "electron": "electron electron/main.js",
23
+ "electron:dev": "PORT=3000 electron electron/main.js"
22
24
  },
23
25
  "dependencies": {
24
26
  "@agentclientprotocol/sdk": "^0.4.1",
@@ -41,6 +43,7 @@
41
43
  "p-retry": "^7.1.1",
42
44
  "pm2": "^5.4.3",
43
45
  "puppeteer-core": "^24.37.5",
46
+ "webjsx": "^0.0.73",
44
47
  "webtalk": "^1.0.31",
45
48
  "ws": "^8.14.2",
46
49
  "xstate": "^5.28.0",
@@ -54,5 +57,8 @@
54
57
  "sharp": "npm:empty-npm-package@1.0.0",
55
58
  "onnxruntime-common": "1.21.0",
56
59
  "onnxruntime-node": "1.21.0"
60
+ },
61
+ "devDependencies": {
62
+ "electron": "^35.0.0"
57
63
  }
58
64
  }
@@ -1,20 +1,50 @@
1
- #!/usr/bin/env node
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const root = path.join(__dirname, '..');
8
-
9
- const copies = [
10
- ['node_modules/xstate/dist/xstate.umd.min.js', 'static/lib/xstate.umd.min.js'],
11
- ];
12
-
13
- for (const [src, dest] of copies) {
14
- const srcPath = path.join(root, src);
15
- const destPath = path.join(root, dest);
16
- if (!fs.existsSync(srcPath)) { console.warn('[copy-vendor] not found:', src); continue; }
17
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
18
- fs.copyFileSync(srcPath, destPath);
19
- console.log('[copy-vendor] copied', src, '->', dest);
20
- }
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const root = path.join(__dirname, '..');
8
+
9
+ const copies = [
10
+ ['node_modules/xstate/dist/xstate.umd.min.js', 'static/lib/xstate.umd.min.js'],
11
+ ];
12
+
13
+ for (const [src, dest] of copies) {
14
+ const srcPath = path.join(root, src);
15
+ const destPath = path.join(root, dest);
16
+ if (!fs.existsSync(srcPath)) { console.warn('[copy-vendor] not found:', src); continue; }
17
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
18
+ fs.copyFileSync(srcPath, destPath);
19
+ console.log('[copy-vendor] copied', src, '->', dest);
20
+ }
21
+
22
+ // Build webjsx IIFE bundle from ESM dist files
23
+ const webjsxDist = path.join(root, 'node_modules/webjsx/dist');
24
+ if (fs.existsSync(webjsxDist)) {
25
+ const ORDER = ['constants', 'elementTags', 'utils', 'renderSuspension', 'attributes', 'createDOMElement', 'createElement', 'applyDiff', 'types'];
26
+
27
+ function stripModule(src) {
28
+ return src
29
+ .replace(/^import\s+.*?from\s+['"][^'"]+['"];?\s*$/gm, '')
30
+ .replace(/^export\s+(const|function|class|async\s+function)\s+/gm, '$1 ')
31
+ .replace(/^export\s+\{[^}]*\}(\s+from\s+['"][^'"]+['"])?\s*;?\s*$/gm, '')
32
+ .replace(/^export\s+\*\s+from\s+['"][^'"]+['"];?\s*$/gm, '')
33
+ .replace(/^\/\/#\s+sourceMappingURL=.*$/gm, '')
34
+ .trim();
35
+ }
36
+
37
+ const stripped = ORDER.map(name => {
38
+ const src = fs.readFileSync(path.join(webjsxDist, `${name}.js`), 'utf8');
39
+ return `// === ${name}.js ===\n${stripModule(src)}`;
40
+ });
41
+
42
+ const iife = `(function(window) {\n"use strict";\n\n${stripped.join('\n\n')}\n\nwindow.webjsx = { createElement, applyDiff, createDOMElement, Fragment };\n})(typeof window !== 'undefined' ? window : globalThis);\n`;
43
+
44
+ const dest = path.join(root, 'static/lib/webjsx.js');
45
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
46
+ fs.writeFileSync(dest, iife);
47
+ console.log('[copy-vendor] built webjsx IIFE ->', 'static/lib/webjsx.js', `(${iife.split('\n').length} lines)`);
48
+ } else {
49
+ console.warn('[copy-vendor] webjsx not found in node_modules — run npm install');
50
+ }
package/static/index.html CHANGED
@@ -3311,6 +3311,7 @@
3311
3311
  <script defer src="/gm/js/event-processor.js"></script>
3312
3312
  <script defer src="/gm/js/streaming-renderer.js"></script>
3313
3313
  <script defer src="/gm/js/image-loader.js"></script>
3314
+ <script defer src="/gm/lib/webjsx.js"></script>
3314
3315
  <script defer src="/gm/lib/xstate.umd.min.js"></script>
3315
3316
  <script defer src="/gm/js/ws-machine.js"></script>
3316
3317
  <script defer src="/gm/js/conv-machine.js"></script>
@@ -156,7 +156,7 @@ class ConversationManager {
156
156
 
157
157
  async fetchHomePath() {
158
158
  try {
159
- const res = await fetch(`${window.BASE_URL || '/gm'}/api/home`);
159
+ const res = await fetch(`${window.__BASE_URL || '/gm'}/api/home`);
160
160
  const data = await res.json();
161
161
  this.folderBrowser.homePath = data.home || '~';
162
162
  this.folderBrowser.cwdPath = data.cwd || null;
@@ -457,46 +457,10 @@ class ConversationManager {
457
457
  }
458
458
  }
459
459
 
460
- render() {
461
- if (!this.listEl) return;
462
-
463
- if (this.conversations.length === 0) {
464
- this.showEmpty();
465
- return;
466
- }
467
-
468
- this.emptyEl.style.display = 'none';
469
-
470
- const sorted = [...this.conversations].sort((a, b) =>
471
- new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
472
- );
473
-
474
- const existingMap = {};
475
- for (const child of Array.from(this.listEl.children)) {
476
- const cid = child.dataset.convId;
477
- if (cid) existingMap[cid] = child;
478
- }
479
-
480
- const frag = document.createDocumentFragment();
481
- for (const conv of sorted) {
482
- const existing = existingMap[conv.id];
483
- if (existing) {
484
- this.updateConversationItem(existing, conv);
485
- delete existingMap[conv.id];
486
- frag.appendChild(existing);
487
- } else {
488
- frag.appendChild(this.createConversationItem(conv));
489
- }
490
- }
491
-
492
- for (const orphan of Object.values(existingMap)) orphan.remove();
493
- this.listEl.appendChild(frag);
494
- }
495
-
496
- updateConversationItem(el, conv) {
460
+ _convVnode(conv) {
461
+ const h = window.webjsx?.createElement;
462
+ if (!h) return null;
497
463
  const isActive = conv.id === this.activeId;
498
- el.classList.toggle('active', isActive);
499
-
500
464
  const isStreaming = this.streamingConversations.has(conv.id);
501
465
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
502
466
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
@@ -505,53 +469,50 @@ class ConversationManager {
505
469
  const wd = conv.workingDirectory ? pathBasename(conv.workingDirectory) : '';
506
470
  const metaParts = [agent + modelLabel, timestamp];
507
471
  if (wd) metaParts.push(wd);
472
+ const badge = isStreaming
473
+ ? h('span', { class: 'conversation-streaming-badge', title: 'Streaming in progress' }, h('span', { class: 'streaming-dot' }))
474
+ : null;
475
+ return h('li', { class: 'conversation-item' + (isActive ? ' active' : ''), 'data-conv-id': conv.id },
476
+ h('div', { class: 'conversation-item-content' },
477
+ h('div', { class: 'conversation-item-title' }, ...(badge ? [badge, title] : [title])),
478
+ h('div', { class: 'conversation-item-meta' }, metaParts.join(' \u2022 '))
479
+ ),
480
+ h('button', { class: 'conversation-item-delete', title: 'Delete conversation', 'data-delete-conv': conv.id },
481
+ h('svg', { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '2' },
482
+ h('polyline', { points: '3 6 5 6 21 6' }),
483
+ h('path', { d: 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' })
484
+ )
485
+ )
486
+ );
487
+ }
508
488
 
509
- const titleEl = el.querySelector('.conversation-item-title');
510
- if (titleEl) {
511
- const badgeHtml = isStreaming
512
- ? '<span class="conversation-streaming-badge" title="Streaming in progress"><span class="streaming-dot"></span></span>'
513
- : '';
514
- titleEl.innerHTML = `${badgeHtml}${this.escapeHtml(title)}`;
489
+ render() {
490
+ if (!this.listEl) return;
491
+ if (this.conversations.length === 0) { this.showEmpty(); return; }
492
+ this.emptyEl.style.display = 'none';
493
+ const sorted = [...this.conversations].sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
494
+ if (window.webjsx?.applyDiff) {
495
+ window.webjsx.applyDiff(this.listEl, sorted.map(conv => this._convVnode(conv)).filter(Boolean));
496
+ } else {
497
+ this._renderFallback(sorted);
515
498
  }
516
-
517
- const metaEl = el.querySelector('.conversation-item-meta');
518
- if (metaEl) metaEl.textContent = metaParts.join(' \u2022 ');
519
499
  }
520
500
 
521
- createConversationItem(conv) {
522
- const li = document.createElement('li');
523
- li.className = 'conversation-item';
524
- li.dataset.convId = conv.id;
525
- if (conv.id === this.activeId) li.classList.add('active');
526
-
527
- const isStreaming = this.streamingConversations.has(conv.id);
528
-
529
- const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
530
- const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
531
- const agent = this.getAgentDisplayName(conv.agentId || conv.agentType);
532
- const modelLabel = this.formatModelLabel(conv.model);
533
- const wd = conv.workingDirectory ? pathBasename(conv.workingDirectory) : '';
534
- const metaParts = [agent + modelLabel, timestamp];
535
- if (wd) metaParts.push(wd);
536
-
537
- const streamingBadge = isStreaming
538
- ? '<span class="conversation-streaming-badge" title="Streaming in progress"><span class="streaming-dot"></span></span>'
539
- : '';
540
-
541
- li.innerHTML = `
542
- <div class="conversation-item-content">
543
- <div class="conversation-item-title">${streamingBadge}${this.escapeHtml(title)}</div>
544
- <div class="conversation-item-meta">${metaParts.join(' • ')}</div>
545
- </div>
546
- <button class="conversation-item-delete" title="Delete conversation" data-delete-conv="${conv.id}">
547
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
548
- <polyline points="3 6 5 6 21 6"></polyline>
549
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
550
- </svg>
551
- </button>
552
- `;
553
-
554
- return li;
501
+ _renderFallback(sorted) {
502
+ const existingMap = {};
503
+ for (const child of Array.from(this.listEl.children)) {
504
+ if (child.dataset.convId) existingMap[child.dataset.convId] = child;
505
+ }
506
+ const frag = document.createDocumentFragment();
507
+ for (const conv of sorted) {
508
+ const el = existingMap[conv.id] || document.createElement('li');
509
+ el.className = 'conversation-item' + (conv.id === this.activeId ? ' active' : '');
510
+ el.dataset.convId = conv.id;
511
+ delete existingMap[conv.id];
512
+ frag.appendChild(el);
513
+ }
514
+ for (const orphan of Object.values(existingMap)) orphan.remove();
515
+ this.listEl.appendChild(frag);
555
516
  }
556
517
 
557
518
  async confirmDelete(convId, title) {
@@ -42,6 +42,8 @@
42
42
  },
43
43
 
44
44
  renderToolCard: function(tool, isRefreshing) {
45
+ var h = window.webjsx && window.webjsx.createElement;
46
+ if (!h) return null;
45
47
  var ui = window.toolsManagerUI;
46
48
  var statusClass = ui.getStatusClass(tool);
47
49
  var ms = window.toolInstallMachineAPI ? window.toolInstallMachineAPI.getState(tool.id) : null;
@@ -53,26 +55,29 @@
53
55
  var canUpdate = ms === 'needs_update' || tool.hasUpdate || tool.status === 'needs_update';
54
56
  var iv = (snap && snap.context.installedVersion) || tool.installedVersion;
55
57
  var pv = (snap && snap.context.publishedVersion) || tool.publishedVersion;
56
- var versionInfo = '';
57
- if (iv || pv) {
58
- versionInfo = '<div class="tool-versions">';
59
- if (iv) versionInfo += '<span class="tool-version-item">v' + ui.esc(iv) + '</span>';
60
- if (pv && iv !== pv) versionInfo += '<span class="tool-version-item">(v' + ui.esc(pv) + ' available)</span>';
61
- versionInfo += '</div>';
62
- }
63
- return '<div class="tool-item">' +
64
- '<div style="display: flex; flex-direction: column; gap: 0.3rem;">' +
65
- '<div class="tool-header"><span class="tool-name">' + ui.esc(tool.name || tool.id) + '</span></div>' +
66
- '<div class="tool-status-indicator ' + statusClass + '"><span class="tool-status-dot"></span><span>' + ui.getStatusText(tool) + '</span></div>' +
67
- versionInfo +
68
- (isInstalling && progress !== undefined ? '<div class="tool-progress-container"><div class="tool-progress-bar"><div class="tool-progress-fill" style="width:' + Math.min(progress, 100) + '%"></div></div></div>' : '') +
69
- (tool.error_message ? '<div class="tool-error-message">Error: ' + ui.esc(tool.error_message.substring(0, 40)) + '</div>' : '') +
70
- '</div>' +
71
- '<div class="tool-actions">' +
72
- (canInstall ? '<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (locked ? 'disabled' : '') + '>Install</button>' :
73
- canUpdate ? '<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.update(\'' + tool.id + '\')" ' + (locked ? 'disabled' : '') + '>Update</button>' :
74
- '<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>&#10003;</button>') +
75
- '</div></div>';
58
+ var versionChildren = [];
59
+ if (iv) versionChildren.push(h('span', { class: 'tool-version-item' }, 'v' + iv));
60
+ if (pv && iv !== pv) versionChildren.push(h('span', { class: 'tool-version-item' }, '(v' + pv + ' available)'));
61
+ var actionBtn = canInstall
62
+ ? h('button', { class: 'tool-btn tool-btn-primary', disabled: locked, onClick: function() { window.toolsManager.install(tool.id); } }, 'Install')
63
+ : canUpdate
64
+ ? h('button', { class: 'tool-btn tool-btn-primary', disabled: locked, onClick: function() { window.toolsManager.update(tool.id); } }, 'Update')
65
+ : h('button', { class: 'tool-btn tool-btn-secondary', disabled: isRefreshing, onClick: function() { window.toolsManager.refresh(); } }, '\u2713');
66
+ return h('div', { class: 'tool-item' },
67
+ h('div', { style: 'display:flex;flex-direction:column;gap:0.3rem;' },
68
+ h('div', { class: 'tool-header' }, h('span', { class: 'tool-name' }, tool.name || tool.id)),
69
+ h('div', { class: 'tool-status-indicator ' + statusClass },
70
+ h('span', { class: 'tool-status-dot' }),
71
+ h('span', {}, ui.getStatusText(tool))
72
+ ),
73
+ versionChildren.length ? h('div', { class: 'tool-versions' }, ...versionChildren) : null,
74
+ isInstalling && progress !== undefined
75
+ ? h('div', { class: 'tool-progress-container' }, h('div', { class: 'tool-progress-bar' }, h('div', { class: 'tool-progress-fill', style: 'width:' + Math.min(progress, 100) + '%' })))
76
+ : null,
77
+ tool.error_message ? h('div', { class: 'tool-error-message' }, 'Error: ' + tool.error_message.substring(0, 40)) : null
78
+ ),
79
+ h('div', { class: 'tool-actions' }, actionBtn)
80
+ );
76
81
  },
77
82
 
78
83
  esc: function(s) {
@@ -124,14 +124,22 @@
124
124
  function render() {
125
125
  var scroll = popup.querySelector('.tools-popup-scroll');
126
126
  if (!scroll) return;
127
- if (!tools.length) { scroll.innerHTML = '<div class="tool-empty-state" style="grid-column: 1 / -1;"><div class="tool-empty-state-text">No tools available</div></div>'; return; }
128
- var cli = tools.filter(t => t.category === 'cli'), plugin = tools.filter(t => t.category === 'plugin'), other = tools.filter(t => !t.category);
127
+ var h = window.webjsx && window.webjsx.createElement;
128
+ var applyDiff = window.webjsx && window.webjsx.applyDiff;
129
129
  var ui = window.toolsManagerUI;
130
- var html = '';
131
- if (cli.length) html += '<div class="tool-section-header">CLI Agents</div>' + cli.map(t => ui.renderToolCard(t, isRefreshing)).join('');
132
- if (plugin.length) html += '<div class="tool-section-header">GM Plugins</div>' + plugin.map(t => ui.renderToolCard(t, isRefreshing)).join('');
133
- if (other.length) html += other.map(t => ui.renderToolCard(t, isRefreshing)).join('');
134
- scroll.innerHTML = html;
130
+ if (!h || !applyDiff) { scroll.innerHTML = '<div class="tool-empty-state"><div class="tool-empty-state-text">Loading...</div></div>'; return; }
131
+ if (!tools.length) {
132
+ applyDiff(scroll, [h('div', { class: 'tool-empty-state', style: 'grid-column:1/-1;' }, h('div', { class: 'tool-empty-state-text' }, 'No tools available'))]);
133
+ return;
134
+ }
135
+ var cli = tools.filter(t => t.category === 'cli');
136
+ var plugin = tools.filter(t => t.category === 'plugin');
137
+ var other = tools.filter(t => !t.category);
138
+ var vnodes = [];
139
+ if (cli.length) vnodes.push(h('div', { class: 'tool-section-header' }, 'CLI Agents'), ...cli.map(t => ui.renderToolCard(t, isRefreshing)).filter(Boolean));
140
+ if (plugin.length) vnodes.push(h('div', { class: 'tool-section-header' }, 'GM Plugins'), ...plugin.map(t => ui.renderToolCard(t, isRefreshing)).filter(Boolean));
141
+ if (other.length) vnodes.push(...other.map(t => ui.renderToolCard(t, isRefreshing)).filter(Boolean));
142
+ applyDiff(scroll, vnodes);
135
143
  }
136
144
 
137
145
  function togglePopup(e) { e.stopPropagation(); if (!popup.classList.contains('open')) { isRefreshing = false; refresh(); } popup.classList.toggle('open'); }
@@ -0,0 +1,700 @@
1
+ (function(window) {
2
+ "use strict";
3
+
4
+ // === constants.js ===
5
+ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
6
+ const MATH_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
7
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
8
+
9
+ // === elementTags.js ===
10
+ // Create a Map for faster lookups
11
+ const KNOWN_ELEMENTS = new Map(Object.entries({
12
+ a: "A",
13
+ abbr: "ABBR",
14
+ address: "ADDRESS",
15
+ area: "AREA",
16
+ article: "ARTICLE",
17
+ aside: "ASIDE",
18
+ audio: "AUDIO",
19
+ b: "B",
20
+ base: "BASE",
21
+ bdi: "BDI",
22
+ bdo: "BDO",
23
+ blockquote: "BLOCKQUOTE",
24
+ body: "BODY",
25
+ br: "BR",
26
+ button: "BUTTON",
27
+ canvas: "CANVAS",
28
+ caption: "CAPTION",
29
+ cite: "CITE",
30
+ code: "CODE",
31
+ col: "COL",
32
+ colgroup: "COLGROUP",
33
+ data: "DATA",
34
+ datalist: "DATALIST",
35
+ dd: "DD",
36
+ del: "DEL",
37
+ details: "DETAILS",
38
+ dfn: "DFN",
39
+ dialog: "DIALOG",
40
+ div: "DIV",
41
+ dl: "DL",
42
+ dt: "DT",
43
+ em: "EM",
44
+ embed: "EMBED",
45
+ fieldset: "FIELDSET",
46
+ figcaption: "FIGCAPTION",
47
+ figure: "FIGURE",
48
+ footer: "FOOTER",
49
+ form: "FORM",
50
+ h1: "H1",
51
+ h2: "H2",
52
+ h3: "H3",
53
+ h4: "H4",
54
+ h5: "H5",
55
+ h6: "H6",
56
+ head: "HEAD",
57
+ header: "HEADER",
58
+ hgroup: "HGROUP",
59
+ hr: "HR",
60
+ html: "HTML",
61
+ i: "I",
62
+ iframe: "IFRAME",
63
+ img: "IMG",
64
+ input: "INPUT",
65
+ ins: "INS",
66
+ kbd: "KBD",
67
+ label: "LABEL",
68
+ legend: "LEGEND",
69
+ li: "LI",
70
+ link: "LINK",
71
+ main: "MAIN",
72
+ map: "MAP",
73
+ mark: "MARK",
74
+ menu: "MENU",
75
+ meta: "META",
76
+ meter: "METER",
77
+ nav: "NAV",
78
+ noscript: "NOSCRIPT",
79
+ object: "OBJECT",
80
+ ol: "OL",
81
+ optgroup: "OPTGROUP",
82
+ option: "OPTION",
83
+ output: "OUTPUT",
84
+ p: "P",
85
+ picture: "PICTURE",
86
+ pre: "PRE",
87
+ progress: "PROGRESS",
88
+ q: "Q",
89
+ rp: "RP",
90
+ rt: "RT",
91
+ ruby: "RUBY",
92
+ s: "S",
93
+ samp: "SAMP",
94
+ script: "SCRIPT",
95
+ section: "SECTION",
96
+ select: "SELECT",
97
+ slot: "SLOT",
98
+ small: "SMALL",
99
+ source: "SOURCE",
100
+ span: "SPAN",
101
+ strong: "STRONG",
102
+ style: "STYLE",
103
+ sub: "SUB",
104
+ summary: "SUMMARY",
105
+ sup: "SUP",
106
+ table: "TABLE",
107
+ tbody: "TBODY",
108
+ td: "TD",
109
+ template: "TEMPLATE",
110
+ textarea: "TEXTAREA",
111
+ tfoot: "TFOOT",
112
+ th: "TH",
113
+ thead: "THEAD",
114
+ time: "TIME",
115
+ title: "TITLE",
116
+ tr: "TR",
117
+ track: "TRACK",
118
+ u: "U",
119
+ ul: "UL",
120
+ var: "VAR",
121
+ video: "VIDEO",
122
+ wbr: "WBR",
123
+ }));
124
+
125
+ // === utils.js ===
126
+ /**
127
+ * Flattens nested virtual nodes by replacing Fragments with their children.
128
+ * @param vnodes Virtual nodes to flatten
129
+ * @returns Array of flattened virtual nodes
130
+ */
131
+ function flattenVNodes(vnodes, result = []) {
132
+ if (Array.isArray(vnodes)) {
133
+ for (const vnode of vnodes) {
134
+ flattenVNodes(vnode, result);
135
+ }
136
+ }
137
+ else if (isValidVNode(vnodes)) {
138
+ result.push(vnodes);
139
+ }
140
+ return result;
141
+ }
142
+ function isValidVNode(vnode) {
143
+ const typeofVNode = typeof vnode;
144
+ return (vnode !== null &&
145
+ vnode !== undefined &&
146
+ (typeofVNode === "string" ||
147
+ typeofVNode === "object" ||
148
+ typeofVNode === "number" ||
149
+ typeofVNode === "bigint"));
150
+ }
151
+ /* Get Child Nodes Efficiently */
152
+ function getChildNodes(parent) {
153
+ const nodes = [];
154
+ let current = parent.firstChild;
155
+ while (current) {
156
+ nodes.push(current);
157
+ current = current.nextSibling;
158
+ }
159
+ return nodes;
160
+ }
161
+ /**
162
+ * Assigns a ref to a DOM node.
163
+ * @param node Target DOM node
164
+ * @param ref Reference to assign (function or object with current property)
165
+ */
166
+ function assignRef(node, ref) {
167
+ if (typeof ref === "function") {
168
+ ref(node);
169
+ }
170
+ else if (ref && typeof ref === "object") {
171
+ ref.current = node;
172
+ }
173
+ }
174
+ function isVElement(vnode) {
175
+ const typeofVNode = typeof vnode;
176
+ return (typeofVNode !== "string" &&
177
+ typeofVNode !== "number" &&
178
+ typeofVNode !== "bigint");
179
+ }
180
+ function isNonBooleanPrimitive(vnode) {
181
+ const typeofVNode = typeof vnode;
182
+ return (typeofVNode === "string" ||
183
+ typeofVNode === "number" ||
184
+ typeofVNode === "bigint");
185
+ }
186
+ function getNamespaceURI(node) {
187
+ return node instanceof Element && node.namespaceURI !== HTML_NAMESPACE
188
+ ? node.namespaceURI ?? undefined
189
+ : undefined;
190
+ }
191
+ function setWebJSXProps(element, props) {
192
+ element.__webjsx_props = props;
193
+ }
194
+ function getWebJSXProps(element) {
195
+ let props = element.__webjsx_props;
196
+ if (!props) {
197
+ props = {};
198
+ element.__webjsx_props = props;
199
+ }
200
+ return props;
201
+ }
202
+ function setWebJSXChildNodeCache(element, childNodes) {
203
+ element.__webjsx_childNodes = childNodes;
204
+ }
205
+ function getWebJSXChildNodeCache(element) {
206
+ return element.__webjsx_childNodes;
207
+ }
208
+
209
+ // === renderSuspension.js ===
210
+ function definesRenderSuspension(el) {
211
+ return !!el.__webjsx_suspendRendering;
212
+ }
213
+ /**
214
+ * Executes a callback with render suspension handling.
215
+ * @param el Element that may have render suspension
216
+ * @param callback Function to execute during suspension
217
+ * @returns Result of the callback
218
+ */
219
+ function withRenderSuspension(el, callback) {
220
+ const isRenderingSuspended = !!el
221
+ .__webjsx_suspendRendering;
222
+ if (isRenderingSuspended) {
223
+ el.__webjsx_suspendRendering();
224
+ }
225
+ try {
226
+ return callback();
227
+ }
228
+ finally {
229
+ if (isRenderingSuspended) {
230
+ el.__webjsx_resumeRendering();
231
+ }
232
+ }
233
+ }
234
+
235
+ // === attributes.js ===
236
+ /* eslint-disable @typescript-eslint/no-explicit-any */
237
+
238
+ /**
239
+ * Updates an event listener on an element.
240
+ * @param el Target element
241
+ * @param eventName Name of the event (without 'on' prefix)
242
+ * @param newHandler New event handler function
243
+ * @param oldHandler Previous event handler function
244
+ */
245
+ function updateEventListener(el, eventName, newHandler, oldHandler) {
246
+ if (oldHandler && oldHandler !== newHandler) {
247
+ el.removeEventListener(eventName, oldHandler);
248
+ }
249
+ if (newHandler && oldHandler !== newHandler) {
250
+ el.addEventListener(eventName, newHandler);
251
+ el.__webjsx_listeners =
252
+ el.__webjsx_listeners ?? {};
253
+ el.__webjsx_listeners[eventName] = newHandler;
254
+ }
255
+ }
256
+ /**
257
+ * Updates a single property or attribute on an element.
258
+ * @param el Target element
259
+ * @param key Property or attribute name
260
+ * @param value New value to set
261
+ */
262
+ function updatePropOrAttr(el, key, value) {
263
+ if (el instanceof HTMLElement) {
264
+ if (key in el) {
265
+ // Fast path: property exists on HTMLElement
266
+ el[key] = value;
267
+ return;
268
+ }
269
+ if (typeof value === "string") {
270
+ el.setAttribute(key, value);
271
+ return;
272
+ }
273
+ // Fallback for non-string values on HTMLElement
274
+ el[key] = value;
275
+ return;
276
+ }
277
+ // SVG/Other namespace elements
278
+ const isSVG = el.namespaceURI === "http://www.w3.org/2000/svg";
279
+ if (isSVG) {
280
+ if (value !== undefined && value !== null) {
281
+ el.setAttribute(key, `${value}`);
282
+ }
283
+ else {
284
+ el.removeAttribute(key);
285
+ }
286
+ return;
287
+ }
288
+ // Fallback for other element types
289
+ if (typeof value === "string") {
290
+ el.setAttribute(key, value);
291
+ }
292
+ else {
293
+ el[key] = value;
294
+ }
295
+ }
296
+ /**
297
+ * Updates all attributes and properties on a DOM element.
298
+ * @param el Target element
299
+ * @param newProps New properties to apply
300
+ * @param oldProps Previous properties for comparison (default empty object)
301
+ */
302
+ function updateAttributesCore(el, newProps, oldProps = {}) {
303
+ // Handle new/updated props
304
+ for (const key of Object.keys(newProps)) {
305
+ const value = newProps[key];
306
+ if (key === "children" ||
307
+ key === "key" ||
308
+ key === "dangerouslySetInnerHTML" ||
309
+ key === "nodes")
310
+ continue;
311
+ if (key.startsWith("on") && typeof value === "function") {
312
+ const eventName = key.substring(2).toLowerCase();
313
+ updateEventListener(el, eventName, value, el.__webjsx_listeners?.[eventName]);
314
+ }
315
+ else if (value !== oldProps[key]) {
316
+ updatePropOrAttr(el, key, value);
317
+ }
318
+ }
319
+ // Handle dangerouslySetInnerHTML
320
+ if (newProps.dangerouslySetInnerHTML) {
321
+ if (!oldProps.dangerouslySetInnerHTML ||
322
+ newProps.dangerouslySetInnerHTML.__html !==
323
+ oldProps.dangerouslySetInnerHTML.__html) {
324
+ const html = newProps.dangerouslySetInnerHTML?.__html || "";
325
+ el.innerHTML = html;
326
+ }
327
+ }
328
+ else {
329
+ if (oldProps.dangerouslySetInnerHTML) {
330
+ el.innerHTML = "";
331
+ }
332
+ }
333
+ // Remove old props/attributes
334
+ for (const key of Object.keys(oldProps)) {
335
+ if (!(key in newProps) &&
336
+ key !== "children" &&
337
+ key !== "key" &&
338
+ key !== "dangerouslySetInnerHTML" &&
339
+ key !== "nodes") {
340
+ if (key.startsWith("on")) {
341
+ const eventName = key.substring(2).toLowerCase();
342
+ const existingListener = el
343
+ .__webjsx_listeners?.[eventName];
344
+ if (existingListener) {
345
+ el.removeEventListener(eventName, existingListener);
346
+ delete el.__webjsx_listeners[eventName];
347
+ }
348
+ }
349
+ else if (key in el) {
350
+ el[key] = undefined;
351
+ }
352
+ else {
353
+ el.removeAttribute(key);
354
+ }
355
+ }
356
+ }
357
+ }
358
+ /**
359
+ * Sets initial attributes and properties on a DOM element.
360
+ * @param el Target element
361
+ * @param props Properties to apply
362
+ */
363
+ function setAttributes(el, props) {
364
+ if (definesRenderSuspension(el)) {
365
+ withRenderSuspension(el, () => {
366
+ updateAttributesCore(el, props);
367
+ });
368
+ }
369
+ else {
370
+ updateAttributesCore(el, props);
371
+ }
372
+ }
373
+ /**
374
+ * Updates existing attributes and properties on a DOM element.
375
+ * @param el Target element
376
+ * @param newProps New properties to apply
377
+ * @param oldProps Previous properties for comparison
378
+ */
379
+ function updateAttributes(el, newProps, oldProps) {
380
+ if (definesRenderSuspension(el)) {
381
+ withRenderSuspension(el, () => {
382
+ updateAttributesCore(el, newProps, oldProps);
383
+ });
384
+ }
385
+ else {
386
+ updateAttributesCore(el, newProps, oldProps);
387
+ }
388
+ }
389
+
390
+ // === createDOMElement.js ===
391
+ /**
392
+ * Creates a real DOM node from a virtual node representation.
393
+ * @param velement Virtual node to convert
394
+ * @param parentNamespaceURI Namespace URI from parent element, if any
395
+ * @returns Created DOM node
396
+ */
397
+ function createDOMElement(velement, parentNamespaceURI) {
398
+ const namespaceURI = velement.props.xmlns !== undefined
399
+ ? velement.props.xmlns
400
+ : velement.type === "svg"
401
+ ? SVG_NAMESPACE
402
+ : parentNamespaceURI ?? undefined;
403
+ const el = velement.props.is !== undefined
404
+ ? namespaceURI !== undefined
405
+ ? document.createElementNS(namespaceURI, velement.type, {
406
+ is: velement.props.is,
407
+ })
408
+ : document.createElement(velement.type, {
409
+ is: velement.props.is,
410
+ })
411
+ : namespaceURI !== undefined
412
+ ? document.createElementNS(namespaceURI, velement.type)
413
+ : document.createElement(velement.type);
414
+ if (velement.props) {
415
+ setAttributes(el, velement.props);
416
+ }
417
+ if (velement.props.key !== undefined) {
418
+ el.__webjsx_key = velement.props.key;
419
+ }
420
+ if (velement.props.ref) {
421
+ assignRef(el, velement.props.ref);
422
+ }
423
+ if (velement.props.children && !velement.props.dangerouslySetInnerHTML) {
424
+ const children = velement.props.children;
425
+ const nodes = [];
426
+ for (let i = 0; i < children.length; i++) {
427
+ const child = children[i];
428
+ const node = isVElement(child)
429
+ ? createDOMElement(child, namespaceURI)
430
+ : document.createTextNode(`${child}`);
431
+ nodes.push(node);
432
+ el.appendChild(node);
433
+ }
434
+ setWebJSXProps(el, velement.props);
435
+ setWebJSXChildNodeCache(el, nodes);
436
+ }
437
+ return el;
438
+ }
439
+
440
+ // === createElement.js ===
441
+ /**
442
+ * Creates a virtual element representing a DOM node or Fragment.
443
+ * @param type Element type (tag name) or Fragment
444
+ * @param props Properties and attributes for the element
445
+ * @param children Child elements or content
446
+ * @returns Virtual element representation
447
+ */
448
+ function createElement(type, props, ...children) {
449
+ if (typeof type === "string") {
450
+ const normalizedProps = props ? props : {};
451
+ const flatChildren = flattenVNodes(children);
452
+ if (flatChildren.length > 0) {
453
+ // Set children property only if dangerouslySetInnerHTML is not present
454
+ if (!normalizedProps.dangerouslySetInnerHTML) {
455
+ normalizedProps.children = flatChildren;
456
+ }
457
+ else {
458
+ normalizedProps.children = [];
459
+ console.warn("WebJSX: Ignoring children since dangerouslySetInnerHTML is set.");
460
+ }
461
+ }
462
+ else {
463
+ normalizedProps.children = [];
464
+ }
465
+ const result = {
466
+ type,
467
+ tagName: KNOWN_ELEMENTS.get(type) ?? type.toUpperCase(),
468
+ props: normalizedProps ?? {},
469
+ };
470
+ return result;
471
+ }
472
+ else {
473
+ return flattenVNodes(children);
474
+ }
475
+ }
476
+ // As called from jsx-runtime.jsx function.
477
+ function createElementJSX(type, props, key) {
478
+ if (typeof type === "string") {
479
+ props = props || {};
480
+ const flatChildren = props
481
+ ? flattenVNodes(props.children)
482
+ : [];
483
+ if (key !== undefined) {
484
+ props.key = key;
485
+ }
486
+ if (flatChildren.length > 0) {
487
+ // Set children property only if dangerouslySetInnerHTML is not present
488
+ if (!props.dangerouslySetInnerHTML) {
489
+ props.children = flatChildren;
490
+ }
491
+ else {
492
+ props.children = [];
493
+ console.warn("WebJSX: Ignoring children since dangerouslySetInnerHTML is set.");
494
+ }
495
+ }
496
+ else {
497
+ props.children = [];
498
+ }
499
+ const result = {
500
+ type,
501
+ tagName: KNOWN_ELEMENTS.get(type) ?? type.toUpperCase(),
502
+ props: props ?? {},
503
+ };
504
+ return result;
505
+ }
506
+ else {
507
+ const flatChildren = props
508
+ ? flattenVNodes(props.children)
509
+ : [];
510
+ return flatChildren;
511
+ }
512
+ }
513
+
514
+ // === applyDiff.js ===
515
+ function applyDiff(parent, vnodes) {
516
+ const newVNodes = flattenVNodes(vnodes);
517
+ const newNodes = diffChildren(parent, newVNodes);
518
+ const props = getWebJSXProps(parent);
519
+ props.children = newVNodes;
520
+ setWebJSXChildNodeCache(parent, newNodes);
521
+ }
522
+ function diffChildren(parent, newVNodes) {
523
+ const parentProps = getWebJSXProps(parent);
524
+ const oldVNodes = parentProps.children ?? [];
525
+ if (newVNodes.length === 0) {
526
+ if (oldVNodes.length > 0) {
527
+ parent.innerHTML = "";
528
+ return [];
529
+ }
530
+ else {
531
+ // If the parent
532
+ // a) never had any nodes
533
+ // b) OR was managing content via dangerouslySetInnerHTML
534
+ // we must not set parent.innerHTML = "";
535
+ return [];
536
+ }
537
+ }
538
+ const changes = [];
539
+ let keyedMap = null;
540
+ const originalChildNodes = getWebJSXChildNodeCache(parent) ?? getChildNodes(parent);
541
+ let hasKeyedNodes = false;
542
+ let nodeOrderUnchanged = true;
543
+ for (let i = 0; i < newVNodes.length; i++) {
544
+ const newVNode = newVNodes[i];
545
+ const oldVNode = oldVNodes[i];
546
+ const currentNode = originalChildNodes[i];
547
+ const newKey = isVElement(newVNode) ? newVNode.props.key : undefined;
548
+ if (newKey !== undefined) {
549
+ if (!keyedMap) {
550
+ hasKeyedNodes = true;
551
+ keyedMap = new Map();
552
+ for (let j = 0; j < oldVNodes.length; j++) {
553
+ const matchingVNode = oldVNodes[j];
554
+ const key = matchingVNode.props.key;
555
+ if (key !== undefined) {
556
+ const node = originalChildNodes[j];
557
+ keyedMap.set(key, { node, oldVNode: matchingVNode });
558
+ }
559
+ }
560
+ }
561
+ const keyedNode = keyedMap.get(newKey);
562
+ if (keyedNode) {
563
+ if (keyedNode.oldVNode !== oldVNode) {
564
+ nodeOrderUnchanged = false;
565
+ }
566
+ changes.push({
567
+ type: "update",
568
+ node: keyedNode.node,
569
+ newVNode,
570
+ oldVNode: keyedNode.oldVNode,
571
+ });
572
+ }
573
+ else {
574
+ nodeOrderUnchanged = false;
575
+ changes.push({ type: "create", vnode: newVNode });
576
+ }
577
+ }
578
+ else {
579
+ if (!hasKeyedNodes &&
580
+ canUpdateVNodes(newVNode, oldVNode) &&
581
+ currentNode) {
582
+ changes.push({
583
+ type: "update",
584
+ node: currentNode,
585
+ newVNode,
586
+ oldVNode,
587
+ });
588
+ }
589
+ else {
590
+ nodeOrderUnchanged = false;
591
+ changes.push({ type: "create", vnode: newVNode });
592
+ }
593
+ }
594
+ }
595
+ if (changes.length) {
596
+ const { nodes, lastNode: lastPlacedNode } = applyChanges(parent, changes, originalChildNodes, nodeOrderUnchanged);
597
+ // Remove any remaining nodes
598
+ while (lastPlacedNode?.nextSibling) {
599
+ parent.removeChild(lastPlacedNode.nextSibling);
600
+ }
601
+ return nodes;
602
+ }
603
+ else {
604
+ return originalChildNodes;
605
+ }
606
+ }
607
+ function canUpdateVNodes(newVNode, oldVNode) {
608
+ if (oldVNode === undefined)
609
+ return false;
610
+ if (isNonBooleanPrimitive(newVNode) && isNonBooleanPrimitive(oldVNode)) {
611
+ return true;
612
+ }
613
+ else {
614
+ if (isVElement(oldVNode) && isVElement(newVNode)) {
615
+ const oldKey = oldVNode.props.key;
616
+ const newKey = newVNode.props.key;
617
+ return (oldVNode.tagName === newVNode.tagName &&
618
+ ((oldKey === undefined && newKey === undefined) ||
619
+ (oldKey !== undefined && newKey !== undefined && oldKey === newKey)));
620
+ }
621
+ else {
622
+ return false;
623
+ }
624
+ }
625
+ }
626
+ function applyChanges(parent, changes, originalNodes, nodeOrderUnchanged) {
627
+ const nodes = [];
628
+ let lastPlacedNode = null;
629
+ for (const change of changes) {
630
+ if (change.type === "create") {
631
+ let node = undefined;
632
+ if (isVElement(change.vnode)) {
633
+ node = createDOMElement(change.vnode, getNamespaceURI(parent));
634
+ }
635
+ else {
636
+ node = document.createTextNode(`${change.vnode}`);
637
+ }
638
+ if (!lastPlacedNode) {
639
+ parent.prepend(node);
640
+ }
641
+ else {
642
+ parent.insertBefore(node, lastPlacedNode.nextSibling ?? null);
643
+ }
644
+ lastPlacedNode = node;
645
+ nodes.push(node);
646
+ }
647
+ else {
648
+ const { node, newVNode, oldVNode } = change;
649
+ if (isVElement(newVNode)) {
650
+ const oldProps = oldVNode?.props || {};
651
+ const newProps = newVNode.props;
652
+ updateAttributes(node, newProps, oldProps);
653
+ if (newVNode.props.key !== undefined) {
654
+ node.__webjsx_key = newVNode.props.key;
655
+ }
656
+ else {
657
+ if (oldVNode.props?.key) {
658
+ delete node.__webjsx_key;
659
+ }
660
+ }
661
+ if (newVNode.props.ref) {
662
+ assignRef(node, newVNode.props.ref);
663
+ }
664
+ if (!newProps.dangerouslySetInnerHTML && newProps.children != null) {
665
+ const childNodes = diffChildren(node, newProps.children);
666
+ setWebJSXProps(node, newProps);
667
+ setWebJSXChildNodeCache(node, childNodes);
668
+ }
669
+ }
670
+ else {
671
+ if (newVNode !== oldVNode) {
672
+ node.textContent = `${newVNode}`;
673
+ }
674
+ }
675
+ if (!nodeOrderUnchanged) {
676
+ if (!lastPlacedNode) {
677
+ if (node !== originalNodes[0]) {
678
+ parent.prepend(node);
679
+ }
680
+ }
681
+ else {
682
+ if (lastPlacedNode.nextSibling !== node) {
683
+ parent.insertBefore(node, lastPlacedNode.nextSibling ?? null);
684
+ }
685
+ }
686
+ }
687
+ lastPlacedNode = node;
688
+ nodes.push(node);
689
+ }
690
+ }
691
+ return { nodes, lastNode: lastPlacedNode };
692
+ }
693
+
694
+ // === types.js ===
695
+ const Fragment = (props) => {
696
+ return flattenVNodes(props.children);
697
+ };
698
+
699
+ window.webjsx = { createElement, applyDiff, createDOMElement, Fragment };
700
+ })(typeof window !== 'undefined' ? window : globalThis);