Skip to content

Migrating from 0.4 to 0.5 ​

0.5.0 renames almost every public name so that a call reads as a sentence, and makes several contracts more explicit. The graph, scoped default, disposal and compile-time checks work as before. This page is the whole path.

1. Run the codemod before you upgrade ​

The codemod is type-aware: it rewrites a call only when the TypeScript checker resolves it to a di-bag declaration, which is how it tells builder.replace from String.prototype.replace. It therefore needs the 0.4.0 types to be installed while it runs. Upgrade afterwards.

sh
npx di-bag-codemod --project tsconfig.json            # dry run: prints what it would change
npx di-bag-codemod --project tsconfig.json --write --report di-bag-codemod-report.json
npm install di-bag@0.5
npx tsc --noEmit

Whatever it could not decide is in the report, with file, line and the reason. It never guesses. What it does not read: Markdown, source code held in strings, and JavaScript without types. A missed removed callable method name still exists at run time as a function that throws DI_BAG_REMOVED_API and names its replacement. Renamed fields, exported types, and import specifiers have no such runtime stub or error-code guarantee; TypeScript, module resolution, and ordinary JavaScript property semantics handle them instead.

2. Changes to review ​

Lifetime names now state their caching boundary. The default remains 'scoped:one-per-container', so existing unmarked providers keep one instance per container. Use 'singleton:one-per-container-tree' for one instance shared by the root and its children, or 'transient:one-per-resolve' for a new instance on every read.

ts
import { DiBag } from 'di-bag';

let nextId = 0;
const clock = DiBag.providerWithLifetime({
  provider: () => ({ now: () => Date.now() }),
  lifetime: 'singleton:one-per-container-tree',
});
const app = DiBag.createBuilder()
  .withServices({ clock, request: () => ({ id: ++nextId }) })
  .buildContainer();
const perRequest = app.createChildContainer();
console.log(perRequest.resolve('clock') === app.resolve('clock'), perRequest.resolve('request') === app.resolve('request'));
await app.close();

That prints true false: the explicit singleton clock is shared, while the unmarked request service uses the scoped default.

A child container replaces only scoped and transient services. Replacing a singleton in a child does not compile, because every other container of the tree would keep the original. Mark the service scoped, or use createIndependentContainer, which may replace anything.

A token is either for one service or for a collection. In 0.4.0 one token could carry a single service and contributions at the same time. Now createToken(symbol).forService<S>() and createToken(symbol).forCollectionOf<Item>() are different tokens, a collection is read with resolveCollection(collectionToken) or by listing the token as a dependency, and DiBag.all, resolveAll and inspectAll are gone. The codemod converts a token that was only ever contributed to. A token used both ways is reported, and you split it by hand into two tokens with distinct symbols.

ts
import { DiBag } from 'di-bag';

const sinks = DiBag.createToken(Symbol('sinks')).forCollectionOf<(line: string) => void>();
const app = DiBag.createBuilder()
  .withCollectionContribution({ collectionToken: sinks, provider: () => (line: string) => console.log(line) })
  .buildContainer();
for (const write of app.resolveCollection(sinks)) write('ready');
await app.close();

Provider helpers use named option bags. createProvider takes the factory and an optional classification object. Source adapters use createProviderFromFunction({ dependencies, factoryFunction }), createProviderFromClass({ dependencies, serviceClass }), and createProviderFromPlugin({ dependencies, pluginDescriptor, factoryReturnKind, isValidPluginOutput }). Provider decorators moved to facade calls such as providerWithDisposal({ provider, disposeService }), providerWithLifetime({ provider, lifetime }), and providerWithTransformedService({ provider, transformService, callbackReceives }). Registration and acquisition metadata now use separate helpers; if an old withMetadata call combined both, compose the two new helpers in the same evaluation order.

There is one entry point. di-bag/node is removed. Import everything from di-bag; the package finds the host's Promise classifier by itself, and DiBag.withConfiguration({ runtime }) still overrides it.

3. Errors ​

A runtime code now names a kind of failure and never a method; the method is in details.operation. Every malformed argument, whatever the call, is DI_BAG_INVALID_ARGUMENT with details: { operation, argument, expected }. If your code branches on error.code, check it against the table of codes below: six old codes were split, and for those the right new code depends on what went wrong, so the codemod reports them instead of rewriting them. DiBagCleanupError is DiBagDisposalError, the startup errors are the service readiness errors, and their cleanup... fields are disposal....

ts
import { DiBag } from 'di-bag';

try {
  DiBag.createProvider(42 as never);
} catch (error) {
  const { operation, argument, expected } = (error as { details: Record<string, unknown> }).details;
  console.error(`${String(operation)}: ${String(argument)} must be ${String(expected)}`);
}

4. Every rename ​

Container, which was Bag ​

