From 1a1bdefc83e0e3118fa14b25dc44da81e2e36e54 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 27 Dec 2025 04:33:27 -0800
Subject: [PATCH 1/2] Move server to TS
Fixes #5509
---
.vscode/settings.json | 1 +
bin/esbuild.mjs | 16 ++++-
demo/{ => client}/client.ts | 0
demo/{ => client}/components/controlBar.ts | 0
.../components/window/addonsWindow.ts | 0
.../components/window/baseWindow.ts | 0
.../components/window/gpuWindow.ts | 0
.../components/window/optionsWindow.ts | 0
.../components/window/styleWindow.ts | 0
.../components/window/testWindow.ts | 4 +-
.../components/window/vtWindow.ts | 0
demo/client/tsconfig.json | 27 ++++++++
demo/{ => client}/unicodeTable.ts | 0
demo/{server.js => server/server.ts} | 62 ++++++++++---------
demo/server/tsconfig.json | 11 ++++
demo/start.js | 2 +-
demo/tsconfig.json | 30 ++-------
17 files changed, 95 insertions(+), 58 deletions(-)
rename demo/{ => client}/client.ts (100%)
rename demo/{ => client}/components/controlBar.ts (100%)
rename demo/{ => client}/components/window/addonsWindow.ts (100%)
rename demo/{ => client}/components/window/baseWindow.ts (100%)
rename demo/{ => client}/components/window/gpuWindow.ts (100%)
rename demo/{ => client}/components/window/optionsWindow.ts (100%)
rename demo/{ => client}/components/window/styleWindow.ts (100%)
rename demo/{ => client}/components/window/testWindow.ts (99%)
rename demo/{ => client}/components/window/vtWindow.ts (100%)
create mode 100644 demo/client/tsconfig.json
rename demo/{ => client}/unicodeTable.ts (100%)
rename demo/{server.js => server/server.ts} (78%)
create mode 100644 demo/server/tsconfig.json
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7fef8b16..2c1653d3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,6 +2,7 @@
"chat.tools.terminal.autoApprove": {
"npm run build": true,
"npm run esbuild": true,
+ "npm run dev": true,
"npm run lint": true,
"npm run lint-fix": true,
"npm run lint-api": true,
diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs
index 1727defe..b1e9e88f 100644
--- a/bin/esbuild.mjs
+++ b/bin/esbuild.mjs
@@ -12,6 +12,7 @@ const config = {
isProd: argv.includes('--prod'),
isWatch: argv.includes('--watch'),
isDemoClient: argv.includes('--demo-client'),
+ isDemoServer: argv.includes('--demo-server'),
isHeadless: argv.includes('--headless'),
addon: argv.find(e => e.startsWith('--addon='))?.replace(/^--addon=/, ''),
};
@@ -126,7 +127,7 @@ if (config.addon) {
} else if (config.isDemoClient) {
bundleConfig = {
...bundleConfig,
- entryPoints: [`demo/client.ts`],
+ entryPoints: [`demo/client/client.ts`],
outfile: 'demo/dist/client-bundle.js',
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
alias: {
@@ -152,6 +153,19 @@ if (config.addon) {
"@xterm/addon-ligatures": "./addons/addon-ligatures/out-esbuild/LigaturesAddon",
}
}
+ skipOut = true;
+ skipOutTest = true;
+} else if (config.isDemoServer) {
+ bundleConfig = {
+ ...bundleConfig,
+ entryPoints: [`demo/server/server.ts`],
+ outfile: 'demo/dist/server-bundle.js',
+ format: 'cjs',
+ platform: 'node',
+ external: ['node-pty'],
+ }
+ skipOut = true;
+ skipOutTest = true;
} else if (config.isHeadless) {
bundleConfig = {
...bundleConfig,
diff --git a/demo/client.ts b/demo/client/client.ts
similarity index 100%
rename from demo/client.ts
rename to demo/client/client.ts
diff --git a/demo/components/controlBar.ts b/demo/client/components/controlBar.ts
similarity index 100%
rename from demo/components/controlBar.ts
rename to demo/client/components/controlBar.ts
diff --git a/demo/components/window/addonsWindow.ts b/demo/client/components/window/addonsWindow.ts
similarity index 100%
rename from demo/components/window/addonsWindow.ts
rename to demo/client/components/window/addonsWindow.ts
diff --git a/demo/components/window/baseWindow.ts b/demo/client/components/window/baseWindow.ts
similarity index 100%
rename from demo/components/window/baseWindow.ts
rename to demo/client/components/window/baseWindow.ts
diff --git a/demo/components/window/gpuWindow.ts b/demo/client/components/window/gpuWindow.ts
similarity index 100%
rename from demo/components/window/gpuWindow.ts
rename to demo/client/components/window/gpuWindow.ts
diff --git a/demo/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts
similarity index 100%
rename from demo/components/window/optionsWindow.ts
rename to demo/client/components/window/optionsWindow.ts
diff --git a/demo/components/window/styleWindow.ts b/demo/client/components/window/styleWindow.ts
similarity index 100%
rename from demo/components/window/styleWindow.ts
rename to demo/client/components/window/styleWindow.ts
diff --git a/demo/components/window/testWindow.ts b/demo/client/components/window/testWindow.ts
similarity index 99%
rename from demo/components/window/testWindow.ts
rename to demo/client/components/window/testWindow.ts
index 99efbad4..bb542595 100644
--- a/demo/components/window/testWindow.ts
+++ b/demo/client/components/window/testWindow.ts
@@ -3,9 +3,9 @@
* @license MIT
*/
-///
+///
-import { writeUnicodeTable } from 'unicodeTable';
+import { writeUnicodeTable } from '../../unicodeTable';
import type { IControlWindow } from '../controlBar';
import { BaseWindow } from './baseWindow';
import type { IDisposable, Terminal } from '@xterm/xterm';
diff --git a/demo/components/window/vtWindow.ts b/demo/client/components/window/vtWindow.ts
similarity index 100%
rename from demo/components/window/vtWindow.ts
rename to demo/client/components/window/vtWindow.ts
diff --git a/demo/client/tsconfig.json b/demo/client/tsconfig.json
new file mode 100644
index 00000000..7676cd37
--- /dev/null
+++ b/demo/client/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2021",
+ "rootDir": ".",
+ "sourceMap": true,
+ "baseUrl": ".",
+ "paths": {
+ "@xterm/addon-attach": ["../../addons/addon-attach"],
+ "@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
+ "@xterm/addon-fit": ["../../addons/addon-fit"],
+ "@xterm/addon-image": ["../../addons/addon-image"],
+ "@xterm/addon-progress": ["../../addons/addon-progress"],
+ "@xterm/addon-search": ["../../addons/addon-search"],
+ "@xterm/addon-serialize": ["../../addons/addon-serialize"],
+ "@xterm/addon-web-links": ["../../addons/addon-web-links"],
+ "@xterm/addon-webgl": ["../../addons/addon-webgl"],
+ "@xterm/addon-unicode11": ["../../addons/addon-unicode11"],
+ "@xterm/addon-unicode-graphemes": ["../../addons/addon-unicode-graphemes"],
+ "@xterm/addon-ligatures": ["../../addons/addon-ligatures"]
+ }
+ },
+ "include": [
+ "client.ts",
+ "../../typings/xterm.d.ts"
+ ]
+}
diff --git a/demo/unicodeTable.ts b/demo/client/unicodeTable.ts
similarity index 100%
rename from demo/unicodeTable.ts
rename to demo/client/unicodeTable.ts
diff --git a/demo/server.js b/demo/server/server.ts
similarity index 78%
rename from demo/server.js
rename to demo/server/server.ts
index 4537d501..cefb343e 100644
--- a/demo/server.js
+++ b/demo/server/server.ts
@@ -4,54 +4,58 @@
* demo to the public as is would introduce security risks for the host.
**/
-// @ts-check
+import * as express from 'express';
+import * as expressWs from 'express-ws';
+import * as os from 'os';
+import * as pty from 'node-pty';
+import * as path from 'path';
+import type { IPty } from 'node-pty';
-const express = require('express');
-const expressWs = require('express-ws');
-const os = require('os');
-const pty = require('node-pty');
+interface IDisposable {
+ dispose(): void;
+}
/** Whether to use binary transport. */
const USE_BINARY = os.platform() !== "win32";
-function startServer() {
+const demoRoot = path.join(__dirname, '..');
+
+function startServer(): void {
const app = express();
const appWs = expressWs(app).app;
- const terminals = {};
- const unsentOutput = {};
- const temporaryDisposable = {};
+ const terminals: { [pid: number]: IPty } = {};
+ const unsentOutput: { [pid: number]: string } = {};
+ const temporaryDisposable: { [pid: number]: IDisposable } = {};
- app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
+ app.use('/xterm.css', express.static(demoRoot + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => {
- res.sendFile(__dirname + '/logo.png');
+ res.sendFile(demoRoot + '/logo.png');
});
app.get('/', (req, res) => {
- res.sendFile(__dirname + '/index.html');
+ res.sendFile(demoRoot + '/index.html');
});
app.get('/test', (req, res) => {
- res.sendFile(__dirname + '/test.html');
+ res.sendFile(demoRoot + '/test.html');
});
app.get('/index.css', (req, res) => {
- res.sendFile(__dirname + '/index.css');
+ res.sendFile(demoRoot + '/index.css');
});
- app.use('/dist', express.static(__dirname + '/dist'));
- app.use('/src', express.static(__dirname + '/src'));
+ app.use('/dist', express.static(demoRoot + '/dist'));
+ app.use('/src', express.static(demoRoot + '/src'));
app.post('/terminals', (req, res) => {
- /** @type {{ [key: string]: string }} */
- const env = {};
+ const env: { [key: string]: string } = {};
for (const k of Object.keys(process.env)) {
const v = process.env[k];
if (v) {
env[k] = v;
}
}
- // const env = Object.assign({}, process.env);
env['COLORTERM'] = 'truecolor';
if (typeof req.query.cols !== 'string' || typeof req.query.rows !== 'string') {
console.error({ req });
@@ -108,10 +112,10 @@ function startServer() {
let userInput = false;
// string message buffering
- function buffer(socket, timeout, maxSize) {
+ function buffer(socket: typeof ws, timeout: number, maxSize: number) {
let s = '';
- let sender = null;
- return (data) => {
+ let sender: ReturnType | null = null;
+ return (data: string) => {
s += data;
if (s.length > maxSize || userInput) {
userInput = false;
@@ -131,11 +135,11 @@ function startServer() {
};
}
// binary message buffering
- function bufferUtf8(socket, timeout, maxSize) {
- const chunks = [];
+ function bufferUtf8(socket: typeof ws, timeout: number, maxSize: number) {
+ const chunks: Buffer[] = [];
let length = 0;
- let sender = null;
- return (data) => {
+ let sender: ReturnType | null = null;
+ return (data: Buffer) => {
chunks.push(data);
length += data.length;
if (length > maxSize || userInput) {
@@ -164,13 +168,13 @@ function startServer() {
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
term.onData(function(data) {
try {
- send(data);
+ send(data as string & Buffer);
} catch (ex) {
// The WebSocket is not open, ignore
}
});
ws.on('message', function(msg) {
- term.write(msg);
+ term.write(msg.toString());
userInput = true;
});
ws.on('close', function () {
@@ -196,4 +200,4 @@ process.on('uncaughtException', (error) => {
}
});
-module.exports = startServer;
+export default startServer;
diff --git a/demo/server/tsconfig.json b/demo/server/tsconfig.json
new file mode 100644
index 00000000..ed5c6a90
--- /dev/null
+++ b/demo/server/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2021",
+ "rootDir": ".",
+ "sourceMap": true
+ },
+ "include": [
+ "server.ts"
+ ]
+}
diff --git a/demo/start.js b/demo/start.js
index b240966f..08f9c059 100644
--- a/demo/start.js
+++ b/demo/start.js
@@ -5,6 +5,6 @@
// @ts-check
-const startServer = require('./server.js');
+const startServer = require('./dist/server-bundle.js').default;
startServer();
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
index 405dbf0f..e9e26053 100644
--- a/demo/tsconfig.json
+++ b/demo/tsconfig.json
@@ -1,27 +1,7 @@
{
- "compilerOptions": {
- "module": "commonjs",
- "target": "es2021",
- "rootDir": ".",
- "sourceMap": true,
- "baseUrl": ".",
- "paths": {
- "@xterm/addon-attach": ["../addons/addon-attach"],
- "@xterm/addon-clipboard": ["../addons/addon-clipboard"],
- "@xterm/addon-fit": ["../addons/addon-fit"],
- "@xterm/addon-image": ["../addons/addon-image"],
- "@xterm/addon-progress": ["../addons/addon-progress"],
- "@xterm/addon-search": ["../addons/addon-search"],
- "@xterm/addon-serialize": ["../addons/addon-serialize"],
- "@xterm/addon-web-links": ["../addons/addon-web-links"],
- "@xterm/addon-webgl": ["../addons/addon-webgl"],
- "@xterm/addon-unicode11": ["../addons/addon-unicode11"],
- "@xterm/addon-unicode-graphemes": ["../addons/addon-unicode-graphemes"],
- "@xterm/addon-ligatures": ["../addons/addon-ligatures"]
- }
- },
- "include": [
- "client.ts",
- "../typings/xterm.d.ts"
- ]
+ "references": [
+ { "path": "./client" },
+ { "path": "./server" }
+ ],
+ "files": []
}
From f83760d02768d165495717f81c3270630f1ebe19 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 27 Dec 2025 04:57:55 -0800
Subject: [PATCH 2/2] Add esbuild server build task
---
.github/workflows/ci.yml | 6 ++++--
.vscode/tasks.json | 18 +++++++++++++++---
bin/esbuild_all.mjs | 2 +-
demo/server/server.ts | 10 +++++-----
demo/server/tsconfig.json | 3 ++-
package.json | 12 ++++++++----
6 files changed, 35 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2e95fb92..865060f8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -199,8 +199,10 @@ jobs:
unzip -o compressed-build.zip
fi
ls -R
- - name: Build demo
- run: npm run esbuild-demo
+ - name: Build demo client
+ run: npm run esbuild-demo-client
+ - name: Build demo server
+ run: npm run esbuild-demo-server
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
- name: Integration tests (addon-attach)
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 0bafdab6..6fe42a3e 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -23,7 +23,7 @@
{
"label": "dev (separate terminals)",
"detail": "Runs all tasks required to run the demo in separate terminals. This does support problem matching.",
- "dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo"],
+ "dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo-client", "esbuild-demo-server"],
"group": "build"
},
@@ -64,9 +64,21 @@
}
},
{
- "label": "esbuild-demo",
+ "label": "esbuild-demo-client",
"type": "npm",
- "script": "esbuild-demo-watch",
+ "script": "esbuild-demo-client-watch",
+ "dependsOn": ["esbuild", "tsc"],
+ "group": "build",
+ "isBackground": true,
+ "problemMatcher": "$esbuild-watch",
+ "presentation": {
+ "group": "xterm-demo"
+ }
+ },
+ {
+ "label": "esbuild-demo-server",
+ "type": "npm",
+ "script": "esbuild-demo-server-watch",
"dependsOn": ["esbuild", "tsc"],
"group": "build",
"isBackground": true,
diff --git a/bin/esbuild_all.mjs b/bin/esbuild_all.mjs
index 0c0e27b7..933bb828 100644
--- a/bin/esbuild_all.mjs
+++ b/bin/esbuild_all.mjs
@@ -4,7 +4,7 @@ import { spawn } from "child_process";
import { readdir } from "fs/promises";
import { argv } from "process";
-/** @type {{cp: import("child_process").ChildProcessByStdio, name: string}[]} */
+/** @type {{cp: import("child_process").ChildProcessByStdio, name: string}[]} */
const jobs = [];
// Core job
diff --git a/demo/server/server.ts b/demo/server/server.ts
index cefb343e..97a91243 100644
--- a/demo/server/server.ts
+++ b/demo/server/server.ts
@@ -2,10 +2,10 @@
* WARNING: This demo is a barebones implementation designed for development and evaluation
* purposes only. It is definitely NOT production ready and does not aim to be so. Exposing the
* demo to the public as is would introduce security risks for the host.
- **/
+ */
-import * as express from 'express';
-import * as expressWs from 'express-ws';
+import express from 'express';
+import expressWs from 'express-ws';
import * as os from 'os';
import * as pty from 'node-pty';
import * as path from 'path';
@@ -16,7 +16,7 @@ interface IDisposable {
}
/** Whether to use binary transport. */
-const USE_BINARY = os.platform() !== "win32";
+const USE_BINARY = os.platform() !== 'win32';
const demoRoot = path.join(__dirname, '..');
@@ -169,7 +169,7 @@ function startServer(): void {
term.onData(function(data) {
try {
send(data as string & Buffer);
- } catch (ex) {
+ } catch {
// The WebSocket is not open, ignore
}
});
diff --git a/demo/server/tsconfig.json b/demo/server/tsconfig.json
index ed5c6a90..fcec9d92 100644
--- a/demo/server/tsconfig.json
+++ b/demo/server/tsconfig.json
@@ -3,7 +3,8 @@
"module": "commonjs",
"target": "es2021",
"rootDir": ".",
- "sourceMap": true
+ "sourceMap": true,
+ "esModuleInterop": true
},
"include": [
"server.ts"
diff --git a/package.json b/package.json
index 5689952c..4365d08e 100644
--- a/package.json
+++ b/package.json
@@ -27,9 +27,11 @@
"xterm"
],
"scripts": {
- "setup": "npm run build",
+ "presetup": "npm run build",
+ "setup": "npm run esbuild",
+ "postsetup": "npm run esbuild-demo-server",
"start": "node demo/start",
- "dev": "concurrently -k -p [{name}] -n tsc,esbuild,esbuild-demo,server -c blue,yellow,cyan,green \"npm:tsc-watch\" \"npm:esbuild-watch\" \"npm:esbuild-demo-watch\" \"npm:start\"",
+ "dev": "concurrently -k -p [{name}] -n tsc,esbuild,esbuild-demo-client,esbuild-demo-server,server -c blue,yellow,cyan,green,magenta \"npm:tsc-watch\" \"npm:esbuild-watch\" \"npm:esbuild-demo-client-watch\" \"npm:esbuild-demo-server-watch\" \"npm:start\"",
"build": "npm run tsc",
"watch": "npm run tsc-watch",
"tsc": "tsc -b ./tsconfig.all.json",
@@ -39,8 +41,10 @@
"esbuild-package": "node bin/esbuild_all.mjs --prod",
"esbuild-package-watch": "node bin/esbuild_all.mjs --prod --watch",
"esbuild-package-headless-only": "node bin/esbuild.mjs --prod --headless",
- "esbuild-demo": "node bin/esbuild.mjs --demo-client",
- "esbuild-demo-watch": "node bin/esbuild.mjs --demo-client --watch",
+ "esbuild-demo-client": "node bin/esbuild.mjs --demo-client",
+ "esbuild-demo-client-watch": "node bin/esbuild.mjs --demo-client --watch",
+ "esbuild-demo-server": "node bin/esbuild.mjs --demo-server",
+ "esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
"test": "npm run test-unit",
"posttest": "npm run lint",
"lint": "eslint --max-warnings 0 src/ addons/",