craftService
Creates a named Angular-friendly service boundary with generated inject, yield, provider, and metadata helpers.
Import
import { craftService } from '@craft-ng/core';Introduction
craftService is the service-oriented composition API for @craft-ng.
It lets you:
- define a service once with a stable name and scope
- inject it through a generated
injectX()helper - compose it into another service through
yield* XToYield() - expose only part of a dependency through derived bindings
- generate typed provider helpers for provider-capable scopes
- keep the full dependency graph available for testing utilities
Unlike ad-hoc inject(...) calls spread across services, craftService makes dependencies explicit and type-visible.
Generated Helpers
For a service named Counter, craftService can generate:
injectCounter(...)to resolve the service in components/directivesCounterToYield(...)to compose it inside anothercraftServiceprovideCounter(...)for provider-capable scopesCOUNTER_META_DATAfor metadata-driven toolingCounterRequirementforabstractservices
The exact helpers depend on the chosen scope.
Supported Scopes
global
- singleton provided at root
- ideal for app-wide services and shared state
- no explicit
provideX()helper
toProvide
- requires
provideX()where the service is mounted - useful for feature-local service trees
- works well with tests that need explicit providers
manuallyProvidedAtRoot
- explicit provider helper, but designed to be mounted at root
- also exposes
XToProvidefor public provider composition - allows this scope to be yielded by global services, which is not possible with
toProvide(it still requires explicit setup when testing withsetupCraftServiceTestingByRegister).
function
- creates a fresh instance on each injection
- useful for reusable factories with bindings and inputs
abstract
- declares a contract without implementation
- exposes a requirement token to force a concrete implementation later
Recommendations For Choosing a Scope
- Prefer
functionfor a service owned by a single component. It avoids an explicit provider and makes it clear the instance is not meant to be shared with other components or child components. - Move to
toProvidewhen the same instance must be shared with child components, or across several components through a common parent or route. In that case, provide it at the component boundary, a parent component, or the route. - Be careful with
toProvide: Angular does not report a compilation error when the provider is missing, so the failure usually appears at runtime instead. - Use
globalwhen the instance is intentionally shared application-wide. - For startup-only logic that should run when the app boots but is not injected elsewhere, prefer
functiontogether withprovideAppInitializer(...). If the same instance also needs to be injected by other services, useglobalinstead.
Basic Example
import { craftService, state } from '@craft-ng/core';
const { injectCounter } = craftService(
{ name: 'Counter', scope: 'global' },
() =>
state(0, ({ update }) => ({
increment: () => update((value) => value + 1),
decrement: () => update((value) => value - 1),
})),
);
const counter = injectCounter();
counter.increment();
console.log(counter());Composition With yield*
const { CounterToYield } = craftService(
{ name: 'Counter', scope: 'global' },
() =>
state(0, ({ update }) => ({
increment: () => update((value) => value + 1),
})),
);
const { injectCounterFacade } = craftService(
{ name: 'CounterFacade', scope: 'global' },
function* () {
const counter = yield* CounterToYield();
return {
read: () => counter(),
increment: () => counter.increment(),
};
},
);Partial Exposure
yield* XToYield() can expose only the part of a dependency that should remain public.
const { CounterToYield } = craftService(
{ name: 'Counter', scope: 'toProvide' },
() =>
state(0, ({ update }) => ({
increment: () => update((value) => value + 1),
decrement: () => update((value) => value - 1),
})),
);
const { injectCounterExtended, provideCounterExtended } = craftService(
{ name: 'CounterExtended', scope: 'toProvide' },
function* () {
return yield* CounterToYield(undefined, ({ $self, increment }) => ({
$self,
incrementCounter: increment,
}));
},
);This keeps the dependency graph precise, which is important for both type inference and testing.
Abstract Requirements
Use scope: 'abstract' to declare a contract that must be implemented elsewhere.
import { abstract, craftService } from '@craft-ng/core';
type CounterContract = {
(): number;
increment(): void;
};
const { CounterRequirement } = craftService(
{ name: 'Counter', scope: 'abstract' },
abstract<CounterContract>(),
);Concrete services can then depend on CounterRequirement.
Testing
Complementary testing helper are designed around craftService metadata:
- setupCraftServiceTestingByRegister for exhaustive flat registers