Skip to content

Commit 5ebc06e

Browse files
Dual ESM/CJS build for CommonJS compatibility (#528)
Produce both ESM and CJS outputs from the esbuild config so that consumers using either module system get a working package automatically. - Add a second esbuild.build() call with format:"cjs" outputting to dist/cjs/ - Write a dist/cjs/package.json with type:"commonjs" so Node treats .js as CJS - Update package.json exports with "import" and "require" conditions for both the main and ./extension entry points - Revert getBundledCliPath() to use import.meta.resolve for ESM, with a createRequire + path-walking fallback for CJS contexts - Update CJS compatibility tests to verify the actual dual build - Update README to document CJS/CommonJS support Co-Authored-By: Copilot <[email protected]>
1 parent 86b6c8a commit 5ebc06e

File tree

5 files changed

+93
-42
lines changed

5 files changed

+93
-42
lines changed

nodejs/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -850,13 +850,18 @@ try {
850850
- Node.js >= 18.0.0
851851
- GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`)
852852

853-
### CJS Bundles (esbuild, VS Code extensions)
853+
### CJS / CommonJS Support
854854

855-
The SDK is ESM-only. When loaded in a CJS-shimmed environment (e.g., a VS Code extension bundled with `esbuild format:"cjs"`), `getBundledCliPath()` resolves the CLI by walking `node_modules`. The `@github/copilot` package **must be present in `node_modules` at runtime** — do not externalize or exclude it from your bundle.
855+
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`:
856+
857+
- `import` / `from` → ESM (`dist/index.js`)
858+
- `require()` → CJS (`dist/cjs/index.cjs`)
859+
860+
This means the SDK works out of the box in CJS environments such as VS Code extensions bundled with `esbuild format:"cjs"`.
856861

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

859-
If you installed the Copilot CLI separately rather than relying on the SDK's bundled copy, `getBundledCliPath()` will not find it (it only searches `node_modules`). Pass `cliPath` explicitly instead:
864+
If you installed the Copilot CLI separately rather than relying on the SDK's bundled copy, pass `cliPath` explicitly:
860865

861866
```typescript
862867
const client = new CopilotClient({

nodejs/esbuild-copilotsdk-nodejs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { execSync } from "child_process";
44

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

7+
// ESM build
78
await esbuild.build({
89
entryPoints,
910
outbase: "src",
@@ -15,5 +16,21 @@ await esbuild.build({
1516
outExtension: { ".js": ".js" },
1617
});
1718

19+
// CJS build — uses .js extension with a "type":"commonjs" package.json marker
20+
await esbuild.build({
21+
entryPoints,
22+
outbase: "src",
23+
outdir: "dist/cjs",
24+
format: "cjs",
25+
platform: "node",
26+
target: "es2022",
27+
sourcemap: false,
28+
outExtension: { ".js": ".js" },
29+
});
30+
31+
// Mark the CJS directory so Node treats .js files as CommonJS
32+
import { writeFileSync } from "fs";
33+
writeFileSync("dist/cjs/package.json", JSON.stringify({ type: "commonjs" }) + "\n");
34+
1835
// Generate .d.ts files
1936
execSync("tsc", { stdio: "inherit" });

nodejs/package.json

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,28 @@
66
},
77
"version": "0.1.8",
88
"description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC",
9-
"main": "./dist/index.js",
9+
"main": "./dist/cjs/index.js",
1010
"types": "./dist/index.d.ts",
1111
"exports": {
1212
".": {
13-
"import": "./dist/index.js",
14-
"types": "./dist/index.d.ts"
13+
"import": {
14+
"types": "./dist/index.d.ts",
15+
"default": "./dist/index.js"
16+
},
17+
"require": {
18+
"types": "./dist/index.d.ts",
19+
"default": "./dist/cjs/index.js"
20+
}
1521
},
1622
"./extension": {
17-
"import": "./dist/extension.js",
18-
"types": "./dist/extension.d.ts"
23+
"import": {
24+
"types": "./dist/extension.d.ts",
25+
"default": "./dist/extension.js"
26+
},
27+
"require": {
28+
"types": "./dist/extension.d.ts",
29+
"default": "./dist/cjs/extension.js"
30+
}
1931
}
2032
},
2133
"type": "module",

nodejs/src/client.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { existsSync } from "node:fs";
1717
import { createRequire } from "node:module";
1818
import { Socket } from "node:net";
1919
import { dirname, join } from "node:path";
20-
import { pathToFileURL } from "node:url";
20+
import { fileURLToPath } from "node:url";
2121
import {
2222
createMessageConnection,
2323
MessageConnection,
@@ -93,27 +93,33 @@ function getNodeExecPath(): string {
9393
* Gets the path to the bundled CLI from the @github/copilot package.
9494
* Uses index.js directly rather than npm-loader.js (which spawns the native binary).
9595
*
96-
* The @github/copilot package only exposes an ESM-only "./sdk" export,
97-
* which breaks in CJS contexts (e.g., VS Code extensions bundled with esbuild).
98-
* Instead of resolving through the package's exports, we locate the package
99-
* root by walking module resolution paths and checking for its directory.
100-
* See: https://github.com/github/copilot-sdk/issues/528
96+
* In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions
97+
* bundled with esbuild format:"cjs"), import.meta is empty so we fall back to
98+
* walking node_modules to find the package.
10199
*/
102100
function getBundledCliPath(): string {
103-
// import.meta.url is defined in ESM; in CJS bundles (esbuild format:"cjs")
104-
// it's undefined, so we fall back to __filename via pathToFileURL.
105-
const require = createRequire(import.meta.url ?? pathToFileURL(__filename).href);
106-
// The @github/copilot package has strict ESM-only exports, so require.resolve
107-
// cannot resolve it. Instead, walk the module resolution paths to find it.
108-
const searchPaths = require.resolve.paths("@github/copilot") ?? [];
101+
if (typeof import.meta.resolve === "function") {
102+
// ESM: resolve via import.meta.resolve
103+
const sdkUrl = import.meta.resolve("@github/copilot/sdk");
104+
const sdkPath = fileURLToPath(sdkUrl);
105+
// sdkPath is like .../node_modules/@github/copilot/sdk/index.js
106+
// Go up two levels to get the package root, then append index.js
107+
return join(dirname(dirname(sdkPath)), "index.js");
108+
}
109+
110+
// CJS fallback: the @github/copilot package has ESM-only exports so
111+
// require.resolve cannot reach it. Walk the module search paths instead.
112+
const req = createRequire(__filename);
113+
const searchPaths = req.resolve.paths("@github/copilot") ?? [];
109114
for (const base of searchPaths) {
110115
const candidate = join(base, "@github", "copilot", "index.js");
111116
if (existsSync(candidate)) {
112117
return candidate;
113118
}
114119
}
115120
throw new Error(
116-
`Could not find @github/copilot package. Searched ${searchPaths.length} paths. Ensure it is installed.`
121+
`Could not find @github/copilot package. Searched ${searchPaths.length} paths. ` +
122+
`Ensure it is installed, or pass cliPath/cliUrl to CopilotClient.`,
117123
);
118124
}
119125

nodejs/test/cjs-compat.test.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
/**
2-
* CJS shimmed environment compatibility test
2+
* Dual ESM/CJS build compatibility tests
33
*
4-
* Verifies that getBundledCliPath() works when the ESM build is loaded in a
5-
* shimmed CJS environment (e.g., VS Code extensions bundled with esbuild
6-
* format:"cjs"). In these environments, import.meta.url may be undefined but
7-
* __filename is available via the CJS shim.
4+
* Verifies that both the ESM and CJS builds exist and work correctly,
5+
* so consumers using either module system get a working package.
86
*
97
* See: https://github.com/github/copilot-sdk/issues/528
108
*/
@@ -14,30 +12,43 @@ import { existsSync } from "node:fs";
1412
import { execFileSync } from "node:child_process";
1513
import { join } from "node:path";
1614

17-
const esmEntryPoint = join(import.meta.dirname, "../dist/index.js");
15+
const distDir = join(import.meta.dirname, "../dist");
1816

19-
describe("CJS shimmed environment compatibility (#528)", () => {
17+
describe("Dual ESM/CJS build (#528)", () => {
2018
it("ESM dist file should exist", () => {
21-
expect(existsSync(esmEntryPoint)).toBe(true);
19+
expect(existsSync(join(distDir, "index.js"))).toBe(true);
2220
});
2321

24-
it("getBundledCliPath() should resolve in a CJS shimmed context", () => {
25-
// Simulate what esbuild format:"cjs" does: __filename is defined,
26-
// import.meta.url may be undefined. The SDK's fallback logic
27-
// (import.meta.url ?? pathToFileURL(__filename).href) handles this.
28-
//
29-
// We test by requiring the ESM build via --input-type=module in a
30-
// subprocess that has __filename available, verifying the constructor
31-
// (which calls getBundledCliPath()) doesn't throw.
22+
it("CJS dist file should exist", () => {
23+
expect(existsSync(join(distDir, "cjs/index.js"))).toBe(true);
24+
});
25+
26+
it("CJS build is requireable and exports CopilotClient", () => {
3227
const script = `
33-
import { createRequire } from 'node:module';
34-
const require = createRequire(import.meta.url);
35-
const sdk = await import(${JSON.stringify(esmEntryPoint)});
28+
const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))});
3629
if (typeof sdk.CopilotClient !== 'function') {
30+
console.error('CopilotClient is not a function');
3731
process.exit(1);
3832
}
33+
console.log('CJS require: OK');
34+
`;
35+
const output = execFileSync(
36+
process.execPath,
37+
["--eval", script],
38+
{
39+
encoding: "utf-8",
40+
timeout: 10000,
41+
cwd: join(import.meta.dirname, ".."),
42+
},
43+
);
44+
expect(output).toContain("CJS require: OK");
45+
});
46+
47+
it("CopilotClient constructor works in CJS context", () => {
48+
const script = `
49+
const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))});
3950
try {
40-
const client = new sdk.CopilotClient({ cliUrl: "8080" });
51+
const client = new sdk.CopilotClient({ cliUrl: "http://localhost:8080" });
4152
console.log('CopilotClient constructor: OK');
4253
} catch (e) {
4354
console.error('constructor failed:', e.message);
@@ -46,7 +57,7 @@ describe("CJS shimmed environment compatibility (#528)", () => {
4657
`;
4758
const output = execFileSync(
4859
process.execPath,
49-
["--input-type=module", "--eval", script],
60+
["--eval", script],
5061
{
5162
encoding: "utf-8",
5263
timeout: 10000,

0 commit comments

Comments
 (0)