Update Inversify to version 8 - #561
Merged
Merged
Conversation
spoenemann
approved these changes
Sep 1, 2026
spoenemann
left a comment
Contributor
There was a problem hiding this comment.
Thank you very much! A huge change with lots of consequences, but we need to stay up-to-date.
| * InversifyJS 8 offers no way to reach the container from a resolution, so a contextual binding | ||
| * cannot be provided by a per-action child container any more. | ||
| */ | ||
| const ACTION_HOLDER = Symbol('ActionHolder'); |
Contributor
There was a problem hiding this comment.
Should it be Symbol.for?
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.
InversifyJS 8 is a rewrite split across
@inversifyjs/{common,container,core}; theinversifypackage is now a thin re-export barrel. This updates all Sprotty packages to~8.2and migrates the codebase.Breaking changes for downstream code
Two of these fail silently — no compiler error, no DI error, just
undefineddependencies at first use.@injectFromBase(). InversifyJS 8 no longer passes injection metadata down to subclasses. A subclass of a Sprotty class with injected members that lacks the decorator resolves with its inherited dependenciesundefinedand raises no error. If the base class declares constructor parameters that are not injected, they now need@unmanaged(), because@injectFromBase()eagerly validates the base class metadata.isInjectableremoved from the public API (see "Design decisions" below).TYPES.Actionis now bound in the container that registers commands rather than in a per-action child container, so it resolves toundefinedwhile no command is being created.TYPES.IVieweris now bound in the main container withwhenParentIsconstraints rather than in child containers.Changelog entries covering these, plus the InversifyJS-level changes that affect any application with a
di.config.ts, are in this PR.Mechanical migration
interfacesnamespace → top-level type exportsContainerModulecallback: positional args → single options objecttoProvider→toFactory(type argument moves tobind<T>, and is the provider type)ctx.container.get(...)→ctx.get(...)ctx.container.isBound(x) ? get : undefined→ctx.get(x, { optional: true })injectable()(Cls)→decorate(injectable(), Cls)(now returnsvoid)@injectFromBase()addedTwo traps worth knowing if you review the diff:
interfaces.Rebindmaps toRebindSync, notRebind— the latter is asynchronous in 8.toFactory's parameter type isT extends Factory<...> ? ... : never. Ifbind<T>names the provided type rather than the provider function type,Tisn't factory-shaped and every callback is rejected asnot assignable to type 'never'.The explicit
import 'reflect-metadata'statements were removed:@inversifyjs/containerside-effect-importsreflect-metadata/liteitself.Design decisions worth reviewing
Sprotty used child containers as a contextual-injection mechanism in two places. InversifyJS 8 removed the tools for that —
createChild(), the mutableContainer.parent, andResolutionContext.container— and there is no path from a module or a resolution back to the owning container. Each needed a different answer.Viewer bindings (
base/di.config.ts) previously built a child container per viewer so twoViewerCacheinstances could get differentIViewerdelegates. Replaced with constrained bindings:whenParentIsmatches on the service identifier that requested the injection, which is what distinguishes the two instances of the same class. Chosen over convertingViewerCache's@injectproperties to constructor parameters plustoResolvedValue, since that would have changed the public shape of an exported class. Both were verified to work; this one leavesviewer-cache.tsuntouched.Command registration (
command-registration.ts) built a child container per dispatched action to bindTYPES.Action. The action only exists per dispatch, so no binding-time construct expresses it. Replaced with a holder bound once per container:It is mutable state, contained to one file behind one unexported symbol, and live only for the duration of a synchronous
get. It is per-container rather than global, because the holder is itself a binding. Save-and-restore rather than clear-to-undefinedbecause property injection runs after the constructor body, so a constructor that dispatched an action would otherwise blank the outer command's fields. Command scope is unchanged: still a fresh instance per dispatch.The alternative was binding each container into itself so a real child container could be created. That needs
container.bind(TYPES.Container).toConstantValue(container)at 36 call sites, cannot be done from inside aContainerModule, and leaves downstream consumers with a runtime failure until they add a self-binding they have never heard of. The holder also avoids allocating a container per dispatched action.isInjectableremoved. It readReflect.getMetadata('inversify:paramtypes', ...), a key InversifyJS 8 never writes, so it returnedfalsefor every class and everyconfigure*utility threw on module load. Reading InversifyJS 8's equivalent key means depending on an internal of@inversifyjs/core, which exports neither the constant nor a public injectability check. Instead theconfigure*utilities now let the binding fail and translate the error:Public API only, no error discrimination needed, and the original error is preserved as
cause. Two consequences:utils/inversify.spec.tsso it isn't mistaken for a bug later.graph/views.spec.tsxwas failing on exactly that — aCircleNodeViewfixture with no decorator and no injected members. It passes now.The message names
@injectFromBaseas well as@injectable, since the former is the failure most people migrating will actually hit.Verification
The examples present in the repo were hand tested to check that everything still works as expected