Skip to content

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

bash
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 repairs

Anything else is shorthand for cli — framework commands work with zero scripts in package.json:

bash
pnpm modularity database:migration:generate AddUsers
pnpm modularity database:migration:run
pnpm modularity assets:collect

Commands 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):

NameSource
serversrc/server.ts
mainsrc/main.ts
indexsrc/index.ts
clisrc/cli.ts
workersrc/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 exits

Piped 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:

bash
echo 'await app.get(HealthService).check()' | pnpm modularity repl

Projects that deviate from the convention declare entrypoints in package.json:

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

  • dev runs the TypeScript source under your project's own tsx (resolved from your node_modules, so the version you pin is the version that runs) with watch mode.
  • start runs node against the mapped build output (src/x.tsdist/x.js, mirroring the standard rootDir/outDir tsconfig).
  • cli prefers the source when present (development) and falls back to dist/ (production) — the same command line works in both contexts.

verify — is the app correctly assembled?

One command, one answer, in order of increasing cost:

  1. environment — the doctor checks.
  2. typechecktsc --noEmit.
  3. 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 — every MJS#### class of error), then exits before any lifecycle hook runs. No database, Redis, or other backend needs to be running — connections happen in onInit, which boot-check never reaches.
  4. testsvitest 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, or src/bootstrap.ts in 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 an internal/ 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 under src/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.ts implementing the CliCommand interface, a module contributing it to CliCommandsPool, and a spec exercising it directly — then wires it in, so pnpm 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 with pnpm 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 a DataSource double (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 its DatabaseEntitiesPool registration (@modularityjs/database-typeorm), and a Zod schema file (@modularityjs/validation-zod).

  • doctor checks the boring failure modes: Node vs engines, packageManager pin, tsx present, entrypoints resolvable, .env keys missing vs .env.example, mixed framework version ranges, the @modularityjs:registry mapping, @modularityjs/* runtime dependencies that are declared but never referenced in source (dead weight — advisory only; the infrastructure primitives like exception and retry are exempt as standing capability), and orphan modules — a local @Module class never referenced in the wiring file, which therefore can never boot (dead code, or a forgotten wiring step). doctor --fix applies the mechanical repairs — pins engines.node, pins packageManager to the pnpm running the command, appends missing .env keys from .env.example, and aligns mixed framework ranges to the highest present (then tells you to pnpm 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-stub marker (the sanctioned escape hatch of the migration-integrity lint) and updates the barrel; regenerate via modularity database:migration:generate <Name> before merge.

  • add also regenerates the autoload wiring shell (modularityjs-autodiscover) when your src/modules.ts uses defineModules(), 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:

bash
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 starts

Attach 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.