0.4.00.5.0
resolveAll(...)by hand: container.resolveCollection(collectionToken); create collectionToken with DiBag.createToken(symbol).forCollectionOf<Item>(), and manually split any mixed single-service and collection uses into tokens with distinct symbols
inspectAll(...)by hand: container.serviceSnapshot(collectionToken); create collectionToken with DiBag.createToken(symbol).forCollectionOf<Item>(), and manually split any mixed single-service and collection uses into tokens with distinct symbols
inspect(...)by hand: container.serviceSnapshot(serviceKey)
inspectGraph(...)by hand: container.graphSnapshot()
createScope(...)by hand: container.createChildContainer(replacedServiceKeys, replacementProviders, { sharedParentServiceKeys }); use container.createChildContainer() or container.createChildContainer({ sharedParentServiceKeys }) when no services are replaced
fork(...)by hand: container.createIndependentContainer(replacedServiceKeys, replacementProviders); use container.createIndependentContainer() when no services are replaced

Builder ​

0.4.00.5.0
buildAndStart(...)by hand: builder.buildContainer().ensureServicesReady(serviceKeys, options); rename signal to abortSignal and timeoutMs to totalTimeoutMs; replace startupOrder with maxConcurrentServiceKeys (omit for parallel, 1 for sequential, or the number); options may be omitted
register(...)by hand: builder.withServices({ key: provider }) or builder.withTokenService(token, provider)
alias(...)by hand: builder.withServiceAlias({ aliasKey, targetServiceKey })
contribute(...)by hand: builder.withCollectionContribution({ collectionToken, provider }); create collectionToken with DiBag.createToken(symbol).forCollectionOf<Item>()
replace(...)withReplacedService(...)
installModule(...)by hand: builder.withInstalledModules([module])
verifyGraph(...)verifyGraphAtCompileTime(...)
buildModule(...)buildModule(...)
build(...)buildContainer(...)

The facade, DiBag ​

0.4.00.5.0
fromFactory(...)by hand: DiBag.createProvider(factory, options); rename acquisitionMode to factoryReturnKind (auto -> auto-detect, raw -> uninspected, nativePromise -> native-promise) and context: 'acquisition' to factoryReceivesContext: true; options may be omitted
fromSyncFactory(...)by hand: DiBag.createProvider(factory, { factoryReturnKind: 'sync-value' }); add factoryReceivesContext: true if the old call used context: 'acquisition'
fromAsyncFactory(...)by hand: DiBag.createProvider(factory, { factoryReturnKind: 'native-promise' }); add factoryReceivesContext: true if the old call used context: 'acquisition'
fromFunction(...)by hand: DiBag.createProviderFromFunction({ dependencies, factoryFunction }); move any acquisitionMode option into this object as factoryReturnKind (auto -> auto-detect, raw -> uninspected, nativePromise -> native-promise)
fromClass(...)by hand: DiBag.createProviderFromClass({ dependencies, serviceClass }); move any acquisitionMode option into this object as factoryReturnKind (auto -> auto-detect, raw -> uninspected, nativePromise -> native-promise)
fromPlugin(...)by hand: DiBag.createProviderFromPlugin({ dependencies, pluginDescriptor, factoryReturnKind, isValidPluginOutput }); rename validate to isValidPluginOutput and map acquisitionMode raw -> uninspected or nativePromise -> native-promise
withDisposal(...)by hand: DiBag.providerWithDisposal({ provider, disposeService })
withLifetime(...)by hand: DiBag.providerWithLifetime({ provider, lifetime }); map root -> singleton:one-per-container-tree, scoped -> scoped:one-per-container, or transient -> transient:one-per-resolve; move allowScopedDependencies into this object as allowsScopedDependencies
withMetadata(...)by hand: DiBag.providerWithRegistrationMetadata({ provider, registrationMetadata }) for static metadata, or DiBag.providerWithAcquisitionMetadata({ provider, describeAcquisition, callbackReceives }) for dynamic metadata; map mode direct -> exposed-service or awaited -> fulfilled-value; combined static and dynamic metadata requires manually composing both calls while preserving evaluation order
transformService(...)by hand: DiBag.providerWithTransformedService({ provider, transformService, callbackReceives }); map mode direct -> exposed-service or awaited -> fulfilled-value; for exposed-service callbacks, rename acquisitionMode to transformReturnKind (auto -> auto-detect, raw -> uninspected, nativePromise -> native-promise)
token(...)by hand: DiBag.createToken(symbol).forService<Service>() or DiBag.createToken(symbol).forCollectionOf<Item>(); choose the kind manually from its uses, and split mixed single-service and collection uses into tokens with distinct symbols
all(...)by hand: the collection token itself, from DiBag.createToken(symbol).forCollectionOf<Item>()

Module ​

