mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix blank mobile terminal on Android devices with outdated WebViews or blocked CDN (#7186)
* fix(mobile): bundle terminal engine and show load errors instead of a blank pane The mobile terminal WebView loaded xterm.js from cdn.jsdelivr.net at runtime; old WebViews (< Chrome 85) fail to parse the modern bundle and blocked-CDN networks fail to fetch it, and the resulting error was silently dropped, leaving the pane permanently blank (#7030). Bundle the engine into the app via exact-pinned npm deps + a postinstall esbuild step (chrome74 target, guarded WeakRef/structuredClone/ replaceChildren shims) emitting a gitignored generated module, inline it into the terminal document, and surface fatal engine failures as a visible overlay with diagnostics and a Reload wired into the existing resubscribe path. Non-fatal errors log without covering a live terminal. Co-authored-by: Orca <help@stably.ai> * fix(mobile): add a native watchdog so a dead terminal document can't stay silently blank CodeRabbit round: if the webview document dies before the glue can post anything (or the RN message bridge never comes up), no error message and no native handler fires. Arm a 15s foreground-gated watchdog per document generation that paints the fatal overlay when web-ready never arrives; first fatal diagnostics win over later cascades. Extract the watchdog and the public contract types to keep TerminalWebView under the line cap, and document the SVG xmlns percent-encoding transform. Co-authored-by: Orca <help@stably.ai> * test(mobile): unmount TerminalWebView renderers so watchdog timers can't leak across tests Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
src/terminal/terminal-webview-engine.generated.ts
|
||||
.expo/
|
||||
dist/
|
||||
/android/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../.oxlintrc.json"],
|
||||
"ignorePatterns": ["src/terminal/terminal-webview-engine.generated.ts"],
|
||||
"rules": {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"start": "node scripts/start-expo.mjs",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"postinstall": "node scripts/build-terminal-webview-engine.mjs",
|
||||
"test": "vitest run",
|
||||
"lint": "oxlint",
|
||||
"format": "oxfmt --write .",
|
||||
@@ -18,6 +19,9 @@
|
||||
"dependencies": {
|
||||
"@orca/expo-two-way-audio": "file:./packages/expo-two-way-audio",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.285",
|
||||
"@xterm/addon-webgl": "0.20.0-beta.284",
|
||||
"@xterm/xterm": "6.1.0-beta.285",
|
||||
"buffer": "^6.0.3",
|
||||
"expo": "^55.0.23",
|
||||
"expo-build-properties": "^55.0.13",
|
||||
@@ -62,6 +66,8 @@
|
||||
"@types/react-native": "^0.73.0",
|
||||
"@types/react-test-renderer": "19.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"acorn": "8.15.0",
|
||||
"esbuild": "0.25.4",
|
||||
"expo-module-scripts": "^55.0.2",
|
||||
"oxfmt": "^0.52.0",
|
||||
"oxlint": "^1.71.0",
|
||||
|
||||
Generated
+317
-47
@@ -14,6 +14,15 @@ importers:
|
||||
'@react-native-async-storage/async-storage':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.285
|
||||
version: 0.10.0-beta.285(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/addon-webgl':
|
||||
specifier: 0.20.0-beta.284
|
||||
version: 0.20.0-beta.284(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.285
|
||||
version: 6.1.0-beta.285
|
||||
buffer:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
@@ -141,9 +150,15 @@ importers:
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
acorn:
|
||||
specifier: 8.15.0
|
||||
version: 8.15.0
|
||||
esbuild:
|
||||
specifier: 0.25.4
|
||||
version: 0.25.4
|
||||
expo-module-scripts:
|
||||
specifier: ^55.0.2
|
||||
version: 55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(eslint@9.39.4)(expo@55.0.23)(jest@29.7.0(@types/node@25.9.3))(prettier@2.8.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
version: 55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(eslint@9.39.4)(expo@55.0.23)(jest@29.7.0(@types/node@25.9.3))(prettier@2.8.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
oxfmt:
|
||||
specifier: ^0.52.0
|
||||
version: 0.52.0
|
||||
@@ -161,10 +176,10 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^8.0.16
|
||||
version: 8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||
version: 8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||
vitest:
|
||||
specifier: ^4.1.9
|
||||
version: 4.1.9(@types/node@25.9.3)(jsdom@20.0.3)(vite@8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
|
||||
version: 4.1.9(@types/node@25.9.3)(jsdom@20.0.3)(vite@8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1157,126 +1172,252 @@ packages:
|
||||
'@emnapi/wasi-threads@1.2.2':
|
||||
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.4':
|
||||
resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.4':
|
||||
resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.4':
|
||||
resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.4':
|
||||
resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.4':
|
||||
resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.4':
|
||||
resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.4':
|
||||
resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.4':
|
||||
resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.4':
|
||||
resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.4':
|
||||
resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.4':
|
||||
resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.4':
|
||||
resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.4':
|
||||
resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.4':
|
||||
resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.4':
|
||||
resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1289,24 +1430,48 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.4':
|
||||
resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.4':
|
||||
resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.4':
|
||||
resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.4':
|
||||
resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1730,56 +1895,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.52.0':
|
||||
resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.52.0':
|
||||
resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.52.0':
|
||||
resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.52.0':
|
||||
resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.52.0':
|
||||
resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.52.0':
|
||||
resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.52.0':
|
||||
resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.52.0':
|
||||
resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==}
|
||||
@@ -1852,56 +2009,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.71.0':
|
||||
resolution: {integrity: sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.71.0':
|
||||
resolution: {integrity: sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.71.0':
|
||||
resolution: {integrity: sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.71.0':
|
||||
resolution: {integrity: sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.71.0':
|
||||
resolution: {integrity: sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.71.0':
|
||||
resolution: {integrity: sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.71.0':
|
||||
resolution: {integrity: sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.71.0':
|
||||
resolution: {integrity: sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw==}
|
||||
@@ -2363,42 +2512,36 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.1.3':
|
||||
resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
|
||||
resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.1.3':
|
||||
resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.1.3':
|
||||
resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.1.3':
|
||||
resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.1.3':
|
||||
resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
|
||||
@@ -2653,6 +2796,19 @@ packages:
|
||||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.285':
|
||||
resolution: {integrity: sha512-rfijFu7UcYpaFx5wzxvTpQbIyyq/amf2PuS9pktywcFQr4ITxRgid5EVzKLRG1vchkApNcQplWeYxGEtjiw0Cw==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.284':
|
||||
resolution: {integrity: sha512-tzkUiEfdpHCY8mXCbuIaP9V67QDfBJvDr9jdxs5jjxNCIQvw+NCoKD97y5sUrQhrIlr7xrDGniPgPYThQ/1FWg==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.285':
|
||||
resolution: {integrity: sha512-S3K58tepMkbpWRBzOGKd0In6AVvt9QPAnNs8DJ8rPUPODYtsCYWAtINHKYtC2OpXcE5EBKM35dl+Dgv03OoE/w==}
|
||||
|
||||
abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
@@ -2681,8 +2837,8 @@ packages:
|
||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3378,6 +3534,11 @@ packages:
|
||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.25.4:
|
||||
resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.28.1:
|
||||
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4660,28 +4821,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
@@ -7675,81 +7832,156 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
@@ -9327,13 +9559,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.9(vite@8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))':
|
||||
'@vitest/mocker@4.1.9(vite@8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||
vite: 8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.9':
|
||||
dependencies:
|
||||
@@ -9363,6 +9595,16 @@ snapshots:
|
||||
|
||||
'@xmldom/xmldom@0.9.10': {}
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.285(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.284(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.285': {}
|
||||
|
||||
abab@2.0.6: {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
@@ -9381,7 +9623,7 @@ snapshots:
|
||||
|
||||
acorn-globals@7.0.1:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
acorn: 8.15.0
|
||||
acorn-walk: 8.3.5
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.17.0):
|
||||
@@ -9390,9 +9632,9 @@ snapshots:
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
acorn: 8.15.0
|
||||
|
||||
acorn@8.16.0: {}
|
||||
acorn@8.15.0: {}
|
||||
|
||||
acorn@8.17.0: {}
|
||||
|
||||
@@ -10235,6 +10477,34 @@ snapshots:
|
||||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
esbuild@0.25.4:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.4
|
||||
'@esbuild/android-arm': 0.25.4
|
||||
'@esbuild/android-arm64': 0.25.4
|
||||
'@esbuild/android-x64': 0.25.4
|
||||
'@esbuild/darwin-arm64': 0.25.4
|
||||
'@esbuild/darwin-x64': 0.25.4
|
||||
'@esbuild/freebsd-arm64': 0.25.4
|
||||
'@esbuild/freebsd-x64': 0.25.4
|
||||
'@esbuild/linux-arm': 0.25.4
|
||||
'@esbuild/linux-arm64': 0.25.4
|
||||
'@esbuild/linux-ia32': 0.25.4
|
||||
'@esbuild/linux-loong64': 0.25.4
|
||||
'@esbuild/linux-mips64el': 0.25.4
|
||||
'@esbuild/linux-ppc64': 0.25.4
|
||||
'@esbuild/linux-riscv64': 0.25.4
|
||||
'@esbuild/linux-s390x': 0.25.4
|
||||
'@esbuild/linux-x64': 0.25.4
|
||||
'@esbuild/netbsd-arm64': 0.25.4
|
||||
'@esbuild/netbsd-x64': 0.25.4
|
||||
'@esbuild/openbsd-arm64': 0.25.4
|
||||
'@esbuild/openbsd-x64': 0.25.4
|
||||
'@esbuild/sunos-x64': 0.25.4
|
||||
'@esbuild/win32-arm64': 0.25.4
|
||||
'@esbuild/win32-ia32': 0.25.4
|
||||
'@esbuild/win32-x64': 0.25.4
|
||||
|
||||
esbuild@0.28.1:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.28.1
|
||||
@@ -10688,7 +10958,7 @@ snapshots:
|
||||
expo: 55.0.23(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@5.9.3)
|
||||
expo-json-utils: 55.0.2
|
||||
|
||||
expo-module-scripts@55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(eslint@9.39.4)(expo@55.0.23)(jest@29.7.0(@types/node@25.9.3))(prettier@2.8.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
expo-module-scripts@55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(eslint@9.39.4)(expo@55.0.23)(jest@29.7.0(@types/node@25.9.3))(prettier@2.8.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
dependencies:
|
||||
'@babel/cli': 7.28.6(@babel/core@7.29.7)
|
||||
'@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.7)
|
||||
@@ -10708,7 +10978,7 @@ snapshots:
|
||||
jest-snapshot-prettier: prettier@2.8.8
|
||||
jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@25.9.3))
|
||||
resolve-workspace-root: 2.0.1
|
||||
ts-jest: 29.0.5(@babel/core@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest@29.7.0(@types/node@25.9.3))(typescript@5.9.3)
|
||||
ts-jest: 29.0.5(@babel/core@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(jest@29.7.0(@types/node@25.9.3))(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -11832,7 +12102,7 @@ snapshots:
|
||||
jsdom@20.0.3:
|
||||
dependencies:
|
||||
abab: 2.0.6
|
||||
acorn: 8.16.0
|
||||
acorn: 8.15.0
|
||||
acorn-globals: 7.0.1
|
||||
cssom: 0.5.0
|
||||
cssstyle: 2.3.0
|
||||
@@ -13463,7 +13733,7 @@ snapshots:
|
||||
terser@5.46.2:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.16.0
|
||||
acorn: 8.15.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
@@ -13527,7 +13797,7 @@ snapshots:
|
||||
picomatch: 4.0.4
|
||||
typescript: 5.9.3
|
||||
|
||||
ts-jest@29.0.5(@babel/core@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest@29.7.0(@types/node@25.9.3))(typescript@5.9.3):
|
||||
ts-jest@29.0.5(@babel/core@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(jest@29.7.0(@types/node@25.9.3))(typescript@5.9.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
@@ -13543,7 +13813,7 @@ snapshots:
|
||||
'@babel/core': 7.29.7
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.29.7)
|
||||
esbuild: 0.28.1
|
||||
esbuild: 0.25.4
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
dependencies:
|
||||
@@ -13700,7 +13970,7 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
vite@8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0):
|
||||
vite@8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
@@ -13709,16 +13979,16 @@ snapshots:
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.3
|
||||
esbuild: 0.28.1
|
||||
esbuild: 0.25.4
|
||||
fsevents: 2.3.3
|
||||
terser: 5.48.0
|
||||
tsx: 4.22.4
|
||||
yaml: 2.9.0
|
||||
|
||||
vitest@4.1.9(@types/node@25.9.3)(jsdom@20.0.3)(vite@8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)):
|
||||
vitest@4.1.9(@types/node@25.9.3)(jsdom@20.0.3)(vite@8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.9
|
||||
'@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
|
||||
'@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.1.9
|
||||
'@vitest/runner': 4.1.9
|
||||
'@vitest/snapshot': 4.1.9
|
||||
@@ -13735,7 +14005,7 @@ snapshots:
|
||||
tinyexec: 1.1.2
|
||||
tinyglobby: 0.2.17
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.1.0(@types/node@25.9.3)(esbuild@0.28.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||
vite: 8.1.0(@types/node@25.9.3)(esbuild@0.25.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.3
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import * as esbuild from 'esbuild'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const scriptDir = import.meta.dirname
|
||||
const mobileRoot = path.resolve(scriptDir, '..')
|
||||
const outputPath = path.join(mobileRoot, 'src', 'terminal', 'terminal-webview-engine.generated.ts')
|
||||
const target = 'chrome74'
|
||||
|
||||
const packages = ['@xterm/xterm', '@xterm/addon-unicode11', '@xterm/addon-webgl']
|
||||
|
||||
async function readPackageVersion(packageName) {
|
||||
// Why: a package.json module specifier must use '/' — path.join emits '\' on
|
||||
// Windows, yielding an unresolvable bare specifier that fails postinstall there.
|
||||
const packageJsonPath = require.resolve(`${packageName}/package.json`)
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
|
||||
return `${packageName}@${packageJson.version}`
|
||||
}
|
||||
|
||||
function htmlText(value, closingTag) {
|
||||
return value.replace(new RegExp(`</${closingTag}`, 'gi'), `<\\/${closingTag}`)
|
||||
}
|
||||
|
||||
async function buildEngineJs() {
|
||||
const result = await esbuild.build({
|
||||
stdin: {
|
||||
contents: `
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
|
||||
// Why: xterm reaches for these runtime APIs on the terminal-bringup path,
|
||||
// and esbuild lowers syntax but not runtime APIs. Guarded shims let the
|
||||
// chrome74-syntax bundle actually run on old WebViews (the #7030 goal)
|
||||
// instead of throwing at construction and only surfacing the error overlay.
|
||||
|
||||
// WeakRef (Chrome 84+): lazily constructed in window-tracking paths.
|
||||
// Strong retention is fine for a single-document terminal WebView.
|
||||
if (typeof window.WeakRef === 'undefined') {
|
||||
window.WeakRef = function WeakRefShim(target) { this.__target = target }
|
||||
window.WeakRef.prototype.deref = function () { return this.__target }
|
||||
}
|
||||
|
||||
// structuredClone (Chrome 98+): xterm clones its plain-data DEC-mode default
|
||||
// objects at Terminal construction; a JSON round-trip clones those correctly
|
||||
// (any undefined-valued keys drop, but every reader treats absent == undefined).
|
||||
if (typeof window.structuredClone === 'undefined') {
|
||||
window.structuredClone = function (value) { return JSON.parse(JSON.stringify(value)) }
|
||||
}
|
||||
|
||||
// Element.prototype.replaceChildren (Chrome 86+): used on the row/selection
|
||||
// render path; polyfill via remove-all + append (Chrome 54+, under the floor).
|
||||
if (typeof Element !== 'undefined' && !Element.prototype.replaceChildren) {
|
||||
Element.prototype.replaceChildren = function () {
|
||||
while (this.firstChild) this.removeChild(this.firstChild)
|
||||
this.append.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
window.Terminal = Terminal
|
||||
window.Unicode11Addon = { Unicode11Addon }
|
||||
window.WebglAddon = { WebglAddon }
|
||||
`,
|
||||
resolveDir: mobileRoot,
|
||||
sourcefile: 'terminal-webview-engine-entry.js'
|
||||
},
|
||||
bundle: true,
|
||||
format: 'iife',
|
||||
minify: true,
|
||||
platform: 'browser',
|
||||
target,
|
||||
legalComments: 'none',
|
||||
write: false,
|
||||
logLevel: 'silent'
|
||||
})
|
||||
|
||||
return result.outputFiles[0].text
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [engineJs, rawEngineCss, ...versions] = await Promise.all([
|
||||
buildEngineJs(),
|
||||
readFile(require.resolve('@xterm/xterm/css/xterm.css'), 'utf8'),
|
||||
...packages.map(readPackageVersion)
|
||||
])
|
||||
// Why: the no-external-URL regression gate bans http(s):// anywhere in the
|
||||
// terminal document. These xmlns URIs live inside data: URLs (never fetched);
|
||||
// percent-encoding the scheme colon satisfies the gate and URI-decodes back
|
||||
// before the SVG is parsed.
|
||||
const engineCss = rawEngineCss
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/http:\/\/www\.w3\.org\/2000\/svg/g, 'http%3A//www.w3.org/2000/svg')
|
||||
|
||||
const source = [
|
||||
'// Generated by scripts/build-terminal-webview-engine.mjs.',
|
||||
`// Packages: ${versions.join(', ')}.`,
|
||||
`// Target: ${target}. Do not edit by hand; regenerate via pnpm postinstall.`,
|
||||
`export const XTERM_ENGINE_JS = ${JSON.stringify(htmlText(engineJs, 'script'))}`,
|
||||
`export const XTERM_ENGINE_CSS = ${JSON.stringify(htmlText(engineCss, 'style'))}`,
|
||||
''
|
||||
].join('\n')
|
||||
|
||||
await writeFile(outputPath, source)
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -1,76 +1,30 @@
|
||||
import { useRef, useCallback, forwardRef, useImperativeHandle, useEffect, useMemo } from 'react'
|
||||
import { StyleSheet, type StyleProp, type ViewStyle } from 'react-native'
|
||||
import { View, type StyleProp, type ViewStyle } from 'react-native'
|
||||
import { WebView } from 'react-native-webview'
|
||||
import type { WebViewMessageEvent } from 'react-native-webview'
|
||||
import type { RuntimeMobileTerminalTheme } from '../../../src/shared/runtime-types'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import type { TerminalOscLinkRange } from './terminal-osc-link-ranges'
|
||||
import { XTERM_HTML } from './terminal-webview-html'
|
||||
import type {
|
||||
MobileTerminalTheme,
|
||||
TerminalSelectionEvents,
|
||||
TerminalWebViewHandle
|
||||
} from './terminal-webview-contract'
|
||||
import {
|
||||
TerminalWebViewEngineErrorOverlay,
|
||||
useTerminalWebViewEngineErrorState
|
||||
} from './terminal-webview-engine-error-state'
|
||||
import { TERMINAL_WEBVIEW_FRAME_STYLES } from './terminal-webview-frame-styles'
|
||||
import { useTerminalWebReadyWatchdog } from './terminal-webview-ready-watchdog'
|
||||
import { XTERM_WEBVIEW_SOURCE } from './terminal-webview-html'
|
||||
import type { TerminalWebViewCommand } from './terminal-webview-messages'
|
||||
import { createTerminalWebViewPendingMessages } from './terminal-webview-pending-messages'
|
||||
|
||||
type TerminalMouseTrackingMode = 'none' | 'x10' | 'vt200' | 'drag' | 'any'
|
||||
type TerminalOscLinks = TerminalOscLinkRange[]
|
||||
|
||||
export type TerminalModes = {
|
||||
bracketedPasteMode: boolean
|
||||
altScreen: boolean
|
||||
mouseTrackingMode: TerminalMouseTrackingMode
|
||||
sgrMouseMode: boolean
|
||||
sgrMousePixelsMode: boolean
|
||||
}
|
||||
|
||||
export type TerminalKeyboardAvoidanceMetrics = {
|
||||
cursorY: number
|
||||
rows: number
|
||||
altScreen: boolean
|
||||
}
|
||||
|
||||
export type MobileTerminalTheme = RuntimeMobileTerminalTheme
|
||||
|
||||
export type TerminalSelectionEvents = {
|
||||
onSelectionMode?: (active: boolean) => void
|
||||
onSelectionCopy?: (text: string) => void
|
||||
onSelectionEvicted?: () => void
|
||||
onModesChanged?: (modes: TerminalModes) => void
|
||||
onKeyboardAvoidanceMetrics?: (metrics: TerminalKeyboardAvoidanceMetrics) => void
|
||||
onHaptic?: (kind: 'selection' | 'success' | 'error' | 'edge-bump') => void
|
||||
onTerminalInput?: (bytes: string) => void
|
||||
onTerminalTap?: () => void
|
||||
// Tap landed on a detected file path; RN resolves + opens it.
|
||||
onFileTap?: (pathText: string, line: number | null, column: number | null) => void
|
||||
// WebView-detected URL tap; RN chooses the mobile routing destination.
|
||||
onOpenUrl?: (url: string) => void
|
||||
// Why: pinch-to-zoom in the terminal snaps to a text-size preset and reports it
|
||||
// here so the app persists it and keeps Settings + other panes in sync.
|
||||
onTextScaleChange?: (scale: number) => void
|
||||
}
|
||||
|
||||
export type TerminalWebViewHandle = {
|
||||
write: (data: string) => void
|
||||
init: (
|
||||
cols: number,
|
||||
rows: number,
|
||||
initialData?: string,
|
||||
preserveScroll?: boolean,
|
||||
oscLinks?: TerminalOscLinks
|
||||
) => void
|
||||
resize: (cols: number, rows: number) => void
|
||||
// Why: reflow the local xterm buffer (scrollback included) to a new width
|
||||
// after a server-side PTY reflow, so older wrapped lines rewrap to match the
|
||||
// latest output. No-op on the alternate screen.
|
||||
reflow: (cols: number, rows: number) => void
|
||||
clear: () => void
|
||||
measureFitDimensions: (containerHeight?: number) => Promise<{ cols: number; rows: number } | null>
|
||||
resetZoom: () => void
|
||||
cancelSelect: () => void
|
||||
doSelectAll: () => void
|
||||
// Why: lets callers await the WebView-side `init` rAF chain (term.open
|
||||
// → renderService population → first paint) so a follow-up measure
|
||||
// doesn't race ahead and find term=null or cellWidth=0. Resolves on
|
||||
// the next 'ready' notify after the most recent init.
|
||||
awaitReady: () => Promise<void>
|
||||
}
|
||||
export type {
|
||||
MobileTerminalTheme,
|
||||
TerminalKeyboardAvoidanceMetrics,
|
||||
TerminalModes,
|
||||
TerminalSelectionEvents,
|
||||
TerminalWebViewHandle
|
||||
} from './terminal-webview-contract'
|
||||
|
||||
type Props = {
|
||||
style?: StyleProp<ViewStyle>
|
||||
@@ -79,18 +33,16 @@ type Props = {
|
||||
// scale; raw xterm fontSize can't drive apparent size because the fit cancels it.
|
||||
textScale?: number
|
||||
onWebReady?: () => void
|
||||
onEngineError?: (message: string) => void
|
||||
} & TerminalSelectionEvents
|
||||
|
||||
// Why: WebView treats source identity as page identity on some platforms; keep
|
||||
// parent/session re-renders from reloading xterm and forcing fresh snapshots.
|
||||
const XTERM_WEBVIEW_SOURCE = { html: XTERM_HTML }
|
||||
|
||||
export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function TerminalWebView(
|
||||
{
|
||||
style,
|
||||
terminalTheme,
|
||||
textScale = 1,
|
||||
onWebReady,
|
||||
onEngineError,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
@@ -119,6 +71,12 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
// race ahead of term.open() / renderService population.
|
||||
const readyPromiseRef = useRef<Promise<void> | null>(null)
|
||||
const readyResolveRef = useRef<(() => void) | null>(null)
|
||||
const { clearEngineError, engineError, reportEngineError, reportNativeEngineError } =
|
||||
useTerminalWebViewEngineErrorState(onEngineError)
|
||||
const { armWebReadyWatchdog, clearWebReadyWatchdog } = useTerminalWebReadyWatchdog(
|
||||
isWebReadyRef,
|
||||
reportEngineError
|
||||
)
|
||||
|
||||
const sendToWebView = useCallback((msg: TerminalWebViewCommand) => {
|
||||
messageIdRef.current += 1
|
||||
@@ -151,6 +109,8 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
|
||||
if (msg.type === 'web-ready') {
|
||||
isWebReadyRef.current = true
|
||||
clearWebReadyWatchdog()
|
||||
clearEngineError()
|
||||
onWebReady?.()
|
||||
flushPendingMessages()
|
||||
} else if (msg.type === 'ready') {
|
||||
@@ -175,6 +135,9 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
const tag = typeof msg.tag === 'string' ? msg.tag : '[fit]'
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(tag, msg.payload)
|
||||
} else if (msg.type === 'error') {
|
||||
const message = typeof msg.message === 'string' ? msg.message : 'Unknown terminal error'
|
||||
reportEngineError(message, msg.fatal !== false)
|
||||
} else if (msg.type === 'set-select-mode') {
|
||||
onSelectionMode?.(!!msg.enabled)
|
||||
} else if (msg.type === 'selection') {
|
||||
@@ -246,6 +209,9 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
},
|
||||
[
|
||||
flushPendingMessages,
|
||||
clearEngineError,
|
||||
clearWebReadyWatchdog,
|
||||
reportEngineError,
|
||||
onWebReady,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
@@ -263,10 +229,16 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
|
||||
const handleLoadStart = useCallback(() => {
|
||||
isWebReadyRef.current = false
|
||||
armWebReadyWatchdog()
|
||||
// Why: messages queued for a previous WebView generation are stale after a reload;
|
||||
// dropping them avoids replaying terminal chunks before the next init snapshot.
|
||||
pendingMessages.clear()
|
||||
}, [pendingMessages])
|
||||
}, [armWebReadyWatchdog, pendingMessages])
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
clearEngineError()
|
||||
webViewRef.current?.reload()
|
||||
}, [clearEngineError])
|
||||
|
||||
useEffect(() => {
|
||||
postMessage({ type: 'set-theme', terminalTheme })
|
||||
@@ -289,7 +261,7 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
rows: number,
|
||||
initialData?: string,
|
||||
preserveScroll?: boolean,
|
||||
oscLinks?: TerminalOscLinks
|
||||
oscLinks?: TerminalOscLinkRange[]
|
||||
) {
|
||||
// Why: arm a fresh ready promise BEFORE posting init. The WebView
|
||||
// resolves it via the 'ready' notify at the end of its rAF chain.
|
||||
@@ -395,29 +367,35 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
)
|
||||
|
||||
return (
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={XTERM_WEBVIEW_SOURCE}
|
||||
style={[styles.webview, style]}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
scrollEnabled={false}
|
||||
// Why: Android parent gesture containers can intercept vertical drags
|
||||
// before the injected xterm scroll router sees them.
|
||||
nestedScrollEnabled
|
||||
scalesPageToFit={false}
|
||||
// Why: Android WebView defaults textZoom to the system font scale, inflating
|
||||
// xterm's DOM glyphs past its canvas-measured cell grid (#4579). iOS ignores it.
|
||||
textZoom={100}
|
||||
onLoadStart={handleLoadStart}
|
||||
onMessage={handleMessage}
|
||||
/>
|
||||
<View style={[TERMINAL_WEBVIEW_FRAME_STYLES.container, style]}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={XTERM_WEBVIEW_SOURCE}
|
||||
style={TERMINAL_WEBVIEW_FRAME_STYLES.webview}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
scrollEnabled={false}
|
||||
// Why: Android parent gesture containers can intercept vertical drags
|
||||
// before the injected xterm scroll router sees them.
|
||||
nestedScrollEnabled
|
||||
scalesPageToFit={false}
|
||||
// Why: Android WebView defaults textZoom to the system font scale, inflating
|
||||
// xterm's DOM glyphs past its canvas-measured cell grid (#4579). iOS ignores it.
|
||||
textZoom={100}
|
||||
onLoadStart={handleLoadStart}
|
||||
onMessage={handleMessage}
|
||||
onError={(event) => reportNativeEngineError('Terminal WebView load failed', event)}
|
||||
onHttpError={(event) => reportNativeEngineError('Terminal WebView HTTP error', event)}
|
||||
onRenderProcessGone={(event) =>
|
||||
reportNativeEngineError('Terminal WebView render process ended', event)
|
||||
}
|
||||
onContentProcessDidTerminate={(event) =>
|
||||
reportNativeEngineError('Terminal WebView content process ended', event)
|
||||
}
|
||||
/>
|
||||
{engineError ? (
|
||||
<TerminalWebViewEngineErrorOverlay message={engineError} onReload={handleReload} />
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
webview: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.terminalBg
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { RuntimeMobileTerminalTheme } from '../../../src/shared/runtime-types'
|
||||
import type { TerminalOscLinkRange } from './terminal-osc-link-ranges'
|
||||
|
||||
type TerminalMouseTrackingMode = 'none' | 'x10' | 'vt200' | 'drag' | 'any'
|
||||
|
||||
export type TerminalModes = {
|
||||
bracketedPasteMode: boolean
|
||||
altScreen: boolean
|
||||
mouseTrackingMode: TerminalMouseTrackingMode
|
||||
sgrMouseMode: boolean
|
||||
sgrMousePixelsMode: boolean
|
||||
}
|
||||
|
||||
export type TerminalKeyboardAvoidanceMetrics = {
|
||||
cursorY: number
|
||||
rows: number
|
||||
altScreen: boolean
|
||||
}
|
||||
|
||||
export type MobileTerminalTheme = RuntimeMobileTerminalTheme
|
||||
|
||||
export type TerminalSelectionEvents = {
|
||||
onSelectionMode?: (active: boolean) => void
|
||||
onSelectionCopy?: (text: string) => void
|
||||
onSelectionEvicted?: () => void
|
||||
onModesChanged?: (modes: TerminalModes) => void
|
||||
onKeyboardAvoidanceMetrics?: (metrics: TerminalKeyboardAvoidanceMetrics) => void
|
||||
onHaptic?: (kind: 'selection' | 'success' | 'error' | 'edge-bump') => void
|
||||
onTerminalInput?: (bytes: string) => void
|
||||
onTerminalTap?: () => void
|
||||
// Tap landed on a detected file path; RN resolves + opens it.
|
||||
onFileTap?: (pathText: string, line: number | null, column: number | null) => void
|
||||
// WebView-detected URL tap; RN chooses the mobile routing destination.
|
||||
onOpenUrl?: (url: string) => void
|
||||
// Why: pinch-to-zoom in the terminal snaps to a text-size preset and reports it
|
||||
// here so the app persists it and keeps Settings + other panes in sync.
|
||||
onTextScaleChange?: (scale: number) => void
|
||||
}
|
||||
|
||||
export type TerminalWebViewHandle = {
|
||||
write: (data: string) => void
|
||||
init: (
|
||||
cols: number,
|
||||
rows: number,
|
||||
initialData?: string,
|
||||
preserveScroll?: boolean,
|
||||
oscLinks?: TerminalOscLinkRange[]
|
||||
) => void
|
||||
resize: (cols: number, rows: number) => void
|
||||
// Why: reflow the local xterm buffer (scrollback included) to a new width
|
||||
// after a server-side PTY reflow, so older wrapped lines rewrap to match the
|
||||
// latest output. No-op on the alternate screen.
|
||||
reflow: (cols: number, rows: number) => void
|
||||
clear: () => void
|
||||
measureFitDimensions: (containerHeight?: number) => Promise<{ cols: number; rows: number } | null>
|
||||
resetZoom: () => void
|
||||
cancelSelect: () => void
|
||||
doSelectAll: () => void
|
||||
// Why: lets callers await the WebView-side `init` rAF chain (term.open
|
||||
// → renderService population → first paint) so a follow-up measure
|
||||
// doesn't race ahead and find term=null or cellWidth=0. Resolves on
|
||||
// the next 'ready' notify after the most recent init.
|
||||
awaitReady: () => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { RefreshCw } from 'lucide-react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
|
||||
export type NativeWebViewEngineEvent = {
|
||||
readonly nativeEvent?: object
|
||||
}
|
||||
|
||||
type TerminalWebViewEngineErrorOverlayProps = {
|
||||
readonly message: string
|
||||
readonly onReload: () => void
|
||||
}
|
||||
|
||||
type NativeWebViewEngineFields = {
|
||||
readonly description?: unknown
|
||||
readonly code?: unknown
|
||||
readonly statusCode?: unknown
|
||||
readonly domain?: unknown
|
||||
readonly didCrash?: unknown
|
||||
}
|
||||
|
||||
export function useTerminalWebViewEngineErrorState(onEngineError?: (message: string) => void) {
|
||||
const [engineError, setEngineError] = useState<string | null>(null)
|
||||
const clearEngineError = useCallback(() => setEngineError(null), [])
|
||||
const reportEngineError = useCallback(
|
||||
(message: string, fatal: boolean) => {
|
||||
onEngineError?.(message)
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[terminal-webview] engine error', message)
|
||||
if (fatal) {
|
||||
// Why: the first fatal report is the root cause; later cascades (e.g. the
|
||||
// web-ready watchdog firing after a process-crash report) must not
|
||||
// overwrite its more specific diagnostics. clearEngineError resets.
|
||||
setEngineError((previous) => previous ?? message)
|
||||
}
|
||||
},
|
||||
[onEngineError]
|
||||
)
|
||||
const reportNativeEngineError = useCallback(
|
||||
(context: string, event?: NativeWebViewEngineEvent) => {
|
||||
reportEngineError(describeNativeWebViewEngineError(context, event), true)
|
||||
},
|
||||
[reportEngineError]
|
||||
)
|
||||
return { clearEngineError, engineError, reportEngineError, reportNativeEngineError }
|
||||
}
|
||||
|
||||
export function describeNativeWebViewEngineError(
|
||||
context: string,
|
||||
event?: NativeWebViewEngineEvent
|
||||
): string {
|
||||
const native = event?.nativeEvent as NativeWebViewEngineFields | undefined
|
||||
const parts = [context]
|
||||
const description = native?.description
|
||||
const statusCode = native?.statusCode
|
||||
const code = native?.code
|
||||
const domain = native?.domain
|
||||
if (typeof description === 'string') {
|
||||
parts.push(description)
|
||||
}
|
||||
if (typeof statusCode === 'number') {
|
||||
parts.push(`status ${statusCode}`)
|
||||
}
|
||||
if (typeof code === 'number') {
|
||||
parts.push(`code ${code}`)
|
||||
}
|
||||
if (typeof domain === 'string') {
|
||||
parts.push(domain)
|
||||
}
|
||||
if (native?.didCrash === true) {
|
||||
parts.push('renderer crashed')
|
||||
}
|
||||
return parts.join(' - ')
|
||||
}
|
||||
|
||||
export function TerminalWebViewEngineErrorOverlay({
|
||||
message,
|
||||
onReload
|
||||
}: TerminalWebViewEngineErrorOverlayProps) {
|
||||
return (
|
||||
<View style={styles.errorOverlay}>
|
||||
<Text style={styles.errorTitle}>Terminal failed to load</Text>
|
||||
<Text style={styles.errorDetail} numberOfLines={4}>
|
||||
{message}
|
||||
</Text>
|
||||
<Pressable accessibilityRole="button" style={styles.reloadButton} onPress={onReload}>
|
||||
<RefreshCw size={16} color={colors.terminalBg} />
|
||||
<Text style={styles.reloadButtonText}>Reload</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
errorOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
padding: 24,
|
||||
backgroundColor: colors.terminalBg
|
||||
},
|
||||
errorTitle: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center'
|
||||
},
|
||||
errorDetail: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
textAlign: 'center'
|
||||
},
|
||||
reloadButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 6,
|
||||
backgroundColor: colors.surfaceBright
|
||||
},
|
||||
reloadButtonText: {
|
||||
color: colors.terminalBg,
|
||||
fontSize: 14,
|
||||
fontWeight: '700'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TerminalWebView } from './TerminalWebView'
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
AppState: { currentState: 'active' },
|
||||
Pressable: 'Pressable',
|
||||
StyleSheet: {
|
||||
absoluteFillObject: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
create: (styles: unknown) => styles
|
||||
},
|
||||
Text: 'Text',
|
||||
View: 'View'
|
||||
}))
|
||||
|
||||
vi.mock('react-native-webview', () => ({
|
||||
WebView: 'WebView',
|
||||
default: 'WebView'
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react-native', () => ({
|
||||
RefreshCw: 'RefreshCw'
|
||||
}))
|
||||
|
||||
function suppressReactTestRendererDeprecationWarning(): () => void {
|
||||
const originalConsoleError = console.error
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
||||
const firstArg = args[0]
|
||||
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
|
||||
return
|
||||
}
|
||||
originalConsoleError(...args)
|
||||
})
|
||||
return () => consoleErrorSpy.mockRestore()
|
||||
}
|
||||
|
||||
// Why: mounting TerminalWebView arms the web-ready watchdog; tests must
|
||||
// unmount so the timer can't survive into later tests and fire in teardown.
|
||||
let activeRenderer: ReactTestRenderer | null = null
|
||||
|
||||
function createTerminalWebViewRenderer(onEngineError = vi.fn()) {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
|
||||
try {
|
||||
act(() => {
|
||||
renderer = create(createElement(TerminalWebView, { onEngineError }))
|
||||
})
|
||||
} finally {
|
||||
restoreConsoleError()
|
||||
}
|
||||
if (!renderer) {
|
||||
throw new Error('TerminalWebView did not render')
|
||||
}
|
||||
activeRenderer = renderer
|
||||
return { onEngineError, renderer }
|
||||
}
|
||||
|
||||
function postWebViewMessage(renderer: ReactTestRenderer, payload: Record<string, unknown>) {
|
||||
const webView = renderer.root.findByType('WebView')
|
||||
act(() => {
|
||||
webView.props.onMessage({ nativeEvent: { data: JSON.stringify(payload) } })
|
||||
})
|
||||
}
|
||||
|
||||
function renderedText(renderer: ReactTestRenderer): string {
|
||||
return renderer.root
|
||||
.findAllByType('Text')
|
||||
.flatMap((node) => node.props.children)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
describe('TerminalWebView engine errors', () => {
|
||||
afterEach(() => {
|
||||
if (activeRenderer) {
|
||||
act(() => {
|
||||
activeRenderer?.unmount()
|
||||
})
|
||||
activeRenderer = null
|
||||
}
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders the reload overlay for fatal engine errors from the WebView', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { onEngineError, renderer } = createTerminalWebViewRenderer()
|
||||
|
||||
postWebViewMessage(renderer, {
|
||||
fatal: true,
|
||||
message: 'terminal engine missing - SyntaxError - Chrome 74',
|
||||
type: 'error'
|
||||
})
|
||||
|
||||
expect(onEngineError).toHaveBeenCalledWith('terminal engine missing - SyntaxError - Chrome 74')
|
||||
expect(renderedText(renderer)).toContain('Terminal failed to load')
|
||||
expect(renderedText(renderer)).toContain('terminal engine missing - SyntaxError - Chrome 74')
|
||||
expect(renderedText(renderer)).toContain('Reload')
|
||||
})
|
||||
|
||||
it('reports non-fatal engine errors without covering a live terminal', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { onEngineError, renderer } = createTerminalWebViewRenderer()
|
||||
|
||||
postWebViewMessage(renderer, {
|
||||
fatal: false,
|
||||
message: 'terminal message failed - malformed chunk',
|
||||
type: 'error'
|
||||
})
|
||||
|
||||
expect(onEngineError).toHaveBeenCalledWith('terminal message failed - malformed chunk')
|
||||
expect(renderedText(renderer)).not.toContain('Terminal failed to load')
|
||||
})
|
||||
|
||||
it('keeps the first fatal diagnostics when later fatal reports cascade', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { renderer } = createTerminalWebViewRenderer()
|
||||
|
||||
postWebViewMessage(renderer, {
|
||||
fatal: true,
|
||||
message: 'terminal engine missing - root cause',
|
||||
type: 'error'
|
||||
})
|
||||
postWebViewMessage(renderer, {
|
||||
fatal: true,
|
||||
message: 'Terminal did not initialize - watchdog cascade',
|
||||
type: 'error'
|
||||
})
|
||||
|
||||
expect(renderedText(renderer)).toContain('terminal engine missing - root cause')
|
||||
expect(renderedText(renderer)).not.toContain('watchdog cascade')
|
||||
})
|
||||
|
||||
// Why: if the document dies before the glue can post anything (or the RN
|
||||
// bridge never comes up), no message and no native error handler fires —
|
||||
// the watchdog must convert that silence into the visible fatal overlay.
|
||||
it('paints the fatal overlay when web-ready never arrives', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
try {
|
||||
const { onEngineError, renderer } = createTerminalWebViewRenderer()
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(15000)
|
||||
})
|
||||
|
||||
expect(onEngineError).toHaveBeenCalledWith(
|
||||
'Terminal did not initialize - no ready signal from the terminal view'
|
||||
)
|
||||
expect(renderedText(renderer)).toContain('Terminal failed to load')
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not fire the watchdog once web-ready has arrived', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
try {
|
||||
const { onEngineError, renderer } = createTerminalWebViewRenderer()
|
||||
|
||||
postWebViewMessage(renderer, { type: 'web-ready' })
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(60000)
|
||||
})
|
||||
|
||||
expect(onEngineError).not.toHaveBeenCalled()
|
||||
expect(renderedText(renderer)).not.toContain('Terminal failed to load')
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { Script } from 'node:vm'
|
||||
import { parse } from 'acorn'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { XTERM_ENGINE_CSS, XTERM_ENGINE_JS } from './terminal-webview-engine.generated'
|
||||
import { XTERM_HTML } from './terminal-webview-html'
|
||||
|
||||
const terminalHtmlSource = readFileSync(
|
||||
new URL('./terminal-webview-html.ts', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
describe('terminal WebView bundled engine', () => {
|
||||
it('keeps the assembled terminal HTML free of external engine URLs', () => {
|
||||
expect(XTERM_HTML).not.toMatch(/\bhttps?:\/\//)
|
||||
expect(XTERM_HTML).not.toContain('cdn.jsdelivr.net')
|
||||
expect(XTERM_HTML).not.toContain('<script src=')
|
||||
expect(XTERM_HTML).not.toContain('rel="stylesheet" href=')
|
||||
})
|
||||
|
||||
it('parses the bundled engine at the Chrome 74 syntax floor', () => {
|
||||
expect(() => parse(XTERM_ENGINE_JS, { ecmaVersion: 2019 })).not.toThrow()
|
||||
})
|
||||
|
||||
// Why: the context deliberately omits WeakRef (Chrome 84+) / structuredClone
|
||||
// (Chrome 98+) and supplies an Element without replaceChildren (Chrome 86+) —
|
||||
// the engine must evaluate on older WebViews via its own guarded runtime shims,
|
||||
// which are the linchpin of the old-WebView support (esbuild lowers syntax only).
|
||||
it('exposes the xterm globals and installs the old-WebView runtime shims', () => {
|
||||
const window: Record<string, unknown> = {}
|
||||
class ElementStub {}
|
||||
const context = {
|
||||
window,
|
||||
self: window,
|
||||
document: {},
|
||||
Element: ElementStub,
|
||||
navigator: {
|
||||
platform: 'Linux armv8l',
|
||||
userAgent: 'Mozilla/5.0 Chrome/74.0.3729.157'
|
||||
},
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
queueMicrotask,
|
||||
URL
|
||||
}
|
||||
|
||||
new Script(XTERM_ENGINE_JS).runInNewContext(context)
|
||||
|
||||
expect(window).toMatchObject({
|
||||
Terminal: expect.any(Function),
|
||||
Unicode11Addon: { Unicode11Addon: expect.any(Function) },
|
||||
WebglAddon: { WebglAddon: expect.any(Function) }
|
||||
})
|
||||
|
||||
const weakRef = window.WeakRef as (new (target: unknown) => { deref(): unknown }) | undefined
|
||||
expect(typeof weakRef).toBe('function')
|
||||
const token = {}
|
||||
expect(new weakRef!(token).deref()).toBe(token)
|
||||
expect(typeof window.structuredClone).toBe('function')
|
||||
expect(typeof (ElementStub.prototype as { replaceChildren?: unknown }).replaceChildren).toBe(
|
||||
'function'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the bundled engine from breaking out of its inline script/style tags', () => {
|
||||
// Why: the engine JS/CSS are inlined into <script>/<style> blocks. </script
|
||||
// and </style are neutralized at build time; the tokenizer-escape openers that
|
||||
// could swallow the rest of the document must also be absent from the bundle.
|
||||
expect(XTERM_ENGINE_JS).not.toMatch(/<\/script/i)
|
||||
expect(XTERM_ENGINE_JS).not.toMatch(/<script/i)
|
||||
expect(XTERM_ENGINE_JS).not.toContain('<!--')
|
||||
expect(XTERM_ENGINE_CSS).not.toMatch(/<\/style/i)
|
||||
})
|
||||
|
||||
it('reports WebView message handler failures instead of swallowing them', () => {
|
||||
const start = terminalHtmlSource.indexOf('function handleIncomingMessage')
|
||||
const end = terminalHtmlSource.indexOf("window.addEventListener('resize'", start)
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
const handlerSource = terminalHtmlSource.slice(start, end)
|
||||
|
||||
expect(handlerSource).toContain('reportEngineError(')
|
||||
expect(handlerSource).toContain("'terminal init failed'")
|
||||
expect(handlerSource).toContain("'terminal message failed'")
|
||||
expect(handlerSource).not.toContain('catch(ex) {}')
|
||||
})
|
||||
|
||||
it('classifies runtime errors by a document-scoped ever-ready latch', () => {
|
||||
// Why: init() flips `ready` false on every re-init (live width reflow keeps the
|
||||
// old surface visible meanwhile), so the fatal default and the init-catch must
|
||||
// key off `everReady` — otherwise a transient reflow error blanks a live
|
||||
// terminal behind the fatal overlay. The latch stays set for the document.
|
||||
expect(terminalHtmlSource).toContain('var everReady = false;')
|
||||
expect(terminalHtmlSource).toContain('everReady = true;')
|
||||
expect(terminalHtmlSource).toContain('fatal === undefined ? !everReady : !!fatal')
|
||||
expect(terminalHtmlSource).toContain("msg.type === 'init' && !everReady")
|
||||
expect(terminalHtmlSource).not.toMatch(/fatal === undefined \? !ready\b/)
|
||||
})
|
||||
|
||||
it('bounds error capture and non-fatal reporting on a degraded engine', () => {
|
||||
// Why: a constructed-but-broken engine can throw per render frame; both
|
||||
// onerror capture sites must cap the buffer and non-fatal notifies must
|
||||
// stop flooding RN while fatal reports always emit.
|
||||
const capSites = terminalHtmlSource.match(/__engineErrors\.length < 20/g) ?? []
|
||||
expect(capSites.length).toBe(2)
|
||||
expect(terminalHtmlSource).toContain('nonFatalErrorNotifies > 5')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
|
||||
export const TERMINAL_WEBVIEW_FRAME_STYLES = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.terminalBg
|
||||
},
|
||||
webview: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.terminalBg
|
||||
}
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import type { RuntimeMobileTerminalTheme } from '../../../src/shared/runtime-typ
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { TERMINAL_TEXT_SCALES } from '../storage/preferences'
|
||||
import { TERMINAL_PATH_TAP_JS } from './terminal-path-tap-injected'
|
||||
import { XTERM_ENGINE_CSS, XTERM_ENGINE_JS } from './terminal-webview-engine.generated'
|
||||
import { TERMINAL_REFLOW_JS } from './terminal-webview-reflow-injected'
|
||||
import { TERMINAL_TAP_DISPATCH_JS } from './terminal-webview-tap-dispatch-injected'
|
||||
import { URL_TAP_WEBVIEW_JS } from './terminal-webview-url-tap'
|
||||
@@ -47,7 +48,15 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.1.0-beta.285/css/xterm.min.css">
|
||||
<script>
|
||||
window.__engineErrors = [];
|
||||
window.onerror = function(msg) {
|
||||
// Why: a degraded engine can throw per frame; cap so the capture buffer
|
||||
// and downstream reporting stay bounded for the document's lifetime.
|
||||
if (window.__engineErrors.length < 20) window.__engineErrors.push(String(msg));
|
||||
};
|
||||
</script>
|
||||
<style>${XTERM_ENGINE_CSS}</style>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
@@ -192,7 +201,7 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
<button id="sel-menu-all">Select All</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.1.0-beta.285/lib/xterm.min.js"></script><script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.10.0-beta.285/lib/addon-unicode11.js"></script><script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.20.0-beta.284/lib/addon-webgl.js"></script>
|
||||
<script>${XTERM_ENGINE_JS}</script>
|
||||
<script>
|
||||
(function() {
|
||||
var surface = document.getElementById('terminal-surface');
|
||||
@@ -214,6 +223,10 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
var afterDrainCallbacks = [];
|
||||
var termObserverDisposables = [];
|
||||
var ready = false;
|
||||
// Why: init() flips ready false on every re-init (live width reflow included)
|
||||
// while the old surface stays visible; a document-scoped latch drives the
|
||||
// fatal/non-fatal decision so a transient reflow cannot blank a live terminal.
|
||||
var everReady = false;
|
||||
var currentScale = 1;
|
||||
// Why: userScale is transient pinch zoom (CSS) for smooth feedback DURING a
|
||||
// gesture only; it resets to 1 on release. The persistent "text size" is the
|
||||
@@ -745,6 +758,7 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
requestAnimationFrame(function() {
|
||||
if (gen !== terminalGeneration) return;
|
||||
ready = true;
|
||||
everReady = true;
|
||||
afterWritesDrained(function() {
|
||||
if (gen !== terminalGeneration) return;
|
||||
if (nextSurface && oldSurface) {
|
||||
@@ -804,6 +818,47 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
}
|
||||
}
|
||||
|
||||
function engineErrorText(err) {
|
||||
if (!err) return '';
|
||||
if (typeof err === 'string') return err;
|
||||
if (err && typeof err.message === 'string') return err.message;
|
||||
try { return String(err); } catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function chromeVersionText() {
|
||||
var match = String(navigator.userAgent || '').match(/(?:Chrome|Chromium)\\/([0-9.]+)/);
|
||||
return match ? 'Chrome ' + match[1] : 'Chrome version unknown';
|
||||
}
|
||||
|
||||
var nonFatalErrorNotifies = 0;
|
||||
|
||||
function reportEngineError(context, err, fatal) {
|
||||
var isFatal = fatal === undefined ? !everReady : !!fatal;
|
||||
if (!isFatal) {
|
||||
// Why: a constructed-but-degraded engine can throw per frame; cap
|
||||
// non-fatal notifies so RN isn't flooded. Fatal reports always emit.
|
||||
nonFatalErrorNotifies++;
|
||||
if (nonFatalErrorNotifies > 5) return;
|
||||
}
|
||||
var parts = [context];
|
||||
var errText = engineErrorText(err);
|
||||
if (errText) parts.push(errText);
|
||||
if (window.__engineErrors && window.__engineErrors.length) {
|
||||
parts.push('captured: ' + window.__engineErrors.join(' | '));
|
||||
}
|
||||
parts.push(chromeVersionText());
|
||||
notify({
|
||||
type: 'error',
|
||||
fatal: isFatal,
|
||||
message: parts.join(' - ')
|
||||
});
|
||||
}
|
||||
|
||||
window.onerror = function(msg, source, line, column, err) {
|
||||
if (window.__engineErrors.length < 20) window.__engineErrors.push(String(msg));
|
||||
reportEngineError('terminal runtime error', err || msg);
|
||||
};
|
||||
|
||||
function measureFitDimensions(containerHeightPx, retriesLeft) {
|
||||
if (typeof retriesLeft !== 'number') retriesLeft = 30;
|
||||
// Why: init and measure are posted back-to-back from React, but
|
||||
@@ -1801,17 +1856,27 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
|
||||
attachSurfaceEventHandlers(surface);
|
||||
|
||||
window.addEventListener('message', function(e) {
|
||||
function handleIncomingMessage(e) {
|
||||
var msg;
|
||||
try {
|
||||
handleMsg(typeof e.data === 'string' ? JSON.parse(e.data) : e.data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
msg = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
handleMsg(msg);
|
||||
} catch(ex) {
|
||||
reportEngineError(
|
||||
msg && msg.type === 'init' ? 'terminal init failed' : 'terminal message failed',
|
||||
ex,
|
||||
msg && msg.type === 'init' && !everReady
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('message', function(e) {
|
||||
try {
|
||||
handleMsg(typeof e.data === 'string' ? JSON.parse(e.data) : e.data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
window.addEventListener('message', handleIncomingMessage);
|
||||
|
||||
document.addEventListener('message', handleIncomingMessage);
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
// Why: viewport changed (keyboard open/close, orientation, RN container
|
||||
@@ -1828,9 +1893,13 @@ export const XTERM_HTML = `<!DOCTYPE html>
|
||||
if (window.Terminal) {
|
||||
notify({ type: 'web-ready' });
|
||||
} else {
|
||||
notify({ type: 'error', message: 'xterm failed to load' });
|
||||
reportEngineError('terminal engine missing', 'xterm failed to load', true);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// Why: WebView treats source identity as page identity on some platforms; keep
|
||||
// parent/session re-renders from reloading xterm and forcing fresh snapshots.
|
||||
export const XTERM_WEBVIEW_SOURCE = { html: XTERM_HTML }
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback, useEffect, useRef, type RefObject } from 'react'
|
||||
import { AppState } from 'react-native'
|
||||
|
||||
// Why: if the document dies before the glue can post anything (or the RN
|
||||
// message bridge never comes up), no webview error and no native handler
|
||||
// fires — without a native watchdog that failure is a silent blank pane.
|
||||
const WEB_READY_WATCHDOG_MS = 15000
|
||||
|
||||
export function useTerminalWebReadyWatchdog(
|
||||
isWebReadyRef: RefObject<boolean>,
|
||||
reportEngineError: (message: string, fatal: boolean) => void
|
||||
) {
|
||||
const watchdogRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const clearWebReadyWatchdog = useCallback(() => {
|
||||
if (watchdogRef.current) {
|
||||
clearTimeout(watchdogRef.current)
|
||||
watchdogRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const armWebReadyWatchdog = useCallback(() => {
|
||||
clearWebReadyWatchdog()
|
||||
const fire = () => {
|
||||
watchdogRef.current = null
|
||||
if (isWebReadyRef.current) {
|
||||
return
|
||||
}
|
||||
if (AppState.currentState !== 'active') {
|
||||
// Why: backgrounded WebViews legitimately stall; only judge foreground loads.
|
||||
watchdogRef.current = setTimeout(fire, WEB_READY_WATCHDOG_MS)
|
||||
return
|
||||
}
|
||||
reportEngineError(
|
||||
'Terminal did not initialize - no ready signal from the terminal view',
|
||||
true
|
||||
)
|
||||
}
|
||||
watchdogRef.current = setTimeout(fire, WEB_READY_WATCHDOG_MS)
|
||||
}, [clearWebReadyWatchdog, isWebReadyRef, reportEngineError])
|
||||
|
||||
useEffect(() => {
|
||||
armWebReadyWatchdog()
|
||||
return clearWebReadyWatchdog
|
||||
}, [armWebReadyWatchdog, clearWebReadyWatchdog])
|
||||
|
||||
return { armWebReadyWatchdog, clearWebReadyWatchdog }
|
||||
}
|
||||
@@ -14,7 +14,7 @@ function iifeSource(): string {
|
||||
|
||||
function bodyMarkup(): string {
|
||||
const start = XTERM_HTML.indexOf('<body>') + '<body>'.length
|
||||
const end = XTERM_HTML.indexOf('<script src=')
|
||||
const end = XTERM_HTML.indexOf('<script>', start)
|
||||
return XTERM_HTML.slice(start, end)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('TerminalWebView text zoom', () => {
|
||||
const end = terminalWebViewSource.indexOf('/>', start)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
const webViewProps = terminalWebViewSource.slice(start, end)
|
||||
expect(terminalWebViewSource).toContain('const XTERM_WEBVIEW_SOURCE = { html: XTERM_HTML }')
|
||||
expect(terminalHtmlSource).toContain('export const XTERM_WEBVIEW_SOURCE = { html: XTERM_HTML }')
|
||||
expect(webViewProps).toContain('source={XTERM_WEBVIEW_SOURCE}')
|
||||
expect(webViewProps).not.toContain('source={{ html: XTERM_HTML }}')
|
||||
})
|
||||
@@ -113,8 +113,8 @@ describe('TerminalWebView text zoom', () => {
|
||||
})
|
||||
|
||||
it('loads Unicode 11 before replaying mobile terminal bytes', () => {
|
||||
expect(terminalHtmlSource).toContain('@xterm/xterm@6.1.0-beta.285')
|
||||
expect(terminalHtmlSource).toContain('@xterm/addon-unicode11@0.10.0-beta.285')
|
||||
expect(terminalHtmlSource).toContain('XTERM_ENGINE_JS')
|
||||
expect(terminalHtmlSource).toContain('window.Unicode11Addon.Unicode11Addon')
|
||||
const open = terminalHtmlSource.indexOf('term.open(surface)')
|
||||
const unicode = terminalHtmlSource.indexOf("term.unicode.activeVersion = '11'")
|
||||
const replay = terminalHtmlSource.indexOf('enqueueWrite(replayData)')
|
||||
@@ -123,8 +123,9 @@ describe('TerminalWebView text zoom', () => {
|
||||
expect(replay).toBeGreaterThan(unicode)
|
||||
})
|
||||
|
||||
it('uses the newer WebGL-capable xterm stack and desktop font fallbacks', () => {
|
||||
expect(terminalHtmlSource).toContain('@xterm/addon-webgl@0.20.0-beta.284')
|
||||
it('uses the bundled WebGL-capable xterm stack and desktop font fallbacks', () => {
|
||||
expect(terminalHtmlSource).not.toContain('cdn.jsdelivr.net')
|
||||
expect(terminalHtmlSource).toContain('window.WebglAddon.WebglAddon')
|
||||
expect(terminalHtmlSource).toContain('"SF Mono", "Menlo", "Monaco", "Cascadia Mono"')
|
||||
expect(terminalHtmlSource).toContain("fontWeight: '300'")
|
||||
expect(terminalHtmlSource).toContain("fontWeightBold: '500'")
|
||||
|
||||
Reference in New Issue
Block a user