sandly 2.1.0 → 3.0.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 +31 -15
- package/dist/index.d.ts +29 -18
- package/dist/index.js +15 -35
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<h1 align="center">Sandly</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/sandly"><img src="https://img.shields.io/npm/v/sandly?color=3178c6&label=npm" alt="npm version"></a>
|
|
5
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0%2B-blue?logo=typescript&logoColor=white" alt="TypeScript"></a>
|
|
6
|
+
<a href="https://codecov.io/gh/borisrakovan/sandly"><img src="https://codecov.io/gh/borisrakovan/sandly/branch/main/graph/badge.svg" alt="coverage"></a>
|
|
7
|
+
<br />
|
|
8
|
+
<a href="https://github.com/borisrakovan/sandly/actions/workflows/ci.yml"><img src="https://github.com/borisrakovan/sandly/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
9
|
+
<a href="https://github.com/borisrakovan/sandly/blob/main/LICENSE"><img src="https://img.shields.io/github/license/borisrakovan/sandly" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/borisrakovan/sandly"><img src="https://img.shields.io/github/stars/borisrakovan/sandly?style=social" alt="GitHub stars"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
Type-safe dependency injection for TypeScript, without decorators or reflection.
|
|
15
|
+
</p>
|
|
4
16
|
|
|
5
17
|
## Why Sandly?
|
|
6
18
|
|
|
@@ -40,6 +52,7 @@ const orders = await container.resolve(OrderService);
|
|
|
40
52
|
|
|
41
53
|
- **Compile-time safety**: TypeScript catches missing dependencies before runtime
|
|
42
54
|
- **No decorators**: Works with standard TypeScript, no experimental features
|
|
55
|
+
- **Inject anything**: Classes, objects, primitives, or functions
|
|
43
56
|
- **Async support**: Factories and cleanup functions can be async
|
|
44
57
|
- **Composable layers**: Organize dependencies into reusable modules
|
|
45
58
|
- **Scoped containers**: Hierarchical dependency management for web servers
|
|
@@ -183,19 +196,23 @@ const cacheLayer = Layer.create({
|
|
|
183
196
|
});
|
|
184
197
|
```
|
|
185
198
|
|
|
186
|
-
Compose layers with `provide()
|
|
199
|
+
Compose layers with `provide()` and `merge()`:
|
|
187
200
|
|
|
188
201
|
```typescript
|
|
189
202
|
// provide: satisfy dependencies, expose only this layer's provisions
|
|
190
203
|
const appLayer = userLayer.provide(dbLayer);
|
|
191
204
|
|
|
192
|
-
// merge: combine
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
// merge: combine layers, exposing both provisions. Internally satisfied
|
|
206
|
+
// requirements are subtracted from the result's requirements.
|
|
207
|
+
const infraLayer = dbLayer.merge(loggerLayer);
|
|
208
|
+
|
|
209
|
+
// merge wires dependencies in either direction. dbLayer requires Config and
|
|
210
|
+
// configLayer provides it - the result has no outstanding requirements.
|
|
211
|
+
const fullInfra = dbLayer.merge(configLayer);
|
|
196
212
|
|
|
197
|
-
//
|
|
198
|
-
const
|
|
213
|
+
// Static helpers for two or more layers
|
|
214
|
+
const appInfra = Layer.merge(dbLayer, loggerLayer);
|
|
215
|
+
const infra = Layer.mergeAll(dbLayer, loggerLayer, cacheLayer);
|
|
199
216
|
```
|
|
200
217
|
|
|
201
218
|
### Scoped Containers
|
|
@@ -483,11 +500,10 @@ try {
|
|
|
483
500
|
| `Layer.mock(tag, implementation)` | Create layer with mock (partial for ServiceTag) |
|
|
484
501
|
| `Layer.create({ requires, apply })` | Create custom layer |
|
|
485
502
|
| `Layer.empty()` | Create empty layer |
|
|
486
|
-
| `Layer.merge(a, b)` | Merge two layers
|
|
487
|
-
| `Layer.mergeAll(...layers)` | Merge multiple layers
|
|
488
|
-
| `layer.provide(dep)` | Satisfy dependencies
|
|
489
|
-
| `layer.
|
|
490
|
-
| `layer.merge(other)` | Merge with another layer |
|
|
503
|
+
| `Layer.merge(a, b)` | Merge two layers (smart subtraction) |
|
|
504
|
+
| `Layer.mergeAll(...layers)` | Merge multiple layers (smart subtraction) |
|
|
505
|
+
| `layer.provide(dep)` | Satisfy dependencies, expose only target's |
|
|
506
|
+
| `layer.merge(other)` | Merge layers, expose both, subtract satisfied |
|
|
491
507
|
|
|
492
508
|
### ScopedContainer
|
|
493
509
|
|
package/dist/index.d.ts
CHANGED
|
@@ -165,9 +165,16 @@ type WithBuilderTags<TBuilder, TNewTags extends AnyTag> = TBuilder extends Scope
|
|
|
165
165
|
* - A ServiceTag (class) whose instances are assignable to T
|
|
166
166
|
* - A ValueTag whose value type is assignable to T
|
|
167
167
|
* - A raw value of type T
|
|
168
|
+
*
|
|
169
|
+
* The conditional uses `[T] extends [object]` (tuple-wrapped) to prevent
|
|
170
|
+
* distribution over union types. Without this, a parameter typed as a
|
|
171
|
+
* string union (e.g. `'SANDBOX' | 'PRODUCTION'`) would distribute into
|
|
172
|
+
* `ValueTag<any, 'SANDBOX'> | ValueTag<any, 'PRODUCTION'>`, and a
|
|
173
|
+
* `ValueTag<any, 'SANDBOX' | 'PRODUCTION'>` would not be assignable to
|
|
174
|
+
* either branch (ValueTag is invariant in its value parameter).
|
|
168
175
|
* @internal
|
|
169
176
|
*/
|
|
170
|
-
type ValidDepFor<T> = (T extends object ? ServiceTag<T> | ValueTag<any, T> : ValueTag<any, T>) | T;
|
|
177
|
+
type ValidDepFor<T> = ([T] extends [object] ? ServiceTag<T> | ValueTag<any, T> : ValueTag<any, T>) | T;
|
|
171
178
|
/**
|
|
172
179
|
* Maps constructor parameters to valid dependency types.
|
|
173
180
|
* Each parameter type T becomes ValidDepFor<T>.
|
|
@@ -239,6 +246,9 @@ interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
|
|
|
239
246
|
* Provides a dependency layer to this layer, creating a pipeline.
|
|
240
247
|
* The result only exposes this layer's provisions (not the dependency's).
|
|
241
248
|
*
|
|
249
|
+
* Requirements satisfied internally (by either layer's provisions) are
|
|
250
|
+
* subtracted from the result's requirements.
|
|
251
|
+
*
|
|
242
252
|
* @example
|
|
243
253
|
* ```typescript
|
|
244
254
|
* const appLayer = apiLayer
|
|
@@ -246,28 +256,26 @@ interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
|
|
|
246
256
|
* .provide(databaseLayer);
|
|
247
257
|
* ```
|
|
248
258
|
*/
|
|
249
|
-
provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<
|
|
259
|
+
provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<Exclude<TRequires | TDepRequires, TProvides | TDepProvides>, TProvides>;
|
|
250
260
|
/**
|
|
251
|
-
*
|
|
252
|
-
* Unlike `.provide()`, this exposes both this layer's and the dependency's provisions.
|
|
261
|
+
* Merges this layer with another layer, exposing both layers' provisions.
|
|
253
262
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* ```
|
|
259
|
-
*/
|
|
260
|
-
provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
|
|
261
|
-
/**
|
|
262
|
-
* Merges this layer with another independent layer.
|
|
263
|
-
* Combines their requirements and provisions.
|
|
263
|
+
* Requirements satisfied internally (by either layer's provisions) are
|
|
264
|
+
* subtracted from the result's requirements. This means a merge of two
|
|
265
|
+
* layers where one provides what the other requires produces a layer with
|
|
266
|
+
* those requirements already satisfied.
|
|
264
267
|
*
|
|
265
268
|
* @example
|
|
266
269
|
* ```typescript
|
|
270
|
+
* // Independent layers
|
|
267
271
|
* const infraLayer = persistenceLayer.merge(loggingLayer);
|
|
272
|
+
*
|
|
273
|
+
* // Self-satisfying merge: dbLayer requires Config, configLayer provides it
|
|
274
|
+
* const fullInfra = dbLayer.merge(configLayer);
|
|
275
|
+
* // Result: requires nothing, provides Database | Config
|
|
268
276
|
* ```
|
|
269
277
|
*/
|
|
270
|
-
merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
|
|
278
|
+
merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<Exclude<TRequires | TOtherRequires, TProvides | TOtherProvides>, TProvides | TOtherProvides>;
|
|
271
279
|
}
|
|
272
280
|
/**
|
|
273
281
|
* Consolidated Layer API for creating and composing dependency layers.
|
|
@@ -443,8 +451,11 @@ declare const Layer: {
|
|
|
443
451
|
/**
|
|
444
452
|
* Merges multiple layers at once.
|
|
445
453
|
*
|
|
454
|
+
* Requirements satisfied internally (by any merged layer's provisions)
|
|
455
|
+
* are subtracted from the result's requirements.
|
|
456
|
+
*
|
|
446
457
|
* @param layers - At least 2 layers to merge
|
|
447
|
-
* @returns A layer combining all requirements
|
|
458
|
+
* @returns A layer combining all provisions, with internally-satisfied requirements removed
|
|
448
459
|
*
|
|
449
460
|
* @example
|
|
450
461
|
* ```typescript
|
|
@@ -455,12 +466,12 @@ declare const Layer: {
|
|
|
455
466
|
* );
|
|
456
467
|
* ```
|
|
457
468
|
*/
|
|
458
|
-
mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
|
|
469
|
+
mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<Exclude<UnionOfRequires<T>, UnionOfProvides<T>>, UnionOfProvides<T>>;
|
|
459
470
|
/**
|
|
460
471
|
* Merges exactly two layers.
|
|
461
472
|
* Equivalent to `layer1.merge(layer2)`.
|
|
462
473
|
*/
|
|
463
|
-
merge<TRequires1 extends AnyTag, TProvides1 extends AnyTag, TRequires2 extends AnyTag, TProvides2 extends AnyTag>(layer1: Layer<TRequires1, TProvides1>, layer2: Layer<TRequires2, TProvides2>): Layer<TRequires1 | TRequires2, TProvides1 | TProvides2>;
|
|
474
|
+
merge<TRequires1 extends AnyTag, TProvides1 extends AnyTag, TRequires2 extends AnyTag, TProvides2 extends AnyTag>(layer1: Layer<TRequires1, TProvides1>, layer2: Layer<TRequires2, TProvides2>): Layer<Exclude<TRequires1 | TRequires2, TProvides1 | TProvides2>, TProvides1 | TProvides2>;
|
|
464
475
|
};
|
|
465
476
|
|
|
466
477
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -793,9 +793,6 @@ function createLayer(applyFn) {
|
|
|
793
793
|
provide(dependency) {
|
|
794
794
|
return createProvidedLayer(dependency, layerImpl);
|
|
795
795
|
},
|
|
796
|
-
provideMerge(dependency) {
|
|
797
|
-
return createComposedLayer(dependency, layerImpl);
|
|
798
|
-
},
|
|
799
796
|
merge(other) {
|
|
800
797
|
return createMergedLayer(layerImpl, other);
|
|
801
798
|
}
|
|
@@ -804,52 +801,38 @@ function createLayer(applyFn) {
|
|
|
804
801
|
}
|
|
805
802
|
/**
|
|
806
803
|
* Creates a layer that only exposes the target's provisions.
|
|
804
|
+
*
|
|
805
|
+
* Runtime is identical to merge: both layers' factories are added to the
|
|
806
|
+
* builder, and resolutions across them work transparently. Only the result
|
|
807
|
+
* type narrows provisions to the target's.
|
|
807
808
|
* @internal
|
|
808
809
|
*/
|
|
809
810
|
function createProvidedLayer(dependency, target) {
|
|
810
|
-
return
|
|
811
|
+
return createMergedLayer(dependency, target);
|
|
811
812
|
}
|
|
812
813
|
/**
|
|
813
|
-
* Creates a
|
|
814
|
-
*
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const withDep = dependency.apply(builder);
|
|
820
|
-
return target.apply(withDep);
|
|
821
|
-
},
|
|
822
|
-
provide(dep) {
|
|
823
|
-
return createProvidedLayer(dep, this);
|
|
824
|
-
},
|
|
825
|
-
provideMerge(dep) {
|
|
826
|
-
return createComposedLayer(dep, this);
|
|
827
|
-
},
|
|
828
|
-
merge(other) {
|
|
829
|
-
return createMergedLayer(this, other);
|
|
830
|
-
}
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Creates a merged layer from two independent layers.
|
|
814
|
+
* Creates a merged layer from two layers, exposing both layers' provisions.
|
|
815
|
+
*
|
|
816
|
+
* Requirements satisfied internally (by either layer's provisions) are
|
|
817
|
+
* subtracted from the result's requirements at the type level. The runtime
|
|
818
|
+
* behavior is unchanged - both layers' factories are added to the builder
|
|
819
|
+
* and resolutions are wired transparently in either direction.
|
|
835
820
|
* @internal
|
|
836
821
|
*/
|
|
837
822
|
function createMergedLayer(layer1, layer2) {
|
|
838
|
-
|
|
823
|
+
const merged = {
|
|
839
824
|
apply: (builder) => {
|
|
840
825
|
const with1 = layer1.apply(builder);
|
|
841
826
|
return layer2.apply(with1);
|
|
842
827
|
},
|
|
843
828
|
provide(dep) {
|
|
844
|
-
return createProvidedLayer(dep,
|
|
845
|
-
},
|
|
846
|
-
provideMerge(dep) {
|
|
847
|
-
return createComposedLayer(dep, this);
|
|
829
|
+
return createProvidedLayer(dep, merged);
|
|
848
830
|
},
|
|
849
831
|
merge(other) {
|
|
850
|
-
return createMergedLayer(
|
|
832
|
+
return createMergedLayer(merged, other);
|
|
851
833
|
}
|
|
852
834
|
};
|
|
835
|
+
return merged;
|
|
853
836
|
}
|
|
854
837
|
/**
|
|
855
838
|
* Consolidated Layer API for creating and composing dependency layers.
|
|
@@ -907,9 +890,6 @@ const Layer = {
|
|
|
907
890
|
provide(dep) {
|
|
908
891
|
return createProvidedLayer(dep, layer);
|
|
909
892
|
},
|
|
910
|
-
provideMerge(dep) {
|
|
911
|
-
return createComposedLayer(dep, layer);
|
|
912
|
-
},
|
|
913
893
|
merge(other) {
|
|
914
894
|
return createMergedLayer(layer, other);
|
|
915
895
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandly",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"typescript",
|
|
6
6
|
"sandly",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"lint:check": "eslint . --max-warnings=0",
|
|
50
50
|
"type:check": "tsc --noEmit",
|
|
51
51
|
"test": "vitest run",
|
|
52
|
+
"test:coverage": "vitest run --coverage",
|
|
52
53
|
"tag": "git tag v$(node -p \"require('./package.json').version\") && git push --tags",
|
|
53
54
|
"release": "changeset version && changeset publish"
|
|
54
55
|
},
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"@changesets/cli": "^2.29.5",
|
|
57
58
|
"@eslint/js": "^9.26.0",
|
|
58
59
|
"@types/node": "^22.15.3",
|
|
60
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
59
61
|
"eslint": "^9.26.0",
|
|
60
62
|
"prettier": "^3.5.3",
|
|
61
63
|
"prettier-plugin-organize-imports": "^4.1.0",
|