Autoload
@modularityjs/autoload is the declarative app-wiring surface: instead of hand-maintaining a createApp({ modules: [...] }) array, you declare which packages to install, how to configure them, which app modules to register, and which profiles exist in one src/modules.ts file — and a codegen step (modularityjs-autodiscover) emits the static wiring shell your bootstrap imports.
Two pieces:
defineModules(config)— a runtime identity function plus the type contract for the config shape. Yoursrc/modules.tsdefault-exports the call.modularityjs-autodiscover(bin) — readssrc/modules.ts, validates it against yourpackage.jsonand the framework manifest, and writessrc/autoloaded.tswith real imports andforRoot(...)calls.
defineModules
// src/modules.ts
import { defineModules } from '@modularityjs/autoload';
import { AppModule } from './app.module.js';
export default defineModules(() => ({
install: [
'@modularityjs/modularity',
'@modularityjs/http',
'@modularityjs/http-fastify',
'@modularityjs/logger',
'@modularityjs/logger-console',
],
config: {
'@modularityjs/http': { port: Number(process.env.PORT ?? 3000) },
},
app: [AppModule],
profiles: {
server: ['@modularityjs/http-fastify'],
},
}));The config shape (ModulesConfig):
| Field | Meaning |
|---|---|
install | @modularityjs/* package names to wire. The codegen looks each up in the manifest and emits the module-class reference (or a forRoot(...) call when a config entry exists). |
config | Per-package configuration, keyed by package name. Keys must be entries in install; values are passed to that package's Module.forRoot(...) at runtime. |
app | App-owned modules — controllers, feature modules under src/modules/, etc. Spread onto the end of the generated modules array. Classes go here, never in install (which holds package-name strings). |
profiles | Profile name → package-name list, translated to module-class references. Mirrors install's package-name vocabulary. See Modules for how app.start('server') selects a profile. |
Object form vs. factory form
defineModules accepts a config object or a factory returning one:
- Object literal — evaluated at import time. Fine when nothing reads the environment.
- Factory (
defineModules(() => ({ ... }))) — evaluated on everybuildModules()call. Required whenever any value readsprocess.env(or other side-effect-bearing globals), so env overrides set in a test'sbeforeEach, or by the runner's--env-file-if-exists=.env, land before the modules are built.
The autodiscover regen loop
pnpm exec modularityjs-autodiscover
# or with explicit paths:
pnpm exec modularityjs-autodiscover ./src/wiring.ts ./src/wiring-gen.tsDefault input is src/modules.ts, default output src/autoloaded.ts; both are configurable in package.json:
{
"modularityjs": {
"autodiscover": {
"input": "src/modules.ts",
"output": "src/autoloaded.ts"
}
}
}The generated src/autoloaded.ts is a static, tree-shake-friendly wiring shell — real import statements per package, a buildModules(): ModuleInput[] function that applies your config entries via forRoot, and an exported profiles map. It is stamped AUTO-GENERATED … Do not edit by hand; modules.ts stays the single source of truth.
Bootstrap consumes the generated file, not modules.ts:
// src/bootstrap.ts
import { inversify } from '@modularityjs/di-inversify';
import { createApp } from '@modularityjs/modularity';
import { buildModules, profiles } from './autoloaded.js';
export function bootstrap() {
return createApp({ di: inversify, modules: buildModules(), profiles });
}Rerun the codegen after every edit to modules.ts and after dependency changes. pnpm modularity add <package> does this automatically when it detects a defineModules project — it patches install, then regenerates autoloaded.ts so the new module actually boots.
Diagnostics
The codegen validates the declaration instead of failing at boot:
- ERROR — a package in
installthat isn't inpackage.jsondependencies, or missing from the manifest. - WARN — an
@modularityjs/*dependency not listed ininstall; aconfigkey not present ininstall. - INFO — a bridge package that would link two installed contracts (e.g. you have
http-auth+database-typeorm; considerhttp-auth-database-typeorm).
A real example
The framework's own apps/demo-full wires ~70 packages this way: install lists the packages in dependency-order groups, config carries the forRoot payloads (database dialect, CSP directives, CSRF secret from env, template directory), app registers its feature modules, and profiles splits server (Fastify) from cli (Commander) so app.start('server') and app.start('cli') boot different listener sets from one wiring file. Driver choices that depend on the environment stay in modules.ts as plain TypeScript — demo-full picks MailNodemailerModule.forRoot(...) in production and MailMemoryModule + the /dev/mail UI otherwise, and passes the result through app.
When to use it
create-scaffolded apps start with an inline modules: [...] array in src/bootstrap.ts — perfectly fine for a handful of modules. Reach for defineModules when the module list grows past what you want to hand-order, when per-package config sprawls, or when you want the codegen's dependency/manifest validation and bridge-package hints. Boot error MJS0003 (module imports an unregistered module) is the class of mistake the regen loop prevents mechanically.
Next Steps
- Modules — module metadata, imports, profiles
- Runner —
pnpm modularity addregenerates the autoload shell - Boot Errors — the MJS catalogue autodiscover helps you avoid