From 2a72f43ce2139b14773b3ea8aff3d548c95c2365 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 9 Aug 2026 23:35:12 +0200 Subject: [PATCH] chore: make 0.1.0 the first published version --- .changeset/fresh-pears-smoke.md | 11 ----- .changeset/no-release-first-publish.md | 6 +++ packages/di/CHANGELOG.md | 67 ++++++++++++++++++++++++++ packages/di/LICENSE | 21 ++++++++ packages/di/README.md | 18 ++++++- 5 files changed, 110 insertions(+), 13 deletions(-) delete mode 100644 .changeset/fresh-pears-smoke.md create mode 100644 .changeset/no-release-first-publish.md create mode 100644 packages/di/CHANGELOG.md create mode 100644 packages/di/LICENSE diff --git a/.changeset/fresh-pears-smoke.md b/.changeset/fresh-pears-smoke.md deleted file mode 100644 index 4d9ac49..0000000 --- a/.changeset/fresh-pears-smoke.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@btravstack/di": patch ---- - -Fix `TS4020` in consumers that export a port. `export class OrderRepository extends -Port("OrderRepository") {}` — the pattern the README teaches — could not emit -declarations: the emitter had no name for the heritage expression's type, so it expanded -down to `PortInstance`'s module-private `unique symbol` brands and reported "has or is -using private name 'ID'". `PortClass` and `ManyPortClass` are now exported as types, which -gives the emitter a name to stop at. The brand symbols themselves stay unexported, so port -identity remains nominal and a port instance remains unforgeable. diff --git a/.changeset/no-release-first-publish.md b/.changeset/no-release-first-publish.md new file mode 100644 index 0000000..c8516e0 --- /dev/null +++ b/.changeset/no-release-first-publish.md @@ -0,0 +1,6 @@ +--- +--- + +Release plumbing only, deliberately no bump: the 0.1.0 CHANGELOG is written by +hand because the changeset it replaces described a fix between two states that +were never published. diff --git a/packages/di/CHANGELOG.md b/packages/di/CHANGELOG.md new file mode 100644 index 0000000..b7469ea --- /dev/null +++ b/packages/di/CHANGELOG.md @@ -0,0 +1,67 @@ +# @btravstack/di + +## 0.1.0 + +Initial release. + +A module-based dependency-injection container for TypeScript. **Ports** are the +vocabulary an application defines for what it needs, **providers** bind a port to +one concrete construction at a single edge, and **modules** group providers while +declaring what they import and what they let anyone else see. Every fallible +construction returns an `unthrown` `Result` rather than throwing. + +Wiring mistakes are compile errors — a missing dependency, an internal port +leaking out of a module, a re-export of something never imported. The two that +types cannot catch, a cycle and two providers registered for the same port, are +raised as defects before any factory runs. + +### The surface + +- **`Port(id)`** declares a port as a nominal token: + `class OrderRepository extends Port("OrderRepository") {}`. Identity is + the token, not the shape, so two ports with identical services stay distinct. + `Port.many(id)` declares a set port that several providers contribute + to — a plugin registry, a list of health checks — and reading it returns every + contribution, accumulated across module boundaries. +- **`Provider(port)(deps?, options)`** binds one port. The options literal picks + exactly one of five mutually exclusive arms — `value`, `sync`, `make`, `class`, + or `acquire` + `release` — and supplying more than one is a compile error. + Every arm also takes optional `onStart` / `onStop` hooks, fired once the whole + graph has constructed and during teardown. `Provider.member(port)` contributes + to a set port. +- **`Module(name)({ imports, provides, exports })`** groups providers. Anything + not exported is private to the module even though the built container is a + single flat map at runtime, and the privacy is enforced at compile time. + `Module.build` builds a graph that needs nothing resourceful, `Module.scoped` + opens a scope for one that does, and `Module.forkScope` layers a short-lived + scope over an already-built parent — per-request services that must not + outlive the request but may read what the parent constructed. +- **`Context`** is the built graph: `ctx.get(port)` returns the service, typed + from the port alone. +- Types: `AnyPort`, `ServiceOf`, `ScopedOptions`, and `Scope`. `PortClass` and + `ManyPortClass` are exported for declaration emit — a consumer compiling with + `declaration: true` and exporting a port needs them nameable — not because + either is meant to be written by hand. + +### What it guarantees + +- **An unmet dependency does not compile.** Every requirement a graph has not + discharged shows up in its `Needs`, and the build call's arity gate rejects it + with an `UNSATISFIED DEPENDENCIES` parameter naming what is missing. +- **A resourceful graph cannot be built without a scope.** `Scope` stays in + `Needs` for any provider with `acquire`/`release` or an `onStop`, and + `Module.scoped` is the only entry point that discharges it. Passing such a + graph to `Module.build` is a compile error, not a runtime leak. +- **Teardown is ordered and survives partial failure.** Finalisers run in + reverse acquisition order, and a graph that fails half-constructed unwinds + exactly what it managed to acquire — on success, on failure, and on the + mid-graph case — before the call resolves. +- **Errors are values.** A failing construction is an `unthrown` `Result` in the + module's own error channel. A wiring mistake is a defect on the separate + channel, because it is a bug rather than an outcome. +- **Port identity is unforgeable.** The brand symbols behind a port are never + exported, so no hand-written object can pass itself off as a port instance. + +### Peer dependency + +`unthrown` (`^5.0.0`) — install it alongside. diff --git a/packages/di/LICENSE b/packages/di/LICENSE new file mode 100644 index 0000000..5892fcc --- /dev/null +++ b/packages/di/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Benoit Travers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/di/README.md b/packages/di/README.md index f520a47..a03e959 100644 --- a/packages/di/README.md +++ b/packages/di/README.md @@ -207,14 +207,28 @@ pnpm add @btravstack/di unthrown ## Public surface ```ts -export { Port, Scope } from "./port.js"; -export type { AnyPort, ServiceOf } from "./port.js"; +export { Port } from "./port.js"; +export type { + AnyPort, + ManyPortClass, + PortClass, + Scope, + ServiceOf, +} from "./port.js"; export { Context } from "./context.js"; export { Provider } from "./provider.js"; export { Module } from "./module.js"; export type { ScopedOptions } from "./build.js"; ``` +`Scope` is a **type** only. Every legitimate use of it is a type position, and +the class value is what would let you write `Provider(Scope)(…)` or widen it to +`AnyPort` — the two ways past the guard. `PortClass` and `ManyPortClass` are +exported so that a consumer compiling with `declaration: true` can export a port +of its own; without them the emitter reaches the module-private brand symbols +and fails with TS4020. The symbols stay unexported, so port instances remain +unforgeable. + Everything else — `unsafeAdd`, `flatten`, `plan`, `run`, `runScoped`, `createScope`, `constructLevel`, `WiringDefect`, and the handful of type-level helpers (`ServicesOf`, `NeedsOf`, `PortInstance`, `Hooks`, …) that exist to make the four