0.4.00.5.0
renameExport(...)by hand: module.withRenamedExport({ currentExportKey, newExportKey })

token() ​

0.4.00.5.0
of(...)by hand: DiBag.createToken(symbol).forService<Service>() or DiBag.createToken(symbol).forCollectionOf<Item>(); choose the kind manually from its uses, and split mixed single-service and collection uses into tokens with distinct symbols

Option names ​

0.4.00.5.0
close({ signal })close({ abortSignal })
close({ timeoutMs })close({ waitTimeoutMs })
withConfiguration({ observers })withConfiguration({ lifecycleObservers })
withLifetime({ allowScopedDependencies })providerWithLifetime({ allowsScopedDependencies })
transformService({ acquisitionMode })providerWithTransformedService({ transformReturnKind })

Fields of options, snapshots, events and errors ​

0.4.00.5.0
Token.keysymbol
CollectionToken.keysymbol
AcquisitionContext.signalabortSignal
Presence.presentisPresent
RegistrationSnapshot.labelbindingLabel
BindingSnapshot.keysserviceKeys
BindingSnapshot.ownedisOwnedByContainer
BindingSnapshot.keytokenSymbol
BindingSnapshot.kinddependencyKind
GraphSnapshot.tokencollectionTokenSymbol
GraphSnapshot.fromconsumerBindingId
GraphSnapshot.todependencyBindingId
BindingSnapshot.acquisitionModefactoryReturnKind
CloseOptions.signalabortSignal
CloseOptions.timeoutMswaitTimeoutMs
StartupOptions.signalabortSignal
StartupOptions.timeoutMstotalTimeoutMs
StartupOptions.startupOrderby hand: startupOrder is gone; use maxConcurrentServiceKeys: omit it for 'parallel', 1 for 'sequential', or the number
ModuleOptions.labelmoduleLabel
DiBagStartupError.cleanupFailuresdisposalFailures
DiBagStartupError.cleanupErrordisposalError
DiBagStartupCancelledError.cleanupPromisedisposalPromise
DiBagCloseCancelledError.timeoutMswaitTimeoutMs
DiBagCloseCancelledError.cleanupPromisedisposalPromise
CleanupFailure.labelbindingLabel
CloseProgress.pendingdisposersStillRunning
CloseProgress.acquiringacquisitionsStillPending
ObserverOptions.onEventonLifecycleEvent
ObserverOptions.onErroronObserverFailure
ScopeOptions.sharesharedParentServiceKeys
ScopeEventFields.scopeIdcontainerId
ScopeEventFields.parentScopeIdparentContainerId
AcquisitionEventFields.scopeIdcontainerId
AcquisitionEventFields.labelbindingLabel
GraphSnapshot.scopeIdcontainerId
Captive.rootsingleton
LifetimeObligation.rootsingleton

String values ​

0.4.00.5.0
'auto' in transformService(...)'auto-detect'
'raw' in transformService(...)'uninspected'
'nativePromise' in transformService(...)'native-promise'
'root' in withLifetime(...)'singleton:one-per-container-tree'
'scoped' in withLifetime(...)'scoped:one-per-container'
'transient' in withLifetime(...)'transient:one-per-resolve'
'root' in BindingSnapshot.lifetime'singleton:one-per-container-tree'
'scoped' in BindingSnapshot.lifetime'scoped:one-per-container'
'transient' in BindingSnapshot.lifetime'transient:one-per-resolve'
'root' in AcquisitionEventFields.lifetime'singleton:one-per-container-tree'
'scoped' in AcquisitionEventFields.lifetime'scoped:one-per-container'
'transient' in AcquisitionEventFields.lifetime'transient:one-per-resolve'
'scope-opened' in LifecycleEvent.kind'container-opened'
'scope-closing' in LifecycleEvent.kind'container-closing'
'scope-closed' in LifecycleEvent.kind'container-closed'
'scope-close-failed' in LifecycleEvent.kind'container-close-failed'
'cleanup-started' in LifecycleEvent.kind'disposal-started'
'cleanup-failed' in LifecycleEvent.kind'disposal-failed'
'cleanup-completed' in LifecycleEvent.kind'disposal-completed'

Exported types and classes ​

