ripple-di 1.0.0 → 1.1.0
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 +267 -120
- package/dist/index.d.mts +15 -1
- package/dist/index.mjs +53 -5
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ripple DI
|
|
2
2
|
|
|
3
|
-
Scoped dependency injection for TypeScript
|
|
3
|
+
Scoped dependency injection for TypeScript with automatic dependency tracking, lifecycle-aware cleanup, and no container lookups in application code.
|
|
4
4
|
|
|
5
5
|
## Example
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ Now replace only the configuration for one operation:
|
|
|
29
29
|
import { provide, withOverrides } from "ripple-di"
|
|
30
30
|
|
|
31
31
|
const users = await withOverrides(
|
|
32
|
-
|
|
32
|
+
provide(useConfig, { databaseUrl: testDatabaseUrl }),
|
|
33
33
|
() => loadUsers(),
|
|
34
34
|
)
|
|
35
35
|
```
|
|
@@ -37,7 +37,13 @@ const users = await withOverrides(
|
|
|
37
37
|
`useDb` reads `useConfig()`, so Ripple DI builds a separate database for the test configuration and loads the users through it.
|
|
38
38
|
The production database and everything unrelated keep the instances they already had, and the temporary database is closed when the callback finishes.
|
|
39
39
|
|
|
40
|
-
This is the ripple: override one input, and only the values
|
|
40
|
+
This is the ripple: override one input, and only the values derived from it are rebuilt.
|
|
41
|
+
|
|
42
|
+
## Design trade-off
|
|
43
|
+
|
|
44
|
+
Ripple DI is an ambient service locator by design: code imports a dependency and calls it instead of receiving it as an argument.
|
|
45
|
+
|
|
46
|
+
This makes dependencies convenient to use and override, but also makes them less explicit: looking at a function's signature does not tell you which Ripple DI dependencies it reads.
|
|
41
47
|
|
|
42
48
|
## Install
|
|
43
49
|
|
|
@@ -48,59 +54,23 @@ npm install ripple-di
|
|
|
48
54
|
Ripple DI needs `node:async_hooks`, so it runs on Node.js 18 or newer, Bun, and Deno with Node compatibility, but not in browsers.
|
|
49
55
|
The package is published as ESM.
|
|
50
56
|
|
|
51
|
-
## Quick start
|
|
52
|
-
|
|
53
|
-
Define each shared input and service once, next to the code that owns it.
|
|
54
|
-
Factories are lazy: they run the first time something reads the value.
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
// config.ts
|
|
58
|
-
import { defineDependency } from "ripple-di"
|
|
59
|
-
|
|
60
|
-
export const useConfig = defineDependency(() => ({
|
|
61
|
-
databaseUrl: process.env.DATABASE_URL,
|
|
62
|
-
}))
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
// db.ts
|
|
67
|
-
import { defineDependency } from "ripple-di"
|
|
68
|
-
|
|
69
|
-
import { useConfig } from "./config"
|
|
70
|
-
|
|
71
|
-
export const useDb = defineDependency(
|
|
72
|
-
() => createDb(useConfig().databaseUrl),
|
|
73
|
-
{ dispose: db => db.close() },
|
|
74
|
-
)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Anything that needs the database imports it and calls it.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { useDb } from "./db"
|
|
81
|
-
|
|
82
|
-
export async function loadUsers() {
|
|
83
|
-
return useDb().query("select * from users")
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
The return type of `useDb()` is inferred from `createDb`.
|
|
88
|
-
|
|
89
57
|
## Which function do I call?
|
|
90
58
|
|
|
91
|
-
| You want to
|
|
92
|
-
|
|
|
93
|
-
| Define an input, derived value, or service
|
|
94
|
-
| Supply the
|
|
95
|
-
| Replace values for one callback
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
|
|
59
|
+
| You want to | Call
|
|
60
|
+
| --------------------------------------------- | --------------------------------------------------
|
|
61
|
+
| Define an input, derived value, or service | `defineDependency`
|
|
62
|
+
| Supply the application's values at startup | `install` with `provide` or `provideFactory`
|
|
63
|
+
| Replace values for one callback | `withOverrides` with `provide` or `provideFactory`
|
|
64
|
+
| Run after the current scope can close | `withDetachedOverrides`
|
|
65
|
+
| Keep one scope open across several operations | `createScope`
|
|
66
|
+
| Shut everything down | `dispose`
|
|
67
|
+
|
|
68
|
+
`createValueOverride` and `createOverrideRunner` in [Advanced usage](#advanced-usage) turn overrides you write repeatedly into reusable helpers.
|
|
101
69
|
|
|
102
70
|
## Define dependencies
|
|
103
71
|
|
|
72
|
+
Define each shared input and service once, in the module that owns it, and import it wherever it is used.
|
|
73
|
+
|
|
104
74
|
Call `defineDependency<T>()` without a factory when a value must come from the application, request, or task boundary.
|
|
105
75
|
|
|
106
76
|
```ts
|
|
@@ -109,7 +79,7 @@ import { defineDependency, provide, withOverrides } from "ripple-di"
|
|
|
109
79
|
const useTenant = defineDependency<Tenant>()
|
|
110
80
|
|
|
111
81
|
await withOverrides(
|
|
112
|
-
|
|
82
|
+
provide(useTenant, tenant),
|
|
113
83
|
() => processRequest(),
|
|
114
84
|
)
|
|
115
85
|
```
|
|
@@ -117,29 +87,57 @@ await withOverrides(
|
|
|
117
87
|
Reading it without a provider throws `MissingProviderError`.
|
|
118
88
|
Pass a factory when the dependency has a built-in value.
|
|
119
89
|
The factory runs lazily, and calls to other dependencies inside it are tracked.
|
|
120
|
-
A function is always
|
|
90
|
+
A function in the first argument is always the factory, so a dependency whose value is itself a function wraps it: `defineDependency(() => handler)`.
|
|
121
91
|
|
|
122
92
|
```ts
|
|
123
93
|
const useClock = defineDependency(() => systemClock)
|
|
124
94
|
const usePublicUrl = defineDependency(() => createPublicUrl(useConfig()))
|
|
125
95
|
```
|
|
126
96
|
|
|
127
|
-
The result is cached and
|
|
128
|
-
|
|
97
|
+
The result is cached, and a scope that overrides one of the dependencies it read gets a separate result.
|
|
98
|
+
Its type comes from the factory, so `useDb()` returns whatever `createDb` returns.
|
|
99
|
+
Add a `dispose` callback when values owned by Ripple DI need cleanup, as shown by `useDb` in the example above.
|
|
100
|
+
|
|
101
|
+
A factory can read dependencies, but cannot create, enter, close, or retire scopes and installations in its own runtime; misuse throws `FactoryScopeOperationError`.
|
|
102
|
+
|
|
103
|
+
### Promises and thenables
|
|
104
|
+
|
|
105
|
+
A factory returns the value itself, synchronously; disposers may be asynchronous.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { asValue, defineDependency } from "ripple-di"
|
|
109
|
+
|
|
110
|
+
// The usual case: the factory returns the value.
|
|
111
|
+
defineDependency(() => createSessionStore())
|
|
112
|
+
|
|
113
|
+
// Reading this throws AsyncFactoryError: reads made after an await are not tracked.
|
|
114
|
+
defineDependency(async () => loadSession())
|
|
115
|
+
|
|
116
|
+
// The promise itself is the cached dependency value.
|
|
117
|
+
defineDependency(() => asValue(loadSession()))
|
|
118
|
+
|
|
119
|
+
// A query builder implements `then` but is an ordinary value.
|
|
120
|
+
defineDependency(() => selectSessions())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A factory is rejected only when it returns a native `Promise` object, so a value that merely implements `then` is stored as it is.
|
|
124
|
+
Wrap the result in `asValue` when the promise itself is the value the dependency holds.
|
|
125
|
+
|
|
126
|
+
`asValue` marks the promise as the value and does nothing else.
|
|
127
|
+
The dependencies its asynchronous work reads after an `await` are still untracked, so a promise cached for the whole application can end up built from the temporary values of whichever scope started it.
|
|
129
128
|
|
|
130
129
|
## Override dependencies
|
|
131
130
|
|
|
132
131
|
`withOverrides` runs a callback with temporary values and cleans up whatever it created for that callback.
|
|
132
|
+
Pass one `provide` or `provideFactory` directly.
|
|
133
133
|
|
|
134
134
|
```ts
|
|
135
135
|
import { provide, withOverrides } from "ripple-di"
|
|
136
136
|
|
|
137
137
|
const users = await withOverrides(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}),
|
|
142
|
-
],
|
|
138
|
+
provide(useConfig, {
|
|
139
|
+
databaseUrl: "postgres://localhost/test",
|
|
140
|
+
}),
|
|
143
141
|
() => loadUsers(),
|
|
144
142
|
)
|
|
145
143
|
```
|
|
@@ -159,43 +157,107 @@ await withOverrides(
|
|
|
159
157
|
)
|
|
160
158
|
```
|
|
161
159
|
|
|
162
|
-
|
|
160
|
+
Overrides survive `await` and stay isolated between parallel callbacks.
|
|
163
161
|
|
|
164
162
|
```ts
|
|
165
|
-
await
|
|
166
|
-
provide(useConfig,
|
|
167
|
-
() => loadUsers(),
|
|
168
|
-
)
|
|
163
|
+
const [leftUsers, rightUsers] = await Promise.all([
|
|
164
|
+
withOverrides(provide(useConfig, leftConfig), () => loadUsers()),
|
|
165
|
+
withOverrides(provide(useConfig, rightConfig), () => loadUsers()),
|
|
166
|
+
])
|
|
169
167
|
```
|
|
170
168
|
|
|
171
|
-
|
|
169
|
+
### Awaitable callback results
|
|
170
|
+
|
|
171
|
+
`withOverrides`, and every helper built on it, awaits what its callback returns before closing the temporary scope, so returning a query builder from one of them runs the query.
|
|
172
|
+
Use such a value inside the callback, and use `createScope` when it has to outlive the callback.
|
|
173
|
+
Wrapping the value in an object prevents it from being awaited, but the temporary scope still closes before the caller receives it.
|
|
174
|
+
Anything that scope owned has already been cleaned up.
|
|
175
|
+
|
|
176
|
+
### Run outside the current scope
|
|
177
|
+
|
|
178
|
+
`withDetachedOverrides` runs work in a temporary scope that does not inherit the current ambient scope.
|
|
179
|
+
Use it when work must continue after a request or another scoped operation can finish.
|
|
180
|
+
Capture every request value the work needs and provide it explicitly.
|
|
172
181
|
|
|
173
182
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
import { provide, withDetachedOverrides } from "ripple-di"
|
|
184
|
+
|
|
185
|
+
const tenant = useTenant()
|
|
186
|
+
|
|
187
|
+
const backgroundTask = withDetachedOverrides(
|
|
188
|
+
provide(useTenant, tenant),
|
|
189
|
+
() => updateTenantSearchIndex(),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
trackBackgroundTask(backgroundTask)
|
|
178
193
|
```
|
|
179
194
|
|
|
180
|
-
|
|
195
|
+
The detached scope inherits from the active installation, or from the runtime root when no installation is active.
|
|
196
|
+
It remains part of that lifecycle: closing the installation or calling `dispose()` force-closes it.
|
|
197
|
+
When no installation is active, an unfinished detached scope also prevents `install()` until its callback and cleanup finish.
|
|
198
|
+
|
|
199
|
+
The returned promise settles after the callback and cleanup finish.
|
|
200
|
+
Keep or observe it so callback and cleanup failures are handled.
|
|
181
201
|
|
|
182
|
-
|
|
202
|
+
## Where a value belongs
|
|
203
|
+
|
|
204
|
+
The same tracked dependency calls that decide when a factory result must be rebuilt also decide which lifecycle owns it and invokes its disposer.
|
|
205
|
+
A factory-created value belongs to the innermost scope that supplied either its factory or one of the dependencies it read.
|
|
206
|
+
When its factory and everything it read belong to the application, the value belongs to the application too, and closing a child scope leaves it alone.
|
|
207
|
+
|
|
208
|
+
**Reading a dependency inside a scope does not make its value scope-local.**
|
|
183
209
|
|
|
184
210
|
```ts
|
|
185
|
-
|
|
211
|
+
// Wrong: every request shares this one transaction.
|
|
212
|
+
const useTransaction = defineDependency(
|
|
213
|
+
() => useDb().transaction(),
|
|
214
|
+
{ dispose: tx => tx.rollback() },
|
|
215
|
+
)
|
|
216
|
+
```
|
|
186
217
|
|
|
187
|
-
|
|
218
|
+
`useDb` belongs to the application, so a transaction built from it belongs to the application too.
|
|
219
|
+
Every request reads the same transaction, and it is rolled back at shutdown rather than when the request ends.
|
|
188
220
|
|
|
189
|
-
|
|
221
|
+
For a value that belongs to one operation, define the dependency without a built-in factory.
|
|
222
|
+
Provide its factory when you create the scope of that operation:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const useTransaction = defineDependency<Transaction>({
|
|
226
|
+
dispose: tx => tx.rollback(),
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
await withOverrides(
|
|
230
|
+
provideFactory(useTransaction, () => useDb().transaction()),
|
|
231
|
+
() => processRequest(),
|
|
232
|
+
)
|
|
190
233
|
```
|
|
191
234
|
|
|
192
|
-
Each call
|
|
193
|
-
|
|
235
|
+
Each call now builds its own transaction, still lazily, and the scope rolls it back when the callback finishes.
|
|
236
|
+
Reading `useTransaction()` outside such a scope throws `MissingProviderError` instead of quietly sharing one.
|
|
237
|
+
|
|
238
|
+
A value also becomes scope-local through what it reads: a factory that reads a dependency the scope provides — a request context, a tenant, a fixed clock — is rebuilt in that scope and cleaned up with it.
|
|
239
|
+
|
|
240
|
+
An installation follows the same rule: a value belongs to it when the installation supplied its factory or one of the dependencies the factory read.
|
|
194
241
|
|
|
195
242
|
```ts
|
|
196
|
-
const
|
|
243
|
+
const useBuiltInConfig = defineDependency(() => rootConfig)
|
|
244
|
+
const useRootPool = defineDependency(() => createPool(useBuiltInConfig()), {
|
|
245
|
+
dispose: pool => pool.end(),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const useInstalledConfig = defineDependency<DatabaseConfig>()
|
|
249
|
+
const useInstalledPool = defineDependency(() => createPool(useInstalledConfig()), {
|
|
250
|
+
dispose: pool => pool.end(),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const installation = install(provide(useInstalledConfig, installedConfig))
|
|
254
|
+
useRootPool()
|
|
255
|
+
useInstalledPool()
|
|
256
|
+
await installation.close() // Before installing replacement providers.
|
|
197
257
|
```
|
|
198
258
|
|
|
259
|
+
The installed pool closes with the installation, while the root pool remains cached until `dispose()` because its factory read only built-in dependencies.
|
|
260
|
+
|
|
199
261
|
## Wire the application at startup
|
|
200
262
|
|
|
201
263
|
Some dependencies get their real value only when the application starts.
|
|
@@ -211,11 +273,11 @@ export const useTenantResolver = defineDependency<TenantResolver>()
|
|
|
211
273
|
|
|
212
274
|
```ts
|
|
213
275
|
// startup.ts
|
|
214
|
-
import { install, provide } from "ripple-di"
|
|
276
|
+
import { dispose, install, provide } from "ripple-di"
|
|
215
277
|
|
|
216
278
|
import { useTenantResolver, useWebConfig } from "./dependencies"
|
|
217
279
|
|
|
218
|
-
|
|
280
|
+
install([
|
|
219
281
|
provide(useWebConfig, config),
|
|
220
282
|
provide(useTenantResolver, resolveTenant),
|
|
221
283
|
])
|
|
@@ -223,20 +285,25 @@ const installation = install([
|
|
|
223
285
|
try {
|
|
224
286
|
await runApplication()
|
|
225
287
|
} finally {
|
|
226
|
-
await
|
|
288
|
+
await dispose()
|
|
227
289
|
}
|
|
228
290
|
```
|
|
229
291
|
|
|
230
292
|
Installed providers are the fallback everywhere: request handlers, background jobs, tests, and any other code running outside a scope.
|
|
231
293
|
Nothing is resolved eagerly, and a scoped override still wins over an installed provider.
|
|
232
|
-
Closing
|
|
294
|
+
Closing an installation removes its providers and cleans up values whose factory or tracked dependencies belong to it.
|
|
295
|
+
It keeps the runtime usable, so application-owned values remain cached for later installations.
|
|
296
|
+
Use `installation.close()` before a controlled replacement; use `dispose()` to shut the application down.
|
|
233
297
|
|
|
234
298
|
- Installing while another installation or any scope is still open throws `InstallationConflictError`, whose message says what is still open.
|
|
235
299
|
Await `installation.close()` before installing a replacement.
|
|
236
300
|
- Providing the same dependency twice in one installation is rejected, exactly like in `withOverrides`.
|
|
237
|
-
-
|
|
301
|
+
- Install once during startup.
|
|
302
|
+
Late installation is supported for a controlled bootstrap or a test setup: it applies to the reads that come after it, and closing it brings the earlier values back.
|
|
303
|
+
- If a resource needs the lifetime of each installation, define its dependency without a built-in factory and export a module provision builder that returns `provideFactory`.
|
|
304
|
+
This keeps the factory body in its owning module, but removes the built-in fallback: reading the dependency without that provision throws `MissingProviderError`.
|
|
238
305
|
- Every worker thread and every process wires its own installation.
|
|
239
|
-
- In tests, install once for the whole process and
|
|
306
|
+
- In tests, install once for the whole process and override per test, as shown in [Test your application](#test-your-application).
|
|
240
307
|
|
|
241
308
|
### Collect the providers of several modules
|
|
242
309
|
|
|
@@ -264,17 +331,19 @@ export function getPlatformProvisions() {
|
|
|
264
331
|
|
|
265
332
|
```ts
|
|
266
333
|
// startup.ts
|
|
267
|
-
|
|
334
|
+
import { dispose, install } from "ripple-di"
|
|
335
|
+
|
|
336
|
+
install([
|
|
268
337
|
...getCoreProvisions(),
|
|
269
338
|
...getPlatformProvisions(),
|
|
270
339
|
])
|
|
271
340
|
|
|
272
|
-
onShutdown(() =>
|
|
341
|
+
onShutdown(() => dispose())
|
|
273
342
|
```
|
|
274
343
|
|
|
275
344
|
- Export a function that builds the provisions rather than a ready-made array, so each installation gets provisions of its own.
|
|
276
345
|
A provision that hands over ownership belongs to a single installation and cannot be reused by the next one.
|
|
277
|
-
- The composition root that installs the provisions
|
|
346
|
+
- The composition root that installs the provisions also calls `dispose()` from the shutdown hook the application already has.
|
|
278
347
|
- Tests have the same single composition root: one preload installs the provisions of every layer the suite needs.
|
|
279
348
|
Separate preloads installing on their own would fail on the second `install` instead of adding their providers.
|
|
280
349
|
|
|
@@ -282,6 +351,7 @@ onShutdown(() => installation.close())
|
|
|
282
351
|
|
|
283
352
|
Call `dispose()` when the application shuts down.
|
|
284
353
|
It closes every scope and cleans up every owned value still held by the module-level API, including an active installation.
|
|
354
|
+
Closing only the installation can leave [application-owned values](#where-a-value-belongs) cached.
|
|
285
355
|
|
|
286
356
|
```ts
|
|
287
357
|
import { dispose } from "ripple-di"
|
|
@@ -294,6 +364,66 @@ Values created inside `withOverrides` never wait for shutdown; they are cleaned
|
|
|
294
364
|
`dispose()` is final: afterwards the runtime cannot resolve dependencies, create scopes, or install providers, and those calls throw `ScopeClosedError`.
|
|
295
365
|
Do not use it to reset state between tests — use `withOverrides` for that, or create a separate runtime for each lifecycle.
|
|
296
366
|
|
|
367
|
+
## Test your application
|
|
368
|
+
|
|
369
|
+
Install the wiring the suite needs once for the whole test process, from the preload or setup file the test runner already has, and give each test the values of its own.
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
import {
|
|
373
|
+
createOverrideRunner,
|
|
374
|
+
createScope,
|
|
375
|
+
provide,
|
|
376
|
+
type Scope,
|
|
377
|
+
withOverrides,
|
|
378
|
+
} from "ripple-di"
|
|
379
|
+
|
|
380
|
+
test("loads users from the test database", () =>
|
|
381
|
+
withOverrides(
|
|
382
|
+
provide(useConfig, testConfig),
|
|
383
|
+
() => loadUsers(),
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The scope lives exactly as long as that callback, so tests running in parallel cannot see one another's overrides.
|
|
389
|
+
|
|
390
|
+
When several tests share one set of values that is expensive to build, create the scope once and run each test inside it.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
let scope: Scope
|
|
394
|
+
|
|
395
|
+
beforeAll(() => {
|
|
396
|
+
scope = createScope(provide(useConfig, testConfig))
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
afterAll(() => scope.close())
|
|
400
|
+
|
|
401
|
+
test("lists users", () => scope.run(() => listUsers()))
|
|
402
|
+
test("creates a user", () => scope.run(() => createUser()))
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Entering a scope in a hook does not carry it into the tests that follow.
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
// Wrong: the scope is current inside this callback and nowhere else.
|
|
409
|
+
beforeAll(() => scope.run(() => {}))
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
- `scope.run` and `withOverrides` make a scope current for their own callback only, so every test that needs the scope runs inside one of them.
|
|
413
|
+
- Tests that share a suite scope also share the values cached in it, so keep that recipe for values they can safely reuse.
|
|
414
|
+
- `scope.run` does not report child scopes a test leaves open, so a test that creates one closes it itself instead of leaving it until `afterAll`.
|
|
415
|
+
|
|
416
|
+
When every test in a file needs the same overrides in a separate scope, a runner applies them to each test independently.
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
const testConfigOverrides = createOverrideRunner(() =>
|
|
420
|
+
provide(useConfig, testConfig),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
test("lists users", testConfigOverrides.wrap(() => listUsers()))
|
|
424
|
+
test("creates a user", testConfigOverrides.wrap(() => createUser()))
|
|
425
|
+
```
|
|
426
|
+
|
|
297
427
|
## Advanced usage
|
|
298
428
|
|
|
299
429
|
Everything below is optional.
|
|
@@ -320,6 +450,7 @@ provide(useDb, fakeDb, { dispose: db => db.closeImmediately() })
|
|
|
320
450
|
|
|
321
451
|
A function passed to `provide` stays an ordinary function value; use `provideFactory` when it should build the value instead.
|
|
322
452
|
The dependencies an override factory reads are tracked like the ones read by the factory it replaces.
|
|
453
|
+
An override factory cannot read the previous value of the dependency it replaces; define a base dependency and a decorated one instead.
|
|
323
454
|
`dispose: true` reuses the disposer from `defineDependency`, so it throws right away when the dependency declares none.
|
|
324
455
|
|
|
325
456
|
A provision that hands over ownership belongs to a single scope or installation.
|
|
@@ -332,15 +463,13 @@ const useQueue = defineDependency<Queue>({
|
|
|
332
463
|
dispose: queue => queue.close(),
|
|
333
464
|
})
|
|
334
465
|
|
|
335
|
-
|
|
336
|
-
provideFactory(useQueue, () => createQueue(queueUrl)),
|
|
337
|
-
])
|
|
466
|
+
install(provideFactory(useQueue, () => createQueue(queueUrl)))
|
|
338
467
|
|
|
339
|
-
// Later, at shutdown:
|
|
340
|
-
await
|
|
468
|
+
// Later, at application shutdown:
|
|
469
|
+
await dispose()
|
|
341
470
|
```
|
|
342
471
|
|
|
343
|
-
The installed factory is lazy, and its queue client is closed with the installation.
|
|
472
|
+
The installed factory is lazy, and its queue client is closed either with the installation during a controlled replacement or with the runtime at application shutdown.
|
|
344
473
|
|
|
345
474
|
### Manage a scope explicitly
|
|
346
475
|
|
|
@@ -350,9 +479,7 @@ Use `createScope` when several operations share the same overrides and close at
|
|
|
350
479
|
```ts
|
|
351
480
|
import { createScope, provide } from "ripple-di"
|
|
352
481
|
|
|
353
|
-
const scope = createScope(
|
|
354
|
-
provide(useConfig, tenantConfig),
|
|
355
|
-
])
|
|
482
|
+
const scope = createScope(provide(useConfig, tenantConfig))
|
|
356
483
|
|
|
357
484
|
try {
|
|
358
485
|
await scope.run(() => processTenant())
|
|
@@ -361,13 +488,36 @@ try {
|
|
|
361
488
|
}
|
|
362
489
|
```
|
|
363
490
|
|
|
364
|
-
- `scope.run` makes the scope current for a callback without closing it.
|
|
491
|
+
- `scope.run` makes the scope current for a callback without closing it, and returns the callback's result unchanged.
|
|
365
492
|
- `scope.resolve(useDb)` reads a dependency from that scope rather than the current one.
|
|
493
|
+
A factory reads from its own scope only, so calling `scope.resolve` on a different scope inside one throws `CrossScopeResolutionError`.
|
|
366
494
|
- `scope.createScope` and `scope.withOverrides` create children of that scope instead of the current one.
|
|
367
495
|
- A child of the scope that `withOverrides` created must be closed before the callback returns, otherwise Ripple DI closes it and throws `LeakedChildScopeError`.
|
|
368
496
|
- `scope.close()` closes the scope and everything below it, while `scope.retire()` waits for child scopes to finish first.
|
|
369
497
|
- Closing disposes what the scope itself created; reused application values stay open.
|
|
370
498
|
- Cleanup continues past a failing disposer and reports every failure in one `AggregateError`.
|
|
499
|
+
- A disposer, and any async work it starts, cannot read dependencies or manage scopes and installations in the runtime being closed; misuse throws `DisposerContextError`.
|
|
500
|
+
Put everything cleanup needs into the dependency value itself.
|
|
501
|
+
|
|
502
|
+
### Name an override you write repeatedly
|
|
503
|
+
|
|
504
|
+
When a dependency is replaced in only one place, keep `withOverrides` with one `provide` at the call site.
|
|
505
|
+
For repeated replacements, `createValueOverride` turns the pattern into a named helper.
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
import { createValueOverride } from "ripple-di"
|
|
509
|
+
|
|
510
|
+
const withOpenAiClient = createValueOverride(useOpenAiClient)
|
|
511
|
+
|
|
512
|
+
await withOpenAiClient(client, () => createTextEmbedding(text))
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Each call supplies the value it is given to one callback, exactly like writing `withOverrides` with a single `provide` by hand.
|
|
516
|
+
Pass the ownership options once, and every call cleans its own value up.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const withConnection = createValueOverride(useConnection, { dispose: true })
|
|
520
|
+
```
|
|
371
521
|
|
|
372
522
|
### Reuse one set of overrides
|
|
373
523
|
|
|
@@ -377,9 +527,9 @@ A long-lived object such as an API caller or a job worker is created once, but e
|
|
|
377
527
|
```ts
|
|
378
528
|
import { createOverrideRunner, provide } from "ripple-di"
|
|
379
529
|
|
|
380
|
-
const jobOverrides = createOverrideRunner(() =>
|
|
530
|
+
const jobOverrides = createOverrideRunner(() =>
|
|
381
531
|
provide(useJobContext, createJobContext(), { dispose: true }),
|
|
382
|
-
|
|
532
|
+
)
|
|
383
533
|
|
|
384
534
|
const worker = createWorker(job => jobOverrides.run(() => handleJob(job)))
|
|
385
535
|
```
|
|
@@ -403,9 +553,9 @@ That also suits a file of independent test cases that share one set of overrides
|
|
|
403
553
|
`extend` returns a runner with one more layer of overrides and leaves the runner it extends unchanged:
|
|
404
554
|
|
|
405
555
|
```ts
|
|
406
|
-
const tenantOverrides = jobOverrides.extend(() =>
|
|
556
|
+
const tenantOverrides = jobOverrides.extend(() =>
|
|
407
557
|
provide(useTenantResolver, tenantResolver),
|
|
408
|
-
|
|
558
|
+
)
|
|
409
559
|
|
|
410
560
|
await tenantOverrides.run(() => handleRequest())
|
|
411
561
|
```
|
|
@@ -449,6 +599,7 @@ await Promise.all([
|
|
|
449
599
|
```
|
|
450
600
|
|
|
451
601
|
A dependency belongs to the runtime that defined it and cannot be read or overridden in another one.
|
|
602
|
+
The module-level `defineDependency` always defines a dependency of the built-in runtime, so an application factory like the one above defines every dependency it needs through its own runtime.
|
|
452
603
|
Pass `name` to `createRuntime` to see that name in error messages.
|
|
453
604
|
Every runtime has the same methods, and each has a module-level counterpart that targets the built-in runtime:
|
|
454
605
|
|
|
@@ -457,6 +608,7 @@ Every runtime has the same methods, and each has a module-level counterpart that
|
|
|
457
608
|
- `resolve`
|
|
458
609
|
- `createScope`
|
|
459
610
|
- `withOverrides`
|
|
611
|
+
- `withDetachedOverrides`
|
|
460
612
|
- `createValueOverride`
|
|
461
613
|
- `createOverrideRunner`
|
|
462
614
|
- `dispose`
|
|
@@ -471,36 +623,31 @@ Errors thrown by your own factory arrive wrapped in `FactoryError` with the orig
|
|
|
471
623
|
A failed factory is not cached, so the next read tries again.
|
|
472
624
|
|
|
473
625
|
These errors name the dependencies involved.
|
|
474
|
-
|
|
626
|
+
An explicit `name` is always used when you provide one:
|
|
475
627
|
|
|
476
628
|
```ts
|
|
477
629
|
const useConfig = defineDependency(loadConfig, { name: "config" })
|
|
478
630
|
```
|
|
479
631
|
|
|
480
632
|
The name only affects messages.
|
|
633
|
+
Without an explicit name, Ripple DI uses a non-empty factory name or shows a generated name with the `defineDependency` call location, such as `dependency#21 (packages/core/src/openai/config.ts:36)`.
|
|
634
|
+
The location follows the runtime's stack and source maps, is captured once when the dependency is defined, and adds no work during resolution.
|
|
481
635
|
|
|
482
|
-
##
|
|
636
|
+
## Do not mix package copies
|
|
637
|
+
|
|
638
|
+
Two copies of Ripple DI, separately installed or bundled, each run their own graph and cannot be combined.
|
|
639
|
+
Do not pass dependencies or provisions between them, and do not call one copy's dependency inside a factory owned by the other.
|
|
640
|
+
Such a call is not detected: the dependency may resolve against its own copy instead of failing at the boundary.
|
|
641
|
+
|
|
642
|
+
Declare `ripple-di` as a peer dependency in any package that exports dependencies of its own.
|
|
643
|
+
|
|
644
|
+
## Caveats
|
|
483
645
|
|
|
484
|
-
- Factories are synchronous; disposers may be asynchronous.
|
|
485
|
-
A factory returns the value itself, so a value that implements `then`, such as a query builder or another awaitable client, is stored as it is.
|
|
486
|
-
A factory is rejected only when it returns a native `Promise` object, because dependency reads made after an `await` are not tracked.
|
|
487
|
-
When the promise is the value, wrap the result in `asValue`: `defineDependency(() => asValue(loadToken()))` defines a `Promise<Token>` dependency created once and awaited by its readers.
|
|
488
|
-
- A `withOverrides` callback that returns an awaitable value has it awaited, exactly like any promise returned from a callback, so returning a query builder runs its query.
|
|
489
|
-
Use the value inside the callback, and use `createScope` when it has to outlive the callback.
|
|
490
|
-
Returning it wrapped in an object avoids the await but hands back a value whose scope is already closed, together with everything that scope owned.
|
|
491
|
-
- A factory can read dependencies, but cannot create, enter, close, or retire scopes and installations in its own runtime; misuse throws `FactoryScopeOperationError`.
|
|
492
|
-
- A disposer, and any async work it starts, cannot read dependencies or manage scopes and installations in the runtime being closed; misuse throws `DisposerContextError`.
|
|
493
|
-
Put everything cleanup needs into the dependency value itself.
|
|
494
|
-
- A factory reads from its own scope only; `scope.resolve` on a different scope throws `CrossScopeResolutionError`.
|
|
495
646
|
- Only the dependency calls made while a factory runs are tracked.
|
|
496
|
-
|
|
497
|
-
- An override factory cannot read the previous value of the dependency it replaces; define a base dependency and a decorated one instead.
|
|
498
|
-
- A factory-created value is scoped only by the dependencies its factory reads.
|
|
499
|
-
A value whose factory reads none stays shared even when it is first requested inside a scope.
|
|
647
|
+
Everything else stays invisible: `process.env`, `Date.now()`, and any dependency called after the factory has returned.
|
|
500
648
|
- Read dependencies where you use them.
|
|
501
649
|
A module-level `const db = useDb()` freezes one scope's value forever.
|
|
502
|
-
-
|
|
503
|
-
Do not pass dependencies or provisions between them, and do not call one copy's dependency inside a factory owned by the other.
|
|
650
|
+
- When a factory catches an error from reading a dependency, its value is not shared: every scope that asks for it builds a new one.
|
|
504
651
|
|
|
505
652
|
## License
|
|
506
653
|
|
package/dist/index.d.mts
CHANGED
|
@@ -189,6 +189,13 @@ interface Runtime {
|
|
|
189
189
|
* for the callback afterward.
|
|
190
190
|
*/
|
|
191
191
|
withOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
192
|
+
/**
|
|
193
|
+
* Runs a callback in a temporary child of the runtime's current base scope.
|
|
194
|
+
*
|
|
195
|
+
* The callback does not inherit the current ambient scope, but its scope
|
|
196
|
+
* remains owned by the active installation or runtime root.
|
|
197
|
+
*/
|
|
198
|
+
withDetachedOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
192
199
|
/**
|
|
193
200
|
* Prepares overrides that are applied again to each call of the returned
|
|
194
201
|
* runner.
|
|
@@ -240,6 +247,13 @@ declare function createScope(provisions?: ProvisionInput): Scope;
|
|
|
240
247
|
* callbacks, and are cleaned up when the callback finishes.
|
|
241
248
|
*/
|
|
242
249
|
declare function withOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
250
|
+
/**
|
|
251
|
+
* Runs a callback with overrides outside the current ambient scope.
|
|
252
|
+
*
|
|
253
|
+
* The temporary scope inherits from the active installation or runtime root
|
|
254
|
+
* and remains part of that lifecycle until the callback finishes.
|
|
255
|
+
*/
|
|
256
|
+
declare function withDetachedOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
243
257
|
/**
|
|
244
258
|
* Prepares dependency overrides that are applied again to each call of the
|
|
245
259
|
* returned runner.
|
|
@@ -376,4 +390,4 @@ declare class LeakedChildScopeError extends RippleError {
|
|
|
376
390
|
constructor(scopeName: string, leakedChildCount: number);
|
|
377
391
|
}
|
|
378
392
|
//#endregion
|
|
379
|
-
export { type AsValue, AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, type Dependency, DependencyCycleError, type DependencyOptions, type Disposer, DisposerContextError, DuplicateProviderError, FactoryError, type FactoryResult, FactoryScopeOperationError, Installation, InstallationConflictError, LeakedChildScopeError, MissingProviderError, type OverrideRunner, OwnedProvisionReuseError, type ProvideOptions, type Provision, type ProvisionFactory, type ProvisionInput, RippleError, Runtime, RuntimeOptions, type Scope, ScopeClosedError, type ScopeState, type ValueOverride, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withOverrides };
|
|
393
|
+
export { type AsValue, AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, type Dependency, DependencyCycleError, type DependencyOptions, type Disposer, DisposerContextError, DuplicateProviderError, FactoryError, type FactoryResult, FactoryScopeOperationError, Installation, InstallationConflictError, LeakedChildScopeError, MissingProviderError, type OverrideRunner, OwnedProvisionReuseError, type ProvideOptions, type Provision, type ProvisionFactory, type ProvisionInput, RippleError, Runtime, RuntimeOptions, type Scope, ScopeClosedError, type ScopeState, type ValueOverride, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withDetachedOverrides, withOverrides };
|
package/dist/index.mjs
CHANGED
|
@@ -170,13 +170,45 @@ var DependencyNodeImpl = class {
|
|
|
170
170
|
dispose;
|
|
171
171
|
dependency;
|
|
172
172
|
constructor(definition) {
|
|
173
|
-
|
|
173
|
+
const generatedName = `dependency#${this.id}`;
|
|
174
|
+
this.name = definition.name ?? (definition.definitionSite ? `${generatedName} (${definition.definitionSite})` : generatedName);
|
|
174
175
|
this.runtime = definition.runtime;
|
|
175
176
|
this.defaultFactory = definition.defaultFactory;
|
|
176
177
|
this.dispose = definition.dispose;
|
|
177
178
|
this.dependency = (() => this.runtime.readCallable(this));
|
|
178
179
|
}
|
|
179
180
|
};
|
|
181
|
+
/** Captures and reduces a definition stack before it enters private metadata. */
|
|
182
|
+
function captureDefinitionSite(caller) {
|
|
183
|
+
const error = /* @__PURE__ */ new Error();
|
|
184
|
+
const captureStackTrace = typeof Error.captureStackTrace === "function" ? Error.captureStackTrace : void 0;
|
|
185
|
+
if (captureStackTrace) captureStackTrace(error, caller);
|
|
186
|
+
const frames = error.stack?.split("\n").slice(1) ?? [];
|
|
187
|
+
const skippedFrames = captureStackTrace ? frames : frames.slice(2);
|
|
188
|
+
for (const frame of skippedFrames) {
|
|
189
|
+
const site = definitionSiteFromFrame(frame);
|
|
190
|
+
if (site) return site;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function definitionSiteFromFrame(frame) {
|
|
194
|
+
let location = frame.trim().replace(/^at\s+/, "");
|
|
195
|
+
const openingParenthesis = location.lastIndexOf("(");
|
|
196
|
+
if (openingParenthesis >= 0 && location.endsWith(")")) location = location.slice(openingParenthesis + 1, -1);
|
|
197
|
+
else {
|
|
198
|
+
const atSign = location.lastIndexOf("@");
|
|
199
|
+
if (atSign >= 0) location = location.slice(atSign + 1);
|
|
200
|
+
location = location.replace(/^async\s+/, "");
|
|
201
|
+
}
|
|
202
|
+
const match = /^(.*):(\d+):\d+$/.exec(location);
|
|
203
|
+
if (!match || !match[1]) return;
|
|
204
|
+
let file = match[1].replace(/^file:\/\//, "").replaceAll("\\", "/");
|
|
205
|
+
try {
|
|
206
|
+
file = decodeURI(file);
|
|
207
|
+
} catch {}
|
|
208
|
+
const workingDirectory = `${process.cwd().replaceAll("\\", "/")}/`;
|
|
209
|
+
if (file.startsWith(workingDirectory)) file = file.slice(workingDirectory.length);
|
|
210
|
+
return `${file}:${match[2]}`;
|
|
211
|
+
}
|
|
180
212
|
/** Creates a property-free callable whose metadata lives only in a `WeakMap`. */
|
|
181
213
|
function createDependency(definition) {
|
|
182
214
|
const node = new DependencyNodeImpl(definition);
|
|
@@ -748,11 +780,15 @@ var RuntimeImpl = class {
|
|
|
748
780
|
this.root = new ScopeImpl(this, void 0, []);
|
|
749
781
|
}
|
|
750
782
|
defineDependency(factoryOrOptions, maybeOptions) {
|
|
783
|
+
return this.defineDependencyAt(factoryOrOptions, maybeOptions, captureDefinitionSite(this.defineDependency));
|
|
784
|
+
}
|
|
785
|
+
defineDependencyAt(factoryOrOptions, maybeOptions, definitionSite) {
|
|
751
786
|
const isFactory = typeof factoryOrOptions === "function";
|
|
752
787
|
const factory = isFactory ? factoryOrOptions : void 0;
|
|
753
788
|
const options = (isFactory ? maybeOptions : factoryOrOptions) ?? {};
|
|
754
789
|
return createDependency({
|
|
755
|
-
name: options.name,
|
|
790
|
+
name: options.name ?? (factory?.name || void 0),
|
|
791
|
+
definitionSite,
|
|
756
792
|
runtime: this,
|
|
757
793
|
defaultFactory: factory,
|
|
758
794
|
dispose: options.dispose
|
|
@@ -781,6 +817,10 @@ var RuntimeImpl = class {
|
|
|
781
817
|
this.assertScopeManagementAllowed("Runtime.withOverrides");
|
|
782
818
|
return withChildScope(this.currentAmbientScope(), provisions, callback);
|
|
783
819
|
}
|
|
820
|
+
withDetachedOverrides(provisions, callback) {
|
|
821
|
+
this.assertScopeManagementAllowed("Runtime.withDetachedOverrides");
|
|
822
|
+
return withChildScope(this.baseScope(), provisions, callback);
|
|
823
|
+
}
|
|
784
824
|
createOverrideRunner(factory) {
|
|
785
825
|
return createOverrideRunnerFor(this, factory);
|
|
786
826
|
}
|
|
@@ -891,8 +931,7 @@ function createRuntime(options = {}) {
|
|
|
891
931
|
}
|
|
892
932
|
const globalRuntime = new RuntimeImpl({ name: "global" });
|
|
893
933
|
function defineDependency(factoryOrOptions, maybeOptions) {
|
|
894
|
-
|
|
895
|
-
return globalRuntime.defineDependency(factoryOrOptions);
|
|
934
|
+
return globalRuntime.defineDependencyAt(factoryOrOptions, maybeOptions, captureDefinitionSite(defineDependency));
|
|
896
935
|
}
|
|
897
936
|
/**
|
|
898
937
|
* Installs long-lived providers for module-level dependencies.
|
|
@@ -921,6 +960,15 @@ function withOverrides(provisions, callback) {
|
|
|
921
960
|
return globalRuntime.withOverrides(provisions, callback);
|
|
922
961
|
}
|
|
923
962
|
/**
|
|
963
|
+
* Runs a callback with overrides outside the current ambient scope.
|
|
964
|
+
*
|
|
965
|
+
* The temporary scope inherits from the active installation or runtime root
|
|
966
|
+
* and remains part of that lifecycle until the callback finishes.
|
|
967
|
+
*/
|
|
968
|
+
function withDetachedOverrides(provisions, callback) {
|
|
969
|
+
return globalRuntime.withDetachedOverrides(provisions, callback);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
924
972
|
* Prepares dependency overrides that are applied again to each call of the
|
|
925
973
|
* returned runner.
|
|
926
974
|
*
|
|
@@ -945,4 +993,4 @@ function dispose() {
|
|
|
945
993
|
return globalRuntime.dispose();
|
|
946
994
|
}
|
|
947
995
|
//#endregion
|
|
948
|
-
export { AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, DependencyCycleError, DisposerContextError, DuplicateProviderError, FactoryError, FactoryScopeOperationError, InstallationConflictError, LeakedChildScopeError, MissingProviderError, OwnedProvisionReuseError, RippleError, ScopeClosedError, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withOverrides };
|
|
996
|
+
export { AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, DependencyCycleError, DisposerContextError, DuplicateProviderError, FactoryError, FactoryScopeOperationError, InstallationConflictError, LeakedChildScopeError, MissingProviderError, OwnedProvisionReuseError, RippleError, ScopeClosedError, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withDetachedOverrides, withOverrides };
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ripple-di",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "Scoped dependency injection for TypeScript with automatic dependency tracking
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"description": "Scoped dependency injection for TypeScript with automatic dependency tracking, lifecycle-aware cleanup, and no container lookups in application code.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dependency-injection",
|
|
8
8
|
"di",
|
|
9
9
|
"inversion-of-control",
|
|
10
10
|
"ioc",
|
|
11
|
+
"service-locator",
|
|
11
12
|
"typescript",
|
|
12
13
|
"scoped",
|
|
14
|
+
"dependency-tracking",
|
|
15
|
+
"dependency-graph",
|
|
13
16
|
"async-context",
|
|
14
17
|
"asynclocalstorage",
|
|
15
18
|
"async-hooks",
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
"engines": {
|
|
23
26
|
"node": ">=18"
|
|
24
27
|
},
|
|
25
|
-
"packageManager": "bun@1.
|
|
28
|
+
"packageManager": "bun@1.4.0",
|
|
26
29
|
"repository": {
|
|
27
30
|
"type": "git",
|
|
28
31
|
"url": "git+https://github.com/IlyaSemenov/ripple-di.git"
|
|
@@ -56,7 +59,7 @@
|
|
|
56
59
|
"@biomejs/biome": "^2.5.5",
|
|
57
60
|
"@changesets/cli": "^2.31.1",
|
|
58
61
|
"@tsconfig/bun": "^1.0.10",
|
|
59
|
-
"@types/bun": "^1.
|
|
62
|
+
"@types/bun": "^1.4.0",
|
|
60
63
|
"publint": "^0.3.22",
|
|
61
64
|
"tsdown": "^0.22.14",
|
|
62
65
|
"typescript": "^7.0.2"
|