@use-everywhere/core 0.3.0 → 0.4.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.
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Minimal message bus. Implementations: BroadcastChannelTransport (same-origin),
3
+ * NoopTransport (SSR / local-only), MemoryTransport (tests). A transport never
4
+ * echoes a client's own posts back to it.
5
+ */
6
+ interface Transport {
7
+ post(data: unknown): void;
8
+ subscribe(listener: (data: unknown) => void): () => void;
9
+ close(): void;
10
+ }
11
+
12
+ type MessageMap = Record<string, unknown>;
13
+ type PeerKind = 'tab' | 'worker' | (string & {});
14
+ /** Per-key logical clock: [counter, clientId]. Ties break by clientId. */
15
+ type Version = readonly [counter: number, clientId: string];
16
+ interface Peer {
17
+ id: string;
18
+ kind: PeerKind;
19
+ lastSeen: number;
20
+ }
21
+ interface MessageMeta {
22
+ clientId: string;
23
+ kind: PeerKind;
24
+ self: boolean;
25
+ }
26
+ interface CommonOptions {
27
+ /** Transport factory, mainly for tests. Defaults to defaultTransport. */
28
+ transport?: (name: string) => Transport;
29
+ /** What this client announces itself as. Defaults to 'worker' when there is no document, else 'tab'. */
30
+ kind?: PeerKind;
31
+ }
32
+
33
+ interface Channel<M extends MessageMap> {
34
+ readonly name: string;
35
+ readonly clientId: string;
36
+ /** Fire-and-forget to every other tab/window/worker on this origin. Not echoed to self. */
37
+ post<K extends keyof M & string>(type: K, payload: M[K]): void;
38
+ on<K extends keyof M & string>(type: K, handler: (payload: M[K], meta: MessageMeta) => void): () => void;
39
+ close(): void;
40
+ }
41
+
42
+ /** Typed pub/sub over the same-origin bus. */
43
+ declare function createChannel<M extends MessageMap>(name: string, options?: CommonOptions): Channel<M>;
44
+
45
+ /**
46
+ * What goes to disk. The version clocks travel *with* the values — that is the
47
+ * whole point: a reopened tab re-enters the last-writer-wins race with its real
48
+ * term instead of a fresh zero, so a restored value can legitimately beat, or
49
+ * legitimately lose to, whatever the live tabs are holding.
50
+ */
51
+ interface Persisted {
52
+ v: 1;
53
+ state: Record<string, unknown>;
54
+ versions: Record<string, Version>;
55
+ }
56
+ interface PersistAdapter {
57
+ /**
58
+ * Prefer a synchronous read. An async adapter cannot hydrate before the store
59
+ * is handed back, so a write made in that gap can be clobbered by the restore.
60
+ */
61
+ read(): Persisted | undefined | Promise<Persisted | undefined>;
62
+ write(snapshot: Persisted): void | Promise<void>;
63
+ remove?(): void | Promise<void>;
64
+ }
65
+ interface PersistOptions {
66
+ adapter: PersistAdapter;
67
+ /** Persist only these keys. Default: every key that has been written. */
68
+ keys?: string[];
69
+ /** Coalesce writes for this long. Default 100. */
70
+ debounceMs?: number;
71
+ }
72
+
73
+ interface SharedStoreOptions extends CommonOptions {
74
+ /**
75
+ * Gatekeeper for incoming remote writes (patches and snapshot merges):
76
+ * return false to ignore them. Lets callers delimit how much is shared —
77
+ * e.g. accept only writes from other tabs, not from workers.
78
+ */
79
+ accept?: (meta: MessageMeta) => boolean;
80
+ /** Restore this store on creation and write it back as it changes. */
81
+ persist?: PersistOptions;
82
+ }
83
+ interface SharedStore<S extends Record<string, unknown>> {
84
+ readonly clientId: string;
85
+ /** Live proxy for imperative use: `store.state.count++` syncs everywhere. */
86
+ readonly state: S;
87
+ /** Immutable snapshot, replaced whenever a change is applied. Safe for useSyncExternalStore. */
88
+ getSnapshot(): Readonly<S>;
89
+ /** The per-key version clocks behind the snapshot. Referentially stable, like getSnapshot. */
90
+ getVersions(): Readonly<Record<string, Version>>;
91
+ set<K extends keyof S & string>(key: K, value: S[K] | ((prev: S[K]) => S[K])): void;
92
+ subscribe(fn: (key: keyof S & string, value: unknown, meta: MessageMeta) => void): () => void;
93
+ subscribeKey(key: keyof S & string, fn: () => void): () => void;
94
+ /**
95
+ * Register a key lazily at version [0, clientId] — any patch or snapshot a
96
+ * peer has already made for it wins over the initial value. No-op if the
97
+ * key already exists.
98
+ */
99
+ registerKey<K extends keyof S & string>(key: K, initial: S[K]): void;
100
+ close(): void;
101
+ }
102
+
103
+ /**
104
+ * State synced across every same-origin tab/window/worker: per-key
105
+ * last-writer-wins version clocks and a hello/snapshot late-joiner handshake.
106
+ * Create at most one store per name per tab (the React package memoizes).
107
+ */
108
+ declare function createSharedStore<S extends Record<string, unknown>>(name: string, initial: S, options?: SharedStoreOptions): SharedStore<S>;
109
+
110
+ /** Everything on the same-origin bus, multiplexed by scope over one BroadcastChannel per name. */
111
+ type BusWire = {
112
+ v: 1;
113
+ scope: 'state';
114
+ type: 'patch';
115
+ key: string;
116
+ value: unknown;
117
+ version: Version;
118
+ clientId: string;
119
+ kind: PeerKind;
120
+ } | {
121
+ v: 1;
122
+ scope: 'state';
123
+ type: 'hello';
124
+ clientId: string;
125
+ kind: PeerKind;
126
+ } | {
127
+ v: 1;
128
+ scope: 'state';
129
+ type: 'snapshot';
130
+ clientId: string;
131
+ kind: PeerKind;
132
+ state: Record<string, unknown>;
133
+ versions: Record<string, Version>;
134
+ } | {
135
+ v: 1;
136
+ scope: 'presence';
137
+ type: 'hello' | 'ping' | 'bye';
138
+ clientId: string;
139
+ kind: PeerKind;
140
+ } | {
141
+ v: 1;
142
+ scope: 'leader';
143
+ type: 'hello';
144
+ clientId: string;
145
+ kind: PeerKind;
146
+ } | {
147
+ v: 1;
148
+ scope: 'leader';
149
+ type: 'claim' | 'heartbeat' | 'resign';
150
+ /** The claimant's term. Arbitrated with newer() — the same clock the store uses. */
151
+ term: Version;
152
+ clientId: string;
153
+ kind: PeerKind;
154
+ } | {
155
+ v: 1;
156
+ scope: 'event';
157
+ type: string;
158
+ payload: unknown;
159
+ clientId: string;
160
+ kind: PeerKind;
161
+ msgId: string;
162
+ };
163
+ interface BusOptions extends CommonOptions {
164
+ /** Presence heartbeat interval in ms. Default 2000. */
165
+ heartbeatMs?: number;
166
+ }
167
+
168
+ interface PresenceOptions extends BusOptions {
169
+ /** Peers silent for longer than this are dropped. Default 5000ms. */
170
+ pruneAfterMs?: number;
171
+ }
172
+ interface Presence {
173
+ readonly clientId: string;
174
+ /** Stable array snapshot (replaced on change) — safe for useSyncExternalStore. */
175
+ getPeers(): readonly Peer[];
176
+ subscribe(fn: () => void): () => void;
177
+ close(): void;
178
+ }
179
+
180
+ /**
181
+ * Tracks the other tabs/windows/workers on this bus. Any message from a peer
182
+ * counts as a liveness signal (state patches, events, and presence pings all
183
+ * piggyback); explicit 'bye' or silence past pruneAfterMs removes them.
184
+ */
185
+ declare function createPresence(name: string, options?: PresenceOptions): Presence;
186
+
187
+ /**
188
+ * Deliberately extends CommonOptions, not BusOptions: `heartbeatMs` here means
189
+ * the leader's re-announce interval, which is a different thing from the bus's
190
+ * presence ping. See the note in leader.ts about forwarding to getBus.
191
+ */
192
+ interface LeaderOptions extends CommonOptions {
193
+ /** How often the leader re-announces itself, in ms. Default 1000. */
194
+ heartbeatMs?: number;
195
+ /** How long a follower tolerates silence before calling the seat empty, in ms. Default 3000. */
196
+ leaseMs?: number;
197
+ /** May this client hold the leadership? Default true. */
198
+ eligible?: boolean;
199
+ }
200
+ interface LeaderSnapshot {
201
+ /** The current leader's clientId, or null while the seat is empty. */
202
+ readonly leaderId: string | null;
203
+ readonly isLeader: boolean;
204
+ }
205
+ interface Leader {
206
+ readonly clientId: string;
207
+ /** Frozen; a new object only when the leader actually changes. */
208
+ getSnapshot(): LeaderSnapshot;
209
+ subscribe(fn: () => void): () => void;
210
+ /** Give up the seat now. Peers take over immediately rather than waiting for the lease. */
211
+ resign(): void;
212
+ /** Turn candidacy on or off. Eligibility is a property of the tab, not a component. */
213
+ setEligible(eligible: boolean): void;
214
+ close(): void;
215
+ }
216
+
217
+ /**
218
+ * Elects exactly one client on the bus to hold a seat: the tab that owns the
219
+ * WebSocket, polls, or refreshes the token, while the others stand by.
220
+ *
221
+ * Lease and claim, with a sticky incumbent. A leader re-announces every
222
+ * heartbeatMs; followers give up on it after leaseMs of silence and claim the
223
+ * seat with a higher term. Terms are Versions, arbitrated by the same newer()
224
+ * the store uses, so simultaneous claims resolve deterministically instead of
225
+ * flapping.
226
+ *
227
+ * Leadership is advisory. It is not a distributed lock, and a hidden tab whose
228
+ * timers are throttled can lose a lease it deserved to keep.
229
+ */
230
+ declare function createLeader(name: string, options?: LeaderOptions): Leader;
231
+
232
+ type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
233
+ /**
234
+ * Persist to any Storage-shaped thing.
235
+ *
236
+ * `storage` may be a thunk, and that is the form the built-in adapters use:
237
+ * merely *reading* `globalThis.localStorage` throws SecurityError when storage
238
+ * is blocked (a sandboxed iframe, third-party cookies off), so evaluating it at
239
+ * module scope would blow up on import — before any try/catch here could help.
240
+ * Behind a thunk, every access happens inside one.
241
+ *
242
+ * Blocked storage, corrupt JSON, a foreign schema, or a full quota all degrade
243
+ * to a silent no-op. Persistence is best-effort; it must never break the store.
244
+ */
245
+ declare function webStorageAdapter(storage: StorageLike | (() => StorageLike | undefined), key: string): PersistAdapter;
246
+ /** Survives closing every tab. */
247
+ declare function localStorageAdapter(key: string): PersistAdapter;
248
+ /** Survives reloads, but dies with the tab. */
249
+ declare function sessionStorageAdapter(key: string): PersistAdapter;
250
+
251
+ interface MessageEventLike {
252
+ data: unknown;
253
+ origin: string;
254
+ source: unknown;
255
+ }
256
+ /** The subset of Window we post to (the other side). */
257
+ interface WindowLike {
258
+ postMessage(data: unknown, targetOrigin: string): void;
259
+ closed?: boolean;
260
+ close?(): void;
261
+ }
262
+ /** The subset of Window we listen on (our side). */
263
+ interface WindowEventTarget {
264
+ addEventListener(type: string, listener: (event: MessageEventLike) => void): void;
265
+ removeEventListener(type: string, listener: (event: MessageEventLike) => void): void;
266
+ }
267
+ interface OpenWindowOptions {
268
+ /** Exact origin of the page being opened, e.g. 'https://pay.example.com'. Required. */
269
+ peerOrigin: string;
270
+ /** window.open feature string, e.g. 'popup,width=480,height=640'. */
271
+ features?: string;
272
+ /** Give up on the ready handshake after this long. Default 15000ms. */
273
+ readyTimeoutMs?: number;
274
+ /** Dev only: accept messages from any origin and post with targetOrigin '*'. */
275
+ allowAnyOrigin?: boolean;
276
+ /** Test seam. Defaults to window.open. */
277
+ openFn?: (url: string, target: string, features?: string) => WindowLike | null;
278
+ /** Test seam. Defaults to the global window. */
279
+ localWindow?: WindowEventTarget;
280
+ }
281
+ interface OpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
282
+ /** The opened window, or null if the popup was blocked. */
283
+ readonly window: WindowLike | null;
284
+ /** Resolves once the child completes the ready handshake. */
285
+ readonly ready: Promise<void>;
286
+ /** Queued until the handshake completes — nothing is dropped while the child loads. */
287
+ post<K extends keyof Out & string>(type: K, payload: Out[K]): void;
288
+ on<K extends keyof In & string>(type: K, handler: (payload: In[K]) => void): () => void;
289
+ /** The child's finish() value. Rejects WindowClosedError / HandshakeTimeoutError. */
290
+ readonly result: Promise<R>;
291
+ /** Resolves when the child window is gone (with or without a result). */
292
+ readonly closed: Promise<void>;
293
+ /** Close the child window. */
294
+ close(): void;
295
+ }
296
+ interface ConnectToOpenerOptions {
297
+ /** Exact origin of the page that opened this window. Required. */
298
+ peerOrigin: string;
299
+ /** Give up on the ready handshake after this long. Default 15000ms. */
300
+ readyTimeoutMs?: number;
301
+ /** Dev only: accept messages from any origin and post with targetOrigin '*'. */
302
+ allowAnyOrigin?: boolean;
303
+ /** Test seam. Defaults to window.opener. */
304
+ opener?: WindowLike | null;
305
+ /** Test seam. Defaults to the global window. */
306
+ localWindow?: WindowEventTarget;
307
+ /** Test seam. Defaults to the ue-cid query parameter. */
308
+ cid?: string;
309
+ }
310
+ interface OpenerConnection<In extends MessageMap, Out extends MessageMap, R> {
311
+ /** Resolves once the opener acknowledges the ready handshake. */
312
+ readonly ready: Promise<void>;
313
+ /** Queued until the handshake completes. */
314
+ post<K extends keyof Out & string>(type: K, payload: Out[K]): void;
315
+ on<K extends keyof In & string>(type: K, handler: (payload: In[K]) => void): () => void;
316
+ /** Deliver the terminal result to the opener. Does not close the window. */
317
+ finish(result: R): void;
318
+ /** Tell the opener we're going away, then close this window. */
319
+ close(): void;
320
+ }
321
+
322
+ declare const CID_PARAM = "ue-cid";
323
+ /**
324
+ * Open a window (possibly on another origin) and get a typed 1:1 channel to it.
325
+ * The child must call connectToOpener(). Every received message is validated:
326
+ * event.origin, envelope brand, per-connection nonce, and event.source.
327
+ */
328
+ declare function openWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(url: string | URL, options: OpenWindowOptions): OpenedWindow<Out, In, R>;
329
+ /**
330
+ * Call from the opened (child) window to connect back to its opener.
331
+ * Throws synchronously when there is no opener or no ue-cid parameter —
332
+ * i.e. the page was not opened via openWindow().
333
+ */
334
+ declare function connectToOpener<In extends MessageMap, Out extends MessageMap, R = unknown>(options: ConnectToOpenerOptions): OpenerConnection<In, Out, R>;
335
+
336
+ /** The opened window closed before delivering a result. */
337
+ declare class WindowClosedError extends Error {
338
+ constructor(message?: string);
339
+ }
340
+
341
+ /** The ready/ready-ack handshake never completed. */
342
+ declare class HandshakeTimeoutError extends Error {
343
+ constructor(message?: string);
344
+ }
345
+
346
+ /** Is `a` newer than `b`? Last-writer-wins; equal counters break ties by clientId. */
347
+ declare function newer(a: Version, b: Version | undefined): boolean;
348
+
349
+ /**
350
+ * The bus name used when a caller does not pick one. A BroadcastChannel is
351
+ * global to the origin, so identity is the name string — everything that
352
+ * omits a name lands on this one bus.
353
+ */
354
+ declare const DEFAULT_NAME = "use-everywhere";
355
+
356
+ /** One wire crossing the bus, in either direction. */
357
+ interface BusEvent {
358
+ /** The bus name the wire crossed. */
359
+ readonly name: string;
360
+ /** 'out' is posted by this client; 'in' is received from a peer. */
361
+ readonly direction: 'in' | 'out';
362
+ readonly wire: BusWire;
363
+ }
364
+ type BusObserver = (event: BusEvent) => void;
365
+ interface DebugOptions {
366
+ /** Bus name to log. Defaults to the default store/channel name. */
367
+ name?: string;
368
+ /** Where to write. Defaults to console.log. */
369
+ log?: (...args: unknown[]) => void;
370
+ }
371
+
372
+ /**
373
+ * Watch every wire crossing the named bus, in both directions. Outbound wires
374
+ * are the interesting half: a post goes straight to the transport, so without
375
+ * this seam nothing this client says is visible to it.
376
+ *
377
+ * Works for buses that do not exist yet — observe first, create later.
378
+ */
379
+ declare function observeBus(name: string, fn: BusObserver): () => void;
380
+ /** Log every wire on a bus to the console. Returns a function to stop. */
381
+ declare function enableDebug(options?: DebugOptions): () => void;
382
+
383
+ /**
384
+ * Get the shared bus for `name`, creating it on first use. Callers must call
385
+ * bus.release() exactly once when done. When a custom transport factory is
386
+ * given (tests), every call creates an isolated bus — one call = one simulated client.
387
+ */
388
+ /**
389
+ * Names of the buses currently alive on this page. Buses built with a custom
390
+ * transport (tests) bypass the registry, so they are not listed.
391
+ */
392
+ declare function getBusNames(): string[];
393
+
394
+ /** Same-origin transport over a real BroadcastChannel. */
395
+ declare class BroadcastChannelTransport implements Transport {
396
+ private bc;
397
+ private listeners;
398
+ constructor(name: string);
399
+ post(data: unknown): void;
400
+ subscribe(listener: (data: unknown) => void): () => void;
401
+ close(): void;
402
+ }
403
+
404
+ /**
405
+ * Silent local transport: nothing leaves this context, nothing arrives.
406
+ * Used for SSR and for state scoped to a single tab.
407
+ */
408
+ declare class NoopTransport implements Transport {
409
+ post(): void;
410
+ subscribe(): () => void;
411
+ close(): void;
412
+ }
413
+
414
+ declare function isBroadcastChannelAvailable(): boolean;
415
+ /** Default factory: real BroadcastChannel when available, otherwise a local no-op. */
416
+ declare function defaultTransport(name: string): Transport;
417
+
418
+ /** One simulated client on a MemoryHub. Create via hub.connect(). */
419
+ declare class MemoryTransport implements Transport {
420
+ private hub;
421
+ private listeners;
422
+ private closed;
423
+ constructor(hub: MemoryHub);
424
+ post(data: unknown): void;
425
+ subscribe(listener: (data: unknown) => void): () => void;
426
+ close(): void;
427
+ /** @internal */
428
+ deliver(data: unknown): void;
429
+ }
430
+
431
+ /**
432
+ * In-memory hub for tests: N transports attached to one hub, each post is
433
+ * delivered to every *other* transport on a microtask (mirrors BroadcastChannel's
434
+ * async, no-self-echo delivery).
435
+ */
436
+ declare class MemoryHub {
437
+ private transports;
438
+ connect(): MemoryTransport;
439
+ /** @internal */
440
+ broadcast(from: MemoryTransport, data: unknown): void;
441
+ /** @internal */
442
+ disconnect(transport: MemoryTransport): void;
443
+ }
444
+
445
+ export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, MemoryHub, MemoryTransport, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, type Transport, type Version, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
package/dist/index.js CHANGED
@@ -211,6 +211,24 @@ function newer(a, b) {
211
211
  return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
212
212
  }
