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. diff --git a/src/bundle-interop.test.ts b/src/bundle-interop.test.ts new file mode 100644 index 0000000..7de5bed --- /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('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).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 }); + } + }); +}); 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..9ce1ef5 100644 --- a/src/money/formatters.ts +++ b/src/money/formatters.ts @@ -1,4 +1,3 @@ -import dinero from 'dinero.js'; 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 089e5ab..8bd902b 100644 --- a/src/money/to-dinero.ts +++ b/src/money/to-dinero.ts @@ -1,6 +1,7 @@ -import dinero, { type Currency, type Dinero } from 'dinero.js'; +import type { Currency, Dinero } from 'dinero.js'; 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).