sandly 2.1.0 → 3.0.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 CHANGED
@@ -1,6 +1,18 @@
1
- # Sandly
2
-
3
- Sandly ("Services And Layers") is a type-safe dependency injection library for TypeScript. No decorators, no runtime reflection, just compile-time safety that catches errors before your code runs.
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()`, `provideMerge()`, and `merge()`:
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 independent layers
193
- const infraLayer = Layer.merge(dbLayer, loggerLayer);
194
- // or
195
- const infraLayer = Layer.mergeAll(dbLayer, loggerLayer, cacheLayer);
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
- // provideMerge: satisfy dependencies and expose both layers
198
- const fullLayer = userLayer.provideMerge(dbLayer);
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.provideMerge(dep)` | Satisfy and merge provisions |
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
@@ -239,6 +239,9 @@ interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
239
239
  * Provides a dependency layer to this layer, creating a pipeline.
240
240
  * The result only exposes this layer's provisions (not the dependency's).
241
241
  *
242
+ * Requirements satisfied internally (by either layer's provisions) are
243
+ * subtracted from the result's requirements.
244
+ *
242
245
  * @example
243
246
  * ```typescript
244
247
  * const appLayer = apiLayer
@@ -246,28 +249,26 @@ interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
246
249
  * .provide(databaseLayer);
247
250
  * ```
248
251
  */
249
- provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
252
+ provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<Exclude<TRequires | TDepRequires, TProvides | TDepProvides>, TProvides>;
250
253
  /**
251
- * Provides a dependency layer and merges both layers' provisions.
252
- * Unlike `.provide()`, this exposes both this layer's and the dependency's provisions.
254
+ * Merges this layer with another layer, exposing both layers' provisions.
253
255
  *
254
- * @example
255
- * ```typescript
256
- * const infraLayer = dbLayer.provideMerge(configLayer);
257
- * // Provides both Database and Config
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.
256
+ * Requirements satisfied internally (by either layer's provisions) are
257
+ * subtracted from the result's requirements. This means a merge of two
258
+ * layers where one provides what the other requires produces a layer with
259
+ * those requirements already satisfied.
264
260
  *
265
261
  * @example
266
262
  * ```typescript
263
+ * // Independent layers
267
264
  * const infraLayer = persistenceLayer.merge(loggingLayer);
265
+ *
266
+ * // Self-satisfying merge: dbLayer requires Config, configLayer provides it
267
+ * const fullInfra = dbLayer.merge(configLayer);
268
+ * // Result: requires nothing, provides Database | Config
268
269
  * ```
269
270
  */
270
- merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
271
+ merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<Exclude<TRequires | TOtherRequires, TProvides | TOtherProvides>, TProvides | TOtherProvides>;
271
272
  }
272
273
  /**
273
274
  * Consolidated Layer API for creating and composing dependency layers.
@@ -443,8 +444,11 @@ declare const Layer: {
443
444
  /**
444
445
  * Merges multiple layers at once.
445
446
  *
447
+ * Requirements satisfied internally (by any merged layer's provisions)
448
+ * are subtracted from the result's requirements.
449
+ *
446
450
  * @param layers - At least 2 layers to merge
447
- * @returns A layer combining all requirements and provisions
451
+ * @returns A layer combining all provisions, with internally-satisfied requirements removed
448
452
  *
449
453
  * @example
450
454
  * ```typescript
@@ -455,12 +459,12 @@ declare const Layer: {
455
459
  * );
456
460
  * ```
457
461
  */
458
- mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
462
+ mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<Exclude<UnionOfRequires<T>, UnionOfProvides<T>>, UnionOfProvides<T>>;
459
463
  /**
460
464
  * Merges exactly two layers.
461
465
  * Equivalent to `layer1.merge(layer2)`.
462
466
  */
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>;
467
+ 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
468
  };
465
469
 
466
470
  //#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 createComposedLayer(dependency, target);
811
+ return createMergedLayer(dependency, target);
811
812
  }
812
813
  /**
813
- * Creates a composed layer that exposes both layers' provisions.
814
- * @internal
815
- */
816
- function createComposedLayer(dependency, target) {
817
- return {
818
- apply: (builder) => {
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
- return {
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, this);
845
- },
846
- provideMerge(dep) {
847
- return createComposedLayer(dep, this);
829
+ return createProvidedLayer(dep, merged);
848
830
  },
849
831
  merge(other) {
850
- return createMergedLayer(this, other);
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": "2.1.0",
3
+ "version": "3.0.0",
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",