Setup
This guide assumes you are integrating type-safe DI/routes into an Angular app that consumes @craft-ng/core.
Prerequisites
Install the runtime package and the dev tooling in your app:
npm install @craft-ng/core
npm install -D @craft-ng/dev-tools1. Add the app-level type check in src/main.ts
WARNING
The current approach is "central-based" and has some limitations due to TypeScript typing context limitations. I will change this setup in favor of a cascading approach.
Your main.ts is where the final app-wide DI check happens.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppCheckedDI, CanRun, toApplicationConfig } from '@craft-ng/core';
import { appConfig } from './app/app.config';
import { AppComponent, GenDeps_AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, toApplicationConfig(appConfig)).catch(
(err) => console.error(err),
);
type CheckAppDI = AppCheckedDI<
GenDeps_AppComponent,
typeof appConfig.APP_CONFIG_META_DATA
>;
type _CanRun = CanRun<CheckAppDI>;AppCheckedDI compares:
- the generated dependencies of your root component
- the generated dependencies declared on every route
- the providers resolved by
craftAppConfig(...)
If a route depends on a service that is not provided, or if a routed component expects an input that the route does not supply, _CanRun turns that mismatch into a TypeScript error in main.ts.
Typical errors look like:
Injected Counter is not provided in path: "some-path"Input "userId" is not provided in path: "some-path"
2. Wrap your routes with craftRoutes
Do not export a plain Angular Routes array directly. Wrap it in craftRoutes(...) and declare componentDeps on each route component.
import { craftRoutes } from '@craft-ng/core';
export const { appRoutes } = craftRoutes('app', [
{
path: '',
loadComponent: () => import('./test'),
componentDeps: {} as import('./test').GenDeps_TestComponent,
},
]);The important part is:
componentDeps: {} as import('./test').GenDeps_TestComponent,That line connects the generated GenDeps_* type of the component to the route metadata checked later by AppCheckedDI.
Then wire the crafted routes into your application config:
import { craftAppConfig } from '@craft-ng/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig = craftAppConfig({
routingDeps: appRoutes.META_DATA,
providers: [provideRouter(appRoutes.toRoutes(), withComponentInputBinding())],
});Notes:
appRoutes.toRoutes()gives Angular the real runtime routes.appRoutes.META_DATAgivescraftAppConfig(...)the compile-time route dependency graph.- For lazy routes,
loadChildrenshould return the named route tree exported by the child collection, for examplechildRoutes.childRoutes.
3. Run the Angular brand codemod through the published script
Add a script in your app:
{
"scripts": {
"craft:brand": "craft-brand --root src/app"
}
}Then run:
npm run craft:brandThis is the step that creates the initial GenDeps_* aliases in your component files, for example:
export type GenDeps_TestComponent = GetDeps<{
deps: {
CommonModule: CommonModule;
Counter: GetInjectedServiceDependencies<typeof injectCounter>;
};
provided: {};
publicProperties: GetPublicComponentProperties<TestComponent>;
}>;Adjust --root to your real source root:
src/appfor a standard Angular appprojects/my-app/src/appfor a workspace applibs/my-feature/srcfor a library
If you use a project-level craft-brand.config.ts, you can extend the script:
{
"scripts": {
"craft:brand": "craft-brand --root src/app --config ./craft-brand.config.ts"
}
}4. Install the exposed ESLint rules
The plugin is exposed from @craft-ng/dev-tools/eslint-rules.
Add it to your ESLint flat config:
import craftRules from '@craft-ng/dev-tools/eslint-rules';
export default [
// keep your existing ESLint config entries
{
files: ['**/*.ts'],
plugins: {
'craft-ng': craftRules,
},
rules: {
'craft-ng/brand-angular-gen-deps-required': 'error',
'craft-ng/brand-angular-deps-match': 'error',
'craft-ng/component-test-gen-deps-match': 'error',
'craft-ng/no-angular-inject': 'error',
'craft-ng/prefer-craft-service': 'error',
'craft-ng/prefer-craft-http-client': 'error',
},
},
];What each rule does:
craft-ng/brand-angular-gen-deps-required: generates a missingGenDeps_*alias for Angular components, directives, and pipes through the ESLint Quick Fixcraft-ng/brand-angular-deps-match: keeps existingGenDeps_*aliases in sync through the same ESLint Quick Fix flowcraft-ng/component-test-gen-deps-match: checkssetupCraftComponentTestingByRegister(Component, {} as GenDeps_Component, ...)pairs in testscraft-ng/no-angular-inject: forbids raw Angularinject()usage so dependencies go throughcraftService(...)ortoCraftService(...)craft-ng/prefer-craft-service: forbids authored Angular@Injectable()/@Service()services in favor ofcraftService(...)andtoCraftService(...)craft-ng/prefer-craft-http-client: forbids AngularHttpClientusage in favor ofCraftHttpClient
The two migration rules also expose a VS Code ESLint Quick Fix suggestion that inserts a temporary local disable comment with the intended migration note when you need to unblock a file before doing the full refactor.
If your project is adopting this progressively, enable both craft-ng/brand-angular-gen-deps-required and craft-ng/brand-angular-deps-match so the same Quick Fix can generate missing aliases and refresh existing ones. craft-ng/no-angular-inject is an architecture-enforcement rule and may require a broader migration.
5. When a component changes, regenerate GenDeps with the Quick Fix
After changing a component's DI-related shape, refresh its generated alias.
Typical triggers:
- adding or removing
inject(...) - changing constructor injection
- changing component
imports - changing
providers - changing
viewProviders
Recommended workflow:
- first generation or bulk refactor:
npm run craft:brand - one file without
GenDeps_*: trigger the VS Code ESLint Quick Fix oncraft-ng/brand-angular-gen-deps-required - one file with
GenDeps_*: trigger the VS Code ESLint Quick Fix oncraft-ng/brand-angular-deps-match - CLI alternative for one file:
eslint --fix src/app/feature/my-component.ts
Important limits:
- the Quick Fix only handles the current file
- if you rename the component class, rerun the generator so the
GenDeps_*alias name stays aligned
WARNING
An Eslint error does not trigger a compilation error, so make sure to run the Quick Fix or eslint --fix after changing a component's DI shape. Otherwise, main.ts will not see the updated GenDeps_* and may miss real DI errors.