|
1 | 1 | import React, { useEffect, useRef, useImperativeHandle, forwardRef } from 'react' |
2 | | -import XTermPkg from '@xterm/xterm' |
3 | | -import FitAddonPkg from '@xterm/addon-fit' |
| 2 | +import * as XTermPkg from '@xterm/xterm' |
| 3 | +import * as FitAddonPkg from '@xterm/addon-fit' |
4 | 4 | import type { Terminal as XTermType, ITerminalOptions, ITerminalInitOnlyOptions } from '@xterm/xterm' |
5 | 5 | import type { FitAddon as FitAddonType } from '@xterm/addon-fit' |
6 | 6 | import { useTheme } from '../hooks/useTheme' |
7 | 7 |
|
8 | | -const XTerm = XTermPkg.Terminal ?? (XTermPkg as unknown as { Terminal: typeof XTermPkg.Terminal }).Terminal |
9 | | -const FitAddon = FitAddonPkg.FitAddon ?? (FitAddonPkg as unknown as { FitAddon: typeof FitAddonPkg.FitAddon }).FitAddon |
| 8 | +// Handle both ESM and CJS module formats |
| 9 | +const XTerm = (XTermPkg as { Terminal?: typeof XTermType }).Terminal |
| 10 | + ?? (XTermPkg as { default?: { Terminal: typeof XTermType } }).default?.Terminal |
| 11 | + ?? (XTermPkg as unknown as typeof XTermType) |
| 12 | +const FitAddon = (FitAddonPkg as { FitAddon?: typeof FitAddonType }).FitAddon |
| 13 | + ?? (FitAddonPkg as { default?: { FitAddon: typeof FitAddonType } }).default?.FitAddon |
| 14 | + ?? (FitAddonPkg as unknown as typeof FitAddonType) |
10 | 15 |
|
11 | 16 | // Inject xterm.css once (inlined for reliability across bundlers) |
12 | 17 | let cssInjected = false |
@@ -248,43 +253,70 @@ export const Terminal = forwardRef<TerminalRef, TerminalProps>(({ |
248 | 253 |
|
249 | 254 | injectXtermCSS() |
250 | 255 |
|
251 | | - const terminal = new XTerm({ |
252 | | - theme: getTheme(), |
253 | | - cursorBlink: true, |
254 | | - fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace', |
255 | | - fontSize: 14, |
256 | | - ...options, |
257 | | - }) |
258 | | - const fitAddon = new FitAddon() |
| 256 | + const container = containerRef.current |
| 257 | + let terminal: XTermType | null = null |
| 258 | + let fitAddon: FitAddonType | null = null |
| 259 | + let resizeObserver: ResizeObserver | null = null |
| 260 | + let initialized = false |
259 | 261 |
|
260 | | - terminal.loadAddon(fitAddon) |
261 | | - terminal.open(containerRef.current) |
262 | | - fitAddon.fit() |
| 262 | + const initTerminal = () => { |
| 263 | + if (initialized || !container) return |
263 | 264 |
|
264 | | - terminalRef.current = terminal |
265 | | - fitAddonRef.current = fitAddon |
| 265 | + // Check container has dimensions before opening |
| 266 | + const rect = container.getBoundingClientRect() |
| 267 | + if (rect.width === 0 || rect.height === 0) return |
266 | 268 |
|
267 | | - if (readline) { |
268 | | - terminal.onData(handleReadlineData) |
269 | | - } else if (onData) { |
270 | | - terminal.onData(onData) |
271 | | - } |
| 269 | + initialized = true |
272 | 270 |
|
273 | | - onReady?.(terminal) |
| 271 | + terminal = new XTerm({ |
| 272 | + theme: getTheme(), |
| 273 | + cursorBlink: true, |
| 274 | + fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace', |
| 275 | + fontSize: 14, |
| 276 | + ...options, |
| 277 | + }) |
| 278 | + fitAddon = new FitAddon() |
| 279 | + |
| 280 | + terminal.loadAddon(fitAddon) |
| 281 | + terminal.open(container) |
| 282 | + fitAddon.fit() |
| 283 | + |
| 284 | + terminalRef.current = terminal |
| 285 | + fitAddonRef.current = fitAddon |
| 286 | + |
| 287 | + if (readline) { |
| 288 | + terminal.onData(handleReadlineData) |
| 289 | + } else if (onData) { |
| 290 | + terminal.onData(onData) |
| 291 | + } |
274 | 292 |
|
275 | | - // Write initial prompt after onReady so welcome messages appear first |
276 | | - if (readline) { |
277 | | - terminal.write(prompt) |
| 293 | + onReady?.(terminal) |
| 294 | + |
| 295 | + // Write initial prompt after onReady so welcome messages appear first |
| 296 | + if (readline) { |
| 297 | + terminal.write(prompt) |
| 298 | + } |
278 | 299 | } |
279 | 300 |
|
280 | | - const resizeObserver = new ResizeObserver(() => { |
281 | | - fitAddon.fit() |
| 301 | + // Use ResizeObserver to wait for container to have dimensions |
| 302 | + resizeObserver = new ResizeObserver((entries) => { |
| 303 | + const entry = entries[0] |
| 304 | + if (!entry) return |
| 305 | + |
| 306 | + if (!initialized) { |
| 307 | + initTerminal() |
| 308 | + } else if (fitAddon) { |
| 309 | + fitAddon.fit() |
| 310 | + } |
282 | 311 | }) |
283 | | - resizeObserver.observe(containerRef.current) |
| 312 | + resizeObserver.observe(container) |
| 313 | + |
| 314 | + // Also try immediately in case container already has dimensions |
| 315 | + requestAnimationFrame(initTerminal) |
284 | 316 |
|
285 | 317 | return () => { |
286 | | - resizeObserver.disconnect() |
287 | | - terminal.dispose() |
| 318 | + resizeObserver?.disconnect() |
| 319 | + terminal?.dispose() |
288 | 320 | terminalRef.current = null |
289 | 321 | fitAddonRef.current = null |
290 | 322 | } |
|
0 commit comments