Skip to content

Commit 73ea09b

Browse files
jaggederestclaude
andauthored
Add package scripts and cli library, enable integration testing (#536)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent bd9d1ca commit 73ea09b

18 files changed

+761
-135
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vitest.config.ts

.eslintrc.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"parser": "@typescript-eslint/parser",
44
"parserOptions": {
55
"ecmaVersion": 6,
6-
"sourceType": "module"
6+
"sourceType": "module",
7+
"project": "./tsconfig.json"
78
},
89
"plugins": ["@typescript-eslint", "prettier"],
910
"extends": [
@@ -15,6 +16,18 @@
1516
"prettier"
1617
],
1718
"overrides": [
19+
{
20+
"files": ["*.ts"],
21+
"rules": {
22+
"require-await": "off",
23+
"@typescript-eslint/require-await": "error"
24+
}
25+
},
26+
{
27+
"extends": ["plugin:package-json/legacy-recommended"],
28+
"files": ["*.json"],
29+
"parser": "jsonc-eslint-parser"
30+
},
1831
{
1932
"files": ["*.md"],
2033
"parser": "markdown-eslint-parser"

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818

1919
- uses: actions/setup-node@v4
2020
with:
21-
node-version: "18"
21+
node-version: "22"
2222

2323
- run: yarn
2424

@@ -36,7 +36,7 @@ jobs:
3636

3737
- uses: actions/setup-node@v4
3838
with:
39-
node-version: "18"
39+
node-version: "22"
4040

4141
- run: yarn
4242

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818

1919
- uses: actions/setup-node@v4
2020
with:
21-
node-version: "18"
21+
node-version: "22"
2222

2323
- run: yarn
2424

.vscode-test.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "@vscode/test-cli";
2+
3+
export default defineConfig({
4+
files: "out/test/**/*.test.js",
5+
extensionDevelopmentPath: ".",
6+
extensionTestsPath: "./out/test",
7+
launchArgs: ["--enable-proposed-api", "coder.coder-remote"],
8+
mocha: {
9+
ui: "tdd",
10+
timeout: 20000,
11+
},
12+
});

.vscodeignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ node_modules/**
1212
**/.editorconfig
1313
**/*.map
1414
**/*.ts
15-
*.gif
15+
*.gif

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Run all tests: `yarn test`
1111
- Run specific test: `vitest ./src/filename.test.ts`
1212
- CI test mode: `yarn test:ci`
13+
- Integration tests: `yarn test:integration`
1314

1415
## Code Style Guidelines
1516

package.json

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
11
{
22
"name": "coder-remote",
3-
"publisher": "coder",
43
"displayName": "Coder",
5-
"description": "Open any workspace with a single click.",
6-
"repository": "https://github.com/coder/vscode-coder",
74
"version": "1.9.1",
8-
"engines": {
9-
"vscode": "^1.73.0"
10-
},
11-
"license": "MIT",
5+
"description": "Open any workspace with a single click.",
6+
"categories": [
7+
"Other"
8+
],
129
"bugs": {
1310
"url": "https://github.com/coder/vscode-coder/issues"
1411
},
15-
"icon": "media/logo.png",
16-
"extensionKind": [
17-
"ui"
18-
],
19-
"capabilities": {
20-
"untrustedWorkspaces": {
21-
"supported": true
22-
}
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/coder/vscode-coder"
2315
},
24-
"categories": [
25-
"Other"
26-
],
27-
"extensionPack": [
28-
"ms-vscode-remote.remote-ssh"
29-
],
30-
"activationEvents": [
31-
"onResolveRemoteAuthority:ssh-remote",
32-
"onCommand:coder.connect",
33-
"onUri"
34-
],
16+
"license": "MIT",
17+
"publisher": "coder",
18+
"type": "commonjs",
3519
"main": "./dist/extension.js",
20+
"scripts": {
21+
"build": "webpack",
22+
"fmt": "prettier --write .",
23+
"lint": "eslint . --ext ts,md,json",
24+
"lint:fix": "yarn lint --fix",
25+
"package": "webpack --mode production --devtool hidden-source-map",
26+
"package:prerelease": "npx vsce package --pre-release",
27+
"pretest": "tsc -p . --outDir out && yarn run build && yarn run lint",
28+
"test": "vitest",
29+
"test:ci": "CI=true yarn test",
30+
"test:integration": "vscode-test",
31+
"vscode:prepublish": "yarn package",
32+
"watch": "webpack --watch"
33+
},
3634
"contributes": {
3735
"configuration": {
3836
"title": "Coder",
@@ -45,8 +43,7 @@
4543
"type": "string",
4644
"pattern": "^[a-zA-Z0-9-]+[=\\s].*$"
4745
},
48-
"scope": "machine",
49-
"default": []
46+
"scope": "machine"
5047
},
5148
"coder.insecure": {
5249
"markdownDescription": "If true, the extension will not verify the authenticity of the remote host. This is useful for self-signed certificates.",
@@ -269,17 +266,30 @@
269266
]
270267
}
271268
},
272-
"scripts": {
273-
"vscode:prepublish": "yarn package",
274-
"build": "webpack",
275-
"watch": "webpack --watch",
276-
"fmt": "prettier --write .",
277-
"package": "webpack --mode production --devtool hidden-source-map",
278-
"package:prerelease": "npx vsce package --pre-release",
279-
"lint": "eslint . --ext ts,md",
280-
"lint:fix": "yarn lint --fix",
281-
"test": "vitest ./src",
282-
"test:ci": "CI=true yarn test"
269+
"activationEvents": [
270+
"onResolveRemoteAuthority:ssh-remote",
271+
"onCommand:coder.connect",
272+
"onUri"
273+
],
274+
"resolutions": {
275+
"semver": "7.7.1",
276+
"trim": "0.0.3",
277+
"word-wrap": "1.2.5"
278+
},
279+
"dependencies": {
280+
"axios": "1.8.4",
281+
"date-fns": "^3.6.0",
282+
"eventsource": "^3.0.6",
283+
"find-process": "https://github.com/coder/find-process#fix/sequoia-compat",
284+
"jsonc-parser": "^3.3.1",
285+
"memfs": "^4.17.1",
286+
"node-forge": "^1.3.1",
287+
"pretty-bytes": "^6.1.1",
288+
"proxy-agent": "^6.4.0",
289+
"semver": "^7.7.1",
290+
"ua-parser-js": "1.0.40",
291+
"ws": "^8.18.2",
292+
"zod": "^3.25.65"
283293
},
284294
"devDependencies": {
285295
"@types/eventsource": "^3.0.0",
@@ -291,6 +301,7 @@
291301
"@types/ws": "^8.18.1",
292302
"@typescript-eslint/eslint-plugin": "^7.0.0",
293303
"@typescript-eslint/parser": "^6.21.0",
304+
"@vscode/test-cli": "^0.0.10",
294305
"@vscode/test-electron": "^2.5.2",
295306
"@vscode/vsce": "^2.21.1",
296307
"bufferutil": "^4.0.9",
@@ -300,8 +311,10 @@
300311
"eslint-config-prettier": "^9.1.0",
301312
"eslint-plugin-import": "^2.31.0",
302313
"eslint-plugin-md": "^1.0.19",
314+
"eslint-plugin-package-json": "^0.40.1",
303315
"eslint-plugin-prettier": "^5.4.1",
304316
"glob": "^10.4.2",
317+
"jsonc-eslint-parser": "^2.4.0",
305318
"nyc": "^17.1.0",
306319
"prettier": "^3.5.3",
307320
"ts-loader": "^9.5.1",
@@ -313,25 +326,20 @@
313326
"webpack": "^5.99.6",
314327
"webpack-cli": "^5.1.4"
315328
},
316-
"dependencies": {
317-
"axios": "1.8.4",
318-
"date-fns": "^3.6.0",
319-
"eventsource": "^3.0.6",
320-
"find-process": "https://github.com/coder/find-process#fix/sequoia-compat",
321-
"jsonc-parser": "^3.3.1",
322-
"memfs": "^4.17.1",
323-
"node-forge": "^1.3.1",
324-
"pretty-bytes": "^6.1.1",
325-
"proxy-agent": "^6.4.0",
326-
"semver": "^7.7.1",
327-
"ua-parser-js": "1.0.40",
328-
"ws": "^8.18.2",
329-
"zod": "^3.25.1"
330-
},
331-
"resolutions": {
332-
"semver": "7.7.1",
333-
"trim": "0.0.3",
334-
"word-wrap": "1.2.5"
329+
"extensionPack": [
330+
"ms-vscode-remote.remote-ssh"
331+
],
332+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
333+
"engines": {
334+
"vscode": "^1.73.0"
335335
},
336-
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
336+
"icon": "media/logo.png",
337+
"extensionKind": [
338+
"ui"
339+
],
340+
"capabilities": {
341+
"untrustedWorkspaces": {
342+
"supported": true
343+
}
344+
}
337345
}

src/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ export async function createHttpAgent(): Promise<ProxyAgent> {
7171
* configuration. The token may be undefined if some other form of
7272
* authentication is being used.
7373
*/
74-
export async function makeCoderSdk(
74+
export function makeCoderSdk(
7575
baseUrl: string,
7676
token: string | undefined,
7777
storage: Storage,
78-
): Promise<Api> {
78+
): Api {
7979
const restClient = new Api();
8080
restClient.setHost(baseUrl);
8181
if (token) {

src/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class CertificateError extends Error {
126126
}
127127

128128
// allowInsecure updates the value of the "coder.insecure" property.
129-
async allowInsecure(): Promise<void> {
129+
allowInsecure(): void {
130130
vscode.workspace
131131
.getConfiguration()
132132
.update("coder.insecure", true, vscode.ConfigurationTarget.Global);

src/extension.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,31 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
2121
//
2222
// Cursor and VSCode are covered by ms remote, and the only other is windsurf for now
2323
// Means that vscodium is not supported by this for now
24+
2425
const remoteSSHExtension =
2526
vscode.extensions.getExtension("jeanp413.open-remote-ssh") ||
2627
vscode.extensions.getExtension("codeium.windsurf-remote-openssh") ||
2728
vscode.extensions.getExtension("anysphere.remote-ssh") ||
2829
vscode.extensions.getExtension("ms-vscode-remote.remote-ssh");
30+
31+
let vscodeProposed: typeof vscode = vscode;
32+
2933
if (!remoteSSHExtension) {
3034
vscode.window.showErrorMessage(
31-
"Remote SSH extension not found, cannot activate Coder extension",
35+
"Remote SSH extension not found, this may not work as expected.\n" +
36+
// NB should we link to documentation or marketplace?
37+
"Please install your choice of Remote SSH extension from the VS Code Marketplace.",
38+
);
39+
} else {
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
vscodeProposed = (module as any)._load(
42+
"vscode",
43+
{
44+
filename: remoteSSHExtension.extensionPath,
45+
},
46+
false,
3247
);
33-
throw new Error("Remote SSH extension not found");
3448
}
35-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36-
const vscodeProposed: typeof vscode = (module as any)._load(
37-
"vscode",
38-
{
39-
filename: remoteSSHExtension?.extensionPath,
40-
},
41-
false,
42-
);
4349

4450
const output = vscode.window.createOutputChannel("Coder");
4551
const storage = new Storage(
@@ -278,7 +284,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
278284
// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
279285
// in package.json we're able to perform actions before the authority is
280286
// resolved by the remote SSH extension.
281-
if (vscodeProposed.env.remoteAuthority) {
287+
//
288+
// In addition, if we don't have a remote SSH extension, we skip this
289+
// activation event. This may allow the user to install the extension
290+
// after the Coder extension is installed, instead of throwing a fatal error
291+
// (this would require the user to uninstall the Coder extension and
292+
// reinstall after installing the remote SSH extension, which is annoying)
293+
if (remoteSSHExtension && vscodeProposed.env.remoteAuthority) {
282294
const remote = new Remote(
283295
vscodeProposed,
284296
storage,

src/test/extension.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as assert from "assert";
2+
import * as vscode from "vscode";
3+
4+
suite("Extension Test Suite", () => {
5+
vscode.window.showInformationMessage("Start all tests.");
6+
7+
test("Extension should be present", () => {
8+
assert.ok(vscode.extensions.getExtension("coder.coder-remote"));
9+
});
10+
11+
test("Extension should activate", async () => {
12+
const extension = vscode.extensions.getExtension("coder.coder-remote");
13+
assert.ok(extension);
14+
15+
if (!extension.isActive) {
16+
await extension.activate();
17+
}
18+
19+
assert.ok(extension.isActive);
20+
});
21+
22+
test("Extension should export activate function", async () => {
23+
const extension = vscode.extensions.getExtension("coder.coder-remote");
24+
assert.ok(extension);
25+
26+
await extension.activate();
27+
// The extension doesn't export anything, which is fine
28+
// The test was expecting exports.activate but the extension
29+
// itself is the activate function
30+
assert.ok(extension.isActive);
31+
});
32+
33+
test("Commands should be registered", async () => {
34+
const extension = vscode.extensions.getExtension("coder.coder-remote");
35+
assert.ok(extension);
36+
37+
if (!extension.isActive) {
38+
await extension.activate();
39+
}
40+
41+
// Give a small delay for commands to register
42+
await new Promise((resolve) => setTimeout(resolve, 100));
43+
44+
const commands = await vscode.commands.getCommands(true);
45+
const coderCommands = commands.filter((cmd) => cmd.startsWith("coder."));
46+
47+
assert.ok(
48+
coderCommands.length > 0,
49+
"Should have registered Coder commands",
50+
);
51+
assert.ok(
52+
coderCommands.includes("coder.login"),
53+
"Should have coder.login command",
54+
);
55+
});
56+
});

0 commit comments

Comments
 (0)