feat(di)!: an import's needs travel without being re-declared - #87
Merged
Conversation
NeedsGate now reads a module's OWN providers alone. A port one of them depends on and nothing here satisfies still has to be named in needs — so a provider can never silently receive a service from whoever composed the module, which is what #50 was about. What is dropped is the re-declaration at every hop: an import's own unmet needs are already published in its type, at the imports entry a reader is looking at, and start still refuses a root that has not discharged them. Measured on this repo: 12 of 22 declarations were pure propagation. Dropping them leaves exactly the modules that read the port — DatabaseModule says needs: [Env] because it reads DATABASE_URL, and the persistence modules and slices that import it say nothing. That is ConfigModule.forFeature's shape, reached without a global. The Needs CHANNEL is unchanged and stays wider than what the gate asks a module to declare: everything outstanding, however it got there.
Twelve declarations go: the three roots, both persistence modules, and the four slices that only inherited what they named. What is left is what actually reads something — DatabaseModule and observability() and each starter for Env, AuditSlice and NotificationsSlice and OrdersSlice and the two stand-in services for Logger, the application modules for their repositories. order-amqp-worker's root keeps needs: [Env], and the reason is the rule working: it provides relayConfig itself, so Env is its own provider's need rather than one inherited from the slices below. Three needs-gate negatives move back to start — the starter's port is owed by an import — and order-application's gains one that separates the two gates: a module whose OWN providers read the repositories, declaring nothing.
The rule in packages/di/CLAUDE.md, modules-and-privacy, compile-time-wiring and reference/di/modules, the needs note on the three starter sugars, the slices bullet and the gate inventory in the root CLAUDE.md, and 38 samples that were declaring what they only inherited. The two example pages describing a starter-port omission as di's declaration gate go back to describing it as start's needs channel, which is what it is again: the port is owed by an import.
There was a problem hiding this comment.
Pull request overview
This PR refines @btravstack/di’s NeedsGate so it only enforces declarations for ports required by a module’s own providers, while allowing an imported module’s unmet needs to propagate outward without being re-declared at every hop. This reduces noisy needs: [Env] boilerplate across the repo while preserving the invariant that no provider can silently consume a service supplied only by an ancestor.
Changes:
- Update
NeedsGateto check only unmet dependencies from the module’s ownprovideslist (not imported modules’ needs). - Remove now-redundant
needs: [Env]declarations throughout starters, examples, and fixtures; update gate/type tests accordingly. - Refresh documentation and changeset text to describe the new “needs travel through imports” model.
Reviewed changes
Copilot reviewed 59 out of 59 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Removes redundant Env import/needs from root example. |
| packages/temporal/src/test-fixtures.ts | Drops propagated needs: [Env] in Temporal test apps. |
| packages/temporal/README.md | Updates worked example to omit redundant needs: [Env]. |
| packages/temporal/CLAUDE.md | Clarifies needs applies to root’s own providers only. |
| packages/observability/src/test-fixtures.ts | Removes propagated needs: [Env] from fixture modules. |
| packages/observability/README.md | Updates example root to omit redundant needs: [Env]. |
| packages/http/src/test-fixtures.ts | Removes propagated needs: [Env] from HTTP fixtures. |
| packages/http/README.md | Updates worked examples to omit redundant needs: [Env]. |
| packages/http/CLAUDE.md | Aligns docs with “imports’ needs travel” rule. |
| packages/di/src/module.ts | Implements the narrower NeedsGate and updates docs. |
| packages/di/src/module.test-d.ts | Adds a type test proving import needs propagate. |
| packages/di/CLAUDE.md | Updates “Module visibility” spec to new gate behavior. |
| packages/core/src/test-fixtures.ts | Adds rationale comment; keeps needs: [Env] where required. |
| packages/config/README.md | Fixes sample import to include Env where used. |
| packages/amqp/src/test-fixtures.ts | Drops propagated needs: [Env] in AMQP fixtures. |
| packages/amqp/src/amqp-runtime.test-d.ts | Moves negative from declaration gate to start gate. |
| packages/amqp/CLAUDE.md | Updates needs explanation to “own providers only”. |
| examples/order-temporal-worker/src/test-fixtures.ts | Removes propagated needs: [Env] in test roots. |
| examples/order-temporal-worker/src/slices/fulfillment/module.ts | Removes propagated needs from slice module. |
| examples/order-temporal-worker/src/slices/billing/module.ts | Removes propagated needs from slice module. |
| examples/order-temporal-worker/src/needs-gate.test-d.ts | Updates negatives to reflect new gate division. |
| examples/order-temporal-worker/src/module.ts | Removes redundant needs: [Env] from worker root. |
| examples/order-temporal-worker/README.md | Updates snippet to omit redundant needs: [Env]. |
| examples/order-infrastructure/src/module.ts | Keeps Env declaration at DatabaseModule; removes propagation. |
| examples/order-infrastructure/README.md | Removes redundant needs: [Env] from importers. |
| examples/order-application/src/needs-gate.test-d.ts | Adjusts negatives to use Module.scoped vs declaration gate. |
| examples/order-api/src/test-fixtures.ts | Drops propagated needs: [Env] in stub APIs. |
| examples/order-api/src/slices/orders/module.ts | Narrows slice needs to Logger only. |
| examples/order-api/src/slices/customers/module.ts | Removes propagated needs: [Env] from customers slice. |
| examples/order-api/src/needs-gate.test-d.ts | Updates negative to fail at start for starter port needs. |
| examples/order-api/src/module.ts | Removes redundant needs: [Env] from API root. |
| examples/order-api/README.md | Updates snippets for new “needs travel” model. |
| examples/order-amqp-worker/src/test-fixtures.ts | Keeps needs: [Env] where root provides env-bound config. |
| examples/order-amqp-worker/src/needs-gate.test-d.ts | Updates negatives to reflect kernel vs di gate split. |
| examples/order-amqp-worker/src/module.ts | Clarifies why this root still declares Env. |
| docs/tutorial/second-runtime.md | Removes redundant Env/needs in tutorial root. |
| docs/tutorial/getting-started.md | Removes redundant Env/needs in tutorial root. |
| docs/reference/temporal.md | Updates reference snippet to omit redundant needs: [Env]. |
| docs/reference/observability.md | Updates reference snippet to omit redundant needs: [Env]. |
| docs/reference/http.md | Updates reference snippet to omit redundant needs: [Env]. |
| docs/reference/di/modules.md | Updates needs semantics and declaration-gate explanation. |
| docs/reference/config.md | Fixes sample import to include Env where used. |
| docs/index.md | Removes redundant Env/needs from landing page sample. |
| docs/how-to/test-an-application.md | Removes redundant needs: [Env] from testing recipe. |
| docs/how-to/split-a-worker-into-slices.md | Updates slice examples to omit propagated needs. |
| docs/how-to/split-a-router-into-controllers.md | Updates slice snippet to omit propagated Env need. |
| docs/how-to/serve-orpc-over-http.md | Removes redundant Env/needs from composition-root step. |
| docs/how-to/run-a-temporal-worker.md | Removes redundant Env/needs from worker root examples. |
| docs/how-to/protect-a-procedure.md | Removes redundant needs: [Env] from protected root snippet. |
| docs/how-to/open-a-per-request-scope.md | Removes redundant needs: [Env] from example module. |
| docs/how-to/log-and-correlate.md | Removes redundant Env/needs from logging recipes. |
| docs/explanation/starters.md | Updates starter example to omit redundant needs: [Env]. |
| docs/explanation/modules-and-privacy.md | Updates explanation to reflect new propagation model. |
| docs/explanation/compile-time-wiring.md | Updates narrative to “own providers” + import propagation. |
| docs/examples/order-temporal-worker.md | Updates example snippets to omit redundant needs: [Env]. |
| docs/examples/order-api.md | Updates example snippets and gate explanation for new model. |
| docs/examples/order-amqp-worker.md | Updates negative example to fail at start via Needs channel. |
| CLAUDE.md | Updates repo-wide guidance on which gate catches starter-port needs. |
| .changeset/declared-module-needs.md | Updates release note text to match narrowed gate semantics. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`needs` was described as not making the port available inside the module, which reads as "a provider cannot use it" — the opposite of what it is for. A provider may depend on a declared need and is handed whatever an ancestor supplies; what declaring does not do is provide the port locally, so it stays outside `Available` and is still not exportable. The three `OrdersSlice` samples still said the environment and the logger were "both named here" while the code beside them named only `Logger`: the sweep rewrote the value and left the comment. They now say which is which and why.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-on to #86, which shipped the
needsgate one notch too strict.The change
NeedsGatenow reads a module's own providers alone. A port one of themdepends on and nothing here satisfies still has to be named — so a provider can
never silently receive a service from whoever composed the module, which is
what #50 was about. What is dropped is the re-declaration at every hop.
An import's own unmet needs are already published in its type, at the
importsentry a reader is looking at, and
startstill refuses a root that has notdischarged them. Restating them bought nothing but lines.
Measured
12 of the 22 declarations in the repo were pure propagation. What is left is
what actually reads something:
DatabaseModule [Env]DATABASE_URLobservability() [Env], each starter[Env]AuditSlice,NotificationsSlice,OrdersSlice[Logger]OrderApplicationModule [OrderRepository, Logger]That is
ConfigModule.forFeature's shape reached without a global — which isthe point
Envmade: six declarations inorder-apibefore, one of them amodule that reads an environment variable.
order-amqp-worker's root keepsneeds: [Env], and that is the ruleworking rather than an exception: it provides
relayConfigitself, soEnvisits own provider's need and not one inherited from the slices below.
What moved back
Three
needs-gate.test-d.tsnegatives return tostart: a starter's port isowed by an import, so di's declaration gate has nothing to say about it.
order-application's gate test gains one that separates the two — a modulewhose OWN providers read the repositories, declaring nothing — so both
mechanisms stay pinned side by side.
Unmet(theNeedschannel) is unchanged and stays deliberately wider thanwhat the gate asks a module to declare: everything outstanding, however it got
there.
Gate
All six green from the root:
format --check,lint,typecheck(31/31),knip,test(30/30),build(10/10). The changeset from #86 is rewrittenrather than added to, since neither has been released.