@storepass/money 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,6 +111,83 @@ const json = m.toJSON();
111
111
 
112
112
  `amount` is always in minor units.
113
113
 
114
+ ### Zod schemas and codecs
115
+
116
+ Zod support is available from a separate entry point, so importing the core
117
+ money primitives does not require Zod:
118
+
119
+ ```ts
120
+ import { Currency, defineCurrency, Money } from "@storepass/money";
121
+ import {
122
+ createMoneyCodec,
123
+ currencyDefinitionSchema,
124
+ moneyCodec,
125
+ moneyInstanceSchema,
126
+ moneySchema,
127
+ } from "@storepass/money/zod";
128
+ import { decode, encode } from "zod";
129
+
130
+ const serialized = moneySchema.parse({ amount: 1234, currency: "USD" });
131
+ const money = decode(moneyCodec, serialized);
132
+
133
+ moneyInstanceSchema.parse(money); // Money
134
+ encode(moneyCodec, money); // { amount: 1234, currency: "USD" }
135
+
136
+ currencyDefinitionSchema.parse(Currency.USD);
137
+
138
+ const XAU = defineCurrency("XAU", 4);
139
+ const goldCodec = createMoneyCodec({ XAU });
140
+ decode(goldCodec, { amount: 12345, currency: "XAU" });
141
+ ```
142
+
143
+ `moneySchema` (also exported as `serializedMoneySchema`) accepts custom
144
+ currency codes and validates that `amount` is a safe integer in minor units.
145
+ `builtInSerializedMoneySchema` and `moneyCodec` restrict values to the
146
+ currencies included in `Currency`.
147
+
148
+ ### Display formatting
149
+
150
+ `Money#format` wraps `Intl.NumberFormat`, honoring the currency's
151
+ `decimalPlaces` and gracefully degrading for non-ISO codes.
152
+
153
+ ```ts
154
+ Money.fromNumber(Currency.USD, 1234.5).format("en-US"); // "$1,234.50"
155
+ Money.fromNumber(Currency.EUR, 1234.56).format("de-DE"); // "1.234,56 €"
156
+ Money.fromNumber(Currency.JPY, 1500).format("en-US"); // "¥1,500"
157
+ Money.fromNumber(Currency.BTC, 0.12345678).format("en-US"); // "BTC 0.12345678"
158
+ ```
159
+
160
+ For non-ISO codes, you can supply a `displayCode` so `Intl` still renders a
161
+ proper currency symbol while arithmetic precision is preserved separately:
162
+
163
+ ```ts
164
+ const USDH = defineCurrency("USDH", 3, { displayCode: "USD" });
165
+ Money.fromNumber(USDH, 12.345).format("en-US"); // "$12.345"
166
+ ```
167
+
168
+ ## ISO 4217 compatibility
169
+
170
+ Standard currencies in `Currency` carry `numericCode` and `iso4217: true`.
171
+ Custom assets (e.g., `BTC`, `USDH`) are flagged `iso4217: false`. When
172
+ defining your own, pass the metadata explicitly:
173
+
174
+ ```ts
175
+ const SLE = defineCurrency("SLE", 2, { numericCode: 925, iso4217: true });
176
+ const XAU = defineCurrency("XAU", 4); // non-monetary, no ISO flags
177
+ ```
178
+
179
+ `defineCurrency` validates `iso4217` codes against `/^[A-Z]{3}$/` and
180
+ requires a numeric code in `0..999`.
181
+
182
+ ### Staying compliant
183
+
184
+ ISO 4217 is amended a few times per year by SIX Interbank Clearing
185
+ (deprecations, redenominations, occasional additions). The hardcoded list
186
+ in [src/currency.ts](./src/currency.ts) tracks a pinned revision (see the
187
+ header comment). To stay current, periodically diff against the
188
+ [official XML list](https://www.six-group.com/en/products-services/financial-information/data-standards.html)
189
+ and update both `decimalPlaces` and `numericCode` for any drift.
190
+
114
191
  ## Notes and Guarantees
115
192
 
116
193
  - Currency mismatch operations throw (for example, adding USD to EUR).
@@ -1,121 +1,203 @@
1
+ /**
2
+ * Currency metadata.
3
+ *
4
+ * For ISO 4217 currencies, `iso4217` is `true` and `numericCode` is the
5
+ * official 3-digit numeric code. `displayCode` is an optional override that
6
+ * `Intl.NumberFormat` should use when the primary `code` is non-standard
7
+ * (e.g., legacy systems calling Bitcoin "BTC" instead of the unofficial "XBT").
8
+ *
9
+ * Standard currencies in this file follow ISO 4217 as of revision 173
10
+ * (published 2024-06). Run `bun run scripts/sync-iso4217.ts` to refresh.
11
+ */
1
12
  export interface CurrencyDefinition {
13
+ /** Alpha code (typically ISO 4217 alpha-3, but free-form for custom assets). */
2
14
  code: string;
15
+ /** Minor-unit precision used for internal arithmetic. */
3
16
  decimalPlaces: number;
17
+ /** ISO 4217 numeric code (0-999). Omitted for non-standard assets. */
18
+ numericCode?: number;
19
+ /** `true` when `code` is an officially registered ISO 4217 alpha-3 code. */
20
+ iso4217?: boolean;
21
+ /**
22
+ * Optional alpha-3 used for `Intl.NumberFormat` display. Falls back to `code`.
23
+ * Useful for non-ISO codes that have a registered alias.
24
+ */
25
+ displayCode?: string;
4
26
  }
5
27
  export declare const Currency: {
6
28
  readonly USD: {
7
29
  readonly code: "USD";
8
30
  readonly decimalPlaces: 2;
31
+ readonly numericCode: 840;
32
+ readonly iso4217: true;
9
33
  };
10
34
  readonly USDH: {
11
35
  readonly code: "USDH";
12
36
  readonly decimalPlaces: 3;
37
+ readonly iso4217: false;
38
+ readonly displayCode: "USD";
13
39
  };
14
40
  readonly EUR: {
15
41
  readonly code: "EUR";
16
42
  readonly decimalPlaces: 2;
43
+ readonly numericCode: 978;
44
+ readonly iso4217: true;
17
45
  };
18
46
  readonly GBP: {
19
47
  readonly code: "GBP";
20
48
  readonly decimalPlaces: 2;
49
+ readonly numericCode: 826;
50
+ readonly iso4217: true;
21
51
  };
22
52
  readonly JPY: {
23
53
  readonly code: "JPY";
24
54
  readonly decimalPlaces: 0;
55
+ readonly numericCode: 392;
56
+ readonly iso4217: true;
25
57
  };
26
58
  readonly CAD: {
27
59
  readonly code: "CAD";
28
60
  readonly decimalPlaces: 2;
61
+ readonly numericCode: 124;
62
+ readonly iso4217: true;
29
63
  };
30
64
  readonly AUD: {
31
65
  readonly code: "AUD";
32
66
  readonly decimalPlaces: 2;
67
+ readonly numericCode: 36;
68
+ readonly iso4217: true;
33
69
  };
34
70
  readonly NZD: {
35
71
  readonly code: "NZD";
36
72
  readonly decimalPlaces: 2;
73
+ readonly numericCode: 554;
74
+ readonly iso4217: true;
37
75
  };
38
76
  readonly CHF: {
39
77
  readonly code: "CHF";
40
78
  readonly decimalPlaces: 2;
79
+ readonly numericCode: 756;
80
+ readonly iso4217: true;
41
81
  };
42
82
  readonly SEK: {
43
83
  readonly code: "SEK";
44
84
  readonly decimalPlaces: 2;
85
+ readonly numericCode: 752;
86
+ readonly iso4217: true;
45
87
  };
46
88
  readonly NOK: {
47
89
  readonly code: "NOK";
48
90
  readonly decimalPlaces: 2;
91
+ readonly numericCode: 578;
92
+ readonly iso4217: true;
49
93
  };
50
94
  readonly DKK: {
51
95
  readonly code: "DKK";
52
96
  readonly decimalPlaces: 2;
97
+ readonly numericCode: 208;
98
+ readonly iso4217: true;
53
99
  };
54
100
  readonly PLN: {
55
101
  readonly code: "PLN";
56
102
  readonly decimalPlaces: 2;
103
+ readonly numericCode: 985;
104
+ readonly iso4217: true;
57
105
  };
58
106
  readonly CZK: {
59
107
  readonly code: "CZK";
60
108
  readonly decimalPlaces: 2;
109
+ readonly numericCode: 203;
110
+ readonly iso4217: true;
61
111
  };
62
112
  readonly HUF: {
63
113
  readonly code: "HUF";
64
114
  readonly decimalPlaces: 2;
115
+ readonly numericCode: 348;
116
+ readonly iso4217: true;
65
117
  };
66
118
  readonly BRL: {
67
119
  readonly code: "BRL";
68
120
  readonly decimalPlaces: 2;
121
+ readonly numericCode: 986;
122
+ readonly iso4217: true;
69
123
  };
70
124
  readonly MXN: {
71
125
  readonly code: "MXN";
72
126
  readonly decimalPlaces: 2;
127
+ readonly numericCode: 484;
128
+ readonly iso4217: true;
73
129
  };
74
130
  readonly ARS: {
75
131
  readonly code: "ARS";
76
132
  readonly decimalPlaces: 2;
133
+ readonly numericCode: 32;
134
+ readonly iso4217: true;
77
135
  };
78
136
  readonly CLP: {
79
137
  readonly code: "CLP";
80
138
  readonly decimalPlaces: 0;
139
+ readonly numericCode: 152;
140
+ readonly iso4217: true;
81
141
  };
82
142
  readonly INR: {
83
143
  readonly code: "INR";
84
144
  readonly decimalPlaces: 2;
145
+ readonly numericCode: 356;
146
+ readonly iso4217: true;
85
147
  };
86
148
  readonly SGD: {
87
149
  readonly code: "SGD";
88
150
  readonly decimalPlaces: 2;
151
+ readonly numericCode: 702;
152
+ readonly iso4217: true;
89
153
  };
90
154
  readonly HKD: {
91
155
  readonly code: "HKD";
92
156
  readonly decimalPlaces: 2;
157
+ readonly numericCode: 344;
158
+ readonly iso4217: true;
93
159
  };
94
160
  readonly KRW: {
95
161
  readonly code: "KRW";
96
162
  readonly decimalPlaces: 0;
163
+ readonly numericCode: 410;
164
+ readonly iso4217: true;
97
165
  };
98
166
  readonly CNY: {
99
167
  readonly code: "CNY";
100
168
  readonly decimalPlaces: 2;
169
+ readonly numericCode: 156;
170
+ readonly iso4217: true;
101
171
  };
102
172
  readonly ZAR: {
103
173
  readonly code: "ZAR";
104
174
  readonly decimalPlaces: 2;
175
+ readonly numericCode: 710;
176
+ readonly iso4217: true;
105
177
  };
106
178
  readonly AED: {
107
179
  readonly code: "AED";
108
180
  readonly decimalPlaces: 2;
181
+ readonly numericCode: 784;
182
+ readonly iso4217: true;
109
183
  };
110
184
  readonly SAR: {
111
185
  readonly code: "SAR";
112
186
  readonly decimalPlaces: 2;
187
+ readonly numericCode: 682;
188
+ readonly iso4217: true;
113
189
  };
114
190
  readonly BTC: {
115
191
  readonly code: "BTC";
116
192
  readonly decimalPlaces: 8;
193
+ readonly iso4217: false;
117
194
  };
118
195
  };
119
196
  export type CurrencyCode = keyof typeof Currency;
120
197
  export type CurrencyType = (typeof Currency)[CurrencyCode];
121
- export declare function defineCurrency(code: string, decimalPlaces: number): CurrencyDefinition;
198
+ export interface DefineCurrencyOptions {
199
+ numericCode?: number;
200
+ iso4217?: boolean;
201
+ displayCode?: string;
202
+ }
203
+ export declare function defineCurrency(code: string, decimalPlaces: number, options?: DefineCurrencyOptions): CurrencyDefinition;
package/dist/index.cjs CHANGED
@@ -47,43 +47,67 @@ module.exports = __toCommonJS(exports_src);
47
47
 
48
48
  // src/currency.ts
49
49
  var Currency = {
50
- USD: { code: "USD", decimalPlaces: 2 },
51
- USDH: { code: "USDH", decimalPlaces: 3 },
52
- EUR: { code: "EUR", decimalPlaces: 2 },
53
- GBP: { code: "GBP", decimalPlaces: 2 },
54
- JPY: { code: "JPY", decimalPlaces: 0 },
55
- CAD: { code: "CAD", decimalPlaces: 2 },
56
- AUD: { code: "AUD", decimalPlaces: 2 },
57
- NZD: { code: "NZD", decimalPlaces: 2 },
58
- CHF: { code: "CHF", decimalPlaces: 2 },
59
- SEK: { code: "SEK", decimalPlaces: 2 },
60
- NOK: { code: "NOK", decimalPlaces: 2 },
61
- DKK: { code: "DKK", decimalPlaces: 2 },
62
- PLN: { code: "PLN", decimalPlaces: 2 },
63
- CZK: { code: "CZK", decimalPlaces: 2 },
64
- HUF: { code: "HUF", decimalPlaces: 2 },
65
- BRL: { code: "BRL", decimalPlaces: 2 },
66
- MXN: { code: "MXN", decimalPlaces: 2 },
67
- ARS: { code: "ARS", decimalPlaces: 2 },
68
- CLP: { code: "CLP", decimalPlaces: 0 },
69
- INR: { code: "INR", decimalPlaces: 2 },
70
- SGD: { code: "SGD", decimalPlaces: 2 },
71
- HKD: { code: "HKD", decimalPlaces: 2 },
72
- KRW: { code: "KRW", decimalPlaces: 0 },
73
- CNY: { code: "CNY", decimalPlaces: 2 },
74
- ZAR: { code: "ZAR", decimalPlaces: 2 },
75
- AED: { code: "AED", decimalPlaces: 2 },
76
- SAR: { code: "SAR", decimalPlaces: 2 },
77
- BTC: { code: "BTC", decimalPlaces: 8 }
50
+ USD: { code: "USD", decimalPlaces: 2, numericCode: 840, iso4217: true },
51
+ USDH: { code: "USDH", decimalPlaces: 3, iso4217: false, displayCode: "USD" },
52
+ EUR: { code: "EUR", decimalPlaces: 2, numericCode: 978, iso4217: true },
53
+ GBP: { code: "GBP", decimalPlaces: 2, numericCode: 826, iso4217: true },
54
+ JPY: { code: "JPY", decimalPlaces: 0, numericCode: 392, iso4217: true },
55
+ CAD: { code: "CAD", decimalPlaces: 2, numericCode: 124, iso4217: true },
56
+ AUD: { code: "AUD", decimalPlaces: 2, numericCode: 36, iso4217: true },
57
+ NZD: { code: "NZD", decimalPlaces: 2, numericCode: 554, iso4217: true },
58
+ CHF: { code: "CHF", decimalPlaces: 2, numericCode: 756, iso4217: true },
59
+ SEK: { code: "SEK", decimalPlaces: 2, numericCode: 752, iso4217: true },
60
+ NOK: { code: "NOK", decimalPlaces: 2, numericCode: 578, iso4217: true },
61
+ DKK: { code: "DKK", decimalPlaces: 2, numericCode: 208, iso4217: true },
62
+ PLN: { code: "PLN", decimalPlaces: 2, numericCode: 985, iso4217: true },
63
+ CZK: { code: "CZK", decimalPlaces: 2, numericCode: 203, iso4217: true },
64
+ HUF: { code: "HUF", decimalPlaces: 2, numericCode: 348, iso4217: true },
65
+ BRL: { code: "BRL", decimalPlaces: 2, numericCode: 986, iso4217: true },
66
+ MXN: { code: "MXN", decimalPlaces: 2, numericCode: 484, iso4217: true },
67
+ ARS: { code: "ARS", decimalPlaces: 2, numericCode: 32, iso4217: true },
68
+ CLP: { code: "CLP", decimalPlaces: 0, numericCode: 152, iso4217: true },
69
+ INR: { code: "INR", decimalPlaces: 2, numericCode: 356, iso4217: true },
70
+ SGD: { code: "SGD", decimalPlaces: 2, numericCode: 702, iso4217: true },
71
+ HKD: { code: "HKD", decimalPlaces: 2, numericCode: 344, iso4217: true },
72
+ KRW: { code: "KRW", decimalPlaces: 0, numericCode: 410, iso4217: true },
73
+ CNY: { code: "CNY", decimalPlaces: 2, numericCode: 156, iso4217: true },
74
+ ZAR: { code: "ZAR", decimalPlaces: 2, numericCode: 710, iso4217: true },
75
+ AED: { code: "AED", decimalPlaces: 2, numericCode: 784, iso4217: true },
76
+ SAR: { code: "SAR", decimalPlaces: 2, numericCode: 682, iso4217: true },
77
+ BTC: { code: "BTC", decimalPlaces: 8, iso4217: false }
78
78
  };
79
- function defineCurrency(code, decimalPlaces) {
79
+ var ISO_ALPHA3 = /^[A-Z]{3}$/;
80
+ function defineCurrency(code, decimalPlaces, options = {}) {
80
81
  if (!code || typeof code !== "string") {
81
82
  throw new Error("Currency code must be a non-empty string");
82
83
  }
83
84
  if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0) {
84
85
  throw new Error("Decimal places must be a non-negative integer");
85
86
  }
86
- return Object.freeze({ code, decimalPlaces });
87
+ const { numericCode, iso4217, displayCode } = options;
88
+ if (iso4217 === true) {
89
+ if (!ISO_ALPHA3.test(code)) {
90
+ throw new Error(`ISO 4217 currency code must be three uppercase letters: got "${code}"`);
91
+ }
92
+ if (numericCode === undefined || !Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
93
+ throw new Error("ISO 4217 currencies require an integer numericCode in 0..999");
94
+ }
95
+ } else if (numericCode !== undefined) {
96
+ if (!Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
97
+ throw new Error("numericCode must be an integer in 0..999");
98
+ }
99
+ }
100
+ if (displayCode !== undefined && !ISO_ALPHA3.test(displayCode)) {
101
+ throw new Error(`displayCode must be three uppercase letters: got "${displayCode}"`);
102
+ }
103
+ const def = { code, decimalPlaces };
104
+ if (numericCode !== undefined)
105
+ def.numericCode = numericCode;
106
+ if (iso4217 !== undefined)
107
+ def.iso4217 = iso4217;
108
+ if (displayCode !== undefined)
109
+ def.displayCode = displayCode;
110
+ return Object.freeze(def);
87
111
  }
88
112
  // src/money.ts
89
113
  var INTERNAL_PRECISION = 20;
@@ -166,6 +190,27 @@ function assertSafeResult(value) {
166
190
  throw new Error("Result too large: exceeds safe integer range");
167
191
  }
168
192
  }
