@trebired/bootstrap
v0.1.0
Published
Convention-based backend bootstrapper that discovers ordered bootstrap modules, injects dependencies by parameter name, and runs them deterministically.
Downloads
130
Maintainers
Readme
@trebired/bootstrap
A backend bootstrap loader for Bun and Node.js that discovers modules, runs them in order, and injects dependencies by parameter name.
@trebired/bootstrap scans a bootstrap directory, finds ordered bootstrap files, injects your dependencies by parameter name, and runs each bootstrap module in a stable order.
Install
Runtime support: Bun 1+ and Node.js 18+.
Bun can import .ts and .mts bootstrap files directly. Node.js users should point dir at compiled ESM .js or .mjs output, or run Node with a TypeScript loader.
npm install @trebired/bootstrapQuick Start
import { bootstrap } from "@trebired/bootstrap";
import { log } from "@trebired/logger";
await bootstrap({
dir: "/srv/app/src/backend",
config,
db,
log,
logger: log,
});dir is required. There is no fallback search path.
What It Loads
Bootstrap starts at the dir you pass in and only scans directories under that path.
It does this:
- reads the first-level child directories inside
dir - walks those directories recursively
- only considers
.js,.mjs,.ts, and.mtsfiles - only runs files whose names end with a numeric suffix like
.1,.2,.3, or a final suffix like.a
It does not do this:
- it does not scan files sitting directly in the root
dir - it does not load
.cjsfiles - it does not guess names like
server,io, orapp
Example:
src/backend/
db/
connect.1.ts
migrate.2.ts
ready.a.ts
helper.ts
http/
middleware.1.ts
jobs/
queue.1.ts
root-file.1.tsLoaded:
db/connect.1.tsdb/migrate.2.tsdb/ready.a.tshttp/middleware.1.tsjobs/queue.1.ts
Ignored:
db/helper.tsIt has no ordering suffix.root-file.1.tsIt sits directly indir, and bootstrap only starts from child directories.
Order is simple:
- numbered files run first in ascending order
- the final suffix runs after the numbered files
With the default final suffix, ready.a.ts runs after connect.1.ts and migrate.2.ts.
Module Shapes
Each bootstrap file must be an ESM module and can export one of these shapes:
export default function attach(db, log) {
log.info("db", "connected");
}export function attach(dependencies) {
dependencies.log.info("http", "middleware attached");
}export default {
attach(config, db) {
db.configure(config.database);
},
};CommonJS patterns such as module.exports = ... are not supported.
About the attach name:
- if you use a named export hook, it must be named
attach - if you use a default exported object hook, the method must be named
attach - if you use a default exported function, the function does not need to be named
attach
So this works:
export default function startServer(app, log) {
log.info("http", "server starting");
}and this works:
export function attach(app, log) {
log.info("http", "routes attached");
}but this does not:
export function start(app, log) {
log.info("http", "routes attached");
}How Dependencies Work
Every non-option top-level key you pass into bootstrap() becomes injectable by parameter name.
This:
await bootstrap({
dir: "/srv/app/src/backend",
config,
db,
log,
logger: log,
});lets a bootstrap file do this:
export default function connect(config, db, log) {
log.info("db", "connected");
}If a bootstrap file wants the whole dependency object, use dependencies or deps:
export function attach(dependencies) {
dependencies.log.info("http", "middleware attached");
}There are no hardcoded dependency names. server, io, app, config, db, log, or any other name only exist if you pass them in.
Reserved option keys are dir, scan, verbose, and logger. If you want your bootstrap files to receive a logger dependency, pass it as log and also set logger: log if you want bootstrap itself to use the same logger.
Scan Config
The public scan config is grouped so it is clearer what applies to directories and what applies to files.
await bootstrap({
dir: "/srv/app/src/backend",
config,
db,
log,
logger: log,
scan: {
dirs: {
include: ["db", "http", "jobs"],
exclude: ["legacy", "db/fixtures"],
allowNodeModules: false,
},
files: {
include: ["connect.1.ts", "http/routes.2.ts"],
exclude: ["types.d.ts", "http/legacy.3.ts"],
excludeSuffixes: ["spec", "test", "d"],
lastSuffix: "a",
},
},
verbose: true,
});What each option means:
dir: required root directory to scanconfig,db,log, and other non-option keys: dependencies that bootstrap files can request by parameter namescan.dirs.include: first-level folders underdirto start scanning fromscan.dirs.exclude: directories to skip by basename or relative pathscan.dirs.allowNodeModules:falseby default, sonode_modulesis skipped unless you explicitly allow itscan.files.include: optional allowlist for files, matched by basename or relative pathscan.files.exclude: files to skip by basename or relative pathscan.files.excludeSuffixes: suffixes to ignore, such asspec,test, ordscan.files.lastSuffix: the suffix that runs last after numbered filesverbose: prints extra bootstrap diagnosticslogger: logger used by bootstrap's own internal messages
Some concrete examples:
scan.dirs.include: ["db", "http"]means bootstrap starts only from/srv/app/src/backend/dband/srv/app/src/backend/httpscan.dirs.exclude: ["legacy", "db/fixtures"]skips a directory literally namedlegacy, and also skips the specific relative pathdb/fixturesscan.dirs.allowNodeModules: trueis the explicit opt-in if you really want bootstrap to scan anode_modulesdirectoryscan.files.include: ["routes.2.ts", "http/server.a.ts"]matches by either basename or relative pathscan.files.exclude: ["types.d.ts", "http/legacy.3.ts"]also matches by either basename or relative pathscan.files.lastSuffix: "a"meansready.a.tsruns afterconnect.1.tsandmigrate.2.ts
When scan.files.include is present, it acts as an allowlist. Only matching supported files are considered, and only attachable files actually run.
For safety, node_modules is excluded by default anywhere in the scan tree. If you really want to scan it, set scan.dirs.allowNodeModules: true.
Full API Example
import { bootstrap } from "@trebired/bootstrap";
import { log } from "@trebired/logger";
const summary = await bootstrap({
dir: "/srv/app/src/backend",
config,
db,
log,
cache,
http,
scan: {
dirs: {
include: ["db", "http", "jobs"],
exclude: ["legacy", "jobs/fixtures"],
allowNodeModules: false,
},
files: {
include: ["connect.1.ts", "http/routes.2.ts", "jobs/ready.a.ts"],
exclude: ["types.d.ts", "http/legacy.3.ts"],
excludeSuffixes: ["spec", "test", "d"],
lastSuffix: "a",
},
},
verbose: true,
logger: log,
});
log.info("bootstrap", "startup complete", summary);That example means:
- bootstrap scans
/srv/app/src/backend - only the
db,http, andjobsroot folders are used as scan starting points legacydirectories andjobs/fixturesare skipped- only files that match the file allowlist are considered
- files ending in
.spec,.test, or.dare ignored - files ending in
.arun after numbered files - bootstrap's internal diagnostics are sent through the same
logobject
Logger Support
@trebired/bootstrap works best with @trebired/logger, and that is the recommended logger.
Why we recommend it:
- it is simple
- it already matches bootstrap's expected method shape
- it keeps application logs and bootstrap logs in one consistent format
The logger style:
log.info("bootstrap", "startup complete", summary);comes from @trebired/logger.
If you pass logger: log, bootstrap will use that same style for its internal messages.
If you want bootstrap files themselves to receive the logger as a dependency, also pass it as a normal top-level dependency:
await bootstrap({
dir,
log,
logger: log,
});Custom loggers are supported too, as long as they follow this structure:
type Logger = {
info(group: string, message: string, metadata?: unknown): void;
warn(group: string, message: string, metadata?: unknown): void;
error(group: string, message: string, metadata?: unknown): void;
fail(group: string, message: string, metadata?: unknown): void;
};What those parts mean:
group: the category or source, such as"bootstrap"or"http"message: a short event descriptionmetadata: optional extra data, usually an object
If no logger is provided, bootstrap falls back to plain console output.
Example App
There is a working example in examples/server.js with matching bootstrap files under examples/server_bootstrap.
That example shows:
- a small app object being built in ordered bootstrap files
- top-level dependency injection
- using the same
logobject for both injected logging and bootstrap's own internal logging
Return Value
bootstrap() returns a summary object:
type BootstrapSummary = {
scanned: number;
loaded: number;
skipped: number;
failed: number;
};