hanzio 3.0.0 → 3.1.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 +114 -3
- package/dist/api-wrapper/connection-auth.d.ts +61 -0
- package/dist/cli/fields.d.ts +30 -0
- package/dist/cli/index.d.ts +122 -0
- package/dist/format/index.d.ts +91 -0
- package/dist/format/phone.d.ts +19 -0
- package/dist/http/index.d.ts +14 -0
- package/dist/i18n/catalog.d.ts +47 -0
- package/dist/i18n/check.d.ts +45 -0
- package/dist/i18n/cli.d.ts +2 -0
- package/dist/i18n/content-rules.d.ts +55 -0
- package/dist/i18n/i18next.d.ts +39 -0
- package/dist/i18n/index.d.ts +16 -1
- package/dist/i18n/locales.d.ts +62 -0
- package/dist/i18n/vite.d.ts +16 -0
- package/dist/i18n/vue.d.ts +93 -0
- package/dist/{index-kj1phgrx.js → index-29mmh343.js} +24 -1
- package/dist/{index-zbj9s3s3.js → index-2kz392vw.js} +1 -1
- package/dist/index-5n2e326q.js +409 -0
- package/dist/{index-hy7kbb99.js → index-5tzqfmnj.js} +1 -1
- package/dist/{index-0zvtcbzy.js → index-6f2w90pp.js} +69 -1
- package/dist/index-mq2qgc20.js +116 -0
- package/dist/index-najbyzpr.js +99 -0
- package/dist/index-nfvq3zax.js +475 -0
- package/dist/index-ww09bm49.js +250 -0
- package/dist/iso/countries.d.ts +262 -0
- package/dist/iso/currencies.d.ts +165 -0
- package/dist/iso/index.d.ts +4 -0
- package/dist/iso/languages.d.ts +195 -0
- package/dist/iso/phone-language.d.ts +5 -0
- package/dist/promise/index.d.ts +31 -0
- package/dist/redis/client.d.ts +49 -0
- package/dist/redis/concurrency-limit.d.ts +63 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/lua-script.d.ts +13 -0
- package/dist/secrets/infisical-identity-file.d.ts +12 -0
- package/dist/src/api-wrapper/connection-auth.js +82 -0
- package/dist/src/api-wrapper/index.js +1 -1
- package/dist/src/cli/index.js +10 -0
- package/dist/src/format/index.js +79 -2
- package/dist/src/format/phone.js +21 -0
- package/dist/src/http/index.js +69 -0
- package/dist/src/i18n/check.js +15 -0
- package/dist/src/i18n/cli.js +71 -0
- package/dist/src/i18n/i18next.js +48 -0
- package/dist/src/i18n/index.js +16 -34
- package/dist/src/i18n/vite.js +25 -0
- package/dist/src/i18n/vue.js +51 -0
- package/dist/src/index.js +14 -4
- package/dist/src/iso/index.js +655 -0
- package/dist/src/redis/index.js +577 -0
- package/dist/src/secrets/index.js +78 -35
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/storage/index.js +8 -117
- package/dist/src/testing/index.js +348 -0
- package/dist/src/zod/index.js +20 -253
- package/dist/string/index.d.ts +21 -0
- package/dist/testing/connection-auth-contract.d.ts +43 -0
- package/dist/testing/helpers.d.ts +26 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/module-mocker.d.ts +30 -0
- package/dist/testing/prisma.d.ts +30 -0
- package/dist/testing/window.d.ts +18 -0
- package/dist/zod/index.d.ts +10 -0
- package/dist/zod/introspect.d.ts +2 -1
- package/package.json +96 -2
- package/src/api-wrapper/README.md +52 -0
- package/src/api-wrapper/connection-auth.ts +142 -0
- package/src/cli/README.md +220 -0
- package/src/cli/fields.ts +213 -0
- package/src/cli/index.ts +533 -0
- package/src/format/README.md +82 -2
- package/src/format/index.ts +148 -0
- package/src/format/phone.ts +43 -0
- package/src/http/README.md +25 -0
- package/src/http/index.ts +94 -0
- package/src/i18n/README.md +206 -3
- package/src/i18n/catalog.ts +139 -0
- package/src/i18n/check.ts +376 -0
- package/src/i18n/cli.ts +90 -0
- package/src/i18n/content-rules.ts +179 -0
- package/src/i18n/i18next.ts +89 -0
- package/src/i18n/index.ts +34 -0
- package/src/i18n/locales.ts +135 -0
- package/src/i18n/vite.ts +40 -0
- package/src/i18n/vue.ts +82 -0
- package/src/index.ts +1 -1
- package/src/iso/README.md +44 -0
- package/src/iso/countries.ts +272 -0
- package/src/iso/currencies.ts +172 -0
- package/src/iso/index.ts +8 -0
- package/src/iso/languages.ts +208 -0
- package/src/iso/phone-language.ts +46 -0
- package/src/promise/index.ts +100 -0
- package/src/redis/README.md +112 -0
- package/src/redis/client.ts +239 -0
- package/src/redis/concurrency-limit.ts +591 -0
- package/src/redis/index.ts +15 -0
- package/src/redis/lua-script.ts +43 -0
- package/src/secrets/README.md +5 -1
- package/src/secrets/infisical-identity-file.ts +69 -0
- package/src/secrets/infisical.ts +18 -3
- package/src/string/index.ts +59 -0
- package/src/testing/README.md +119 -0
- package/src/testing/connection-auth-contract.ts +303 -0
- package/src/testing/helpers.ts +41 -0
- package/src/testing/index.ts +12 -0
- package/src/testing/module-mocker.ts +50 -0
- package/src/testing/prisma.ts +78 -0
- package/src/testing/window.ts +119 -0
- package/src/zod/index.ts +20 -0
- package/src/zod/introspect.ts +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ logging.
|
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
bun add hanzio
|
|
12
|
-
# only needed for hanzio/zod, hanzio/api-wrapper and hanzio/
|
|
12
|
+
# only needed for hanzio/zod, hanzio/api-wrapper, hanzio/secrets and hanzio/cli
|
|
13
13
|
bun add zod
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -19,6 +19,11 @@ from `dist`. `zod` is an optional peer dependency: the Zod helpers, the API
|
|
|
19
19
|
wrapper and the secrets loader share the consumer application's Zod instance,
|
|
20
20
|
while the root entry never loads it.
|
|
21
21
|
|
|
22
|
+
The other optional peers belong to one subpath each: `libphonenumber-js` for
|
|
23
|
+
`hanzio/format/phone`, `vue-i18n` for `hanzio/i18n/vue`, `i18next` for
|
|
24
|
+
`hanzio/i18n/i18next`, `vite` for `hanzio/i18n/vite`, and
|
|
25
|
+
`@intlify/message-compiler` for checking vue-i18n catalogs with `hanzio-i18n`.
|
|
26
|
+
|
|
22
27
|
## Imports
|
|
23
28
|
|
|
24
29
|
The root export contains only dependency-free, runtime-neutral helpers (array,
|
|
@@ -37,17 +42,28 @@ subpath export:
|
|
|
37
42
|
| `hanzio/zod` | Branded IDs, JSON schemas, schema introspection and defaults, schema-to-type rendering, min/max filters |
|
|
38
43
|
| `hanzio/api-wrapper` | Typed HTTP clients ([docs](src/api-wrapper/README.md)) |
|
|
39
44
|
| `hanzio/api-wrapper/testing` | `createMockFetch` / `jsonResponse` for testing API clients |
|
|
45
|
+
| `hanzio/api-wrapper/connection-auth` | One shared token exchange per connection for API clients; needs `node:crypto` ([docs](src/api-wrapper/README.md#sharing-a-login-across-clients)) |
|
|
40
46
|
| `hanzio/jwt` | HS256/384/512 JWTs via Web Crypto ([docs](src/jwt/README.md)) |
|
|
41
47
|
| `hanzio/sitemap` | Sitemap discovery and crawling ([docs](src/sitemap/README.md)) |
|
|
42
48
|
| `hanzio/p-queue` | Promise queue ([docs](src/p-queue/README.md)) |
|
|
43
49
|
| `hanzio/cool-console-log` | Colored terminal logging |
|
|
44
|
-
| `hanzio/format` | Cached `Intl` formatters: numbers, currency, bytes, durations, relative time ([docs](src/format/README.md)) |
|
|
50
|
+
| `hanzio/format` | Cached `Intl` formatters: numbers, currency, bytes, durations, dates, relative time ([docs](src/format/README.md)) |
|
|
51
|
+
| `hanzio/format/phone` | Phone numbers as people read them; needs `libphonenumber-js` ([docs](src/format/README.md#phone-numbers)) |
|
|
45
52
|
| `hanzio/crypto` | Web Crypto helpers: constant-time compare, HMAC/webhook verification, sealing, tokens ([docs](src/crypto/README.md)) |
|
|
46
53
|
| `hanzio/storage` | Typed localStorage/AsyncStorage items that never throw ([docs](src/storage/README.md)) |
|
|
47
|
-
| `hanzio/i18n` | Typed `{placeholder}` interpolation and
|
|
54
|
+
| `hanzio/i18n` | Typed `{placeholder}` interpolation, translators, `TypedTranslate` and locale registries ([docs](src/i18n/README.md)) |
|
|
55
|
+
| `hanzio/i18n/vue` | vue-i18n setup with locale persistence and a typed composer ([docs](src/i18n/README.md#vue-i18n)) |
|
|
56
|
+
| `hanzio/i18n/i18next` | i18next setup with device locale, persistence and server sync ([docs](src/i18n/README.md#i18next)) |
|
|
57
|
+
| `hanzio/i18n/vite` | Vite dev plugin that reloads edited vue-i18n catalogs ([docs](src/i18n/README.md#vue-i18n)) |
|
|
58
|
+
| `hanzio/i18n/check` | Catalog checks behind the `hanzio-i18n` bin ([docs](src/i18n/README.md#catalog-checks)) |
|
|
59
|
+
| `hanzio/iso` | ISO country, currency and language codes with English names ([docs](src/iso/README.md)) |
|
|
48
60
|
| `hanzio/secrets` | Typed secret sets from env/Infisical ([docs](src/secrets/README.md)) |
|
|
49
61
|
| `hanzio/secrets/vite` | Vite `define` helpers for secret sets |
|
|
50
62
|
| `hanzio/secrets/worker` | Secret sets for Cloudflare Workers |
|
|
63
|
+
| `hanzio/redis` | Bun Redis: self-healing client with shared subscriptions, fleet-wide concurrency limiter ([docs](src/redis/README.md)) |
|
|
64
|
+
| `hanzio/http` | SSE keep-alive comments and anti-buffering headers ([docs](src/http/README.md)) |
|
|
65
|
+
| `hanzio/cli` | Typed CLIs from Zod schemas: flags, help, completion, exit codes; needs `node:util` ([docs](src/cli/README.md)) |
|
|
66
|
+
| `hanzio/testing` | `bun:test` kit: restorable module mocks, Prisma deep mock, window shim, auth race contract ([docs](src/testing/README.md)) |
|
|
51
67
|
|
|
52
68
|
`scripts/verify-exports.mjs` imports every entry above from the packed tarball
|
|
53
69
|
in CI, so this table and `package.json#exports` can't drift apart.
|
|
@@ -257,6 +273,28 @@ if (parsed.ok) parsed.data
|
|
|
257
273
|
new RegExp(`\\b${escapeRegExp(term)}\\b`, 'i')
|
|
258
274
|
```
|
|
259
275
|
|
|
276
|
+
`escapeHtml` escapes text for HTML. `stripEmoji` removes emoji from text that
|
|
277
|
+
is spoken, tidying the spaces they leave. `fileExtension` reads a lowercased
|
|
278
|
+
extension, or `undefined`.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { escapeHtml, fileExtension, stripEmoji } from 'hanzio'
|
|
282
|
+
|
|
283
|
+
`<title>${escapeHtml(name)}</title>`
|
|
284
|
+
stripEmoji('Hi there 👋, welcome') // "Hi there, welcome"
|
|
285
|
+
fileExtension('uploads/Photo.JPG') // "jpg"
|
|
286
|
+
fileExtension('README') ?? 'bin' // "bin"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`toJsonValue` normalizes a value the way a JSON round trip does (dropping
|
|
290
|
+
`undefined` properties), so it can go into a JSON column.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { toJsonValue } from 'hanzio'
|
|
294
|
+
|
|
295
|
+
toJsonValue({ ok: true, error: undefined }) // { ok: true }
|
|
296
|
+
```
|
|
297
|
+
|
|
260
298
|
### Promise
|
|
261
299
|
|
|
262
300
|
`tryCatch` converts a promise, or a function that may throw synchronously, into
|
|
@@ -300,6 +338,19 @@ import { pMap } from 'hanzio'
|
|
|
300
338
|
const users = await pMap(ids, (id) => fetchUser(id), { concurrency: 4 })
|
|
301
339
|
```
|
|
302
340
|
|
|
341
|
+
`createTaskPool` bounds work that arrives over time, such as pages a crawler
|
|
342
|
+
discovers. The first failure or an abort discards queued tasks, and `drain`
|
|
343
|
+
rejects with it once the active tasks settle. Pass `stopOnError: false` to keep
|
|
344
|
+
going after a failed task.
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { createTaskPool } from 'hanzio'
|
|
348
|
+
|
|
349
|
+
const pool = createTaskPool({ concurrency: 4, signal })
|
|
350
|
+
for await (const page of discoverPages(signal)) void pool.run(() => scrape(page, signal))
|
|
351
|
+
await pool.drain()
|
|
352
|
+
```
|
|
353
|
+
|
|
303
354
|
`promiseTimeout` resolves after a delay (and rejects early if its signal
|
|
304
355
|
aborts). `backgroundPromise` runs async work and reports success or failure
|
|
305
356
|
through hooks; `backgroundPromiseSync` schedules it in a microtask.
|
|
@@ -773,6 +824,17 @@ type UserId = GenericId<'User'>
|
|
|
773
824
|
const userId: UserId = UserId.parse('usr_123')
|
|
774
825
|
```
|
|
775
826
|
|
|
827
|
+
`zOptionalE164` is a form field for an optional phone number: `''` and `null`
|
|
828
|
+
parse to `null`, anything else must match `E164_REGEX`.
|
|
829
|
+
|
|
830
|
+
```ts
|
|
831
|
+
import { E164_REGEX, zOptionalE164 } from 'hanzio/zod'
|
|
832
|
+
|
|
833
|
+
zOptionalE164.parse('+4512345678') // '+4512345678'
|
|
834
|
+
zOptionalE164.parse('') // null
|
|
835
|
+
z.string().regex(E164_REGEX, 'Use E.164, e.g. +4512345678')
|
|
836
|
+
```
|
|
837
|
+
|
|
776
838
|
`zSingleOrArray` accepts either one schema value or an array of schema values.
|
|
777
839
|
|
|
778
840
|
```ts
|
|
@@ -1160,6 +1222,55 @@ Cloudflare bindings without Node compatibility. See the
|
|
|
1160
1222
|
[migration notes](src/secrets/README.md#migration-notes) for stricter environment
|
|
1161
1223
|
validation and browser exposure rules.
|
|
1162
1224
|
|
|
1225
|
+
### Redis (`hanzio/redis`)
|
|
1226
|
+
|
|
1227
|
+
For Bun servers. `createManagedRedis` keeps one command connection and one
|
|
1228
|
+
subscriber connection that reconnect on their own; subscriptions are shared
|
|
1229
|
+
per channel and come back after a reconnect. `createRedisConcurrencyLimit` caps
|
|
1230
|
+
how many calls run at once across every process, using Redis' clock, and runs
|
|
1231
|
+
uncapped when Redis is unreachable. See the [Redis docs](src/redis/README.md).
|
|
1232
|
+
|
|
1233
|
+
```ts
|
|
1234
|
+
import { createManagedRedis, createRedisConcurrencyLimit } from 'hanzio/redis'
|
|
1235
|
+
|
|
1236
|
+
const redis = createManagedRedis({ url: () => getSecret('REDIS_URL') })
|
|
1237
|
+
const unsubscribe = await redis.subscribe('events', (message) => handle(message))
|
|
1238
|
+
|
|
1239
|
+
const withLimit = createRedisConcurrencyLimit({
|
|
1240
|
+
key: 'openai',
|
|
1241
|
+
max: 20,
|
|
1242
|
+
slotTtlMs: 30_000,
|
|
1243
|
+
send: (command, args) => redis.getClient().send(command, args)
|
|
1244
|
+
})
|
|
1245
|
+
await withLimit(() => callOpenAi(request), signal)
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
### HTTP (`hanzio/http`)
|
|
1249
|
+
|
|
1250
|
+
`withSseKeepAlive(response)` adds `:` comments to a quiet SSE stream and
|
|
1251
|
+
headers that stop proxies from buffering it. See the [HTTP docs](src/http/README.md).
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
import { withSseKeepAlive } from 'hanzio/http'
|
|
1255
|
+
|
|
1256
|
+
return withSseKeepAlive(createUIMessageStreamResponse({ stream }))
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
### Testing (`hanzio/testing`)
|
|
1260
|
+
|
|
1261
|
+
Helpers for `bun:test`: a `mock.module` that can be undone, a deep Prisma mock,
|
|
1262
|
+
a minimal `window` for browser-facing code, and a contract suite for API
|
|
1263
|
+
clients built on `ConnectionAuthCache`. See the [testing docs](src/testing/README.md).
|
|
1264
|
+
|
|
1265
|
+
```ts
|
|
1266
|
+
import { createPrismaMock, ModuleMocker } from 'hanzio/testing'
|
|
1267
|
+
|
|
1268
|
+
const prismaMock = createPrismaMock<typeof prisma>(mock)
|
|
1269
|
+
const moduleMocker = new ModuleMocker()
|
|
1270
|
+
await moduleMocker.mock(posthogModulePath, () => ({ captureException: mock() }))
|
|
1271
|
+
afterAll(() => moduleMocker.clear())
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1163
1274
|
## Development
|
|
1164
1275
|
|
|
1165
1276
|
```bash
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type ConnectionAuthToken = {
|
|
2
|
+
token: string;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Shares one login per connection across every client instance in the process,
|
|
7
|
+
* for APIs that exchange credentials for a short-lived token.
|
|
8
|
+
*
|
|
9
|
+
* - Concurrent callers share one exchange; aborting one caller's wait leaves the
|
|
10
|
+
* exchange running for the others.
|
|
11
|
+
* - Changing any of `authInputs` (rotated password, new client id) retires the
|
|
12
|
+
* old token: a retired client may finish its request but cannot store its
|
|
13
|
+
* token. Inputs are kept only as an HMAC under a random per-cache key, so
|
|
14
|
+
* the cache never holds, and cannot be used to guess, the credentials.
|
|
15
|
+
* - On a 401, `reject(token)` drops the refused token, unless a newer one has
|
|
16
|
+
* already replaced it, and the next `get()` exchanges again.
|
|
17
|
+
* - `get({ fresh: true })` always exchanges, for an explicit credential check;
|
|
18
|
+
* its failure never falls back to the previous token.
|
|
19
|
+
* - At most `maxEntries` connections are kept, least recently used first;
|
|
20
|
+
* connections with an exchange in flight are never evicted.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const auth = new ConnectionAuthCache()
|
|
24
|
+
*
|
|
25
|
+
* class ShopApi {
|
|
26
|
+
* private readonly token
|
|
27
|
+
* constructor(credentials: { connectionId: string; apiKey: string; secret: string }) {
|
|
28
|
+
* this.token = auth.bind({
|
|
29
|
+
* connectionId: credentials.connectionId,
|
|
30
|
+
* authInputs: [credentials.apiKey, credentials.secret],
|
|
31
|
+
* refreshMarginMs: 60_000,
|
|
32
|
+
* exchange: () => login(credentials) // resolves { token, expiresAt }
|
|
33
|
+
* })
|
|
34
|
+
* }
|
|
35
|
+
* async read(signal?: AbortSignal) {
|
|
36
|
+
* const current = await this.token.get({ signal })
|
|
37
|
+
* // On a 401: this.token.reject(current), then retry once with this.token.get().
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*/
|
|
41
|
+
export declare class ConnectionAuthCache {
|
|
42
|
+
private readonly maxEntries;
|
|
43
|
+
private readonly key;
|
|
44
|
+
private readonly entries;
|
|
45
|
+
constructor(maxEntries?: number);
|
|
46
|
+
get size(): number;
|
|
47
|
+
clear(): void;
|
|
48
|
+
private prune;
|
|
49
|
+
bind(input: {
|
|
50
|
+
connectionId: string;
|
|
51
|
+
authInputs: readonly (string | number)[];
|
|
52
|
+
refreshMarginMs: number;
|
|
53
|
+
exchange: () => Promise<ConnectionAuthToken>;
|
|
54
|
+
}): {
|
|
55
|
+
get: (options?: {
|
|
56
|
+
fresh?: boolean | undefined;
|
|
57
|
+
signal?: AbortSignal | undefined;
|
|
58
|
+
} | undefined) => Promise<ConnectionAuthToken>;
|
|
59
|
+
reject(token: ConnectionAuthToken): void;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Thrown for bad argv. The CLI prints it with a `--help` hint and exits 2. */
|
|
3
|
+
export declare class UsageError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** One key of an input or globals object, read as a flag or a positional. */
|
|
6
|
+
export type Field = {
|
|
7
|
+
key: string;
|
|
8
|
+
flag: string;
|
|
9
|
+
kind: 'string' | 'number' | 'boolean';
|
|
10
|
+
multiple: boolean;
|
|
11
|
+
positional: boolean;
|
|
12
|
+
optional: boolean;
|
|
13
|
+
hasDefault: boolean;
|
|
14
|
+
defaultValue: unknown;
|
|
15
|
+
description?: string;
|
|
16
|
+
alias?: string;
|
|
17
|
+
env?: string;
|
|
18
|
+
choices?: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function fieldsOf(schema: z.ZodObject | undefined): Field[];
|
|
21
|
+
/**
|
|
22
|
+
* Builds the raw object Zod validates: flag values and positionals by key,
|
|
23
|
+
* numbers coerced, `.meta({ env })` as fallback, and a missing required
|
|
24
|
+
* boolean or array read as `false` or `[]`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readFields(fields: Field[], values: Record<string, string | boolean | (string | boolean)[] | undefined>, positionals: string[], env: Record<string, string | undefined>): Record<string, unknown>;
|
|
27
|
+
export declare function fieldLabel(field: Field): string;
|
|
28
|
+
export declare function fieldHelp(field: Field): string;
|
|
29
|
+
/** Closest candidate within a few edits, for "Did you mean ...?" hints. */
|
|
30
|
+
export declare function closest(input: string, candidates: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
declare module 'zod' {
|
|
3
|
+
interface GlobalMeta {
|
|
4
|
+
/** hanzio/cli: read this key from positional arguments, in key order. */
|
|
5
|
+
positional?: boolean;
|
|
6
|
+
/** hanzio/cli: one-letter short flag, e.g. `'r'` for `-r`. */
|
|
7
|
+
alias?: string;
|
|
8
|
+
/** hanzio/cli: environment variable read when the flag is missing. */
|
|
9
|
+
env?: string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
declare const commandKey: unique symbol;
|
|
13
|
+
/**
|
|
14
|
+
* An expected failure: the CLI prints the message without a stack trace and
|
|
15
|
+
* exits with `exitCode` (default 1).
|
|
16
|
+
*/
|
|
17
|
+
export declare class CliError extends Error {
|
|
18
|
+
readonly exitCode: number;
|
|
19
|
+
constructor(message: string, options?: {
|
|
20
|
+
exitCode?: number;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export type CliWriter = {
|
|
25
|
+
write(text: string): unknown;
|
|
26
|
+
};
|
|
27
|
+
/** Where `cli.run()` writes and reads env fallbacks; defaults to `process`. */
|
|
28
|
+
export type CliIO = {
|
|
29
|
+
stdout?: CliWriter;
|
|
30
|
+
stderr?: CliWriter;
|
|
31
|
+
env?: Record<string, string | undefined>;
|
|
32
|
+
};
|
|
33
|
+
export type CommandContext<C = unknown> = {
|
|
34
|
+
/** What `defineCli({ context })` returned. */
|
|
35
|
+
readonly context: C;
|
|
36
|
+
/** The validated `defineCli({ globals })` values. */
|
|
37
|
+
readonly globals: Record<string, unknown>;
|
|
38
|
+
/** Command names from the root, e.g. `['deal', 'list']`. */
|
|
39
|
+
readonly path: readonly string[];
|
|
40
|
+
/** Every argument after the command names, unparsed. */
|
|
41
|
+
readonly argv: readonly string[];
|
|
42
|
+
/** Arguments after `--`. */
|
|
43
|
+
readonly passthrough: readonly string[];
|
|
44
|
+
/** `--json` was passed (as a global or a command option). */
|
|
45
|
+
readonly json: boolean;
|
|
46
|
+
readonly stdout: CliWriter;
|
|
47
|
+
readonly stderr: CliWriter;
|
|
48
|
+
readonly env: Record<string, string | undefined>;
|
|
49
|
+
/** Set to exit non-zero without printing an error. */
|
|
50
|
+
exitCode: number;
|
|
51
|
+
};
|
|
52
|
+
export type Command<C = unknown> = {
|
|
53
|
+
readonly [commandKey]: true;
|
|
54
|
+
readonly description?: string;
|
|
55
|
+
readonly input?: AnyObject;
|
|
56
|
+
readonly raw?: boolean;
|
|
57
|
+
readonly run?: (input: never, ctx: CommandContext<C>) => unknown;
|
|
58
|
+
readonly output?: (result: never, ctx: CommandContext<C>) => unknown;
|
|
59
|
+
readonly load?: LazyCommand<C>;
|
|
60
|
+
};
|
|
61
|
+
/** `() => import('./commands/heavy')`: loaded only when the command runs. */
|
|
62
|
+
export type LazyCommand<C = unknown> = () => Promise<Command<C> | {
|
|
63
|
+
default: Command<C>;
|
|
64
|
+
}>;
|
|
65
|
+
export type CommandGroup<C = unknown> = {
|
|
66
|
+
readonly [name: string]: CommandNode<C>;
|
|
67
|
+
};
|
|
68
|
+
export type CommandNode<C = unknown> = Command<C> | LazyCommand<C> | CommandGroup<C>;
|
|
69
|
+
type AnyObject = z.ZodObject<z.core.$ZodShape, z.core.$ZodObjectConfig>;
|
|
70
|
+
type EmptyObject = z.ZodObject<Record<never, never>>;
|
|
71
|
+
export type Cli = {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
/** Runs the CLI and resolves to its exit code: 0 ok, 1 failure, 2 usage error. */
|
|
74
|
+
run(argv?: readonly string[], io?: CliIO): Promise<number>;
|
|
75
|
+
};
|
|
76
|
+
export type CliConfig<G extends AnyObject, C> = {
|
|
77
|
+
name: string;
|
|
78
|
+
version?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
/** Options accepted before or after any command, e.g. `--org`. */
|
|
81
|
+
globals?: G;
|
|
82
|
+
/** Runs before every command; throw a `CliError` to refuse it. */
|
|
83
|
+
guard?: (call: {
|
|
84
|
+
path: readonly string[];
|
|
85
|
+
input: Record<string, unknown>;
|
|
86
|
+
globals: z.output<G>;
|
|
87
|
+
context: C;
|
|
88
|
+
}) => unknown;
|
|
89
|
+
/** A command (single-command CLI) or nested groups of commands. */
|
|
90
|
+
commands: CommandNode<C>;
|
|
91
|
+
} & (undefined extends C ? {
|
|
92
|
+
context?: ContextFunction<G, C>;
|
|
93
|
+
} : {
|
|
94
|
+
context: ContextFunction<G, C>;
|
|
95
|
+
});
|
|
96
|
+
/** Builds the `ctx.context` every command receives. */
|
|
97
|
+
type ContextFunction<G extends AnyObject, C> = (globals: z.output<G>) => C | Promise<C>;
|
|
98
|
+
/**
|
|
99
|
+
* A command. `input` is a Zod object whose keys become `--kebab-case` flags,
|
|
100
|
+
* or positionals with `.meta({ positional: true })`. `run`'s return value is
|
|
101
|
+
* printed by `output` (default: strings as-is, anything else as JSON).
|
|
102
|
+
*/
|
|
103
|
+
export declare function defineCommand<S extends AnyObject = EmptyObject, R = unknown, C = unknown>(definition: {
|
|
104
|
+
description?: string;
|
|
105
|
+
input?: S;
|
|
106
|
+
run: (input: z.output<S>, ctx: CommandContext<C>) => R;
|
|
107
|
+
output?: (result: Awaited<R>, ctx: CommandContext<C>) => unknown;
|
|
108
|
+
}): Command<C>;
|
|
109
|
+
/** A command that skips parsing: everything after its name is in `ctx.argv`. */
|
|
110
|
+
export declare function defineCommand<R = unknown, C = unknown>(definition: {
|
|
111
|
+
description?: string;
|
|
112
|
+
raw: true;
|
|
113
|
+
run: (input: Record<string, never>, ctx: CommandContext<C>) => R;
|
|
114
|
+
output?: (result: Awaited<R>, ctx: CommandContext<C>) => unknown;
|
|
115
|
+
}): Command<C>;
|
|
116
|
+
/** A lazy command whose description shows in help before it loads. */
|
|
117
|
+
export declare function defineCommand<C = unknown>(definition: {
|
|
118
|
+
description?: string;
|
|
119
|
+
load: LazyCommand<C>;
|
|
120
|
+
}): Command<C>;
|
|
121
|
+
export declare function defineCli<G extends AnyObject = EmptyObject, C = undefined>(config: CliConfig<G, C>): Cli;
|
|
122
|
+
export {};
|
package/dist/format/index.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export declare function formatPercent(value: number, locale: string, options?: I
|
|
|
17
17
|
* `{ maximumFractionDigits: 0 }` for whole-unit prices.
|
|
18
18
|
*/
|
|
19
19
|
export declare function formatCurrency(amount: number, currency: string, locale: string, options?: Intl.NumberFormatOptions): string;
|
|
20
|
+
/**
|
|
21
|
+
* A price: whole amounts without minor units (`'2.500 kr.'`), fractional ones
|
|
22
|
+
* with the currency's usual digits (`'199,95 kr.'`) instead of rounding them
|
|
23
|
+
* into a price nobody charges.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatPrice(amount: number, currency: string, locale: string): string;
|
|
20
26
|
/** Short compact notation: `1234` -> `"1.2K"` (en-US), `"1,2 t"` (da-DK). */
|
|
21
27
|
export declare function formatCompactNumber(value: number, locale: string): string;
|
|
22
28
|
export type FormatBytesOptions = {
|
|
@@ -88,8 +94,79 @@ export declare function formatWait(value: DateInput, locale: string, options?: {
|
|
|
88
94
|
* and friends combine with that default.
|
|
89
95
|
*/
|
|
90
96
|
export declare function formatDateRange(start: DateInput, end: DateInput, locale: string, options?: Intl.DateTimeFormatOptions): string;
|
|
97
|
+
declare const DATE_PRESETS: {
|
|
98
|
+
readonly 'DD/MM/YYYY': {
|
|
99
|
+
readonly day: "2-digit";
|
|
100
|
+
readonly month: "2-digit";
|
|
101
|
+
readonly year: "numeric";
|
|
102
|
+
};
|
|
103
|
+
readonly 'DD/MM/YYYY HH:mm': {
|
|
104
|
+
readonly day: "2-digit";
|
|
105
|
+
readonly month: "2-digit";
|
|
106
|
+
readonly year: "numeric";
|
|
107
|
+
readonly hour: "2-digit";
|
|
108
|
+
readonly minute: "2-digit";
|
|
109
|
+
readonly hourCycle: "h23";
|
|
110
|
+
};
|
|
111
|
+
readonly 'HH:mm': {
|
|
112
|
+
readonly hour: "2-digit";
|
|
113
|
+
readonly minute: "2-digit";
|
|
114
|
+
readonly hourCycle: "h23";
|
|
115
|
+
};
|
|
116
|
+
readonly ddd: {
|
|
117
|
+
readonly weekday: "short";
|
|
118
|
+
};
|
|
119
|
+
readonly 'D MMM': {
|
|
120
|
+
readonly day: "numeric";
|
|
121
|
+
readonly month: "short";
|
|
122
|
+
};
|
|
123
|
+
readonly 'D MMM YYYY': {
|
|
124
|
+
readonly day: "numeric";
|
|
125
|
+
readonly month: "short";
|
|
126
|
+
readonly year: "numeric";
|
|
127
|
+
};
|
|
128
|
+
readonly 'dddd, MMMM D': {
|
|
129
|
+
readonly weekday: "long";
|
|
130
|
+
readonly day: "numeric";
|
|
131
|
+
readonly month: "long";
|
|
132
|
+
};
|
|
133
|
+
readonly 'MMMM YYYY': {
|
|
134
|
+
readonly month: "long";
|
|
135
|
+
readonly year: "numeric";
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* A named date layout. The names read like dayjs tokens, but the output
|
|
140
|
+
* follows the locale: `'D MMM'` is "5 Mar" in en-GB and "Mar 5" in en-US.
|
|
141
|
+
*/
|
|
142
|
+
export type DatePreset = keyof typeof DATE_PRESETS;
|
|
143
|
+
/** A date in one of the `DatePreset` layouts, in local time. */
|
|
144
|
+
export declare function formatDate(date: DateInput, preset: DatePreset, locale: string): string;
|
|
145
|
+
/** Medium date and short time: "5 Mar 2026, 14:30" (en-GB). */
|
|
146
|
+
export declare function formatDateTime(date: DateInput, locale: string): string;
|
|
147
|
+
/**
|
|
148
|
+
* Compact activity timestamp for a list row: the time today, "yesterday",
|
|
149
|
+
* else the date (with the year when it is not this year). Unparseable input
|
|
150
|
+
* returns `''`.
|
|
151
|
+
*/
|
|
152
|
+
export declare function formatCompactTime(date: DateInput, locale: string, options?: {
|
|
153
|
+
now?: DateInput;
|
|
154
|
+
}): string;
|
|
155
|
+
/**
|
|
156
|
+
* Day-divider label for a chat timeline: "today", "yesterday", else weekday
|
|
157
|
+
* and date (with the year when it is not this year). Unparseable input
|
|
158
|
+
* returns `''`.
|
|
159
|
+
*/
|
|
160
|
+
export declare function formatDayDivider(date: DateInput, locale: string, options?: {
|
|
161
|
+
now?: DateInput;
|
|
162
|
+
}): string;
|
|
91
163
|
/** "a, b, and c". Falls back to a comma join where `Intl.ListFormat` is missing. */
|
|
92
164
|
export declare function formatList(items: readonly string[], locale: string, options?: Intl.ListFormatOptions): string;
|
|
165
|
+
/**
|
|
166
|
+
* Stands for "no value" in a cell or detail row that must keep its height, so
|
|
167
|
+
* absent values cannot drift between a hyphen and an em dash. Not translated.
|
|
168
|
+
*/
|
|
169
|
+
export declare const EM_DASH = "\u2014";
|
|
93
170
|
/** Badge count: `150` -> `"99+"`. Negative or NaN is `"0"`. */
|
|
94
171
|
export declare function formatCappedCount(count: number, max?: number): string;
|
|
95
172
|
/**
|
|
@@ -99,4 +176,18 @@ export declare function formatCappedCount(count: number, max?: number): string;
|
|
|
99
176
|
export declare function pluralize(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>> & {
|
|
100
177
|
other: string;
|
|
101
178
|
}, locale: string): string;
|
|
179
|
+
/**
|
|
180
|
+
* The date formatters with the locale read on every call, for apps whose
|
|
181
|
+
* locale lives in their i18n runtime:
|
|
182
|
+
* `export const fmt = withLocale(() => uiLocales.tag(appI18n.locale()))`.
|
|
183
|
+
*/
|
|
184
|
+
export declare function withLocale(getLocale: () => string): {
|
|
185
|
+
formatDate: (date: DateInput, preset: "D MMM" | "D MMM YYYY" | "DD/MM/YYYY" | "DD/MM/YYYY HH:mm" | "HH:mm" | "MMMM YYYY" | "ddd" | "dddd, MMMM D") => string;
|
|
186
|
+
formatDateTime: (date: DateInput) => string;
|
|
187
|
+
formatCompactTime: (date: DateInput) => string;
|
|
188
|
+
formatDayDivider: (date: DateInput) => string;
|
|
189
|
+
formatRelativeTime: (date: DateInput, options?: FormatRelativeTimeOptions | undefined) => string;
|
|
190
|
+
formatNumber: (value: number, options?: Intl.NumberFormatOptions | undefined) => string;
|
|
191
|
+
formatPrice: (amount: number, currency: string) => string;
|
|
192
|
+
};
|
|
102
193
|
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stored phone number as a person reads it. `country` is the READER's ISO
|
|
3
|
+
* 3166-1 alpha-2 country (the organization's, say), not the number's: a local
|
|
4
|
+
* number prints the local way, any other with its country code.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* formatPhone('+4520304050', 'DK') // '20 30 40 50'
|
|
8
|
+
* formatPhone('+4520304050') // '+45 20 30 40 50'
|
|
9
|
+
* formatPhone('+4915112345678', 'DK') // '+49 1511 2345678'
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Nothing is lost: an unparseable value comes back trimmed but verbatim, and
|
|
13
|
+
* the national form is used only for a VALID number (`+4512345678` parses as
|
|
14
|
+
* DK but is not a real number, so it keeps its `+45`). A country
|
|
15
|
+
* libphonenumber cannot dial falls back to international. Empty input is
|
|
16
|
+
* `''`; the caller decides what stands in for "no number". Use the raw E.164
|
|
17
|
+
* value in `tel:` links.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatPhone(value: string | null | undefined, country?: string | null): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const SSE_KEEPALIVE_COMMENT: NodeJS.NonSharedUint8Array;
|
|
2
|
+
/** Passes bytes through, writing a comment first and after every `intervalMs` of silence. */
|
|
3
|
+
export declare function createSseKeepAliveTransform(intervalMs?: number, timers?: {
|
|
4
|
+
setTimeout: (fn: () => void, ms: number) => unknown;
|
|
5
|
+
clearTimeout: (id: unknown) => void;
|
|
6
|
+
}): TransformStream<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>;
|
|
7
|
+
/**
|
|
8
|
+
* Copies an SSE response with keep-alive comments in its body and headers that
|
|
9
|
+
* stop proxies from buffering it.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* return withSseKeepAlive(createUIMessageStreamResponse({ stream }))
|
|
13
|
+
*/
|
|
14
|
+
export declare function withSseKeepAlive(response: Response, intervalMs?: number): Response;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Messages } from '.';
|
|
2
|
+
/** How a catalog's runtime reads its messages: plural syntax and escaping. */
|
|
3
|
+
export type CatalogRuntime = 'vue-i18n' | 'i18next' | 'plain';
|
|
4
|
+
export type CatalogConfig = {
|
|
5
|
+
/** Shown in check output. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Folder of `<locale>.json` files, relative to the config file. */
|
|
8
|
+
dir: string;
|
|
9
|
+
runtime: CatalogRuntime;
|
|
10
|
+
/** Where `hanzio-i18n generate` writes the key → placeholder map. */
|
|
11
|
+
generatedTypes?: string;
|
|
12
|
+
/** Exported type name in `generatedTypes`. Default `TranslationParams`. */
|
|
13
|
+
generatedTypeName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Source folders scanned for key references. Set them and the check also
|
|
16
|
+
* fails on keys nothing references. Leave unset when keys are built at
|
|
17
|
+
* runtime from a prefix (`t(\`widget.${key}\`)` everywhere), since every key
|
|
18
|
+
* would read as used.
|
|
19
|
+
*/
|
|
20
|
+
sourceRoots?: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* Key prefixes composed from data rather than written in source (labels
|
|
23
|
+
* sent by the backend, for example), kept even when nothing references them.
|
|
24
|
+
* A prefix a template literal spells out is found by the scan already.
|
|
25
|
+
*/
|
|
26
|
+
dynamicKeyPrefixes?: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
export type CatalogIssue = {
|
|
29
|
+
catalog: string;
|
|
30
|
+
locale?: string;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Validates one parsed JSON catalog: every leaf a string, no empty or dotted
|
|
35
|
+
* key segments, NFC-normalized text and no mojibake.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseMessages(value: unknown, catalog: string, locale: string): {
|
|
38
|
+
messages?: Messages;
|
|
39
|
+
issues: CatalogIssue[];
|
|
40
|
+
};
|
|
41
|
+
export declare function flattenCatalog(messages: Messages): Map<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* The TypeScript key → placeholder-name map for `TypedTranslate`, generated
|
|
44
|
+
* from the base catalog. Plural messages also take `count`: vue-i18n's
|
|
45
|
+
* `a | b | c` forms, and i18next's `key_one`/`key_other` (merged into `key`).
|
|
46
|
+
*/
|
|
47
|
+
export declare function generateCatalogTypes(catalog: Pick<CatalogConfig, 'runtime' | 'generatedTypeName'>, baseMessages: Messages, command?: string): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type CatalogConfig, type CatalogIssue } from './catalog';
|
|
2
|
+
import { type ContentRulesConfig } from './content-rules';
|
|
3
|
+
export type { CatalogConfig, CatalogIssue, CatalogRuntime } from './catalog';
|
|
4
|
+
export { generateCatalogTypes, parseMessages } from './catalog';
|
|
5
|
+
export type { ContentRulesConfig } from './content-rules';
|
|
6
|
+
export type I18nConfig<Locale extends string = string, Catalog extends string = string> = {
|
|
7
|
+
/** The source-of-truth locale; usually `'en'`. */
|
|
8
|
+
baseLocale: NoInfer<Locale>;
|
|
9
|
+
/** Every shipped locale, the base included. Each needs a catalog file. */
|
|
10
|
+
locales: readonly Locale[];
|
|
11
|
+
catalogs: readonly (CatalogConfig & {
|
|
12
|
+
name: Catalog;
|
|
13
|
+
})[];
|
|
14
|
+
/**
|
|
15
|
+
* Groups of catalogs merged into one runtime (`{ ...platform, ...widget }`),
|
|
16
|
+
* so their top-level keys must not overlap.
|
|
17
|
+
*/
|
|
18
|
+
disjointNamespaces?: readonly (readonly NoInfer<Catalog>[])[];
|
|
19
|
+
rules?: ContentRulesConfig<NoInfer<Locale>>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Types an `i18n.config.ts` default export: `baseLocale`, the catalog names
|
|
23
|
+
* in `disjointNamespaces` and the locales in `rules.identical.allowKeys` must
|
|
24
|
+
* be ones the config declares.
|
|
25
|
+
*/
|
|
26
|
+
export declare function defineI18nConfig<const Locale extends string, const Catalog extends string>(config: I18nConfig<Locale, Catalog>): I18nConfig<Locale, Catalog>;
|
|
27
|
+
/**
|
|
28
|
+
* Every problem in the configured catalogs, empty when they are valid. Paths
|
|
29
|
+
* in `config` are relative to `root`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateCatalogs(config: I18nConfig, options: {
|
|
32
|
+
root: string;
|
|
33
|
+
checkGeneratedTypes?: boolean;
|
|
34
|
+
}): Promise<CatalogIssue[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Writes each catalog's `generatedTypes` from its base locale. Returns the
|
|
37
|
+
* written paths, or the issues that stopped generation (nothing is written
|
|
38
|
+
* when any base catalog is invalid).
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeCatalogTypes(config: I18nConfig, options: {
|
|
41
|
+
root: string;
|
|
42
|
+
}): {
|
|
43
|
+
written: string[];
|
|
44
|
+
issues: CatalogIssue[];
|
|
45
|
+
};
|