193
+ function minorToDecimalString(minor, decimalPlaces) {
194
+ const negative = minor < 0n;
195
+ const absMinor = negative ? -minor : minor;
196
+ if (decimalPlaces === 0) {
197
+ return `${negative ? "-" : ""}${absMinor}`;
198
+ }
199
+ const scale = 10n ** BigInt(decimalPlaces);
200
+ const whole = absMinor / scale;
201
+ const fraction = (absMinor % scale).toString().padStart(decimalPlaces, "0");
202
+ return `${negative ? "-" : ""}${whole}.${fraction}`;
203
+ }
204
+ function resolveFractionDigits(decimalPlaces, options) {
205
+ const { minimumFractionDigits, maximumFractionDigits } = options;
206
+ return {
207
+ minimumFractionDigits: minimumFractionDigits ?? (maximumFractionDigits === undefined ? decimalPlaces : Math.min(decimalPlaces, maximumFractionDigits)),
208
+ maximumFractionDigits: maximumFractionDigits ?? (minimumFractionDigits === undefined ? decimalPlaces : Math.max(decimalPlaces, minimumFractionDigits))
209
+ };
210
+ }
211
+ function formatExactDecimal(format, value) {
212
+ return format.format(value);
213
+ }
169
214
 
170
215
  class Money {
171
216
  #minor;
@@ -430,4 +475,27 @@ class Money {
430
475
  currency: this.#currency.code
431
476
  };
432
477
  }
