Skip to content

Commit ff54b03

Browse files
Detect Bun and worked runtimes in platform headers (#11)
Added detection for Bun and Cloudflare Workers (workerd) runtimes in platform header generation. Updated types and logic in detect-platform.ts, and added tests for new runtime detection.
1 parent 8fd662f commit ff54b03

File tree

3 files changed

+668
-700
lines changed

3 files changed

+668
-700
lines changed

src/internal/detect-platform.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,54 @@ export const isRunningInBrowser = () => {
1313
);
1414
};
1515

16-
type DetectedPlatform = 'deno' | 'node' | 'edge' | 'unknown';
16+
type DetectedPlatform = 'deno' | 'bun' | 'workerd' | 'edge' | 'node' | 'unknown';
1717

1818
/**
1919
* Note this does not detect 'browser'; for that, use getBrowserInfo().
2020
*/
2121
function getDetectedPlatform(): DetectedPlatform {
22+
// Deno
2223
if (typeof Deno !== 'undefined' && Deno.build != null) {
2324
return 'deno';
2425
}
26+
// Bun (must be checked before Node since Bun provides a Node-compatible process)
27+
if (typeof Bun !== 'undefined') {
28+
return 'bun';
29+
}
30+
// Cloudflare Workers / workerd
31+
// Heuristics: WebSocketPair global and/or navigator.userAgent === 'Cloudflare-Workers'
32+
if (
33+
typeof WebSocketPair !== 'undefined' ||
34+
(typeof navigator !== 'undefined' && navigator && (navigator as any).userAgent === 'Cloudflare-Workers')
35+
) {
36+
return 'workerd';
37+
}
38+
// Vercel Edge Runtime
2539
if (typeof EdgeRuntime !== 'undefined') {
2640
return 'edge';
2741
}
42+
// Node.js
2843
if (
2944
Object.prototype.toString.call(
3045
typeof (globalThis as any).process !== 'undefined' ? (globalThis as any).process : 0,
3146
) === '[object process]'
3247
) {
3348
return 'node';
3449
}
50+
// Fallback Node.js heuristic for environments where toString check fails (e.g., some test runners)
51+
if (typeof (globalThis as any).process !== 'undefined') {
52+
const p = (globalThis as any).process;
53+
if ((p?.versions && typeof p.versions.node === 'string') || p?.release?.name === 'node') {
54+
return 'node';
55+
}
56+
}
3557
return 'unknown';
3658
}
3759

