Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/nodejs-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ jobs:
- name: Typecheck SDK
run: npm run typecheck

- name: Build SDK
run: npm run build

- name: Install test harness dependencies
working-directory: ./test/harness
run: npm ci --ignore-scripts
Expand Down
20 changes: 20 additions & 0 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,26 @@ try {
- Node.js >= 18.0.0
- GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`)

### CJS / CommonJS Support

The SDK ships both ESM and CJS builds. Node.js and bundlers (esbuild, webpack, etc.) automatically select the correct format via the `exports` field in `package.json`:

- `import` / `from` → ESM (`dist/index.js`)
- `require()` → CJS (`dist/cjs/index.cjs`)

This means the SDK works out of the box in CJS environments such as VS Code extensions bundled with `esbuild format:"cjs"`.

### System-installed CLI (winget, brew, apt)

If you installed the Copilot CLI separately rather than relying on the SDK's bundled copy, pass `cliPath` explicitly:

```typescript
const client = new CopilotClient({
cliPath: '/usr/local/bin/copilot', // macOS/Linux
// cliPath: 'C:\\path\\to\\copilot.exe', // Windows (winget, etc.)
});
```

## License

MIT
18 changes: 18 additions & 0 deletions nodejs/esbuild-copilotsdk-nodejs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { execSync } from "child_process";

const entryPoints = globSync("src/**/*.ts");

// ESM build
await esbuild.build({
entryPoints,
outbase: "src",
Expand All @@ -15,5 +16,22 @@ await esbuild.build({
outExtension: { ".js": ".js" },
});

// CJS build — uses .js extension with a "type":"commonjs" package.json marker
await esbuild.build({
entryPoints,
outbase: "src",
outdir: "dist/cjs",
format: "cjs",
platform: "node",
target: "es2022",
sourcemap: false,
outExtension: { ".js": ".js" },
logOverride: { "empty-import-meta": "silent" },
});

// Mark the CJS directory so Node treats .js files as CommonJS
import { writeFileSync } from "fs";
writeFileSync("dist/cjs/package.json", JSON.stringify({ type: "commonjs" }) + "\n");

// Generate .d.ts files
execSync("tsc", { stdio: "inherit" });
22 changes: 17 additions & 5 deletions nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,28 @@
},
"version": "0.1.8",
"description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC",
"main": "./dist/index.js",
"main": "./dist/cjs/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./extension": {
"import": "./dist/extension.js",
"types": "./dist/extension.d.ts"
"import": {
"types": "./dist/extension.d.ts",
"default": "./dist/extension.js"
},
"require": {
"types": "./dist/extension.d.ts",
"default": "./dist/cjs/extension.js"
}
}
},
"type": "module",
Expand Down
34 changes: 28 additions & 6 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { spawn, type ChildProcess } from "node:child_process";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { Socket } from "node:net";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -91,14 +92,35 @@ function getNodeExecPath(): string {
/**
* Gets the path to the bundled CLI from the @github/copilot package.
* Uses index.js directly rather than npm-loader.js (which spawns the native binary).
*
* In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions
* bundled with esbuild format:"cjs"), import.meta is empty so we fall back to
* walking node_modules to find the package.
*/
function getBundledCliPath(): string {
// Find the actual location of the @github/copilot package by resolving its sdk export
const sdkUrl = import.meta.resolve("@github/copilot/sdk");
const sdkPath = fileURLToPath(sdkUrl);
// sdkPath is like .../node_modules/@github/copilot/sdk/index.js
// Go up two levels to get the package root, then append index.js
return join(dirname(dirname(sdkPath)), "index.js");
if (typeof import.meta.resolve === "function") {
// ESM: resolve via import.meta.resolve
const sdkUrl = import.meta.resolve("@github/copilot/sdk");
const sdkPath = fileURLToPath(sdkUrl);
// sdkPath is like .../node_modules/@github/copilot/sdk/index.js
// Go up two levels to get the package root, then append index.js
return join(dirname(dirname(sdkPath)), "index.js");
}

// CJS fallback: the @github/copilot package has ESM-only exports so
// require.resolve cannot reach it. Walk the module search paths instead.
const req = createRequire(__filename);
const searchPaths = req.resolve.paths("@github/copilot") ?? [];
for (const base of searchPaths) {
const candidate = join(base, "@github", "copilot", "index.js");
if (existsSync(candidate)) {
return candidate;
}
}
throw new Error(
`Could not find @github/copilot package. Searched ${searchPaths.length} paths. ` +
`Ensure it is installed, or pass cliPath/cliUrl to CopilotClient.`
);
}

/**
Expand Down
72 changes: 72 additions & 0 deletions nodejs/test/cjs-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Dual ESM/CJS build compatibility tests
*
* Verifies that both the ESM and CJS builds exist and work correctly,
* so consumers using either module system get a working package.
*
* See: https://github.com/github/copilot-sdk/issues/528
*/

import { describe, expect, it } from "vitest";
import { existsSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { join } from "node:path";

const distDir = join(import.meta.dirname, "../dist");

describe("Dual ESM/CJS build (#528)", () => {
it("ESM dist file should exist", () => {
expect(existsSync(join(distDir, "index.js"))).toBe(true);
});

it("CJS dist file should exist", () => {
expect(existsSync(join(distDir, "cjs/index.js"))).toBe(true);
});

it("CJS build is requireable and exports CopilotClient", () => {
const script = `
const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))});
if (typeof sdk.CopilotClient !== 'function') {
console.error('CopilotClient is not a function');
process.exit(1);
}
console.log('CJS require: OK');
`;
const output = execFileSync(process.execPath, ["--eval", script], {
encoding: "utf-8",
timeout: 10000,
cwd: join(import.meta.dirname, ".."),
});
expect(output).toContain("CJS require: OK");
});

it("CJS build resolves bundled CLI path", () => {
const script = `
const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))});
const client = new sdk.CopilotClient({ autoStart: false });
console.log('CJS CLI resolved: OK');
`;
const output = execFileSync(process.execPath, ["--eval", script], {
encoding: "utf-8",
timeout: 10000,
cwd: join(import.meta.dirname, ".."),
});
expect(output).toContain("CJS CLI resolved: OK");
});

it("ESM build resolves bundled CLI path", () => {
const esmPath = join(distDir, "index.js");
const script = `
import { pathToFileURL } from 'node:url';
const sdk = await import(pathToFileURL(${JSON.stringify(esmPath)}).href);
const client = new sdk.CopilotClient({ autoStart: false });
console.log('ESM CLI resolved: OK');
`;
const output = execFileSync(process.execPath, ["--input-type=module", "--eval", script], {
encoding: "utf-8",
timeout: 10000,
cwd: join(import.meta.dirname, ".."),
});
expect(output).toContain("ESM CLI resolved: OK");
});
});
Loading