From 990308a351d2916e9c2a9c590a5ce12b657b804b Mon Sep 17 00:00:00 2001 From: Viljami + Claude Date: Wed, 21 Oct 2026 06:52:29 +0000 Subject: [PATCH 1/4] fix: bundle dinero.js to avoid CJS/ESM default interop error in consumers Co-Authored-By: Claude Sonnet 5.5 --- esbuild.config.js | 8 +++++- src/bundle-interop.test.ts | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/bundle-interop.test.ts diff --git a/esbuild.config.js b/esbuild.config.js index 41ad941..ca6ac07 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -2,10 +2,16 @@ import fs from 'fs/promises'; import esbuild from 'esbuild'; import packageJson from './package.json' with { type: 'json' }; +// dinero.js is bundled rather than left external: it ships a CJS build (`main`) and an ESM +// build (`module`). When this package's CJS entry `require('dinero.js')` is itself bundled by a +// consumer using `mainFields: ['module', 'main']` (e.g. esbuild for Lambdas), the ESM build is +// picked and `.default` becomes the module namespace -> "TypeError: (0, x.default) is not a function". +const bundledDeps = new Set(['dinero.js']); + const externalDeps = [ ...Object.keys(packageJson.peerDependencies ?? {}), ...Object.keys(packageJson.dependencies ?? {}), -]; +].filter((dep) => !bundledDeps.has(dep)); const buildFormats = [ { diff --git a/src/bundle-interop.test.ts b/src/bundle-interop.test.ts new file mode 100644 index 0000000..b0af991 --- /dev/null +++ b/src/bundle-interop.test.ts @@ -0,0 +1,55 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { build } from 'esbuild'; +import { beforeAll, describe, expect, it } from 'vitest'; + +const root = path.resolve(__dirname, '..'); + +/** + * Regression: consumers that bundle this package's CJS entry with + * `mainFields: ['module', 'main']` resolved `require('dinero.js')` to dinero's ESM build, so + * `.default` was the module namespace and `formatAmount` threw + * "TypeError: (0, x.default) is not a function". + */ +describe('dist bundle interop', () => { + beforeAll(() => { + execFileSync('node', ['esbuild.config.js'], { cwd: root, stdio: 'ignore' }); + }, 60_000); + + it('does not leave a bare dinero.js import in dist', () => { + for (const file of ['dist/cjs/index.cjs', 'dist/esm/index.mjs']) { + const code = fs.readFileSync(path.join(root, file), 'utf8'); + expect(code).not.toMatch(/require\(["']dinero\.js["']\)|from\s*["']dinero\.js["']/); + } + }); + + it.each([ + ['cjs', 'dist/cjs/index.cjs'], + ['esm', 'dist/esm/index.mjs'], + ] as const)('formatAmount works when the %s entry is re-bundled with mainFields module,main', async (_, entry) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pricing-interop-')); + try { + const outfile = path.join(dir, 'out.mjs'); + await build({ + stdin: { + contents: `import { formatAmount } from ${JSON.stringify(path.join(root, entry))}; +console.log(formatAmount({ amount: 12345, currency: 'EUR', locale: 'en' }));`, + resolveDir: root, + }, + outfile, + bundle: true, + platform: 'node', + format: 'esm', + mainFields: ['module', 'main'], + banner: { js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);" }, + logLevel: 'silent', + }); + const out = execFileSync('node', [outfile], { encoding: 'utf8' }); + expect(out).toMatch(/123[.,]45/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); +}); From 69ce80f78dbf30911e45aa7f966548faea0a7694 Mon Sep 17 00:00:00 2001 From: Viljami + Claude Date: Wed, 21 Oct 2026 07:06:06 +0000 Subject: [PATCH 2/4] fix(money): resolve dinero.js factory regardless of CJS/ESM resolution Keep dinero.js external; unwrap nested default exports so consumers bundling with mainFields module,main no longer hit '(0, x.default) is not a function'. Co-Authored-By: Claude Sonnet 5.5 --- esbuild.config.js | 8 +------- src/bundle-interop.test.ts | 4 ++-- src/money/dinero.ts | 20 ++++++++++++++++++++ src/money/formatters.ts | 2 +- src/money/to-dinero.ts | 3 ++- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 src/money/dinero.ts diff --git a/esbuild.config.js b/esbuild.config.js index ca6ac07..41ad941 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -2,16 +2,10 @@ import fs from 'fs/promises'; import esbuild from 'esbuild'; import packageJson from './package.json' with { type: 'json' }; -// dinero.js is bundled rather than left external: it ships a CJS build (`main`) and an ESM -// build (`module`). When this package's CJS entry `require('dinero.js')` is itself bundled by a -// consumer using `mainFields: ['module', 'main']` (e.g. esbuild for Lambdas), the ESM build is -// picked and `.default` becomes the module namespace -> "TypeError: (0, x.default) is not a function". -const bundledDeps = new Set(['dinero.js']); - const externalDeps = [ ...Object.keys(packageJson.peerDependencies ?? {}), ...Object.keys(packageJson.dependencies ?? {}), -].filter((dep) => !bundledDeps.has(dep)); +]; const buildFormats = [ { diff --git a/src/bundle-interop.test.ts b/src/bundle-interop.test.ts index b0af991..7de5bed 100644 --- a/src/bundle-interop.test.ts +++ b/src/bundle-interop.test.ts @@ -18,10 +18,10 @@ describe('dist bundle interop', () => { execFileSync('node', ['esbuild.config.js'], { cwd: root, stdio: 'ignore' }); }, 60_000); - it('does not leave a bare dinero.js import in dist', () => { + it('keeps dinero.js external in dist', () => { for (const file of ['dist/cjs/index.cjs', 'dist/esm/index.mjs']) { const code = fs.readFileSync(path.join(root, file), 'utf8'); - expect(code).not.toMatch(/require\(["']dinero\.js["']\)|from\s*["']dinero\.js["']/); + expect(code).toMatch(/require\(["']dinero\.js["']\)|from\s*["']dinero\.js["']/); } }); diff --git a/src/money/dinero.ts b/src/money/dinero.ts new file mode 100644 index 0000000..4d1d14a --- /dev/null +++ b/src/money/dinero.ts @@ -0,0 +1,20 @@ +import * as DineroModule from 'dinero.js'; +import type DineroFactory from 'dinero.js'; + +/** + * dinero.js v1 ships a CJS build (`main`) and an ESM build (`module`, `export default`). + * When a consumer bundles this package's CJS entry with `mainFields: ['module', 'main']` + * (e.g. esbuild for Lambdas), `require('dinero.js')` resolves to the ESM build and the + * default import becomes the module namespace instead of the factory + * ("TypeError: (0, x.default) is not a function"). Unwrap nested `default`s so the factory + * is found regardless of how the consumer resolved the package. + */ +const resolveFactory = (mod: unknown): typeof DineroFactory => { + let current = mod as { default?: unknown }; + for (let depth = 0; depth < 3 && typeof current !== 'function'; depth++) { + current = current?.default as { default?: unknown }; + } + return current as unknown as typeof DineroFactory; +}; + +export const dinero = resolveFactory(DineroModule); diff --git a/src/money/formatters.ts b/src/money/formatters.ts index 771404a..1908859 100644 --- a/src/money/formatters.ts +++ b/src/money/formatters.ts @@ -1,4 +1,4 @@ -import dinero from 'dinero.js'; +import { dinero } from './dinero'; import type { Currency } from 'dinero.js'; import { CURRENCIES_SUBUNITS, DEFAULT_CURRENCY, DEFAULT_SUBUNIT } from './constants'; import { diff --git a/src/money/to-dinero.ts b/src/money/to-dinero.ts index 089e5ab..1e4842b 100644 --- a/src/money/to-dinero.ts +++ b/src/money/to-dinero.ts @@ -1,4 +1,5 @@ -import dinero, { type Currency, type Dinero } from 'dinero.js'; +import type { Currency, Dinero } from 'dinero.js'; +import { dinero } from './dinero'; import { DEFAULT_CURRENCY } from './constants'; import { DECIMAL_PRECISION } from './constants'; From 2b4a2ad6280ba43ca57b947dc77c0cb14a30e06b Mon Sep 17 00:00:00 2001 From: Viljami + Claude Date: Wed, 21 Oct 2026 07:11:48 +0000 Subject: [PATCH 3/4] style: fix import order lint errors Co-Authored-By: Claude Sonnet 5.5 --- src/money/formatters.ts | 2 +- src/money/to-dinero.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/money/formatters.ts b/src/money/formatters.ts index 1908859..9ce1ef5 100644 --- a/src/money/formatters.ts +++ b/src/money/formatters.ts @@ -1,4 +1,3 @@ -import { dinero } from './dinero'; import type { Currency } from 'dinero.js'; import { CURRENCIES_SUBUNITS, DEFAULT_CURRENCY, DEFAULT_SUBUNIT } from './constants'; import { @@ -10,6 +9,7 @@ import { GENERIC_UNIT_DISPLAY_LABEL, MAX_SUPPORTED_FORMAT_PRECISION, } from './constants'; +import { dinero } from './dinero'; import { getCurrencySymbol } from './get-currency-symbol'; import { toDinero } from './to-dinero'; diff --git a/src/money/to-dinero.ts b/src/money/to-dinero.ts index 1e4842b..8bd902b 100644 --- a/src/money/to-dinero.ts +++ b/src/money/to-dinero.ts @@ -1,7 +1,7 @@ import type { Currency, Dinero } from 'dinero.js'; -import { dinero } from './dinero'; import { DEFAULT_CURRENCY } from './constants'; import { DECIMAL_PRECISION } from './constants'; +import { dinero } from './dinero'; /** * Utility mapper from Integer amount into DineroJS object using DECIMAL_PRECISION (12). From f5edc139cc3ac71d7f91c4c43607c34962ff5ff7 Mon Sep 17 00:00:00 2001 From: Viljami + Claude Date: Wed, 21 Oct 2026 07:15:08 +0000 Subject: [PATCH 4/4] chore: add patch changeset for dinero.js interop fix Co-Authored-By: Claude Sonnet 5.5 --- .changeset/fix-dinero-interop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-dinero-interop.md diff --git a/.changeset/fix-dinero-interop.md b/.changeset/fix-dinero-interop.md new file mode 100644 index 0000000..d16e52e --- /dev/null +++ b/.changeset/fix-dinero-interop.md @@ -0,0 +1,5 @@ +--- +"@epilot/pricing": patch +--- + +Fix `TypeError: (0, x.default) is not a function` when a consumer bundles `@epilot/pricing` with `mainFields: ['module','main']`. The `dinero.js` factory is now resolved regardless of whether the CJS or ESM build is picked up.