478
+ format(locales, options = {}) {
479
+ const value = minorToDecimalString(this.#minor, this.#currency.decimalPlaces);
480
+ const dp = this.#currency.decimalPlaces;
481
+ const displayCode = this.#currency.displayCode ?? this.#currency.code;
482
+ const canUseCurrencyStyle = this.#currency.iso4217 === true || this.#currency.displayCode !== undefined;
483
+ const fractionDigits = resolveFractionDigits(dp, options);
484
+ if (canUseCurrencyStyle) {
485
+ try {
486
+ return formatExactDecimal(new Intl.NumberFormat(locales, {
487
+ ...options,
488
+ ...fractionDigits,
489
+ style: "currency",
490
+ currency: displayCode
491
+ }), value);
492
+ } catch {}
493
+ }
494
+ const number = formatExactDecimal(new Intl.NumberFormat(locales, {
495
+ ...options,
496
+ ...fractionDigits,
497
+ style: "decimal"
498
+ }), value);
499
+ return `${this.#currency.code} ${number}`;
500
+ }
433
501
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { Currency, type CurrencyCode, type CurrencyDefinition, type CurrencyType, defineCurrency, } from "./currency";
2
- export { type FromNumberOptions, Money, type RoundingMode } from "./money";
1
+ export { Currency, type CurrencyCode, type CurrencyDefinition, type CurrencyType, type DefineCurrencyOptions, defineCurrency, } from "./currency";
2
+ export { type FromNumberOptions, Money, type RoundingMode, type SerializedMoney, } from "./money";
package/dist/index.mjs CHANGED
@@ -1,42 +1,66 @@
1
1
  // src/currency.ts
2
2
  var Currency = {
3
- USD: { code: "USD", decimalPlaces: 2 },
4
- USDH: { code: "USDH", decimalPlaces: 3 },
5
- EUR: { code: "EUR", decimalPlaces: 2 },
6
- GBP: { code: "GBP", decimalPlaces: 2 },
7
- JPY: { code: "JPY", decimalPlaces: 0 },
8
- CAD: { code: "CAD", decimalPlaces: 2 },
9
- AUD: { code: "AUD", decimalPlaces: 2 },
10
- NZD: { code: "NZD", decimalPlaces: 2 },
11
- CHF: { code: "CHF", decimalPlaces: 2 },
12
- SEK: { code: "SEK", decimalPlaces: 2 },
13
- NOK: { code: "NOK", decimalPlaces: 2 },
14
- DKK: { code: "DKK", decimalPlaces: 2 },
15
- PLN: { code: "PLN", decimalPlaces: 2 },
16
- CZK: { code: "CZK", decimalPlaces: 2 },
17
- HUF: { code: "HUF", decimalPlaces: 2 },
18
- BRL: { code: "BRL", decimalPlaces: 2 },
19
- MXN: { code: "MXN", decimalPlaces: 2 },
20
- ARS: { code: "ARS", decimalPlaces: 2 },
21
- CLP: { code: "CLP", decimalPlaces: 0 },
22
- INR: { code: "INR", decimalPlaces: 2 },
23
- SGD: { code: "SGD", decimalPlaces: 2 },
24
- HKD: { code: "HKD", decimalPlaces: 2 },
25
- KRW: { code: "KRW", decimalPlaces: 0 },
26
- CNY: { code: "CNY", decimalPlaces: 2 },
27
- ZAR: { code: "ZAR", decimalPlaces: 2 },
28
- AED: { code: "AED", decimalPlaces: 2 },
29
- SAR: { code: "SAR", decimalPlaces: 2 },
30
- BTC: { code: "BTC", decimalPlaces: 8 }
3
+ USD: { code: "USD", decimalPlaces: 2, numericCode: 840, iso4217: true },
4
+ USDH: { code: "USDH", decimalPlaces: 3, iso4217: false, displayCode: "USD" },
5
+ EUR: { code: "EUR", decimalPlaces: 2, numericCode: 978, iso4217: true },
6
+ GBP: { code: "GBP", decimalPlaces: 2, numericCode: 826, iso4217: true },
7
+ JPY: { code: "JPY", decimalPlaces: 0, numericCode: 392, iso4217: true },
8
+ CAD: { code: "CAD", decimalPlaces: 2, numericCode: 124, iso4217: true },
9
+ AUD: { code: "AUD", decimalPlaces: 2, numericCode: 36, iso4217: true },
10
+ NZD: { code: "NZD", decimalPlaces: 2, numericCode: 554, iso4217: true },
11
+ CHF: { code: "CHF", decimalPlaces: 2, numericCode: 756, iso4217: true },
12
+ SEK: { code: "SEK", decimalPlaces: 2, numericCode: 752, iso4217: true },
13
+ NOK: { code: "NOK", decimalPlaces: 2, numericCode: 578, iso4217: true },
14
+ DKK: { code: "DKK", decimalPlaces: 2, numericCode: 208, iso4217: true },
15
+ PLN: { code: "PLN", decimalPlaces: 2, numericCode: 985, iso4217: true },
16
+ CZK: { code: "CZK", decimalPlaces: 2, numericCode: 203, iso4217: true },
17
+ HUF: { code: "HUF", decimalPlaces: 2, numericCode: 348, iso4217: true },
18
+ BRL: { code: "BRL", decimalPlaces: 2, numericCode: 986, iso4217: true },
19
+ MXN: { code: "MXN", decimalPlaces: 2, numericCode: 484, iso4217: true },
20
+ ARS: { code: "ARS", decimalPlaces: 2, numericCode: 32, iso4217: true },
21
+ CLP: { code: "CLP", decimalPlaces: 0, numericCode: 152, iso4217: true },
22
+ INR: { code: "INR", decimalPlaces: 2, numericCode: 356, iso4217: true },
23
+ SGD: { code: "SGD", decimalPlaces: 2, numericCode: 702, iso4217: true },
24
+ HKD: { code: "HKD", decimalPlaces: 2, numericCode: 344, iso4217: true },
25
+ KRW: { code: "KRW", decimalPlaces: 0, numericCode: 410, iso4217: true },
26
+ CNY: { code: "CNY", decimalPlaces: 2, numericCode: 156, iso4217: true },
27
+ ZAR: { code: "ZAR", decimalPlaces: 2, numericCode: 710, iso4217: true },
28
+ AED: { code: "AED", decimalPlaces: 2, numericCode: 784, iso4217: true },
29
+ SAR: { code: "SAR", decimalPlaces: 2, numericCode: 682, iso4217: true },
30
+ BTC: { code: "BTC", decimalPlaces: 8, iso4217: false }
31
31
  };
32
- function defineCurrency(code, decimalPlaces) {
32
+ var ISO_ALPHA3 = /^[A-Z]{3}$/;
33
+ function defineCurrency(code, decimalPlaces, options = {}) {
33
34
  if (!code || typeof code !== "string") {
34
35
  throw new Error("Currency code must be a non-empty string");
35
36
  }
36
37
  if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0) {
37
38
  throw new Error("Decimal places must be a non-negative integer");
38
39
  }
39
- return Object.freeze({ code, decimalPlaces });
40
+ const { numericCode, iso4217, displayCode } = options;
41
+ if (iso4217 === true) {
42
+ if (!ISO_ALPHA3.test(code)) {
43
+ throw new Error(`ISO 4217 currency code must be three uppercase letters: got "${code}"`);
44
+ }
45
+ if (numericCode === undefined || !Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
46
+ throw new Error("ISO 4217 currencies require an integer numericCode in 0..999");
47
+ }
48
+ } else if (numericCode !== undefined) {
49
+ if (!Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
50
+ throw new Error("numericCode must be an integer in 0..999");
51
+ }
52
+ }
53
+ if (displayCode !== undefined && !ISO_ALPHA3.test(displayCode)) {
54
+ throw new Error(`displayCode must be three uppercase letters: got "${displayCode}"`);
55
+ }
56
+ const def = { code, decimalPlaces };
57
+ if (numericCode !== undefined)
58
+ def.numericCode = numericCode;
59
+ if (iso4217 !== undefined)
60
+ def.iso4217 = iso4217;
61
+ if (displayCode !== undefined)
62
+ def.displayCode = displayCode;
63
+ return Object.freeze(def);
40
64
  }
41
65
  // src/money.ts
42
66
  var INTERNAL_PRECISION = 20;
@@ -119,6 +143,27 @@ function assertSafeResult(value) {
119
143
  throw new Error("Result too large: exceeds safe integer range");
120
144
  }
121
145
  }
