@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 +21 -0
- package/README.md +74 -2
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -0
- package/lib/node/find-port.d.ts +40 -0
- package/lib/node/find-port.d.ts.map +1 -0
- package/lib/node/find-port.js +54 -0
- package/lib/node/find-port.js.map +1 -0
- package/lib/node/index.d.ts +11 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +27 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/port-commands.d.ts +25 -0
- package/lib/node/port-commands.d.ts.map +1 -0
- package/lib/node/port-commands.js +30 -0
- package/lib/node/port-commands.js.map +1 -0
- package/package.json +65 -4
- package/src/index.ts +13 -0
- package/src/node/find-port.ts +68 -0
- package/src/node/index.ts +11 -0
- package/src/node/port-commands.ts +32 -0
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
|
-
#
|
|
1
|
+
# `@hydranium/client-vscode`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/lib/index.js.map
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|