From d29b5ca38b724078a714c1091f24be4b1732d04a Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:20:58 +1000 Subject: [PATCH 01/10] add node and bun tests to release pipeline --- .github/workflows/release.yml | 21 +++++- .gitignore | 2 + test/fixtures/load-esm-worker-bundle.mjs | 36 ++++++++++ test/worker-bundle.test.ts | 90 ++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/load-esm-worker-bundle.mjs create mode 100644 test/worker-bundle.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8be3996..ff896ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,24 @@ jobs: with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' + cache: 'npm' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install deps + run: npm ci + + - name: Build + run: npm run build + + - name: Run Node.js tests + run: npm test + + - name: Run Bun tests + run: bun test - name: Ensure package version is 3.x.x run: | @@ -28,7 +46,8 @@ jobs: 3.*) echo "OK: version is 3.x.x" ;; *) echo "ERROR: package.json version must be 3.x.x for v3 tags" && exit 1 ;; esac + - name: Publish to NPM with dist-tag "latest" run: npm run prepack && npm publish --tag latest --//registry.npmjs.org/:_authToken="$NPM_TOKEN" env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 76588e4..47b1694 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ node_modules *js +!test/fixtures/**/*.mjs +!test/scripts/**/*.mjs cjs esm .idea diff --git a/test/fixtures/load-esm-worker-bundle.mjs b/test/fixtures/load-esm-worker-bundle.mjs new file mode 100644 index 0000000..e5ecac9 --- /dev/null +++ b/test/fixtures/load-esm-worker-bundle.mjs @@ -0,0 +1,36 @@ +/** + * Runs inside a Node worker thread ({ type: 'module' }) to ensure the ESM + * worker bundle parses and exposes webhook exports in an isolated context. + */ +import { parentPort, workerData } from 'node:worker_threads'; +import { pathToFileURL } from 'node:url'; +import path from 'node:path'; + +const required = [ + 'default', + 'WebhookEventType', + 'WebhookContentType', + 'basicAuthValidator', + 'WebhookError', + 'WebhookAuthenticationError', + 'WebhookPayloadValidationError', + 'WebhookPayloadParseError', +]; + +try { + const root = workerData.root; + const bundlePath = path.join(root, 'esm/chargebee.esm.worker.js'); + const mod = await import(pathToFileURL(bundlePath).href); + const missing = required.filter((k) => mod[k] === undefined); + if (missing.length) { + parentPort.postMessage({ ok: false, error: 'missing exports', missing }); + } else { + parentPort.postMessage({ ok: true }); + } +} catch (err) { + parentPort.postMessage({ + ok: false, + error: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + }); +} diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts new file mode 100644 index 0000000..c1a4225 --- /dev/null +++ b/test/worker-bundle.test.ts @@ -0,0 +1,90 @@ +import { expect } from 'chai'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Worker } from 'node:worker_threads'; + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.join(testDir, '..'); +const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); + +const hasBuiltEsmWorker = fs.existsSync(esmWorkerPath); + +function describeIfEsmBuilt(title: string, fn: () => void): void { + if (hasBuiltEsmWorker) { + describe(title, fn); + return; + } + describe.skip( + `${title} (skipped: run \`npm run build\` or \`npm run test:worker-bundle\`)`, + fn, + ); +} + +const REQUIRED_VALUE_EXPORTS = [ + 'WebhookEventType', + 'WebhookContentType', + 'basicAuthValidator', + 'WebhookError', + 'WebhookAuthenticationError', + 'WebhookPayloadValidationError', + 'WebhookPayloadParseError', +] as const; + +function assertWebhookExports(mod: Record, label: string): void { + for (const name of REQUIRED_VALUE_EXPORTS) { + expect(mod, `${label} must export ${name}`).to.have.property(name); + expect(mod[name], `${label}.${name}`).to.not.equal(undefined); + } + + expect(mod.default, `${label} default export`).to.be.a('function'); + expect(mod.WebhookEventType, `${label}.WebhookEventType`).to.be.an('object'); + expect(mod.WebhookContentType, `${label}.WebhookContentType`).to.equal(mod.WebhookEventType); + expect(mod.basicAuthValidator, `${label}.basicAuthValidator`).to.be.a('function'); + + const Err = mod.WebhookError as typeof Error; + expect(new Err('x')).to.be.instanceof(Error); + expect((new Err('x') as Error).name).to.equal('WebhookError'); + + const AuthErr = mod.WebhookAuthenticationError as typeof Error; + expect(new AuthErr('a')).to.be.instanceof(Err); + + const ValErr = mod.WebhookPayloadValidationError as typeof Error; + expect(new ValErr('v')).to.be.instanceof(Err); + + const ParseErr = mod.WebhookPayloadParseError as typeof Error; + expect(new ParseErr('p')).to.be.instanceof(Err); +} + +describeIfEsmBuilt('Worker entry bundle (ESM)', () => { + it('exposes webhook value exports from built ESM worker entry', async function () { + const mod = (await import(pathToFileURL(esmWorkerPath).href)) as Record; + assertWebhookExports(mod, 'esm/chargebee.esm.worker.js'); + }); +}); + +describeIfEsmBuilt('Worker thread can load ESM worker bundle', () => { + it('parses the bundle and receives all webhook exports inside a worker', function (done) { + const fixture = path.join(testDir, 'fixtures', 'load-esm-worker-bundle.mjs'); + const worker = new Worker(fixture, { + workerData: { root: repoRoot }, + type: 'module', + }); + worker.on('message', (msg: { ok: boolean; error?: string; missing?: string[]; stack?: string }) => { + void worker.terminate().then(() => { + try { + expect(msg.ok, JSON.stringify(msg)).to.be.true; + done(); + } catch (e) { + done(e as Error); + } + }); + }); + worker.on('error', done); + worker.on('exit', (code) => { + if (code !== 0) { + done(new Error(`worker exited with code ${code}`)); + } + }); + }); +}); From 40aa24159f776a8151a2c35563b6b8082f288a1c Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:27:46 +1000 Subject: [PATCH 02/10] fix pipelines --- .github/workflows/ci.yml | 54 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 17 ----------- 2 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d7625c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +jobs: + node: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22, 24] + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + + - name: Install deps + run: npm ci + + - name: Build + run: npm run build + + - name: Run Node.js tests + run: npm test + + bun: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install deps + run: npm ci + + - name: Build + run: npm run build + + - name: Run Node.js tests + run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff896ac..4c95eff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,23 +21,6 @@ jobs: registry-url: 'https://registry.npmjs.org' cache: 'npm' - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install deps - run: npm ci - - - name: Build - run: npm run build - - - name: Run Node.js tests - run: npm test - - - name: Run Bun tests - run: bun test - - name: Ensure package version is 3.x.x run: | VERSION=$(node -p "require('./package.json').version") From bb4960a321e9d8e561982e9ddcd58bb50299a7e4 Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:39:22 +1000 Subject: [PATCH 03/10] fix bun test --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7625c4..0976e48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: run: npm ci - name: Build - run: npm run build + run: bun run build - - name: Run Node.js tests - run: npm test + - name: Run Bun tests + run: bun test From 091494545aa77c4d9aa35bd4b003fdcd559cd5ed Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:48:33 +1000 Subject: [PATCH 04/10] fix tests on node 20 --- test/worker-bundle.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index c1a4225..e7869f8 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -1,10 +1,10 @@ import { expect } from 'chai'; -import fs from 'node:fs'; -import path from 'node:path'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Worker } from 'node:worker_threads'; -const testDir = path.dirname(fileURLToPath(import.meta.url)); +const testDir = path.dirname(fileURLToPath(__dirname)); const repoRoot = path.join(testDir, '..'); const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); From bf0b613d786dda66c2891d13e4976d425e06f7e4 Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:49:56 +1000 Subject: [PATCH 05/10] fix typo --- test/worker-bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index e7869f8..47e24e2 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Worker } from 'node:worker_threads'; -const testDir = path.dirname(fileURLToPath(__dirname)); +const testDir = __dirname; const repoRoot = path.join(testDir, '..'); const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); From 474b1b1d3541596fc0e328d2ea79674228123f5b Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:51:44 +1000 Subject: [PATCH 06/10] try again --- test/worker-bundle.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index 47e24e2..928e974 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -4,7 +4,8 @@ import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Worker } from 'node:worker_threads'; -const testDir = __dirname; +const __filename = fileURLToPath(import.meta.url); +const testDir = path.dirname(__filename); const repoRoot = path.join(testDir, '..'); const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); From 302273ad9e545be3b3f8929ab1a5052cd22eb48a Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:55:49 +1000 Subject: [PATCH 07/10] fix worker termination --- test/worker-bundle.test.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index 928e974..4505754 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -71,20 +71,35 @@ describeIfEsmBuilt('Worker thread can load ESM worker bundle', () => { workerData: { root: repoRoot }, type: 'module', }); - worker.on('message', (msg: { ok: boolean; error?: string; missing?: string[]; stack?: string }) => { - void worker.terminate().then(() => { - try { - expect(msg.ok, JSON.stringify(msg)).to.be.true; + let settled = false; + const finish = (err?: Error): void => { + if (settled) { + return; + } + settled = true; + void worker.terminate().finally(() => { + if (err) { + done(err); + } else { done(); - } catch (e) { - done(e as Error); } }); + }; + worker.on('message', (msg: { ok: boolean; error?: string; missing?: string[]; stack?: string }) => { + try { + expect(msg.ok, JSON.stringify(msg)).to.be.true; + finish(); + } catch (e) { + finish(e as Error); + } }); - worker.on('error', done); + worker.on('error', finish); worker.on('exit', (code) => { + if (settled) { + return; + } if (code !== 0) { - done(new Error(`worker exited with code ${code}`)); + finish(new Error(`worker exited with code ${code}`)); } }); }); From 0938da828de54e58662cfcb8f8055813d3665dd9 Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 18:59:57 +1000 Subject: [PATCH 08/10] try again --- test/worker-bundle.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index 4505754..002a14d 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -4,8 +4,14 @@ import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Worker } from 'node:worker_threads'; -const __filename = fileURLToPath(import.meta.url); -const testDir = path.dirname(__filename); +let testDir; + +if (globalThis.__dirname) { + testDir = globalThis.__dirname; +} else { + testDir = path.dirname(fileURLToPath(import.meta.url)); +} + const repoRoot = path.join(testDir, '..'); const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); From 8ad9c12a0abecbc2b3356a86c0d9d292b49b464d Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 19:11:53 +1000 Subject: [PATCH 09/10] try again --- package.json | 2 +- test/worker-bundle.test.ts | 17 ++++++++--------- tsconfig.test.json | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 tsconfig.test.json diff --git a/package.json b/package.json index 5b1d0c2..27666cd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A library for integrating with Chargebee.", "scripts": { "prepack": "npm install && npm run build", - "test": "mocha -r ts-node/register 'test/**/*.test.ts'", + "test": "TS_NODE_PROJECT=./tsconfig.test.json mocha -r ts-node/register 'test/**/*.test.ts'", "build": "npm run build-esm && npm run build-cjs", "build-esm": "rm -rf esm && mkdir -p esm && tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > esm/package.json", "build-cjs": "rm -rf cjs && mkdir -p cjs && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > cjs/package.json", diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index 002a14d..f1fe0f2 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -4,14 +4,12 @@ import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Worker } from 'node:worker_threads'; -let testDir; - -if (globalThis.__dirname) { - testDir = globalThis.__dirname; -} else { - testDir = path.dirname(fileURLToPath(import.meta.url)); -} +/** `type` is supported at runtime for ESM workers; widen options until typings always include it. */ +type WorkerThreadOptions = NonNullable[1]> & { + type?: 'module' | 'classic'; +}; +const testDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.join(testDir, '..'); const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); @@ -73,10 +71,11 @@ describeIfEsmBuilt('Worker entry bundle (ESM)', () => { describeIfEsmBuilt('Worker thread can load ESM worker bundle', () => { it('parses the bundle and receives all webhook exports inside a worker', function (done) { const fixture = path.join(testDir, 'fixtures', 'load-esm-worker-bundle.mjs'); - const worker = new Worker(fixture, { + const workerOptions: WorkerThreadOptions = { workerData: { root: repoRoot }, type: 'module', - }); + }; + const worker = new Worker(fixture, workerOptions); let settled = false; const finish = (err?: Error): void => { if (settled) { diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..26fe2a1 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.cjs.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "ES2022", + "moduleResolution": "node", + "target": "ES2022", + "types": ["node", "mocha"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "ts-node": { + "experimentalResolver": true, + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node" + } + } +} From 9476221f84ba8279ae36d06b0d3fb6146611face Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 22 Apr 2026 19:28:18 +1000 Subject: [PATCH 10/10] fix tests in all runtimes --- test/worker-bundle.test.ts | 11 ++++++++--- tsconfig.test.json | 1 - 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/worker-bundle.test.ts b/test/worker-bundle.test.ts index f1fe0f2..862b9fb 100644 --- a/test/worker-bundle.test.ts +++ b/test/worker-bundle.test.ts @@ -9,7 +9,7 @@ type WorkerThreadOptions = NonNullable[1]> type?: 'module' | 'classic'; }; -const testDir = path.dirname(fileURLToPath(import.meta.url)); +const testDir = path.resolve(process.cwd(), 'test'); const repoRoot = path.join(testDir, '..'); const esmWorkerPath = path.join(repoRoot, 'esm/chargebee.esm.worker.js'); @@ -63,8 +63,13 @@ function assertWebhookExports(mod: Record, label: string): void describeIfEsmBuilt('Worker entry bundle (ESM)', () => { it('exposes webhook value exports from built ESM worker entry', async function () { - const mod = (await import(pathToFileURL(esmWorkerPath).href)) as Record; - assertWebhookExports(mod, 'esm/chargebee.esm.worker.js'); + try { + const mod = (await import(pathToFileURL(esmWorkerPath).href)) as Record; + assertWebhookExports(mod, 'esm/chargebee.esm.worker.js'); + } catch (error) { + const mod = require(esmWorkerPath); + assertWebhookExports(mod, 'esm/chargebee.esm.worker.js'); + } }); }); diff --git a/tsconfig.test.json b/tsconfig.test.json index 26fe2a1..0fdfe8d 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,5 +1,4 @@ { - "extends": "./tsconfig.cjs.json", "compilerOptions": { "noEmit": true, "rootDir": ".",