Runner
@modularityjs/runner ships the modularity bin — a unified way to run any ModularityJS project. It replaces the pile of hand-written package.json scripts (tsx watch --env-file-if-exists=.env src/server.ts, …) that otherwise diverges across projects: the runner resolves entrypoints by convention, applies environment handling consistently, and passes framework CLI commands straight through to your app.
Projects scaffolded with pnpm create @modularityjs get it as a devDependency automatically; existing projects add it with pnpm add -D @modularityjs/runner.
Commands
pnpm modularity dev [entry] # run from source with reload-on-change (tsx watch)
pnpm modularity start [entry] # run the built app from dist/ (node)
pnpm modularity repl [entry] # boot into an interactive session (`app` in scope)
pnpm modularity cli [command...] # run the app's CLI entrypoint
pnpm modularity entrypoints # list what the runner resolved
# Run options (dev, start, repl):
# --inspect[=port] enable the Node.js inspector on the spawned process (default port 9229)
# --inspect-brk[=port] enable the inspector and break before user code starts
# --quiet dev only: don't force MODULARITY_BOOT_REPORT=1
pnpm modularity add <package> # install a framework package + wire its module
pnpm modularity info <package> # kind, targets, module class, exports
pnpm modularity generate domain <name> # scaffold a full layered domain (config/service/controller/entity/schemas/types/internal)
pnpm modularity generate integration <name> # scaffold an external integration behind a contract seam
pnpm modularity generate cli-command <name> # scaffold an app CLI command wired into CliCommandsPool
pnpm modularity generate module <name> # scaffold a minimal feature folder under src/modules/
pnpm modularity generate migration-stub <Name> # placeholder migration (lint-approved)
pnpm modularity verify [--json] # is the app correctly assembled?
pnpm modularity doctor [--fix] # check the project; --fix applies mechanical repairsAnything else is shorthand for cli — framework commands work with zero scripts in package.json:
pnpm modularity database:migration:generate AddUsers
pnpm modularity database:migration:run
pnpm modularity assets:collectCommands come from whatever *-cli extension packages your app wires (they contribute to CliCommandsPool); installing a new one makes its commands available through the same invocation immediately.
Entrypoint resolution
By convention, the runner looks for (in default-selection priority order):
| Name | Source |
|---|---|
server | src/server.ts |
main | src/main.ts |
index | src/index.ts |
cli | src/cli.ts |
worker | src/worker.ts |
pnpm modularity dev with no argument picks the first one that exists; pnpm modularity dev worker names one explicitly. An entrypoint is also discovered when only its build output (dist/server.js) exists — so pnpm modularity start and CLI passthrough work in production images that ship no sources.
REPL
pnpm modularity repl boots the app into a Node REPL with app (the Application) in scope — the tinker/console equivalent. It sets MODULARITY_REPL=1, which makes createApp hand the booted app to the session after afterLoad/onInit: database and Redis connections are live, but onReady never fires, so no port is bound and no queue consumer starts while you poke at the container.
modularity> const users = app.get(UserService)
modularity> await users.find('1')
modularity> app.getAll(HealthIndicatorsPool)
modularity> .exit # runs onShutdown/onDestroy, then exitsPiped stdin works the same way — the REPL evaluates the input, then closes the session, which runs onShutdown/onDestroy and exits. That makes a one-liner smoke test or a docker exec -i health probe a supported invocation:
echo 'await app.get(HealthService).check()' | pnpm modularity replProjects that deviate from the convention declare entrypoints in package.json:
{
"modularityjs": {
"entrypoints": {
"server": "src/http/main.ts",
"consumer": "src/queue-consumer.ts"
}
}
}Configured names merge over the conventions and can add entries beyond the four conventional ones (pnpm modularity dev consumer).
Environment handling
Every invocation passes --env-file-if-exists=.env: drop a .env into the project root and it is picked up with no script changes; nothing happens when the file is absent. This is the one flag projects most often forget to add by hand — with the runner it's structural.
Execution model
devruns the TypeScript source under your project's owntsx(resolved from yournode_modules, so the version you pin is the version that runs) with watch mode.startrunsnodeagainst the mapped build output (src/x.ts→dist/x.js, mirroring the standardrootDir/outDirtsconfig).cliprefers the source when present (development) and falls back todist/(production) — the same command line works in both contexts.
verify — is the app correctly assembled?
One command, one answer, in order of increasing cost:
- environment — the
doctorchecks. - typecheck —
tsc --noEmit. - boot — runs the default entrypoint with
MODULARITY_BOOT_CHECK=1: the framework wires and validates the entire module graph (contract fulfillment, pool ownership, preference resolution, named injections — everyMJS####class of error), then exits before any lifecycle hook runs. No database, Redis, or other backend needs to be running — connections happen inonInit, which boot-check never reaches. - tests —
vitest run(skipped when the project doesn't declare vitest).
--json emits { ok, steps: [{ name, ok, durationMs, detail }] } — the machine-readable contract coding agents iterate against until green. Exit code 0 only when every step passes, so it drops into CI as-is.
Project commands
add <package>resolves the name against the installed manifest catalogue, installs the package plus any missing contract it extends (a driver is useless without its contract), and wires the module class(es) into the wiring file —src/modules.ts, orsrc/bootstrap.tsin a freshly scaffolded app — appended last, so a newly added driver wins preference tie-breaks, then re-formats the file with your own lint config. If the wiring file doesn't match the expected shape, it prints exact manual instructions instead of guessing.generate domain <name>scaffolds the full layered domain blueprint —<name>.config.ts(the module's one env-reading spot),<name>.service.ts(owns persistence), a plural/api/v1/<plural>controller, entity, schemas, input DTOs,<name>-types.ts, and aninternal/folder for helpers — adapting to installed capabilities, wiring the module in, and formatting the output with your project's own lint/prettier. Reach for this over hand-assembly so every domain comes out identical.generate integration <name>scaffolds an external integration behind a contract seam undersrc/integrations/<name>/— an abstract injectable client (<Name>Client, the seam your business code depends on), an HTTP driver (Http<Name>Client) that reads credentials from a config class and times out every request, a module binding the driver to the seam via a preference, and a test seeding the fake-swap pattern. Business code injects the abstract client, so a third-party API never leaks into it and the driver is swappable for a fake in tests. Reach for this whenever you talk to a system you don't own.generate cli-command <name>scaffolds an app-local CLI command — a*.command.tsimplementing theCliCommandinterface, a module contributing it toCliCommandsPool, and a spec exercising it directly — then wires it in, sopnpm modularity cli <name>runs it through the booted app (DI, config, and lifecycle for free — no ad-hoc side scripts). Names may be colon-namespaced per the house convention (report:daily). Requires@modularityjs/cli(install the stack withpnpm modularity add cli-commander).The generators scaffold a
__tests__/spec alongside the code — a persistence-free domain service is tested directly, a persistence-backed one via aDataSourcedouble (no live DB), an integration via a fake subclass of the seam, and a CLI command by direct instantiation. The generated app inherits a testing pattern instead of inventing one.generate module <name>scaffolds the minimal feature folder per the recommended structure: module class, barrel, and — depending on what your project has installed — a controller (@modularityjs/http), an entity with itsDatabaseEntitiesPoolregistration (@modularityjs/database-typeorm), and a Zod schema file (@modularityjs/validation-zod).doctorchecks the boring failure modes: Node vsengines,packageManagerpin,tsxpresent, entrypoints resolvable,.envkeys missing vs.env.example, mixed framework version ranges, the@modularityjs:registrymapping,@modularityjs/*runtime dependencies that are declared but never referenced in source (dead weight — advisory only; the infrastructure primitives likeexceptionandretryare exempt as standing capability), and orphan modules — a local@Moduleclass never referenced in the wiring file, which therefore can never boot (dead code, or a forgotten wiring step).doctor --fixapplies the mechanical repairs — pinsengines.node, pinspackageManagerto the pnpm running the command, appends missing.envkeys from.env.example, and aligns mixed framework ranges to the highest present (then tells you topnpm install). Judgment calls (registry URL, installing tsx) stay report-only.info <package>answers "what is this package" from the installed catalogue — kind, what it extends, its module class, and public exports — with did-you-mean suggestions on typos.generate migration-stub <Name>emits a placeholder migration carrying the@modularityjs:migration-stubmarker (the sanctioned escape hatch of the migration-integrity lint) and updates the barrel; regenerate viamodularity database:migration:generate <Name>before merge.addalso regenerates the autoload wiring shell (modularityjs-autodiscover) when yoursrc/modules.tsusesdefineModules(), so the new module actually boots without a manual step.
pnpm modularity dev also sets MODULARITY_BOOT_REPORT=1, so development boots print the framework's boot report — modules in load order, pool fill counts, and which module won each preference. Pass --quiet to opt out.
Debugging
dev, start, and repl accept the Node inspector flags and thread them to the spawned process:
pnpm modularity dev --inspect # inspector on the default port 9229
pnpm modularity dev --inspect=9230 # explicit port
pnpm modularity start --inspect-brk # break before user code startsAttach with your editor's Node debugger or chrome://inspect. Under dev the flag rides through tsx watch, so the debugger reattaches across reloads.
What stays in package.json
Project-specific scripts. The scaffolder still emits "dev": "modularity dev" and friends as discoverability sugar, but they are one-liners delegating to the runner — the invocation logic lives in one versioned place instead of being copy-pasted per project. Anything genuinely yours (a custom seed script, a compound watch) remains an ordinary script; the runner standardizes the framework-shaped invocations, it doesn't forbid your own.