213
213
 
214
+ // src/dev-freeze.ts
215
+ var inDev = false;
216
+ try {
217
+ inDev = process.env.NODE_ENV !== "production";
218
+ } catch {
219
+ }
220
+ function deepFreeze(value) {
221
+ if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
222
+ Object.freeze(value);
223
+ for (const key of Object.keys(value)) {
224
+ deepFreeze(value[key]);
225
+ }
226
+ }
227
+ function freezeShared(value) {
228
+ if (inDev) deepFreeze(value);
229
+ return value;
230
+ }
231
+
214
232
  // src/shared-store.ts
215
233
  function createSharedStore(name, initial, options = {}) {
216
234
  const bus = getBus(name, options);
@@ -218,7 +236,10 @@ function createSharedStore(name, initial, options = {}) {
218
236
  const accept = options.accept;
219
237
  const state = { ...initial };
220
238
  const versions = {};
221
- for (const k in state) versions[k] = [0, clientId];
239
+ for (const k in state) {
240
+ versions[k] = [0, clientId];
241
+ freezeShared(state[k]);
242
+ }
222
243
  let snapshot = Object.freeze({ ...state });
223
244
  let versionsSnapshot = Object.freeze({ ...versions });
224
245
  const listeners = /* @__PURE__ */ new Set();
@@ -233,7 +254,7 @@ function createSharedStore(name, initial, options = {}) {
233
254
  function applyRemote(key, value, version, meta) {
234
255
  if (!newer(version, versions[key])) return;
235
256
  versions[key] = version;
236
- state[key] = value;
257
+ state[key] = freezeShared(value);
237
258
  notify(key, value, meta);
238
259
  }
239
260
  const unsubscribe = bus.subscribe((wire) => {
@@ -264,7 +285,7 @@ function createSharedStore(name, initial, options = {}) {
264
285
  function setKey(key, value) {
265
286
  const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
266
287
  versions[key] = version;
267
- state[key] = value;
288
+ state[key] = freezeShared(value);
268
289
  bus.post({
269
290
  v: 1,
270
291
  scope: "state",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-everywhere/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Cross-tab shared state, events, presence, and cross-origin window channels",
5
5
  "license": "MIT",
6
6
  "author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
@@ -25,12 +25,19 @@
25
25
  },
26
26
  "type": "module",
27
27
  "sideEffects": false,
28
- "main": "./dist/index.js",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
29
30
  "types": "./dist/index.d.ts",
30
31
  "exports": {
31
32
  ".": {
32
- "types": "./dist/index.d.ts",
33
- "import": "./dist/index.js"
33
+ "import": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/index.d.cts",
39
+ "default": "./dist/index.cjs"
40
+ }
34
41
  }
35
42
  },
36
43
  "files": [
@@ -99,18 +106,22 @@
99
106
  }
100
107
  ],
101
108
  "devDependencies": {
102
- "@size-limit/preset-small-lib": "^12.1.0",
109
+ "@arethetypeswrong/cli": "^0.18.5",
110
+ "@size-limit/preset-small-lib": "^13.0.1",
103
111
  "@vitest/coverage-v8": "^4.1.10",
104
- "happy-dom": "^20.10.6",
105
- "size-limit": "^12.1.0",
106
- "tsup": "^8.5.0",
107
- "typescript": "^5.8.3",
112
+ "happy-dom": "^20.11.1",
113
+ "publint": "^0.3.21",
114
+ "size-limit": "^13.0.1",
115
+ "tsup": "^8.5.1",
116
+ "typescript": "^6.0.3",
108
117
  "vitest": "^4.1.10"
109
118
  },
110
119
  "scripts": {
111
120
  "build": "tsup",
112
121
  "test": "vitest run --coverage",
113
122
  "typecheck": "tsc --noEmit",
114
- "size": "size-limit"
123
+ "size": "size-limit",
124
+ "check:exports": "publint --strict && attw --pack .",
125
+ "pack:smoke": "node --import tsx ../tooling/pack-smoke.ts"
115
126
  }
116
127
  }