0.4.00.5.0
AcquisitionModeFactoryReturnKind; map literal values 'auto' to 'auto-detect', 'raw' to 'uninspected', 'nativePromise' to 'native-promise'
AcquisitionContextFactoryContext
CompositionArgumentsPositionalFactoryArguments
CompositionFunctionPositionalFactoryFunction
PluginAcquisitionModePluginReturnKind; map literal values 'raw' to 'uninspected', 'nativePromise' to 'native-promise'
PluginOptionsCreateProviderFromPluginOptions; in generic argument 1, map 'raw' to 'uninspected', 'nativePromise' to 'native-promise'
PluginProviderFactoryCreateProviderFromPlugin
PluginProviderPluginProvider; in generic argument 3, map 'raw' to 'uninspected', 'nativePromise' to 'native-promise'
StartupOptionsEnsureServicesReadyOptions
DiBagStartupErrorDiBagServiceReadinessError
DiBagStartupCancelledErrorDiBagServiceReadinessCancelledError
DiBagCleanupErrorDiBagDisposalError
CleanupFailureDisposalFailure
BuilderContributeBuilderWithCollectionContribution
BagContainer
ScopeOptionsCreateChildContainerOptions
CheckedScopeLifetimesCheckedChildContainerLifetimes
DisjointScopeSelectionDisjointChildContainerSelection
ObserverOptionsLifecycleObserver
RegistrationProviderOrFactory
FactoryWithDisposalProvider
ScopeEventFieldsContainerEventFields

Runtime error codes ​

0.4.00.5.0
DI_BAG_MISSING_REGISTRATIONDI_BAG_UNKNOWN_SERVICE_KEY
DI_BAG_INVALID_REPLACEMENTDI_BAG_UNKNOWN_SERVICE_KEY
DI_BAG_INVALID_ALIASDI_BAG_UNKNOWN_SERVICE_KEY
DI_BAG_DUPLICATE_REGISTRATIONDI_BAG_DUPLICATE_SERVICE_KEY
DI_BAG_DUPLICATE_METADATADI_BAG_DUPLICATE_METADATA_KEY
DI_BAG_CYCLEDI_BAG_DEPENDENCY_CYCLE
DI_BAG_STARTUP_FAILEDDI_BAG_SERVICE_READINESS_FAILED
DI_BAG_STARTUP_CANCELLEDDI_BAG_SERVICE_READINESS_CANCELLED
DI_BAG_STARTUP_TIMEOUTDI_BAG_SERVICE_READINESS_TIMEOUT
DI_BAG_CLEANUP_FAILEDDI_BAG_DISPOSAL_FAILED
DI_BAG_CLEANUP_AFTER_FACTORYDI_BAG_DISPOSER_PUSHED_AFTER_FACTORY
DI_BAG_INVALID_ACQUISITION_MODEDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_CLEANUPDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_CLOSEDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_CONFIGURATIONDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_CONSTRUCTORDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_FACTORYDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_FUNCTIONDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_LIFETIMEDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_PLUGIN_OPTIONSDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_TRANSFORMDI_BAG_INVALID_ARGUMENT
DI_BAG_INVALID_EXPORTby hand: DI_BAG_INVALID_EXPORT was split: DI_BAG_UNKNOWN_SERVICE_KEY for a key the module does not export, DI_BAG_DUPLICATE_SERVICE_KEY for a rename onto an existing export, DI_BAG_INVALID_ARGUMENT for malformed input
DI_BAG_INVALID_METADATAby hand: DI_BAG_INVALID_METADATA was split: DI_BAG_INVALID_ACQUISITION_METADATA when a describe callback returns a bad record, DI_BAG_INVALID_ARGUMENT for malformed input
DI_BAG_INVALID_OVERRIDEby hand: DI_BAG_INVALID_OVERRIDE was split: DI_BAG_UNKNOWN_SERVICE_KEY, DI_BAG_MISSING_REPLACEMENT_PROVIDER, or DI_BAG_INVALID_ARGUMENT for malformed input
DI_BAG_INVALID_REGISTRATIONby hand: DI_BAG_INVALID_REGISTRATION was split: DI_BAG_INVALID_PROVIDER for a value that is neither a function nor a provider, DI_BAG_INVALID_ARGUMENT for malformed input
DI_BAG_INVALID_SCOPEby hand: DI_BAG_INVALID_SCOPE was split: DI_BAG_UNKNOWN_SERVICE_KEY, DI_BAG_MISSING_REPLACEMENT_PROVIDER, DI_BAG_CONFLICTING_SERVICE_SELECTION, or DI_BAG_INVALID_ARGUMENT for malformed input
DI_BAG_INVALID_STARTUPby hand: DI_BAG_INVALID_STARTUP was split: DI_BAG_UNKNOWN_SERVICE_KEY for an unknown key, DI_BAG_INVALID_ARGUMENT for malformed input

Import specifiers ​

0.4.00.5.0
'di-bag/node''di-bag'

5. If something is still red ​

  • A name the compiler does not know: find it in the tables above.
  • DI_BAG_REMOVED_API from a removed callable at run time: the message names the replacement.
  • A singleton that now captures a scoped service does not compile: either the captured service should be a singleton too, or the capturing one should be scoped. See the errors page.
  • A child cannot replace an explicit singleton: use an independent container, or make the service scoped when each child should own an instance.

Ordinary services. Checked composition. Explicit ownership.