146
+ function minorToDecimalString(minor, decimalPlaces) {
147
+ const negative = minor < 0n;
148
+ const absMinor = negative ? -minor : minor;
149
+ if (decimalPlaces === 0) {
150
+ return `${negative ? "-" : ""}${absMinor}`;
151
+ }
152
+ const scale = 10n ** BigInt(decimalPlaces);
153
+ const whole = absMinor / scale;
154
+ const fraction = (absMinor % scale).toString().padStart(decimalPlaces, "0");
155
+ return `${negative ? "-" : ""}${whole}.${fraction}`;
156
+ }
157
+ function resolveFractionDigits(decimalPlaces, options) {
158
+ const { minimumFractionDigits, maximumFractionDigits } = options;
159
+ return {
160
+ minimumFractionDigits: minimumFractionDigits ?? (maximumFractionDigits === undefined ? decimalPlaces : Math.min(decimalPlaces, maximumFractionDigits)),
161
+ maximumFractionDigits: maximumFractionDigits ?? (minimumFractionDigits === undefined ? decimalPlaces : Math.max(decimalPlaces, minimumFractionDigits))
162
+ };
163
+ }
164
+ function formatExactDecimal(format, value) {
165
+ return format.format(value);
166
+ }
122
167
 
123
168
  class Money {
124
169
  #minor;
@@ -383,6 +428,29 @@ class Money {
383
428
  currency: this.#currency.code
384
429
  };
385
430
  }
