Skip to content

Commit aae413c

Browse files
Verify Bun and Cloudflare Workers support in CI and document supported runtimes
1 parent ebbdcb2 commit aae413c

7 files changed

Lines changed: 288 additions & 0 deletions

File tree

.github/workflows/tests.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,33 @@ jobs:
3131

3232
- name: Build and test
3333
run: npm test
34+
35+
# Verifies the built library on non-Node runtimes: Bun and the Cloudflare
36+
# Workers runtime (workerd, via Miniflare).
37+
runtimes:
38+
runs-on: ubuntu-latest
39+
steps:
40+
- uses: actions/checkout@v7
41+
42+
- name: Use Node.js
43+
uses: actions/setup-node@v6
44+
with:
45+
node-version: 24.x
46+
47+
- name: Use Bun
48+
uses: oven-sh/setup-bun@v2
49+
50+
- name: Install dependencies
51+
run: npm install
52+
53+
- name: Build
54+
run: npm run build
55+
56+
- name: Smoke test on Node.js
57+
run: npm run test:smoke
58+
59+
- name: Smoke test on Bun
60+
run: bun test/smoke.mjs
61+
62+
- name: Smoke test on Cloudflare Workers (workerd)
63+
run: npm run test:workers

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,26 @@ Browser support:
110110
More samples are available in the [samples](https://github.com/ipregistry/ipregistry-javascript/tree/master/samples)
111111
folder.
112112

113+
## Supported runtimes
114+
115+
The library has zero runtime dependencies and only relies on web-standard APIs
116+
(`fetch`, `AbortController`, timers), so it runs on any modern JavaScript
117+
runtime:
118+
119+
- **Node.js 20+** — CommonJS and ESM, with bundled TypeScript declarations for
120+
both. Verified continuously on Node 20, 22 and 24.
121+
- **Bun** — verified continuously.
122+
- **Cloudflare Workers** — verified continuously against the real Workers
123+
runtime (workerd). Note that retry backoff waits count toward a Worker's
124+
wall-clock duration; on latency-sensitive Workers consider
125+
`withMaxRetries(1)` or a lower `withRetryInterval`.
126+
- **Deno** and other web-standard runtimes are expected to work through the
127+
same APIs.
128+
- **Browsers** — through a bundler (the ESM build is tree-shakable) or a
129+
`<script>` tag exposing the `ipregistry` global, as shown above. Use an
130+
origin-restricted API key for browser deployments so your key cannot be
131+
reused from other websites.
132+
113133
## Caching
114134

115135
The Ipregistry client library has built-in support for in-memory caching.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
"test": "node --import ./test/register.mjs --test test/*.test.ts",
6363
"pretest:integration": "npm run clean && npm run build",
6464
"test:integration": "node --loader ts-node/esm integration_test/ipregistry.ts",
65+
"test:smoke": "node test/smoke.mjs",
66+
"test:workers": "node test/workers/run.mjs",
6567
"tsdoc": "typedoc --out docs ./src/index.ts"
6668
},
6769
"author": "Ipregistry <support@ipregistry.co>",
@@ -72,6 +74,7 @@
7274
"chai": "^6.2.2",
7375
"eslint": "^10.6.0",
7476
"eslint-config-prettier": "^10.1.8",
77+
"miniflare": "^4.20260701.0",
7578
"prettier": "^3.9.4",
7679
"terser": "^5.48.0",
7780
"ts-node": "^10.9.2",