3860
declare const Deno: any;
3961
declare const EdgeRuntime: any;
62+
declare const Bun: any;
63+
declare const WebSocketPair: any;
4064
type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown';
4165
type PlatformName =
4266
| 'MacOS'
@@ -54,7 +78,7 @@ type PlatformProperties = {
5478
'X-Stainless-Package-Version': string;
5579
'X-Stainless-OS': PlatformName;
5680
'X-Stainless-Arch': Arch;
57-
'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown';
81+
'X-Stainless-Runtime': 'node' | 'deno' | 'bun' | 'workerd' | 'edge' | `browser:${Browser}` | 'unknown';
5882
'X-Stainless-Runtime-Version': string;
5983
};
6084
const getPlatformProperties = (): PlatformProperties => {
@@ -70,6 +94,28 @@ const getPlatformProperties = (): PlatformProperties => {
7094
typeof Deno.version === 'string' ? Deno.version : Deno.version?.deno ?? 'unknown',
7195
};
7296
}
97+
if (detectedPlatform === 'bun') {
98+
return {
99+
'X-Stainless-Lang': 'js',
100+
'X-Stainless-Package-Version': VERSION,
101+
'X-Stainless-OS': normalizePlatform((globalThis as any).process?.platform ?? 'unknown'),
102+
'X-Stainless-Arch': normalizeArch((globalThis as any).process?.arch ?? 'unknown'),
103+
'X-Stainless-Runtime': 'bun',
104+
'X-Stainless-Runtime-Version': (globalThis as any).Bun?.version ??
105+
(globalThis as any).process?.versions?.bun ?? 'unknown',
106+
};
107+
}
108+
if (detectedPlatform === 'workerd') {
109+
return {
110+
'X-Stainless-Lang': 'js',
111+
'X-Stainless-Package-Version': VERSION,
112+
'X-Stainless-OS': 'Unknown',
113+
'X-Stainless-Arch': 'unknown',
114+
'X-Stainless-Runtime': 'workerd',
115+
'X-Stainless-Runtime-Version':
116+
(typeof navigator !== 'undefined' && (navigator as any)?.userAgent) || 'unknown',
117+
};
118+
}
73119
if (typeof EdgeRuntime !== 'undefined') {
74120
return {
75121
'X-Stainless-Lang': 'js',
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// @ts-nocheck
2+
/**
3+
* Tests for runtime detection headers in src/internal/detect-platform.ts
4+
*/
5+
6+
describe('getPlatformHeaders runtime detection', () => {
7+
const g: any = globalThis as any;
8+
9+
const saveGlobals = () => ({
10+
Deno: g.Deno,
11+
Bun: g.Bun,
12+
EdgeRuntime: g.EdgeRuntime,
13+
WebSocketPair: g.WebSocketPair,
14+
navigator: g.navigator,
15+
process: g.process,
16+
});
17+
18+
const restoreGlobals = (saved: any) => {
19+
g.Deno = saved.Deno;
20+
g.Bun = saved.Bun;
21+
g.EdgeRuntime = saved.EdgeRuntime;
22+
g.WebSocketPair = saved.WebSocketPair;
23+
g.navigator = saved.navigator;
24+
g.process = saved.process;
25+
};
26+
27+
beforeEach(() => {
28+
jest.resetModules();
29+
});
30+
31+
test('detects Bun runtime', async () => {
32+
const saved = saveGlobals();
33+
try {
34+
g.Bun = { version: '1.1.0' };
35+
g.process = { platform: 'linux', arch: 'x64' };
36+
37+
jest.resetModules();
38+
const mod = await import('../../src/internal/detect-platform');
39+
const headers = (mod as any).getPlatformHeaders();
40+
expect(headers['X-Stainless-Runtime']).toBe('bun');
41+
expect(headers['X-Stainless-Runtime-Version']).toBe('1.1.0');
42+
expect(headers['X-Stainless-OS']).toBe('Linux');
43+
expect(headers['X-Stainless-Arch']).toBe('x64');
44+
} finally {
45+
restoreGlobals(saved);
46+
}
47+
});
48+
49+
test('detects Cloudflare Workers (workerd)', async () => {
50+
const saved = saveGlobals();
51+
try {
52+
g.WebSocketPair = function () {};
53+
g.navigator = { userAgent: 'Cloudflare-Workers' };
54+
55+
jest.resetModules();
56+
const mod = await import('../../src/internal/detect-platform');
57+
const headers = (mod as any).getPlatformHeaders();
58+
expect(headers['X-Stainless-Runtime']).toBe('workerd');
59+
expect(headers['X-Stainless-Runtime-Version']).toBe('Cloudflare-Workers');
60+
} finally {
61+
restoreGlobals(saved);
62+
}
63+
});
64+
65+
test('detects Edge runtime', async () => {
66+
const saved = saveGlobals();
67+
try {
68+
g.EdgeRuntime = 'vercel-edge-1';
69+
g.process = { version: 'v18.18.0' };
70+
71+
jest.resetModules();
72+
const mod = await import('../../src/internal/detect-platform');
73+
const headers = (mod as any).getPlatformHeaders();
74+
expect(headers['X-Stainless-Runtime']).toBe('edge');
75+
expect(headers['X-Stainless-Runtime-Version']).toBe('v18.18.0');
76+
} finally {
77+
restoreGlobals(saved);
78+
}
79+
});
80+
81+
test('falls back to Node when no special runtime', async () => {
82+
const saved = saveGlobals();
83+
try {
84+
delete g.Deno;
85+
delete g.Bun;
86+
delete g.EdgeRuntime;
87+
delete g.WebSocketPair;
88+
delete g.navigator;
89+
g.process = {
90+
platform: 'win32',
91+
arch: 'x64',
92+
version: 'v18.18.0',
93+
versions: { node: '18.18.0' },
94+
};
95+
96+
jest.resetModules();
97+
const mod = await import('../../src/internal/detect-platform');
98+
const headers = (mod as any).getPlatformHeaders();
99+
expect(headers['X-Stainless-Runtime']).toBe('node');
100+
expect(headers['X-Stainless-OS']).toBe('Windows');
101+
expect(headers['X-Stainless-Arch']).toBe('x64');
102+
expect(headers['X-Stainless-Runtime-Version']).toBe('v18.18.0');
103+
} finally {
104+
restoreGlobals(saved);
105+
}
106+
});
107+
});

0 commit comments

Comments
 (0)