From cc7aee91701dfb0c58b0fe2d4b64faa850a500b5 Mon Sep 17 00:00:00 2001 From: wangxiuliang Date: Fri, 8 May 2026 21:36:11 +0800 Subject: [PATCH] refactor: bundle xterm.js with esbuild instead of CDN runtime loading Switch from loading xterm.js modules dynamically from esm.sh CDN at runtime to bundling them with esbuild at build time. This eliminates runtime CDN dependency, improves load reliability, and enables offline usage. Key changes: - Add @xterm/* npm packages as direct dependencies - Replace dynamic CDN imports with static ES module imports - Use esbuild to bundle JS and inline CSS - Add tsconfig.server.json for server-side compilation - Remove XtermModules interface and loadModules() helper --- package.json | 15 ++++++--- src/index.ts | 80 ++++++++++---------------------------------- tsconfig.json | 3 +- tsconfig.server.json | 17 ++++++++++ 4 files changed, 48 insertions(+), 67 deletions(-) create mode 100644 tsconfig.server.json diff --git a/package.json b/package.json index b93e584..33a281e 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,23 @@ "type": "module", "description": "Full-featured web terminal plugin for CloudCLI UI", "scripts": { - "build": "tsc", - "dev": "tsc --watch" + "build": "tsc -p tsconfig.server.json && esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --loader:.css=text", + "dev": "tsc -p tsconfig.server.json --watch" }, "dependencies": { + "@xterm/addon-clipboard": "^0.2.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-unicode11": "^0.9.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^6.0.0", "node-pty": "^1.1.0", "ws": "^8.14.0" }, "devDependencies": { - "typescript": "^5.5.0", "@types/node": "^20.0.0", - "@types/ws": "^8.5.0" + "@types/ws": "^8.5.0", + "esbuild": "^0.28.0", + "typescript": "^5.5.0" } } diff --git a/src/index.ts b/src/index.ts index e8aadac..d7c2d65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,15 +6,13 @@ */ import type { PluginAPI } from './types.js'; - -// ── CDN version pins ────────────────────────────────────────────────────────── -const CDN = 'https://esm.sh'; -const XTERM_VER = '5.5.0'; -const FIT_VER = '0.10.0'; -const WEBLINKS_VER = '0.11.0'; -const WEBGL_VER = '0.18.0'; -const CLIPBOARD_VER = '0.1.0'; -const UNICODE11_VER = '0.8.0'; +import { Terminal } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import { WebLinksAddon } from '@xterm/addon-web-links'; +import { WebglAddon } from '@xterm/addon-webgl'; +import { ClipboardAddon } from '@xterm/addon-clipboard'; +import { Unicode11Addon } from '@xterm/addon-unicode11'; +import xtermCSS from '@xterm/xterm/css/xterm.css'; // ── Types ───────────────────────────────────────────────────────────────────── @@ -37,17 +35,7 @@ interface Prefs { fontFamily?: string; } -interface XtermModules { - Terminal: any; - FitAddon: any; - WebLinksAddon: any; - WebglAddon: any; - ClipboardAddon: any; - Unicode11Addon: any; -} - interface GlobalState { - modules: XtermModules | null; sessions: Map; prefs: Prefs | null; tabCounter: number; @@ -123,7 +111,7 @@ declare global { } if (!window.__wtState) { - window.__wtState = { modules: null, sessions: new Map(), prefs: null, tabCounter: 0, activeId: null }; + window.__wtState = { sessions: new Map(), prefs: null, tabCounter: 0, activeId: null }; } const _G: GlobalState = window.__wtState; @@ -150,10 +138,10 @@ function divider(): HTMLElement { return el('div', 'wt-divider'); } function injectStyles(): void { if (document.getElementById('wt-css')) return; - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = `${CDN}/@xterm/xterm@${XTERM_VER}/css/xterm.css`; - document.head.appendChild(link); + const xtermStyle = document.createElement('style'); + xtermStyle.id = 'wt-xterm-css'; + xtermStyle.textContent = xtermCSS; + document.head.appendChild(xtermStyle); const s = document.createElement('style'); s.id = 'wt-css'; @@ -584,45 +572,14 @@ class TerminalSession { } } -// ── Module loader (cached) ──────────────────────────────────────────────────── -async function loadModules(): Promise { - if (_G.modules) return _G.modules; - const results = await Promise.all([ - import(CDN + '/@xterm/xterm@' + XTERM_VER), - import(CDN + '/@xterm/addon-fit@' + FIT_VER), - import(CDN + '/@xterm/addon-web-links@' + WEBLINKS_VER), - import(CDN + '/@xterm/addon-webgl@' + WEBGL_VER), - import(CDN + '/@xterm/addon-clipboard@' + CLIPBOARD_VER).catch(() => ({ ClipboardAddon: null })), - import(CDN + '/@xterm/addon-unicode11@' + UNICODE11_VER).catch(() => ({ Unicode11Addon: null })), - ]); - _G.modules = { - Terminal: results[0].Terminal, FitAddon: results[1].FitAddon, - WebLinksAddon: results[2].WebLinksAddon, WebglAddon: results[3].WebglAddon, - ClipboardAddon: results[4].ClipboardAddon, Unicode11Addon: results[5].Unicode11Addon, - }; - return _G.modules; -} - // ── Mount ───────────────────────────────────────────────────────────────────── export async function mount(container: HTMLElement, api: PluginAPI): Promise { injectStyles(); - let mods: XtermModules; - try { - mods = await loadModules(); - } catch (err) { - const errDiv = el('div'); - errDiv.style.cssText = 'display:flex;align-items:center;justify-content:center;height:100%;color:#f14c4c;padding:24px;text-align:center;font-family:sans-serif'; - const inner = el('div'); - inner.appendChild(el('div', null, 'Failed to load xterm.js')); - (inner.firstChild as HTMLElement).style.cssText = 'font-size:16px;font-weight:600;margin-bottom:8px'; - const detail = el('div', null, (err as Error).message); - detail.style.cssText = 'font-size:12px;opacity:.7'; - inner.appendChild(detail); - errDiv.appendChild(inner); - container.appendChild(errDiv); - return; - } + let Clipboard: any = ClipboardAddon; + try { if (!Clipboard) throw new Error(); } catch { Clipboard = null; } + let Unicode11: any = Unicode11Addon; + try { if (!Unicode11) throw new Error(); } catch { Unicode11 = null; } if (!_G.prefs) { _G.prefs = loadPrefs() as Prefs; @@ -772,9 +729,8 @@ export async function mount(container: HTMLElement, api: PluginAPI): Promise