test/smoke-scenario.mjs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2019 Ipregistry (https://ipregistry.co).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// The shared scenario behind the runtime smoke tests. Stubs globalThis.fetch
18+
// (restoring it afterwards) and drives lookups, caching, retries, and error
19+
// mapping through the built ESM bundle using only web-standard APIs.
20+
21+
import {
22+
ApiError,
23+
InMemoryCache,
24+
IpregistryClient,
25+
IpregistryConfigBuilder,
26+
} from '../dist/index.mjs'
27+
28+
function assertEqual(actual, expected, label) {
29+
if (actual !== expected) {
30+
throw new Error(`${label}: expected ${expected}, got ${actual}`)
31+
}
32+
}
33+
34+
function jsonResponse(body, status = 200, headers = {}) {
35+
return new Response(JSON.stringify(body), {
36+
status,
37+
headers: { 'content-type': 'application/json', ...headers },
38+
})
39+
}
40+
41+
export async function runSmokeTest() {
42+
const originalFetch = globalThis.fetch
43+
let requests = 0
44+
45+
globalThis.fetch = async url => {
46+
requests++
47+
const path = new URL(url).pathname
48+
49+
if (path === '/1.2.3.4') {
50+
return jsonResponse(
51+
{ ip: '1.2.3.4', location: { country: { code: 'AU' } } },
52+
200,
53+
{ 'ipregistry-credits-consumed': '1' },
54+
)
55+
}
56+
57+
if (path === '/5.6.7.8') {
58+
// Fail once with a 503 to exercise the retry path.
59+
return requests % 2 === 0
60+
? jsonResponse({ ip: '5.6.7.8' })
61+
: jsonResponse(
62+
{ code: 'INTERNAL', message: 'oops', resolution: '-' },
63+
503,
64+
)
65+
}
66+
67+
return jsonResponse(
68+
{
69+
code: 'INVALID_IP_ADDRESS',
70+
message: 'Invalid IP',
71+
resolution: '-',
72+
},
73+
400,
74+
)
75+
}
76+
77+
try {
78+
const client = new IpregistryClient(
79+
new IpregistryConfigBuilder('tryout').withRetryInterval(1).build(),
80+
new InMemoryCache(),
81+
)
82+
83+
// Plain lookup, response headers, and caching.
84+
const first = await client.lookupIp('1.2.3.4')
85+
assertEqual(first.data.location.country.code, 'AU', 'country code')
86+
assertEqual(first.credits.consumed, 1, 'credits consumed')
87+
88+
const requestsBeforeCachedCall = requests
89+
const second = await client.lookupIp('1.2.3.4')
90+
assertEqual(second.data.ip, '1.2.3.4', 'cached ip')
91+
assertEqual(requests, requestsBeforeCachedCall, 'cache hit')
92+
93+
// Retry on server error.
94+
const retried = await client.lookupIp('5.6.7.8')
95+
assertEqual(retried.data.ip, '5.6.7.8', 'retried ip')
96+
97+
// API error mapping.
98+
try {
99+
await client.lookupIp('not-an-ip')
100+
throw new Error('expected lookupIp to throw an ApiError')
101+
} catch (error) {
102+
if (!(error instanceof ApiError)) {
103+
throw error
104+
}
105+
assertEqual(error.code, 'INVALID_IP_ADDRESS', 'error code')
106+
}
107+
} finally {
108+
globalThis.fetch = originalFetch
109+
}
110+
}

test/smoke.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2019 Ipregistry (https://ipregistry.co).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Runtime smoke test: exercises the built library against a stubbed fetch so
18+
// it can run unchanged on any web-standard runtime (Node.js, Bun, Deno,
19+
// workerd). Plain JavaScript on purpose: no test framework, no TypeScript
20+
// loader. Exits non-zero (or rejects) on failure.
21+
22+
import { runSmokeTest } from './smoke-scenario.mjs'
23+
24+
try {
25+
await runSmokeTest()
26+
console.log('smoke test OK')
27+
} catch (error) {
28+
console.error('smoke test FAILED:', error)
29+
if (globalThis.process) {
30+
globalThis.process.exit(1)
31+
}
32+
throw error
33+
}

test/workers/run.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2019 Ipregistry (https://ipregistry.co).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Runs the smoke scenario inside the real Cloudflare Workers runtime
18+
// (workerd, via Miniflare). The worker entry is bundled first with esbuild so
19+
// workerd receives a single self-contained module.
20+
21+
import { build } from 'esbuild'
22+
import { Miniflare } from 'miniflare'
23+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
24+
import { tmpdir } from 'node:os'
25+
import { join } from 'node:path'
26+
27+
const outDir = await mkdtemp(join(tmpdir(), 'ipregistry-worker-smoke-'))
28+
const outFile = join(outDir, 'worker.mjs')
29+
30+
try {
31+
await build({
32+
bundle: true,
33+
entryPoints: [new URL('worker.mjs', import.meta.url).pathname],
34+
format: 'esm',
35+
outfile: outFile,
36+
target: 'es2022',
37+
})
38+
39+
const miniflare = new Miniflare({
40+
compatibilityDate: '2026-01-01',
41+
modules: true,
42+
script: await readFile(outFile, 'utf-8'),
43+
})
44+
45+
try {
46+
const response = await miniflare.dispatchFetch('http://smoke.test/')
47+
const body = await response.text()
48+
49+
if (response.status !== 200 || body !== 'OK') {
50+
console.error(`workerd smoke test FAILED (${response.status}):`)
51+
console.error(body)
52+
process.exit(1)
53+
}
54+
55+
console.log('workerd smoke test OK')
56+
} finally {
57+
await miniflare.dispose()
58+
}
59+
} finally {
60+
await rm(outDir, { force: true, recursive: true })
61+
}

test/workers/worker.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2019 Ipregistry (https://ipregistry.co).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Cloudflare Workers smoke test entry: runs the shared smoke scenario inside
18+
// workerd and reports the outcome over HTTP.
19+
20+
import { runSmokeTest } from '../smoke-scenario.mjs'
21+
22+
export default {
23+
async fetch() {
24+
try {
25+
await runSmokeTest()
26+
return new Response('OK')
27+
} catch (error) {
28+
return new Response(`${error?.stack ?? error}`, { status: 500 })
29+
}
30+
},
31+
}

0 commit comments

Comments
 (0)