431
+ format(locales, options = {}) {
432
+ const value = minorToDecimalString(this.#minor, this.#currency.decimalPlaces);
433
+ const dp = this.#currency.decimalPlaces;
434
+ const displayCode = this.#currency.displayCode ?? this.#currency.code;
435
+ const canUseCurrencyStyle = this.#currency.iso4217 === true || this.#currency.displayCode !== undefined;
436
+ const fractionDigits = resolveFractionDigits(dp, options);
437
+ if (canUseCurrencyStyle) {
438
+ try {
439
+ return formatExactDecimal(new Intl.NumberFormat(locales, {
440
+ ...options,
441
+ ...fractionDigits,
442
+ style: "currency",
443
+ currency: displayCode
444
+ }), value);
445
+ } catch {}
446
+ }
447
+ const number = formatExactDecimal(new Intl.NumberFormat(locales, {
448
+ ...options,
449
+ ...fractionDigits,
450
+ style: "decimal"
451
+ }), value);
452
+ return `${this.#currency.code} ${number}`;
453
+ }
386
454
  }
387
455
  export {
388
456
  defineCurrency,
package/dist/money.d.ts CHANGED
@@ -4,6 +4,15 @@ export interface FromNumberOptions {
4
4
  rounding?: RoundingMode;
5
5
  strict?: boolean;
6
6
  }
7
+ /** JSON-safe representation of a Money value. Amount is in minor units. */
8
+ export interface SerializedMoney {
9
+ amount: number;
10
+ currency: string;
11
+ }
12
+ type MoneyFormatOptions = Omit<Intl.NumberFormatOptions, "style" | "currency" | "minimumFractionDigits" | "maximumFractionDigits"> & {
13
+ minimumFractionDigits?: number;
14
+ maximumFractionDigits?: number;
15
+ };
7
16
  export declare class Money {
8
17
  #private;
9
18
  private constructor();
@@ -60,8 +69,18 @@ export declare class Money {
60
69
  decrementByPercent(percent: number, options?: {
61
70
  rounding?: RoundingMode;
62
71
  }): Money;
63
- toJSON(): {
64
- amount: number;
65
- currency: string;
66
- };
72
+ toJSON(): SerializedMoney;
73
+ /**
74
+ * Format this Money for display using `Intl.NumberFormat`.
75
+ *
76
+ * - For ISO 4217 currencies, uses `style: "currency"` with the standard code
77
+ * (or `displayCode` if provided), respecting the currency's
78
+ * `decimalPlaces` (overrides Intl's locale-default fraction digits).
79
+ * - For non-ISO codes without a `displayCode`, falls back to a decimal
80
+ * format prefixed with the currency code (e.g., `"BTC 0.12345678"`).
81
+ * - If a runtime rejects the currency code (older engines, exotic locales),
82
+ * the same decimal fallback is used.
83
+ */
84
+ format(locales?: string | string[], options?: MoneyFormatOptions): string;
67
85
  }
86
+ export {};
package/dist/zod.cjs ADDED
@@ -0,0 +1,119 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // src/zod.ts
40
+ var exports_zod = {};
41
+ __export(exports_zod, {
42
+ serializedMoneySchema: () => serializedMoneySchema,
43
+ moneySchema: () => moneySchema,
44
+ moneyInstanceSchema: () => moneyInstanceSchema,
45
+ moneyCodec: () => moneyCodec,
46
+ currencyDefinitionSchema: () => currencyDefinitionSchema,
47
+ currencyCodeSchema: () => currencyCodeSchema,
48
+ createMoneyCodec: () => createMoneyCodec,
49
+ builtInSerializedMoneySchema: () => builtInSerializedMoneySchema
50
+ });
51
+ module.exports = __toCommonJS(exports_zod);
52
+ var import_money = require("@storepass/money");
53
+ var import_zod = require("zod");
54
+ var builtInCurrencyCodes = Object.keys(import_money.Currency);
55
+ var currencyCodeSchema = import_zod.z.enum(builtInCurrencyCodes);
56
+ var currencyDefinitionSchema = import_zod.z.object({
57
+ code: import_zod.z.string().min(1),
58
+ decimalPlaces: import_zod.z.number().int().nonnegative(),
59
+ numericCode: import_zod.z.number().int().min(0).max(999).optional(),
60
+ iso4217: import_zod.z.boolean().optional(),
61
+ displayCode: import_zod.z.string().regex(/^[A-Z]{3}$/).optional()
62
+ }).superRefine((currency, context) => {
63
+ try {
64
+ import_money.defineCurrency(currency.code, currency.decimalPlaces, {
65
+ numericCode: currency.numericCode,
66
+ iso4217: currency.iso4217,
67
+ displayCode: currency.displayCode
68
+ });
69
+ } catch (error) {
70
+ context.addIssue({
71
+ code: "custom",
72
+ message: error instanceof Error ? error.message : "Invalid currency definition"
73
+ });
74
+ }
75
+ });
76
+ var serializedMoneySchema = import_zod.z.object({
77
+ amount: import_zod.z.number().int(),
78
+ currency: import_zod.z.string().min(1)
79
+ });
80
+ var moneySchema = serializedMoneySchema;
81
+ var builtInSerializedMoneySchema = serializedMoneySchema.extend({
82
+ currency: currencyCodeSchema
83
+ });
84
+ var moneyInstanceSchema = import_zod.z.custom((value) => value instanceof import_money.Money, { error: "Expected Money instance" });
85
+ function assertCurrencyRegistry(currencies) {
86
+ for (const [code, currency] of Object.entries(currencies)) {
87
+ if (currency.code !== code) {
88
+ throw new Error(`Currency registry key "${code}" does not match definition code "${currency.code}"`);
89
+ }
90
+ currencyDefinitionSchema.parse(currency);
91
+ }
92
+ }
93
+ function createMoneyCodec(currencies) {
94
+ assertCurrencyRegistry(currencies);
95
+ const registeredCurrencyCodes = new Set(Object.keys(currencies));
96
+ const registeredCurrencyCodeSchema = import_zod.z.custom((value) => typeof value === "string" && registeredCurrencyCodes.has(value), { error: "Unknown currency code" });
97
+ const registeredSerializedMoneySchema = serializedMoneySchema.extend({
98
+ currency: registeredCurrencyCodeSchema
99
+ });
100
+ return import_zod.z.codec(registeredSerializedMoneySchema, moneyInstanceSchema, {
101
+ decode: ({ amount, currency }) => {
102
+ const registeredCurrency = currencies[currency];
103
+ if (registeredCurrency === undefined) {
104
+ throw new Error(`Unknown currency code: ${currency}`);
105
+ }
106
+ return import_money.Money.fromMinor(registeredCurrency, amount);
107
+ },
108
+ encode: (money) => {
109
+ const serialized = money.toJSON();
110
+ const currency = serialized.currency;
111
+ const registeredCurrency = currencies[currency];
112
+ if (registeredCurrency === undefined || registeredCurrency.decimalPlaces !== money.currency.decimalPlaces) {
113
+ throw new Error(`Money currency ${serialized.currency} is not registered with matching precision`);
114
+ }
115
+ return { amount: serialized.amount, currency };
116
+ }
117
+ });
118
+ }
119
+ var moneyCodec = createMoneyCodec(import_money.Currency);
package/dist/zod.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { type CurrencyDefinition, Money } from "@storepass/money";
2
+ import { z } from "zod";
3
+ /** Schema for the currency codes included with @storepass/money. */
4
+ export declare const currencyCodeSchema: z.ZodEnum<{
5
+ USD: "USD";
6
+ USDH: "USDH";
7
+ EUR: "EUR";
8
+ GBP: "GBP";
9
+ JPY: "JPY";
10
+ CAD: "CAD";
11
+ AUD: "AUD";
12
+ NZD: "NZD";
13
+ CHF: "CHF";
14
+ SEK: "SEK";
15
+ NOK: "NOK";
16
+ DKK: "DKK";
17
+ PLN: "PLN";
18
+ CZK: "CZK";
19
+ HUF: "HUF";
20
+ BRL: "BRL";
21
+ MXN: "MXN";
22
+ ARS: "ARS";
23
+ CLP: "CLP";
24
+ INR: "INR";
25
+ SGD: "SGD";
26
+ HKD: "HKD";
27
+ KRW: "KRW";
28
+ CNY: "CNY";
29
+ ZAR: "ZAR";
30
+ AED: "AED";
31
+ SAR: "SAR";
32
+ BTC: "BTC";
33
+ }>;
34
+ /** Schema for built-in and custom currency definitions. */
35
+ export declare const currencyDefinitionSchema: z.ZodObject<{
36
+ code: z.ZodString;
37
+ decimalPlaces: z.ZodNumber;
38
+ numericCode: z.ZodOptional<z.ZodNumber>;
39
+ iso4217: z.ZodOptional<z.ZodBoolean>;
40
+ displayCode: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strip>;
42
+ /**
43
+ * Schema for Money's JSON-safe wire representation.
44
+ *
45
+ * `amount` is a safe integer in minor units. Custom currency codes are allowed.
46
+ */
47
+ export declare const serializedMoneySchema: z.ZodObject<{
48
+ amount: z.ZodNumber;
49
+ currency: z.ZodString;
50
+ }, z.core.$strip>;
51
+ /** Short alias for serializedMoneySchema. */
52
+ export declare const moneySchema: z.ZodObject<{
53
+ amount: z.ZodNumber;
54
+ currency: z.ZodString;
55
+ }, z.core.$strip>;
56
+ /** Serialized Money restricted to the package's built-in currencies. */
57
+ export declare const builtInSerializedMoneySchema: z.ZodObject<{
58
+ amount: z.ZodNumber;
59
+ currency: z.ZodEnum<{
60
+ USD: "USD";
61
+ USDH: "USDH";
62
+ EUR: "EUR";
63
+ GBP: "GBP";
64
+ JPY: "JPY";
65
+ CAD: "CAD";
66
+ AUD: "AUD";
67
+ NZD: "NZD";
68
+ CHF: "CHF";
69
+ SEK: "SEK";
70
+ NOK: "NOK";
71
+ DKK: "DKK";
72
+ PLN: "PLN";
73
+ CZK: "CZK";
74
+ HUF: "HUF";
75
+ BRL: "BRL";
76
+ MXN: "MXN";
77
+ ARS: "ARS";
78
+ CLP: "CLP";
79
+ INR: "INR";
80
+ SGD: "SGD";
81
+ HKD: "HKD";
82
+ KRW: "KRW";
83
+ CNY: "CNY";
84
+ ZAR: "ZAR";
85
+ AED: "AED";
86
+ SAR: "SAR";
87
+ BTC: "BTC";
88
+ }>;
89
+ }, z.core.$strip>;
90
+ /** Schema for an already-hydrated Money instance. */
91
+ export declare const moneyInstanceSchema: z.ZodCustom<Money, Money>;
92
+ export type BuiltInSerializedMoney = z.infer<typeof builtInSerializedMoneySchema>;
93
+ export type CurrencyRegistry = Readonly<Record<string, CurrencyDefinition>>;
94
+ /**
95
+ * Creates a bidirectional Zod codec for a currency registry.
96
+ *
97
+ * Decoding turns serialized minor units into Money. Encoding calls toJSON and
98
+ * rejects Money values whose currency is not represented by the registry with
99
+ * the same precision.
100
+ */
101
+ export declare function createMoneyCodec<const TCurrencyRegistry extends CurrencyRegistry>(currencies: TCurrencyRegistry): z.ZodCodec<z.ZodObject<{
102
+ amount: z.ZodNumber;
103
+ currency: z.ZodCustom<Extract<keyof TCurrencyRegistry, string>, Extract<keyof TCurrencyRegistry, string>>;
104
+ }, z.core.$strip>, z.ZodCustom<Money, Money>>;
105
+ /** Codec for the currencies included with @storepass/money. */
106
+ export declare const moneyCodec: z.ZodCodec<z.ZodObject<{
107
+ amount: z.ZodNumber;
108
+ currency: z.ZodCustom<"USD" | "USDH" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD" | "NZD" | "CHF" | "SEK" | "NOK" | "DKK" | "PLN" | "CZK" | "HUF" | "BRL" | "MXN" | "ARS" | "CLP" | "INR" | "SGD" | "HKD" | "KRW" | "CNY" | "ZAR" | "AED" | "SAR" | "BTC", "USD" | "USDH" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD" | "NZD" | "CHF" | "SEK" | "NOK" | "DKK" | "PLN" | "CZK" | "HUF" | "BRL" | "MXN" | "ARS" | "CLP" | "INR" | "SGD" | "HKD" | "KRW" | "CNY" | "ZAR" | "AED" | "SAR" | "BTC">;
109
+ }, z.core.$strip>, z.ZodCustom<Money, Money>>;
package/dist/zod.mjs ADDED
@@ -0,0 +1,83 @@
1
+ // src/zod.ts
2
+ import {
3
+ Currency,
4
+ defineCurrency,
5
+ Money
6
+ } from "@storepass/money";
7
+ import { z } from "zod";
8
+ var builtInCurrencyCodes = Object.keys(Currency);
9
+ var currencyCodeSchema = z.enum(builtInCurrencyCodes);
10
+ var currencyDefinitionSchema = z.object({
11
+ code: z.string().min(1),
12
+ decimalPlaces: z.number().int().nonnegative(),
13
+ numericCode: z.number().int().min(0).max(999).optional(),
14
+ iso4217: z.boolean().optional(),
15
+ displayCode: z.string().regex(/^[A-Z]{3}$/).optional()
16
+ }).superRefine((currency, context) => {
17
+ try {
18
+ defineCurrency(currency.code, currency.decimalPlaces, {
19
+ numericCode: currency.numericCode,
20
+ iso4217: currency.iso4217,
21
+ displayCode: currency.displayCode
22
+ });
23
+ } catch (error) {
24
+ context.addIssue({
25
+ code: "custom",
26
+ message: error instanceof Error ? error.message : "Invalid currency definition"
27
+ });
28
+ }
29
+ });
30
+ var serializedMoneySchema = z.object({
31
+ amount: z.number().int(),
32
+ currency: z.string().min(1)
33
+ });
34
+ var moneySchema = serializedMoneySchema;
35
+ var builtInSerializedMoneySchema = serializedMoneySchema.extend({
36
+ currency: currencyCodeSchema
37
+ });
38
+ var moneyInstanceSchema = z.custom((value) => value instanceof Money, { error: "Expected Money instance" });
39
+ function assertCurrencyRegistry(currencies) {
40
+ for (const [code, currency] of Object.entries(currencies)) {
41
+ if (currency.code !== code) {
42
+ throw new Error(`Currency registry key "${code}" does not match definition code "${currency.code}"`);
43
+ }
44
+ currencyDefinitionSchema.parse(currency);
45
+ }
46
+ }
47
+ function createMoneyCodec(currencies) {
48
+ assertCurrencyRegistry(currencies);
49
+ const registeredCurrencyCodes = new Set(Object.keys(currencies));
50
+ const registeredCurrencyCodeSchema = z.custom((value) => typeof value === "string" && registeredCurrencyCodes.has(value), { error: "Unknown currency code" });
51
+ const registeredSerializedMoneySchema = serializedMoneySchema.extend({
52
+ currency: registeredCurrencyCodeSchema
53
+ });
54
+ return z.codec(registeredSerializedMoneySchema, moneyInstanceSchema, {
55
+ decode: ({ amount, currency }) => {
56
+ const registeredCurrency = currencies[currency];
57
+ if (registeredCurrency === undefined) {
58
+ throw new Error(`Unknown currency code: ${currency}`);
59
+ }
60
+ return Money.fromMinor(registeredCurrency, amount);
61
+ },
62
+ encode: (money) => {
63
+ const serialized = money.toJSON();
64
+ const currency = serialized.currency;
65
+ const registeredCurrency = currencies[currency];
66
+ if (registeredCurrency === undefined || registeredCurrency.decimalPlaces !== money.currency.decimalPlaces) {
67
+ throw new Error(`Money currency ${serialized.currency} is not registered with matching precision`);
68
+ }
69
+ return { amount: serialized.amount, currency };
70
+ }
71
+ });
72
+ }
73
+ var moneyCodec = createMoneyCodec(Currency);
74
+ export {
75
+ serializedMoneySchema,
76
+ moneySchema,
77
+ moneyInstanceSchema,
78
+ moneyCodec,
79
+ currencyDefinitionSchema,
80
+ currencyCodeSchema,
81
+ createMoneyCodec,
82
+ builtInSerializedMoneySchema
83
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storepass/money",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Money primitive library",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -15,6 +15,16 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "default": "./dist/index.cjs"
17
17
  }
18
+ },
19
+ "./zod": {
20
+ "import": {
21
+ "types": "./dist/zod.d.ts",
22
+ "default": "./dist/zod.mjs"
23
+ },
24
+ "require": {
25
+ "types": "./dist/zod.d.ts",
26
+ "default": "./dist/zod.cjs"
27
+ }
18
28
  }
19
29
  },
20
30
  "files": [
@@ -22,8 +32,8 @@
22
32
  ],
23
33
  "scripts": {
24
34
  "build": "bun run build:esm && bun run build:cjs && bun run build:types",
25
- "build:esm": "bun build ./src/index.ts --outfile ./dist/index.mjs --format esm --target node",
26
- "build:cjs": "bun build ./src/index.ts --outfile ./dist/index.cjs --format cjs --target node",
35
+ "build:esm": "bun build ./src/index.ts --outfile ./dist/index.mjs --format esm --target node && bun build ./src/zod.ts --outfile ./dist/zod.mjs --format esm --target node --external '@storepass/money' --external zod",
36
+ "build:cjs": "bun build ./src/index.ts --outfile ./dist/index.cjs --format cjs --target node && bun build ./src/zod.ts --outfile ./dist/zod.cjs --format cjs --target node --external '@storepass/money' --external zod",
27
37
  "build:types": "tsc -p tsconfig.build.json",
28
38
  "test": "bun test",
29
39
  "typecheck": "tsc --noEmit",
@@ -54,9 +64,18 @@
54
64
  "registry": "https://registry.npmjs.org",
55
65
  "access": "public"
56
66
  },
67
+ "peerDependencies": {
68
+ "zod": "^4.1.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "zod": {
72
+ "optional": true
73
+ }
74
+ },
57
75
  "devDependencies": {
58
76
  "@biomejs/biome": "2.3.11",
59
77
  "@types/bun": "latest",
60
- "typescript": "^5.9.3"
78
+ "typescript": "^5.9.3",
79
+ "zod": "^4.4.3"
61
80
  }
62
81
  }