Corev0.1.39
@tekir/core
Router, kernel, DI container, lazy-resolved singletons, and server bootstrap via tekir().
Installation
$
bun add @tekir/coreFeatures
- tekir() bootstraps app, router, logger, and server in one call
- Trie-based router with route groups, resources, and param matchers
- Built-in DI container with lazy-resolved singletons via service()
- WebSocket support with channels, presence, and broadcast
- SSE streaming and Server-Timing helpers
- HTTP exceptions with built-in status classes (404, 422, etc.)
Quick Example
TypeScript
import { tekir } from '@tekir/core'
const app = await tekir()
app.router.get('/hello', (ctx) => {
return ctx.response.json({ message: 'Hello!' })
})
app.start()Changelog
v0.1.39LatestSeptember 16, 2026
- Routing now applies domain constraints and precedence consistently on native Bun and Node servers, while compiled handlers preserve request, cookie, response, and error semantics.
- Published output now uses the shared Node-targeted ESM bundle pipeline with external dependencies and generated TypeScript declarations, while Bun consumers keep the native source export.
v0.1.38August 22, 2026
- Graceful shutdown now stops accepting new connections and waits for active requests to drain before application hooks and providers close their resources. The public
gracefulflag is mapped to Bun'scloseActiveConnectionsargument correctly, while immediate shutdown still force-closes active connections.
v0.1.37July 23, 2026
- Body-parser middleware now owns multipart consumption without an eager
formData()pass, while request helpers read the parsed middleware result directly.
v0.1.35July 16, 2026
- Compiled handlers now create real response state lazily whenever handlers or middleware consume
ctx.response; cookies, headers, status codes, finish callbacks, streams, and wrapped native Responses are no longer silently dropped. - Compiled request contexts expose the complete request API, include a Cookie-header fallback, accept structured JSON MIME types, preserve multipart files, and reject dangerous query/input keys.
- Compiled route and middleware failures now use the configured exception pipeline, while debug and
trustedHostssettings are isolated per server instance. Plain-value routes remain on the lightweight fast path.
v0.1.34June 13, 2026
- The compiled route fast path no longer rebuilds handlers from their source text at startup. Every route now calls your real handler closure directly, so handlers keep working unchanged after a minifier or transpiler rewrites their source, and a whole class of source-reparse edge cases is gone.
- Request body size is now capped by default (10 MB, from
bodyParser.maxSize) on both the Bun and Node paths. Oversized requests get a413before the handler runs instead of being buffered into memory. - Query strings and request helpers (
input,all,only,except) now reject__proto__,constructor, andprototypekeys and build their output on null-prototype objects, closing a prototype pollution vector. - Response headers carrying
CR/LF(includingVary) are now dropped, closing a response-splitting surface. Invalid percent-encoding in route params and wildcards no longer throws; the raw segment is used instead. response.redirect.back()now validates the referer against an optionaltrustedHostsallowlist (exact host plus*.subdomainwildcards) and otherwise keeps only the same-originpathname + search. A path that registered specific methods now answers unmatched methods with a405 Allowresponse and routesOPTIONSthrough the global middleware chain.generate:keyno longer prints the generatedAPP_KEYto stdout. Signed cookie verification is unified on a single constant-time reader, and gracefulSIGINT/SIGTERMshutdown now runs in every mode.- Found and fixed with Fable.
v0.1.33May 28, 2026
- The server now binds the configured host. Setting
hostinconfig/app.ts(ortekir({ config: { app: { host } } })) is honoured instead of always listening on every interface. Previously the value was read for the Node fallback runtime but never passed toBun.serve, so on Bun the bind address silently stayed0.0.0.0. Usehost: '127.0.0.1'to accept only local connections,host: process.env.HOST ?? '0.0.0.0'to wire it from the environment, or leave it unset to keep the default0.0.0.0(all interfaces).hostnameis accepted as an alias forhost(matching Bun.serve's option name);hostwins if both are set.
v0.1.32May 28, 2026
- Fixed:
async function*route handlers streamed nothing and returned{}with a JSON content type. Async generators exposeSymbol.asyncIteratorrather thanSymbol.iterator, and the handler dispatcher only recognised the latter, so the generator object fell through to JSON serialisation (which has no enumerable keys). Both the middleware and no-middleware dispatch paths now detect either iterator protocol. Syncfunction*handlers on a route with no middleware were also affected — that path had no generator detection at all — and now stream correctly too. - Fixed: streamed responses always went out as
Content-Type: text/plaineven when the handler yielded SSE frames (data: ...). The SSE-vs-plain decision ran inside the stream'spull()callback, butnew Response(stream, init)snapshots its headers at construction, beforepull()ever fires, so the detection was dead code. The first chunk is now pulled up front, so an SSE generator correctly emitsContent-Type: text/event-stream(plusX-Accel-Buffering: noso proxies do not buffer the stream). NativeEventSourceclients work against generator routes now; previously only manualfetch().body.getReader()consumers did. Object streams that are not SSE keep their newline-delimitedtext/plainJSON shape.
v0.1.31May 17, 2026
- Inline routes that return a fully static literal (
() => ({ message: 'Hello' }),() => [1, 2, 3], primitives, deeply-nested literal objects, etc.) now serialise their body once at registration time and emit anew Response(precomputedString, frozenInit)per request. Routes that touchparams,query,body, or any closure variable keep going through the existing compiled path so behaviour is unchanged; only handlers whose entire return expression is JSON-safe and free of identifiers likenew,function,this,globalThis,Bun, etc. opt in. - The synthetic 404 fallback now lives under Bun.serve's native
/*route instead of going through thefetchcallback when the server has no WebSocket routes, no domain routes, and no user-suppliedserver.fallback(...). In that caseserveConfig.fetchis dropped entirely, so unmatched requests dispatch through the radix tree without a JS callback round trip. Apps that use websockets, multi-tenant subdomain routing, or callserver.fallback(handler)keep the previous behaviour. - Shared
JSON_RESPONSE_INITconstant used by every JSON response path (compiled handlers,response.json(),response.send(obj),response.ok(obj),response.created(obj), etc.) so the per-request header allocation drops out of the hot path. Functionally identical to the previousResponse.json(...)call; the wire format does not change.
v0.1.30May 17, 2026
response.encryptedCookie(name, value, secret)now produces an authenticated AES-256-GCM ciphertext instead of a base64-encoded payload with an HMAC tag. Anyone observing the cookie value can no longer decode its contents; tampering fails the auth tag check on the read side. The cookie shape changes from${base64url}.${signature}to${iv}.${ciphertext}.${authTag}; cookies issued by older releases will not decrypt with the new reader and need to be re-issued (clear and let the next request mint a fresh one).- New top-level helpers
encryptCookieValue(value, secret),decryptCookieValue<T>(token, secret), andverifySignedCookieValue(token, secret)exported from@tekir/core. Use them on the request side to read back cookies set viaresponse.encryptedCookie(...)andresponse.signedCookie(...). Both readers returnnullon tampering, expiry, or malformed input so callers can branch on a single nullable result. response.download(path)andresponse.attachment(path, name?)now keep theContent-Dispositionheader. The helpers staged it on the response builder but the runtime file response replaced the entireHeadersobject, so browsers fell back to inline display for any extension the OS happened to know. The merged response also picks up any cookies queued viaresponse.cookie(...)before the download return.
v0.1.29May 10, 2026
tekir buildno longer evaluates the user entry's full top-level. The build path now parses the entry withoxc-parser, walks back from theawait tekir({...})call, keeps only the imports and declarations its argument expression depends on, and writes that to a temporary file the cli imports in place of the original. Thetekir()call still fires (soonBuildhooks register andBun.buildruns against the original entry), butapp.router.registerDir(...),app.start(...), eager service constructors with TCP connects, scheduler ticks, and fs watchers are skipped — none of which the bundler ever needed. Builds are faster and stop hanging on a misconfigured remote dependency that would never have been needed at build time. NewgenerateBuildEntry(entryPath)is exported for tools that want to drive the same extraction. Dynamic config insidetekir({...})(env-derived ports, conditional frontend types, computed providers) is preserved verbatim because the call expression is kept as-is; only unreachable top-level statements are dropped.process.env.TEKIR_RUNNERis set to'build'(via??=, so an outer caller can pin a different value first) whenever the entry detectscliCmd === 'build'. Pairs with the'test'valuetekir testalready exports. Most library code is no longer imported during build at all (the entry extractor sees to that), so the flag is a belt-and-suspenders safety net for the rare entry shape that falls back to a full-entry import; libraries that want to short-circuit eager module-init side effects can still gate onTEKIR_RUNNER === 'build' || TEKIR_RUNNER === 'test'. The contract is documented at/advanced/runner-modes.
v0.1.28May 8, 2026
app.start()honours thetekir testrunner signal. When the cli'stestsubcommand exportsTEKIR_RUNNER=testbefore launching the runtime's native test command, a user entry's top-levelapp.start(callback)short-circuits instead of binding the env-configured port. The canonical entry shape becomes the unconditionalapp.start(cb); the per-appif (env !== 'test')guard goes away. Integration tests that genuinely want a real socket passapp.start({ force: true })to opt back in. The lower-levelserver.start()(used by@tekir/testing'screateTestApp) is unaffected, so request-fixture tests keep working without changes.StartOptions.force?: booleanexported alongside the existingmodeandcallbackfields, for code paths that want a real socket regardless of how the process was launched (dashboards, smoke checks,frontend-env-exposure-style integration tests).
v0.1.27May 8, 2026
- Cleaned up error code naming on every built-in
HttpExceptionsubclass: theE_prefix is removed in favour of plainUPPER_SNAKE_CASEmatching the HTTP status name (NOT_FOUND,BAD_REQUEST,UNAUTHORIZED, ...). Aligns with gRPC, Google Cloud, AWS Cognito, and Stripe'ssnake_caseconventions;error.codealready implies "this is an error" so the prefix is redundant. **Breaking**: callers that branch onerr.code === 'E_NOT_FOUND'(etc.) need to drop the prefix. The companion releases ship the same change in@tekir/auth(UNAUTHORIZED),@tekir/authorize(AUTHORIZATION_FAILURE),@tekir/db(ROW_NOT_FOUND), and@tekir/validator(VALIDATION_ERROR). SSE.retrytyped asnumber | string. The runtime path always coerces withString(data.retry)and strips newlines, so passing either shape is safe; the type now matches the implementation. Letsretryflow through configs that hold the value as a string without an intermediate cast.
v0.1.26May 6, 2026
- **Breaking**:
tekir startis removed. Usetekir servefor every long-running server invocation (dev, local prod, deploy targets like PM2/Docker/systemd). The two diverged historically only becausestartcarried a buggy non-awaited auto-dispatch that double-bound the port when a user entry also calledapp.start(callback); collapsing to a single command means user code is identical across every launch shape. Migration is a one-linepackage.jsonedit:"start": "tekir serve --entry ./dist/index.js ..."(the npm script name keeps working, only the CLI subcommand changes).
v0.1.25May 6, 2026
- Command dispatch is flag-aware. Operators can put options before the command word and the right path still fires:
./server --port 8080 build,bun run index.ts --watch serve, andtekir --entry foo migrateall resolve to their command instead of treating the leading flag as a positional. Critical for compiled binaries, where flag-first invocations are the common shape (PM2/Docker/systemd-style env and port flags). The same scan also feeds the earlyNODE_ENVsetter and theenvironmentdetector, so all three layers agree on what the user actually asked for. tekir serve(without--dev) defaultsNODE_ENVtoproductionwhen the shell did not set it, matching thetekir buildprecedent. The cli bin already exportsNODE_ENV='development'before re-execing the watch child for--dev, so the dev path is preserved. Combined with thebun buildbanner shipped in 0.1.24, every prod entry path now sees the right env without an explicitNODE_ENV=productionon the command line.
v0.1.24May 6, 2026
tekir buildnow defaultsprocess.env.NODE_ENVtoproductionat bundle-load time. Apps started with a rawbun ./dist/index.js(the shape PM2, Docker, and systemd typically use) no longer need an explicitNODE_ENV=productionon every command line. Runtime-set values still win, so dev-style overrides keep working. The default lands via a bracket-access banner so the bundler's compile-time fold ofprocess.env.NODE_ENVreads is unaffected.
v0.1.22May 5, 2026
- Fix: production builds with
router.registerDir,cron.registerDir, oremitter.registerDirnow reliably register every file in the target directory. The previous release silently registered nothing in some bundles, which surfaced as/api/*routes falling through to the SPA fallback in production while working fine in dev. - The fix is generic across decorators. Controllers, jobs, and listeners tagged with any framework-provided or user-defined decorator (
@Controller,@Schedule,@OnEvent, custom@Cron,@Subscribe, anything that stamps the registry metadata convention) are picked up the same way. No allowlist of decorator names; the build follows what the runtime would have picked.
v0.1.21May 5, 2026
tekir()auto-detectsappRootby walking the call stack to find the file that called it and using that file'sdirname. SameError.captureStackTracemechanismrouter.registerDiralready uses for caller-relative resolution. Falls back toprocess.cwd()only when no user frame is recoverable. Eliminates theprocess.chdir(import.meta.dir)workaround monorepo apps need when launched from a parent dir (turbo from repo root, pm2 from/, etc.). Frontend module resolution (createRequire) and config discovery now use the resolvedappRootinstead ofprocess.cwd()so the user's local@tekir/viteis found regardless of launch dir.- Frontend setup signature extended:
setup(server, frontendConfig, { configStore, appRoot }). The third arg is optional, so older(server, config)integrations stay drop-in compatible. The added context lets frontend middleware read and rewrite config (notablyapp.port) beforeserver.start()reads the value, which is what@tekir/vite0.1.2 uses to flip the dev architecture and own the user port as a gateway.
v0.1.20May 4, 2026
- AST inliner now also folds literal-path runtime fs reads into the bundle, so apps that read small config / template files at startup ship as a single self-contained artifact and run from any working directory. Recognized shapes:
readFileSync('./x.json', 'utf-8')becomes a string literal;readFileSync('./x.bin')becomesBuffer.from('<base64>', 'base64');readFilefromfs/promisesandBun.file('./x').text()/.arrayBuffer()chains becomePromise.resolve(<literal>). Detection covers named, aliased ({ readFileSync as rfs }), namespace (* as fs), and default imports fromfs,node:fs,fs/promises, andnode:fs/promises. Eliminates the postbuild scripts, manual file copying, andprocess.cwd()-relative path probing that monorepo bundles otherwise need at boot. - Files larger than 1 MB, dynamic paths (template literals, variables), dynamic encodings, and callback-style
fs.readFileare silently skipped, leaving those calls as runtime fs lookups so the inliner never changes call semantics. The inliner is also a no-op when none of the recognized helpers appear in the source, so existing files pay zero analysis cost.
v0.1.19May 4, 2026
- AST inliner now emits a per-call-site IIFE picker instead of a shared
__tekir_pickhelper. 0.1.18 wrapped the helper body innew Function("m", "<body>"), but Bun's bundler optimizer still parses the literal body and folds the call sites back tom.default ?? mbecause every call site's argument is a static namespace import whose shape is known at bundle time. A separate IIFE per call site, plus a reflection probe throughObject.prototype.hasOwnProperty.call(_m, "default"), blocks the static-shape analysis: the bundler cannot prove_mis an own-property holder fordefaultpurely from the namespace synthesis, so the body survives intact in the output. The fix has been verified on real production bundles (148hasOwnProperty.callreferences survive in a typical sevk-shaped app, controllers and cron jobs all register).
v0.1.18May 4, 2026
- Hardens the AST inliner's
__tekir_pickhelper against Bun's bundle-time optimizer. 0.1.17 emitted the picker as a regular function declaration; Bun's optimizer was inlining the body into each call site and constant-folding the result down tom.default ?? m, which on a named-export controller (export class FooController, no default) collapsed back to the bare module namespace and crashedregister(...)withObject is not a constructor. The picker is now built from a string literal vianew Function("m", "<body>")so the bundler only sees the literal at build time and cannot fold the body. Function compiles once at app boot, no per-request impact.
v0.1.17May 4, 2026
- AST inliner now picks the right export for
export class FooController(named export, nodefault) bundles. The previous output emitted(__tekir_inline_X.default ?? __tekir_inline_X)for every imported file, which collapsed to the namespace object when no default existed and maderegister(...arr)callnew <namespace>, producingObject is not a constructorat boot in production builds. Each rewritten registerDir/loadDir call now goes through an injected__tekir_pick(mod)helper that mirrors the runtimedefaultPick(default first, then single named export, then decorator-tagged class via__prefix/__routes/__schedules/__listeners, then first function-typed named export, then the namespace as a last resort). Build and runtime now resolve the same export shape for the same file. - Helper is inlined into the transformed source (one definition per file that has
loadDir/registerDircall sites), so the picker logic rides along inside the bundle without adding a new runtime dependency on@tekir/corefor files that did not already import it.
v0.1.16May 4, 2026
- Fixes 0.1.15's caller capture in
router.registerDir. The previous version dynamically importedloadDirandcaptureCallerFileinside the registerDir method, which placed the call across anawaitboundary, so by the time the stack was inspected the user's frame was gone and Bun's only remaining frames (native:1:11) leaked through to be used as the resolution base. The bin would then warn(resolved against native)and load nothing. The fix moves both helpers to top-level static imports so the caller is captured synchronously on entry, before any await runs. captureCallerFile's stack-frame parser tightened: a frame's path must look like an absolute filesystem path (Unix/...or Windows<drive>:\...) or afile://URL, otherwise it is rejected. That blocks Bun'snative(after the:1:11suffix is stripped), Node'snode:internal/..., anonymous<anonymous>frames, and any other synthetic engine markers from being treated as user code.
v0.1.15May 4, 2026
- **Breaking**:
router.registerDir(...)resolves a relative path against the caller's own directory, notprocess.cwd(). Aligns runtime resolution with what the AST inliner already does at build time, so the standard monorepo dev pattern (cd <root> && tekir serve --dev --entry api/index.ts) works without rewriting paths:await router.registerDir('./controllers')fromapi/index.tsresolves toapi/controllersregardless of the cwd. Base directory captured viaError.captureStackTrace. Passoptions.from = process.cwd()to keep the old cwd-relative behavior for a specific call site. - New
LoadDirOptions.fromaccepts afile://URL or absolute path (typicallyimport.meta.url) to set the resolution base explicitly when the auto-captured caller is not the right answer. captureCallerFile(boundary)exported from@tekir/coreso other registries (cron, emitter, custom) can apply the same caller-relative resolution. Both Bun and Node honorError.captureStackTrace(obj, fn); the helper handles the format differences (Bun raw paths vs. Nodefile://URLs) internally.
v0.1.14May 4, 2026
runBuild,parseBuildArgs, andBuildArgsErrorare now public exports of@tekir/core, so the new@tekir/clipackage and any user drivingBun.buildprogrammatically share one implementation. Same flag surface as the in-processbun run index.ts builddispatcher, plus optionalextraPlugins/extraExternals/loggeroverrides on the JS API for advanced setups.- Build flag parser rewritten on top of
node:util.parseArgs(Node stdlib, also available in Bun) so unknown flags, missing values, and bad--define/--sourcemap/--format/--envvalues surface as clear errors instead of being silently dropped. - Forwarded flags expanded to match
bun buildmore completely:--format esm|cjs|iife,--banner,--footer,--drop(multi),--env inline|disable|<PREFIX>*,--public-path,--no-bundle,--keep-names, granular--minify-syntax / --minify-whitespace / --minify-identifiers,--entry-naming,--chunk-naming, plus--metafile <path>and--metafile-md <path>for bundle analysis output. Granular minify flags emit Bun's object form so users can pick a subset (e.g.--minify-syntaxalone). - Refused with a clear error when invoked from inside a compiled binary. Detection is hybrid:
Bun.mainvirtual-fs marker (~BUN) plusprocess.execPathbasename check, two independent signals so a single Bun version drift does not break detection.
v0.1.13May 4, 2026
- Pairs with the new
@tekir/clipackage. The CLI'stekir buildcommand importscreateInlinerPluginfrom@tekir/coreand runsBun.builddirectly without touching the entry file, so apps with side-effect-heavy module loads (Redis subscribers, message-bus clients, fs watchers) stay quiet during build. Drop-in replacement forbun build api/index.ts --outdir ./dist [...]once you install@tekir/cli. - Default
loadDir/registerDirpicker handlesexport class FooController(named export, no default) automatically. The picker triesmod.defaultfirst, falls through to the single named export, prefers a decorator-tagged class (__prefix/__routes/__schedules/__listeners) when there are multiple named exports, then the first function-typed export, and finally returns the namespace itself. Apps no longer need a custompick: m => m.default ?? Object.values(m).find(...)for every registry call. registerDirwarnings now name the source file:[router.registerDir] core/controllers/typo.ts: skipped (unrecognized export shape: object). Same wording oncron.registerDirandemitter.registerDir.loadDirEntries(path, options)is exported alongsideloadDirfor callers that need the file path next to every picked export.registerDir(router, cron, emitter) prints a single warning when it loaded zero modules, with a hint pointing at the inliner plugin. Replaces the previous silent failure where a misconfigured production bundle would just have no controllers/jobs/listeners with no log line explaining why.createInlinerPluginis exported from@tekir/coreso plainbun build --outdir ./distbundles (without--compile) can pick up the same inlining:Bun.build({ plugins: [await createInlinerPlugin()] }). Without it, runtimeregisterDircalls in those bundles can't see the source files and silently load nothing.bun run index.ts build --outdir ./distnow runs a plain Bun bundle through the tekir CLI (no--compilerequired). The inliner plugin is auto-injected, soloadDir/registerDircalls are still followed by the bundler, and the existing--target/--minify/--sourcemap/--external/--define/--plugin/--splittingflags are all forwarded. CLI-only build setups can keepawait router.registerDir('./controllers')instead of writing aBun.build({...})script.
v0.1.12May 4, 2026
loadDir(path)returns the default export of every file in a directory, so registries like controllers, cron jobs, listeners, and commands no longer need a 25-line import block. Pass a custompickcallback to grab a named export,match/ignoreregexes to filter, andrecursive: trueto walk subdirectories. Works on Bun and Node by routing through@tekir/runtime'sreadDirRecursive, which usesBun.Globon Bun for the directory scan and falls through tonode:fs/promiseson Node.router.registerDir(path)wires up a whole controllers folder in one line. It auto-detects three export shapes per file: decorator classes (the@Controller+@Get/@Post/...pattern, registered viarouter.register), functional registrars (export default (router) => { ... }, invoked with the router), and classes with aregister(router)method (a fresh instance is constructed and itsregisteris called). Files whose default export does not match any pattern are skipped with aconsole.warnso misconfigured exports surface during boot.bun build --compilenow bundles the files referenced byloadDir('path')and*.registerDir('path')calls. The compile pipeline auto-injects an AST-based inliner (powered byoxc-parser) that finds literal-string folder calls, lists the directory at build time, and replaces each call with explicit static imports so Bun's bundler can follow them. Comments, string literals, computed-arg calls, and unrelated identifiers are left alone.oxc-parseris an optional peer dependency; install it withbun add -d oxc-parserto opt in. When the parser is missing, compile prints a one-line install hint so the silent failure mode does not bite.
v0.1.11May 3, 2026
ctx.$responseHeadersis the new way for middleware to attach response headers. Anything written here lands on the outgoing response right before it goes on the wire, on success, error, and framework-handled-error paths alike. CORS, request id, server timing, and any other header-attaching middleware now work from any position in the chain instead of breaking silently when an error handler sat between them and the route.- Unmatched paths now run the global middleware chain before responding 404, so
cors(), request loggers, and other hooks observe the request and stamp their headers on the response. Previously a stray request to/non-existentskipped the chain entirely and the browser saw a generic CORS error on what was actually a 404. ctx.requestexposespath,host,hostname,protocol,origin, andcompleteUrlas direct properties. Routes that touch any of these get a single upfront URL parse; routes that only readrequest.url/request.methodskip the parse entirely.Varyis appended (not overwritten) when both a handler and middleware set it, so aVary: Accept-Encodingfrom the cache layer keeps living next to theOrigintoken CORS adds.
v0.1.10May 3, 2026
- Server idle timeout default is now
120seconds, comfortably above typical SSE keepalive intervals (15-30 s) and long-poll cycles, while still reaping stuck or slowloris-style connections. Apps that need genuinely long-lived idle connections can passidleTimeout: 0to disable the timeout entirely; any other finite value is honored.
v0.1.9May 3, 2026
- Long-lived streams (Server-Sent Events, long-polling, slow file downloads) no longer get cut off mid-flight. The server's idle timeout default is configurable; set
app.idleTimeoutin your config (or passidleTimeouttoserver.configure({...})) to override the framework default.
v0.1.8May 3, 2026
- **Breaking**:
tekir()no longer scans<root>/env.ts,<root>/src/env.ts,<root>/config/,<root>/start/, or<root>/commands/automatically. PassenvFile,configDir, andstartDirexplicitly to keep a file-based layout:await tekir({ envFile: 'env.ts', configDir: 'config', startDir: 'start' }). With nothing set, tekir loads no files; everything is inline. This stops the framework from running unrelated root scripts namedenv.ts(e.g. interactive.envsetup CLIs in monorepos) when an app boots. - OPTIONS preflight on a path that registered only specific methods (e.g.
POST /login) now reaches the global middleware chain. Before, Bun.serve returned 405 andcors()never saw the preflight. The router now synthesizes a 204 OPTIONS handler at every path that did not register one explicitly, so middleware can intercept and short-circuit with the proper preflight response.
v0.1.7May 2, 2026
request.headers()no longer requires theDOM.Iterablelib in the consumer's tsconfig. Some app tsconfigs only pull inDOM, which made the previousHeaders.entries()call fail to type-check at the consumer side. Switched toHeaders.forEach, available in plainDOM.
v0.1.6May 2, 2026
- Body parser failures no longer crash routes with a generic 500. When the declared
Content-Typedoes not match the actual payload (empty body withapplication/json, malformed urlencoded, etc.) the parse error is captured onctx.bodyErrorso handlers and middleware can respond with a real 400. ctx.response.status(code).json(...)now actually carries the status across the chain. The compiled fast path used to silently fall back to 200; routes that chain a status setter automatically switch to a stateful response object.- Middleware return values are picked up automatically. Returning a
Response(or anything else) from a middleware sets it as the route result, soreturn response.unauthorized()works the way Express, Koa, and Hono users expect without rememberingctx.$result =.
v0.1.5April 30, 2026
tekir()accepts an inlineroutescallback so single-file apps can register routes without destructuring the router first. The callback runs after providers boot, soservice()resolves to live instances inside it, and the methods passed in are pre-bound, so destructuring({ get, post })works without losingthis.
v0.1.4April 29, 2026
response.redirect.back(fallback?)sends users back to the page they came from. Reads theRefererheader and restricts it to same-origin URLs, so attackers cannot bounce users off-site through a crafted referer. Falls back to the provided URL (or/) when the referer is missing or cross-origin.
v0.1.3April 27, 2026
- Added a
NOTICE.mdand inline attribution comments crediting Elysia (MIT, Copyright 2022 saltyAom) for the implementation details that were adapted from its source: the AOT body parser'scharCodeAt(12)content-type switch, the arrow-handler source separator, the query parser's bit-flag layout, the SSE helper, and thebeforeHandle/afterHandlelifecycle hooks.
v0.1.2April 27, 2026
AppConfiginterface added soconfig/app.tscan be authored withsatisfies AppConfigfor autocomplete on the framework-known fields without losing extensibility.- Service providers can now expose CLI commands via a
static commands = [...]array. Registered providers contribute their commands automatically, so apps no longer need astart/commands.tsto surface things likemigrateorseed.
v0.1.1April 27, 2026
bun run index.ts build --compilenow exposes the full Bun compile surface:--define KEY=VAL,--exec-argv,--asset-naming,--splitting --outdir,--plugin, plus autoload toggles for tsconfig, package.json,.env, and bunfig.frontend: { type: 'vite' }apps can now be compiled into a single executable.- Compiled builds auto-clean their intermediate
dist/<buildDir>after writing the binary. Pass--keep-artifactsto inspect the build output.
v0.1.0April 1, 2026
- Initial release