@nextrush/dev
v1.1.2
Published
Development server and build tools for NextRush - multi-runtime support
Downloads
22
Maintainers
Readme
@nextrush/dev
Development server and build tools for NextRush with multi-runtime support.
Support tier: Public - tooling (stable). See ADR-0005.
Quick Start
# Install
pnpm add -D @nextrush/dev
# Start development server (auto-detects everything)
nextrush dev
# Build for production
nextrush buildThat's it! No configuration needed. The CLI auto-detects:
- Entry file (
src/index.ts,src/main.ts, etc.) - Runtime (Node.js, Bun, or Deno)
- TypeScript settings from
tsconfig.json
The
nextrushcommand is also provided by thenextrushmeta-package's launcher, which delegates here when@nextrush/devis installed and prints an actionable install hint when it is not (see ADR-0013). Either package installing this toolkit makesnextrush dev/build/generatework.
The Problem
TypeScript decorators with dependency injection require emitDecoratorMetadata to work. This compiler option emits type information at runtime:
// This TypeScript:
@Controller('/users')
class UserController {
constructor(private userService: UserService) {}
}
// Needs to emit this metadata:
Reflect.metadata('design:paramtypes', [UserService]);Without this metadata, the DI container cannot resolve constructor parameters:
TypeInfo not known for UserControllerThe Problem with Modern Bundlers
Most modern bundlers strip types without emitting decorator metadata:
| Tool | Speed | Decorator Metadata | DI Works? |
| -------------------- | ------------- | ------------------ | --------- |
| tsup / esbuild | Fast | Not emitted | No |
| tsx | Fast | Not emitted | No |
| node --strip-types | Fast | Not emitted | No |
| tsc | Slow (build step) | Emitted | Yes |
| nextrush dev | Fast | Emitted | Yes |
| nextrush build | Fast | Emitted | Yes |
Installation
pnpm add -D @nextrush/devCommands
nextrush dev - Development Server
Start a development server with auto-restart on change and decorator support. (Changes trigger a full process restart via the runtime's native watcher - this is auto-restart, not state-preserving HMR.)
# Auto-detects entry file
nextrush dev
# Specify entry file
nextrush dev ./src/server.ts
# Custom port
nextrush dev --port 4000
# Enable debugger
nextrush dev --inspectOptions:
| Option | Alias | Default | Description |
| ---------------- | ----- | ------- | --------------------------- |
| --port | -p | 8080 | Port number (PORT env when --port omitted) |
| --watch | -w | src | Paths to watch (repeatable) |
| --inspect | - | false | Enable Node.js inspector |
| --inspect-port | - | 9229 | Inspector port |
| --no-clear | - | - | Don't clear screen on start |
| --verbose | -v | false | Verbose output |
nextrush build - Production Build
Build for production with SWC, emitting decorator metadata.
# Build with defaults
nextrush build
# Custom output directory
nextrush build --outDir dist
# Minify output
nextrush build --minify
# Target ES2020
nextrush build --target es2020Options:
| Option | Alias | Default | Description |
| ------------------------- | ----- | -------- | ---------------------------- |
| --outDir | -o | dist | Output directory |
| --target | -t | es2022 | Target ES version |
| --sourcemap | - | true | Generate sourcemaps |
| --no-sourcemap | - | - | Disable sourcemaps |
| --minify | -m | false | Minify output |
| --no-decorator-metadata | - | - | Skip decorator metadata |
| --dts / --no-dts | - | --dts | Emit .d.ts declarations (fails the build on error unless --no-dts) |
| --no-cache | - | - | Bypass the incremental build cache |
| --no-clean | - | - | Don't clean output directory |
| --verbose | -v | false | Verbose output |
nextrush generate - Code Generator
Generate modules, controllers, services, middleware, guards, and routes.
# Generate a controller class
nextrush generate controller user
# Short alias
nextrush g controller user
# Generate all types
nextrush g m todos # Module
nextrush g s user-profile # Service
nextrush g mw logger # Middleware
nextrush g guard auth # Guard
nextrush g r products # RouteTypes:
| Type | Alias | Default Output Path | Module project (src/modules/ exists) | Style |
| ------------ | ----- | ------------------------------------------ | -------------------------------------- | ----------- |
| module | m | src/modules/<name>/<name>.module.ts | src/modules/<name>/<name>.module.ts | Class-based |
| controller | c | src/controllers/<name>.controller.ts | src/modules/<name>/<name>.controller.ts | Class-based |
| service | s | src/services/<name>.service.ts | src/modules/<name>/<name>.service.ts | Class-based |
| middleware | mw | src/middleware/<name>.ts | src/middleware/<name>.ts | Functional |
| guard | g | src/guards/<name>.guard.ts | src/guards/<name>.guard.ts | Functional |
| route | r | src/routes/<name>.ts | src/routes/<name>.ts | Functional |
Controllers and services co-locate into a feature module (src/modules/<name>/) when the
project has a src/modules/ directory (the layout create-nextrush's class-based template
emits); module-less projects keep the flat directories.
Generated Controller Example:
// nextrush g controller user -> src/modules/user/user.controller.ts (module project)
import { Body, Controller, Get, Param, Post } from 'nextrush/class';
import { UserService } from './user.service.js';
@Controller('/user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
@Get('/:id')
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
@Post()
create(@Body() data: unknown) {
return this.userService.create(data);
}
}Generated Service Example:
// nextrush g s order -> src/modules/order/order.service.ts (module project)
import { HttpError } from 'nextrush';
import { Service } from 'nextrush/class';
@Service()
export class OrderService {
findAll() {
return [];
}
findOne(id: string) {
if (!id) throw new HttpError(404, 'Not found');
return { id };
}
create(data: unknown) {
if (!data || typeof data !== 'object') throw new HttpError(400, 'Invalid input');
return data;
}
}Naming Rules:
- Use lowercase letters, numbers, and hyphens:
user,user-profile,v2 - Multi-word names are converted to PascalCase:
user-profile->UserProfileController - Duplicate file detection: won't overwrite existing files
nextrush generate adapter - Scaffold a Runtime Adapter
A distinct, multi-file scaffold (not a single-file generator): creates a
contract-conformant adapter package skeleton under <name>/ - source stub,
conformance test wired to the shared suite, fixtures, README, and a CI snippet.
nextrush generate adapter my-runtime
nextrush g ad my-runtime| Type | Alias | Output | Contents |
| --------- | ----- | -------------------- | ------------------------------------------------------------ |
| adapter | ad | <name>/ (directory) | src/adapter.ts, src/__tests__/conformance.test.ts, fixtures/, README.md, CI snippet |
nextrush codemod - Automated Code Transformations
Runs a codemod against files matching a glob pattern.
nextrush codemod consolidate-imports src/**/*.ts
# Preview changes without writing to disk
nextrush codemod consolidate-imports 'src/**/*.{ts,tsx}' --dry-runAvailable codemods:
| Codemod | What it does |
| ----------------------- | ------------------------------------------------------------------------------------------------------ |
| consolidate-imports | Rewrites @nextrush/decorators and @nextrush/controllers imports to nextrush/class, merging and deduplicating; leaves @nextrush/di untouched |
Options:
| Option | Default | Description |
| ----------- | ------- | ------------------------------------- |
| --dry-run | false | Preview changes without writing to disk |
The rewrite is surgical: only the matched import statements are touched. A leading license/header comment, unrelated imports, and all other code are preserved byte-for-byte - the file is never reprinted whole.
Package.json Scripts
{
"scripts": {
"dev": "nextrush dev",
"build": "nextrush build",
"start": "node dist/index.js",
"generate": "nextrush generate"
}
}Multi-Runtime Support
The CLI automatically detects and adapts to your runtime environment:
| Runtime | Dev Server | Production Build | Decorator Metadata |
| ----------- | ----------------------- | ----------------- | ------------------ |
| Node.js | Yes - @swc-node/register | Yes - @swc/core | Yes - Full support |
| Bun | Yes - Native --watch | Yes - Native bundler | Yes - Full support |
| Deno | Yes - Native --watch | Yes - npm:@swc/core | Yes - Full support |
How It Works
Node.js:
- Dev: Uses
@swc-node/registerfor SWC-powered TypeScript execution with decorator metadata - Build: Uses
@swc/coretransform API withdecoratorMetadata: true
Bun:
- Dev: Native TypeScript execution with
bun --watch - Build: Native
Bun.build()bundler (preserves decorator metadata!)
Deno:
- Dev: Native TypeScript execution with
deno run --watch - Build: Uses
npm:@swc/corefor consistent decorator metadata emission
Runtime Detection
import { detectRuntime, getRuntimeInfo } from '@nextrush/dev';
const runtime = detectRuntime(); // 'node' | 'bun' | 'deno'
const info = getRuntimeInfo();
// {
// runtime: 'node',
// version: '22.0.0',
// supportsTypeScript: false,
// supportsWatch: true,
// needsSwc: true
// }Deno Permissions
nextrush dev spawns Deno with a fixed default permission set:
--allow-net --allow-read --allow-env. If your app needs more (writing files,
FFI, spawning subprocesses, ...), extend the default set via nextrush.config.ts:
// nextrush.config.ts
import type { NextRushConfig } from '@nextrush/dev';
export default {
dev: {
deno: {
permissions: ['--allow-write', '--allow-ffi'],
},
},
} satisfies NextRushConfig;Configured permissions are merged into the default set - they extend it, they
never replace it. --allow-net, --allow-read, and --allow-env are always present
even when you add more; a permission you configure that's already in the default set
is simply not duplicated. Scoped forms are supported as pass-through strings, e.g.
--allow-read=./data or --allow-write=./dist.
Each configured value must begin with --allow- or --deny-. An invalid value
(missing that prefix) fails the command before Deno is spawned, naming the offending
value in the error.
Adding permissions weakens Deno's sandbox. Only grant what your application actually needs - never configure
--allow-allas a default. The CLI itself never adds--allow-allautomatically, and there is currently no way to remove a default permission (extend-only by design); if you genuinely need a narrower sandbox than the defaults, rundenodirectly instead of throughnextrush dev.
Programmatic API (Optional)
Note: The programmatic API is optional. Most users only need the CLI commands (
nextrush devandnextrush build), which auto-detect everything.
The programmatic API is useful for:
- Build tool integration
- Custom build scripts
- Monorepo setups
- Testing frameworks
dev(entry?, options?): Promise<SpawnResult>
Start the development server programmatically.
import { dev } from '@nextrush/dev';
// Simple - auto-detect entry
await dev();
// With entry file
await dev('./src/app.ts');
// With options
await dev('./src/app.ts', {
port: 4000,
inspect: true,
watch: ['./src', './config'],
env: { DATABASE_URL: 'postgres://...' },
});DevOptions:
| Option | Type | Default | Description |
| ------------- | ------------------------ | --------- | --------------------- |
| entry | string | auto | Entry file path |
| port | number | 8080 | Port number |
| inspect | boolean | false | Enable debugger |
| inspectPort | number | 9229 | Debugger port |
| watch | string[] | ['src'] | Watch paths |
| env | Record<string, string> | {} | Environment variables |
| clearScreen | boolean | true | Clear screen on start |
| verbose | boolean | false | Verbose output |
build(entry?, options?): Promise<void>
Build for production programmatically.
import { build } from '@nextrush/dev';
// Simple
await build();
// With options
await build('./src/index.ts', {
outDir: 'dist',
minify: true,
sourcemap: true,
target: 'es2022',
});BuildOptions:
| Option | Type | Default | Description |
| ------------------- | --------- | ---------- | ----------------------- |
| entry | string | auto | Entry file path |
| outDir | string | 'dist' | Output directory |
| target | string | 'es2022' | ES target |
| sourcemap | boolean | true | Generate sourcemaps |
| minify | boolean | false | Minify output |
| decoratorMetadata | boolean | true | Emit decorator metadata |
| clean | boolean | true | Clean output first |
| verbose | boolean | false | Verbose output |
Auto-Detection
Entry file detection order:
package.jsonmainormodulefield (convertsdist/tosrc/,.jsto.ts)src/index.tssrc/main.tssrc/app.tssrc/server.tsindex.tsmain.tsapp.tsserver.ts
Requirements
- Node.js >= 22.0.0 (for native
--watchsupport) tsconfig.jsonwith:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Why Not Use tsup/esbuild?
tl;dr: They don't emit decorator metadata, breaking DI.
// Your code
@Service()
class UserService {
constructor(private db: Database) {}
}
// After tsup/esbuild (metadata LOST):
let UserService = class {
constructor(db) {}
};
// After nextrush build (metadata PRESERVED):
let UserService = class {
constructor(db) {}
};
Reflect.defineMetadata('design:paramtypes', [Database], UserService);The nextrush build command uses SWC with decoratorMetadata: true, which properly emits the reflection metadata required by tsyringe and other DI containers.
API Reference
Runtime Detection
import {
detectRuntime, // () => 'node' | 'bun' | 'deno'
getRuntimeInfo, // () => RuntimeInfo
isNode, // () => boolean
isBun, // () => boolean
isDeno, // () => boolean
} from '@nextrush/dev';Configuration
import {
findEntry, // () => string
loadConfig, // () => Promise<NextRushConfig>
getDefaultWatchPaths, // () => string[]
} from '@nextrush/dev';Code Generation
import {
generate, // (type, name, cwd?) => Promise<string>
generateCli, // (args: string[]) => Promise<void>
GENERATOR_TYPES, // ['controller', 'service', 'middleware', 'guard', 'route']
} from '@nextrush/dev';
// Programmatic usage
const filePath = await generate('controller', 'user', process.cwd());Production Readiness
Current Status: Beta
Every row below is backed by a permanent, real-runtime regression test - not asserted. "Stable" means the behavior is proven under CI on that runtime; "Experimental" means it runs but has no automated regression guard yet.
| Feature | Status | Evidence |
| ------------- | -------------- | --------------------------------------------------------------------------- |
| Node.js dev | Yes - Stable | dev-http-liveness.test.ts (real HTTP response), dev-restart-on-change.test.ts (real --watch restart) |
| Node.js build | Yes - Stable | build-e2e-integration.test.ts, swc-builder-integration.test.ts (cache, .d.ts, nested layout) |
| Bun dev | Experimental | Native support; no dedicated nextrush dev regression test on Bun yet |
| Bun build | Yes - Stable | build-bun-decorator-integration.test.ts - asserts design:paramtypes literally appears in Bun-built output |
| Deno dev | Experimental | Native support; no dedicated nextrush dev regression test on Deno yet |
| Deno build | Yes - Stable | build-deno-integration.test.ts - asserts non-empty, correctly-mapped .js output under real Deno |
| Generate | Yes - Stable | generators/*.test.ts - all 5 generator types |
Bun/Deno build and dev regression tests run in CI on their real binaries via the
dev-tooling-cross-runtime job in runtime-conformance.yml (pinned Deno 2.6.3 / Bun 1.3.14).
All Runtimes Support Decorator Metadata
Decorator metadata emission is verified, not asserted, on every runtime nextrush build targets:
- Node.js:
@swc/coretransform API -swc-builder-integration.test.ts - Bun: native bundler preserves
Reflect.metadata/design:paramtypes-build-bun-decorator-integration.test.ts - Deno:
npm:@swc/corevia thenpm:specifier -build-deno-integration.test.ts
Architecture Documentation
For a deep dive into how this package works, see ARCHITECTURE.md.
License
MIT (c) NextRush Team
Behavior & Cross-Platform Notes
- Auto-restart, not HMR.
nextrush devuses the runtime's native watcher (node --watch,bun --watch,deno run --watch). A change restarts the process; module state is not preserved. - Watch paths are honored per runtime.
--watch <path>(repeatable) maps tonode --watch-path=<path>,deno --watch=<paths>; on Bun (no path-scoped watch) it warns and falls back to watching imported files. - Flags accept
--flag=valueand--flag value. Unknown flags are a hard error (non-zero exit), not silently ignored. - Cross-platform. The SWC dev loader is resolved as a
file://URL (correct on Windows), path handling usesnode:path, and Node child processes are spawned via the running Node binary - no reliance onnpx/PATH shims. - Declarations are deterministic.
.d.tsfiles are generated with the project's locally-installed TypeScript (nonpx, no network); a declaration failure fails the build unless--no-dtsis passed. - Safe cleaning.
nextrush buildrefuses to clean an output directory that is the project root, an ancestor, the source directory, or outside the project. - Output is ESM (
module: es6);.ts/.tsx->.js,.mts->.mjs,.cts->.cjs. An incremental content-hash cache skips unchanged files (--no-cacheto bypass).
Monorepo / Workspace Build Scoping
nextrush build resolves its scan root to the nearest enclosing package.json
directory - walking upward from the entry file's own directory (e.g. from src/ for
the common src/index.ts layout) until it finds one. That directory is the package
boundary: the scan never ascends above it, and any subdirectory inside the scanned
tree that carries its own package.json is excluded entirely - a nested or
vendored package is never pulled into the current package's build output.
my-package/
|-- package.json <- scan root resolves here (not src/)
|-- config.ts Yes - scanned - sibling of src/, at the package root
|-- src/
| |-- index.ts Yes - scanned
| |-- utils.ts Yes - scanned
| `-- vendor/
| |-- package.json Excluded - this makes `vendor/` a separate package -
| `-- lib.ts excluded entirely, never descended into
`-- dist/ (build output)In a pnpm/npm/Turborepo workspace, a sibling package (e.g. packages/other-package
next to packages/my-package) is excluded because the scan stops ascending the moment
it finds packages/my-package/package.json - it never continues upward into the
workspace root or sideways into a directory outside that boundary. If no
package.json can be found anywhere above the entry file (an unusual, non-package
layout), the build falls back to scanning from the entry's own directory - the
behavior this feature builds on. This holds for every layout: single-package
projects, workspace packages, and projects with no package.json at all (which
scan exactly as they did before this feature).
In short: if you have a directory nested inside your source tree that is its
own package (has its own package.json), it is always excluded from the build -
this is intended, not a bug, and there is no config flag to change it.
