@hydranium/client-vscode 0.0.0-stage → 1.0.0-next.315

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CrossBreeze, EclipseSource and others.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,75 @@
1
- # Temporary Holding Version
1
+ # `@hydranium/client-vscode`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The cross-head VS Code side of a
4
+ [Hydranium](https://github.com/eclipse-emfcloud/hydranium) host. Install it
5
+ when a VS Code extension starts your language server and a socket head (data
6
+ or GLSP) runs beside it: it finds each head's port from the extension host,
7
+ and publishes the ports to a Theia app that sideloads the extension.
8
+
9
+ ## What it gives you
10
+
11
+ - A head's port, asked from the running language client and polled until the
12
+ server answers, bounded so a request the server never publishes fails
13
+ instead of waiting forever.
14
+ - One host command per head that answers its port, which is how a Theia app's
15
+ backend reaches the heads of a server the extension started.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @hydranium/client-vscode
21
+ ```
22
+
23
+ No peer dependencies; the extension host provides `vscode`.
24
+
25
+ ## Wiring
26
+
27
+ After the language client has started, register the port commands a Theia
28
+ host polls, and ask for a port where a head's client connects. Ask once per
29
+ connection rather than caching the answer: a restarted server binds a fresh
30
+ port.
31
+
32
+ <!-- snippet-preamble
33
+ import type { ExtensionContext } from 'vscode';
34
+ import type { LanguageClient } from 'vscode-languageclient/node.js';
35
+ declare const context: ExtensionContext;
36
+ declare const client: LanguageClient;
37
+ -->
38
+
39
+ ```ts
40
+ import { findPort, registerPortCommands } from '@hydranium/client-vscode/node';
41
+
42
+ registerPortCommands(context, client, {
43
+ 'my-language.port.dataServer': 'my-language/data-server/port',
44
+ 'my-language.port.glsp': 'my-language/glsp-server/port'
45
+ });
46
+ const glspPort = await findPort(client, 'my-language/glsp-server/port');
47
+ ```
48
+
49
+ `@hydranium/data-client-vscode` and `@hydranium/glsp-client-vscode` take the
50
+ lookup as their `findPort` option. For the whole setup, see *Host in VS
51
+ Code* in
52
+ [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
53
+
54
+ ## Entry points
55
+
56
+ | Subpath | Use it for | Runs in |
57
+ | -------- | ------------------------------------- | -------------------------- |
58
+ | `.` | Nothing yet | anywhere |
59
+ | `./node` | The port lookup and the port commands | the VS Code extension host |
60
+
61
+ The subpaths need a TypeScript `moduleResolution` that reads `exports`
62
+ (`NodeNext` or `Bundler`); see *Requirements* in
63
+ [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
64
+
65
+ ## Status
66
+
67
+ Alpha: every release is a prerelease that may break the API, so pin an exact
68
+ version. Guides and known limitations:
69
+ [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
70
+
71
+ ## License
72
+
73
+ `MIT` — see this package's [`LICENSE`](./LICENSE), and the repository
74
+ [`NOTICE.md`](https://github.com/eclipse-emfcloud/hydranium/blob/main/NOTICE.md)
75
+ for third-party notices.
package/lib/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ export {};
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAKlF,OAAO,EAAE,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF"}
@@ -0,0 +1,40 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ /** Options for {@link findPort}. */
10
+ export interface FindPortOptions {
11
+ /**
12
+ * How many requests to send before giving up, the first included, so at
13
+ * least 1. The Theia handler's `findPortAttempts` counts the retries after
14
+ * the first instead.
15
+ */
16
+ readonly attempts?: number;
17
+ /** The wait after each failed attempt. */
18
+ readonly intervalMs?: number;
19
+ }
20
+ /** {@link FindPortOptions.attempts} when none is given. */
21
+ export declare const DEFAULT_FIND_PORT_ATTEMPTS = 40;
22
+ /** {@link FindPortOptions.intervalMs} when none is given. */
23
+ export declare const DEFAULT_FIND_PORT_INTERVAL_MS = 500;
24
+ /**
25
+ * Ask the language server for a socket head's listening port.
26
+ *
27
+ * The server registers its port requests after the heads bind their sockets,
28
+ * so the first requests legitimately fail and this polls. The poll is bounded,
29
+ * unlike the Theia handler's default, so a request the server never publishes
30
+ * surfaces as an error instead of a connection that waits forever.
31
+ *
32
+ * Call it once per connection rather than caching the answer: a restarted
33
+ * server binds a fresh ephemeral port. `client` is typed by the one method
34
+ * used, which `vscode-languageclient`'s `LanguageClient` has, so this package
35
+ * does not pin the adopter's copy.
36
+ */
37
+ export declare function findPort(client: {
38
+ sendRequest<R>(method: string): Promise<R>;
39
+ }, request: string, options?: FindPortOptions): Promise<number>;
40
+ //# sourceMappingURL=find-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-port.d.ts","sourceRoot":"","sources":["../../src/node/find-port.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,2DAA2D;AAC3D,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAC7C,6DAA6D;AAC7D,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,wBAAsB,QAAQ,CAC3B,MAAM,EAAE;IAAE,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,EACtD,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,CAwBjB"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.DEFAULT_FIND_PORT_INTERVAL_MS = exports.DEFAULT_FIND_PORT_ATTEMPTS = void 0;
12
+ exports.findPort = findPort;
13
+ /** {@link FindPortOptions.attempts} when none is given. */
14
+ exports.DEFAULT_FIND_PORT_ATTEMPTS = 40;
15
+ /** {@link FindPortOptions.intervalMs} when none is given. */
16
+ exports.DEFAULT_FIND_PORT_INTERVAL_MS = 500;
17
+ /**
18
+ * Ask the language server for a socket head's listening port.
19
+ *
20
+ * The server registers its port requests after the heads bind their sockets,
21
+ * so the first requests legitimately fail and this polls. The poll is bounded,
22
+ * unlike the Theia handler's default, so a request the server never publishes
23
+ * surfaces as an error instead of a connection that waits forever.
24
+ *
25
+ * Call it once per connection rather than caching the answer: a restarted
26
+ * server binds a fresh ephemeral port. `client` is typed by the one method
27
+ * used, which `vscode-languageclient`'s `LanguageClient` has, so this package
28
+ * does not pin the adopter's copy.
29
+ */
30
+ async function findPort(client, request, options = {}) {
31
+ const attempts = options.attempts ?? exports.DEFAULT_FIND_PORT_ATTEMPTS;
32
+ const intervalMs = options.intervalMs ?? exports.DEFAULT_FIND_PORT_INTERVAL_MS;
33
+ if (!Number.isInteger(attempts) || attempts < 1) {
34
+ throw new RangeError(`findPort: attempts must be a whole number of at least 1, got ${attempts}`);
35
+ }
36
+ let lastError;
37
+ for (let attempt = 0; attempt < attempts; attempt++) {
38
+ try {
39
+ const port = await client.sendRequest(request);
40
+ if (typeof port === 'number' && port > 0) {
41
+ return port;
42
+ }
43
+ }
44
+ catch (error) {
45
+ lastError = error;
46
+ }
47
+ if (attempt + 1 < attempts) {
48
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
49
+ }
50
+ }
51
+ const detail = lastError instanceof Error ? ` The last attempt failed: ${lastError.message}` : '';
52
+ throw new Error(`No port answered '${request}' after ${attempts} ${attempts === 1 ? 'attempt' : 'attempts'}. Check that the server publishes it.${detail}`);
53
+ }
54
+ //# sourceMappingURL=find-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-port.js","sourceRoot":"","sources":["../../src/node/find-port.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;AAclF,2DAA2D;AAC9C,QAAA,0BAA0B,GAAG,EAAE,CAAC;AAC7C,6DAA6D;AAChD,QAAA,6BAA6B,GAAG,GAAG,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACI,KAAK,mBACT,MAAsD,EACtD,OAAe,EACf,OAAO,GAAoB,EAAE;IAE7B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAA,0BAA0B,CAAC;IAChE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,QAAA,6BAA6B,CAAC;IACvE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,UAAU,CAAC,gEAAgE,QAAQ,EAAE,CAAC,CAAC;IACpG,CAAC;IACD,IAAI,SAAkB,CAAC;IACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;QACnD,IAAI,CAAC;YACF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,WAAW,CAAU,OAAO,CAAC,CAAC;YACxD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;gBACxC,OAAO,IAAI,CAAC;YACf,CAAC;QACJ,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACvB,SAAS,GAAG,KAAK,CAAC;QACrB,CAAC;QACD,IAAI,OAAO,GAAG,CAAC,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;QACjE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,6BAA6B,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClG,MAAM,IAAI,KAAK,CACZ,qBAAqB,OAAO,WAAW,QAAQ,IAAI,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,wCAAwC,MAAM,EAAE,CAC5I,CAAC;AACL,CAAC"}
@@ -0,0 +1,11 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ export * from './find-port';
10
+ export * from './port-commands';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ __exportStar(require("./find-port"), exports);
26
+ __exportStar(require("./port-commands"), exports);
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,8CAA4B;AAC5B,kDAAgC"}
@@ -0,0 +1,25 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ import { type ExtensionContext } from 'vscode';
10
+ /**
11
+ * Register a host command per socket head that answers the head's port, keyed
12
+ * command id to the LSP request the server publishes the port under.
13
+ *
14
+ * A Theia app that sideloads the extension reaches each port through these
15
+ * commands, since its backend has no language client of its own, and its
16
+ * connection handler retries a command nobody registered forever.
17
+ *
18
+ * Each command hands back the request's promise and nothing else. Awaiting a
19
+ * notification there would make the answer wait for a user to dismiss it,
20
+ * which hangs the Theia backend with nothing logged.
21
+ */
22
+ export declare function registerPortCommands(context: Pick<ExtensionContext, 'subscriptions'>, client: {
23
+ sendRequest<R>(method: string): Promise<R>;
24
+ }, portCommands: Readonly<Record<string, string>>): void;
25
+ //# sourceMappingURL=port-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-commands.d.ts","sourceRoot":"","sources":["../../src/node/port-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAY,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CACjC,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,EAChD,MAAM,EAAE;IAAE,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,EACtD,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,IAAI,CAIN"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.registerPortCommands = registerPortCommands;
12
+ const vscode_1 = require("vscode");
13
+ /**
14
+ * Register a host command per socket head that answers the head's port, keyed
15
+ * command id to the LSP request the server publishes the port under.
16
+ *
17
+ * A Theia app that sideloads the extension reaches each port through these
18
+ * commands, since its backend has no language client of its own, and its
19
+ * connection handler retries a command nobody registered forever.
20
+ *
21
+ * Each command hands back the request's promise and nothing else. Awaiting a
22
+ * notification there would make the answer wait for a user to dismiss it,
23
+ * which hangs the Theia backend with nothing logged.
24
+ */
25
+ function registerPortCommands(context, client, portCommands) {
26
+ for (const [commandId, request] of Object.entries(portCommands)) {
27
+ context.subscriptions.push(vscode_1.commands.registerCommand(commandId, () => client.sendRequest(request)));
28
+ }
29
+ }
30
+ //# sourceMappingURL=port-commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-commands.js","sourceRoot":"","sources":["../../src/node/port-commands.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAElF,mCAAyD;AAEzD;;;;;;;;;;;GAWG;AACH,8BACG,OAAgD,EAChD,MAAsD,EACtD,YAA8C;IAE9C,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/D,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,iBAAQ,CAAC,eAAe,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAS,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9G,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@hydranium/client-vscode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-next.315",
4
+ "description": "Cross-head VS Code client primitives for a hydranium host: the extension host asks the language server for each socket head's port, bounded, and registers the host commands through which a Theia app that sideloads the extension reaches those ports.",
5
+ "keywords": [
6
+ "hydranium",
7
+ "langium",
8
+ "language-server",
9
+ "lsp",
10
+ "vscode",
11
+ "vscode-extension"
12
+ ],
13
+ "homepage": "https://github.com/eclipse-emfcloud/hydranium/tree/main/packages/client-vscode",
14
+ "bugs": {
15
+ "url": "https://github.com/eclipse-emfcloud/hydranium/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": {
19
+ "name": "Hydranium Team"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/eclipse-emfcloud/hydranium.git",
24
+ "directory": "packages/client-vscode"
25
+ },
26
+ "files": [
27
+ "lib",
28
+ "src",
29
+ "!lib/**/*.tsbuildinfo"
30
+ ],
31
+ "sideEffects": false,
32
+ "main": "lib/index.js",
33
+ "types": "lib/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./lib/index.d.ts",
37
+ "default": "./lib/index.js"
38
+ },
39
+ "./node": {
40
+ "types": "./lib/node/index.d.ts",
41
+ "default": "./lib/node/index.js"
42
+ }
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -b",
49
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
50
+ "lint": "oxlint --format stylish --config ../../oxlint.config.cjs src test --max-warnings 0",
51
+ "prepack": "node -e \"const m=require('./package.json'),fs=require('node:fs');const missing=[m.main,...Object.values(m.bin||{})].filter(entry=>entry&&!fs.existsSync(entry));if(missing.length){console.error('prepack '+m.name+': not built ('+missing.join(', ')+' missing). Run the build before packing: a files entry that matches nothing is skipped silently, so the tarball would ship src only.');process.exit(1);}\"",
52
+ "test": "npm run typecheck:test && vitest run",
53
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
54
+ "watch": "tsc -b -w --preserveWatchOutput"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.0.0",
58
+ "@types/vscode": "^1.101.0",
59
+ "rimraf": "^5.0.0",
60
+ "typescript": "^7.0.2"
61
+ },
62
+ "engines": {
63
+ "node": ">=22.13",
64
+ "vscode": "^1.101.0"
65
+ },
66
+ "//prepack": "The publish guard, a `prepack` rather than a `prepare`: `prepack` runs only when a tarball is made (`npm pack`, `npm publish`) and never on install, so it cannot break the install it has no business touching. What it defends against: `files` lists `lib`, `lib` is gitignored, and a `files` entry matching nothing is skipped SILENTLY — so `npm publish` from an unbuilt tree emits a tarball of `src` and nothing else, with no error."
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ // Nothing here is neutral yet: every export needs the extension host and sits
11
+ // behind `./node`, so a webview bundle importing the bare specifier pulls in
12
+ // neither `vscode` nor Node.
13
+ export {};
@@ -0,0 +1,68 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /** Options for {@link findPort}. */
11
+ export interface FindPortOptions {
12
+ /**
13
+ * How many requests to send before giving up, the first included, so at
14
+ * least 1. The Theia handler's `findPortAttempts` counts the retries after
15
+ * the first instead.
16
+ */
17
+ readonly attempts?: number;
18
+ /** The wait after each failed attempt. */
19
+ readonly intervalMs?: number;
20
+ }
21
+
22
+ /** {@link FindPortOptions.attempts} when none is given. */
23
+ export const DEFAULT_FIND_PORT_ATTEMPTS = 40;
24
+ /** {@link FindPortOptions.intervalMs} when none is given. */
25
+ export const DEFAULT_FIND_PORT_INTERVAL_MS = 500;
26
+
27
+ /**
28
+ * Ask the language server for a socket head's listening port.
29
+ *
30
+ * The server registers its port requests after the heads bind their sockets,
31
+ * so the first requests legitimately fail and this polls. The poll is bounded,
32
+ * unlike the Theia handler's default, so a request the server never publishes
33
+ * surfaces as an error instead of a connection that waits forever.
34
+ *
35
+ * Call it once per connection rather than caching the answer: a restarted
36
+ * server binds a fresh ephemeral port. `client` is typed by the one method
37
+ * used, which `vscode-languageclient`'s `LanguageClient` has, so this package
38
+ * does not pin the adopter's copy.
39
+ */
40
+ export async function findPort(
41
+ client: { sendRequest<R>(method: string): Promise<R> },
42
+ request: string,
43
+ options: FindPortOptions = {}
44
+ ): Promise<number> {
45
+ const attempts = options.attempts ?? DEFAULT_FIND_PORT_ATTEMPTS;
46
+ const intervalMs = options.intervalMs ?? DEFAULT_FIND_PORT_INTERVAL_MS;
47
+ if (!Number.isInteger(attempts) || attempts < 1) {
48
+ throw new RangeError(`findPort: attempts must be a whole number of at least 1, got ${attempts}`);
49
+ }
50
+ let lastError: unknown;
51
+ for (let attempt = 0; attempt < attempts; attempt++) {
52
+ try {
53
+ const port = await client.sendRequest<unknown>(request);
54
+ if (typeof port === 'number' && port > 0) {
55
+ return port;
56
+ }
57
+ } catch (error: unknown) {
58
+ lastError = error;
59
+ }
60
+ if (attempt + 1 < attempts) {
61
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
62
+ }
63
+ }
64
+ const detail = lastError instanceof Error ? ` The last attempt failed: ${lastError.message}` : '';
65
+ throw new Error(
66
+ `No port answered '${request}' after ${attempts} ${attempts === 1 ? 'attempt' : 'attempts'}. Check that the server publishes it.${detail}`
67
+ );
68
+ }
@@ -0,0 +1,11 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ export * from './find-port';
11
+ export * from './port-commands';
@@ -0,0 +1,32 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { commands, type ExtensionContext } from 'vscode';
11
+
12
+ /**
13
+ * Register a host command per socket head that answers the head's port, keyed
14
+ * command id to the LSP request the server publishes the port under.
15
+ *
16
+ * A Theia app that sideloads the extension reaches each port through these
17
+ * commands, since its backend has no language client of its own, and its
18
+ * connection handler retries a command nobody registered forever.
19
+ *
20
+ * Each command hands back the request's promise and nothing else. Awaiting a
21
+ * notification there would make the answer wait for a user to dismiss it,
22
+ * which hangs the Theia backend with nothing logged.
23
+ */
24
+ export function registerPortCommands(
25
+ context: Pick<ExtensionContext, 'subscriptions'>,
26
+ client: { sendRequest<R>(method: string): Promise<R> },
27
+ portCommands: Readonly<Record<string, string>>
28
+ ): void {
29
+ for (const [commandId, request] of Object.entries(portCommands)) {
30
+ context.subscriptions.push(commands.registerCommand(commandId, () => client.sendRequest<number>(request)));
31
+ }
32
+ }