tekir
All Packages
Corev0.1.39

@tekir/core

Router, kernel, DI container, lazy-resolved singletons, and server bootstrap via tekir().

Installation

$bun add @tekir/core

Features

  • 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 graceful flag is mapped to Bun's closeActiveConnections argument 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 trustedHosts settings 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 a 413 before the handler runs instead of being buffered into memory.
  • Query strings and request helpers (input, all, only, except) now reject __proto__, constructor, and prototype keys and build their output on null-prototype objects, closing a prototype pollution vector.
  • Response headers carrying CR/LF (including Vary) 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 optional trustedHosts allowlist (exact host plus *.subdomain wildcards) and otherwise keeps only the same-origin pathname + search. A path that registered specific methods now answers unmatched methods with a 405 Allow response and routes OPTIONS through the global middleware chain.
  • generate:key no longer prints the generated APP_KEY to stdout. Signed cookie verification is unified on a single constant-time reader, and graceful SIGINT/SIGTERM shutdown now runs in every mode.
  • Found and fixed with Fable.
v0.1.33May 28, 2026
  • The server now binds the configured host. Setting host in config/app.ts (or tekir({ 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 to Bun.serve, so on Bun the bind address silently stayed 0.0.0.0. Use host: '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 default 0.0.0.0 (all interfaces). hostname is accepted as an alias for host (matching Bun.serve's option name); host wins 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 expose Symbol.asyncIterator rather than Symbol.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. Sync function* 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/plain even when the handler yielded SSE frames (data: ...). The SSE-vs-plain decision ran inside the stream's pull() callback, but new Response(stream, init) snapshots its headers at construction, before pull() ever fires, so the detection was dead code. The first chunk is now pulled up front, so an SSE generator correctly emits Content-Type: text/event-stream (plus X-Accel-Buffering: no so proxies do not buffer the stream). Native EventSource clients work against generator routes now; previously only manual fetch().body.getReader() consumers did. Object streams that are not SSE keep their newline-delimited text/plain JSON 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 a new Response(precomputedString, frozenInit) per request. Routes that touch params, 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 like new, function, this, globalThis, Bun, etc. opt in.
  • The synthetic 404 fallback now lives under Bun.serve's native /* route instead of going through the fetch callback when the server has no WebSocket routes, no domain routes, and no user-supplied server.fallback(...). In that case serveConfig.fetch is 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 call server.fallback(handler) keep the previous behaviour.
  • Shared JSON_RESPONSE_INIT constant 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 previous Response.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), and verifySignedCookieValue(token, secret) exported from @tekir/core. Use them on the request side to read back cookies set via response.encryptedCookie(...) and response.signedCookie(...). Both readers return null on tampering, expiry, or malformed input so callers can branch on a single nullable result.
  • response.download(path) and response.attachment(path, name?) now keep the Content-Disposition header. The helpers staged it on the response builder but the runtime file response replaced the entire Headers object, so browsers fell back to inline display for any extension the OS happened to know. The merged response also picks up any cookies queued via response.cookie(...) before the download return.
v0.1.29May 10, 2026
  • tekir build no longer evaluates the user entry's full top-level. The build path now parses the entry with oxc-parser, walks back from the await 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. The tekir() call still fires (so onBuild hooks register and Bun.build runs against the original entry), but app.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. New generateBuildEntry(entryPath) is exported for tools that want to drive the same extraction. Dynamic config inside tekir({...}) (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_RUNNER is set to 'build' (via ??=, so an outer caller can pin a different value first) whenever the entry detects cliCmd === 'build'. Pairs with the 'test' value tekir test already 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 on TEKIR_RUNNER === 'build' || TEKIR_RUNNER === 'test'. The contract is documented at /advanced/runner-modes.
v0.1.28May 8, 2026
  • app.start() honours the tekir test runner signal. When the cli's test subcommand exports TEKIR_RUNNER=test before launching the runtime's native test command, a user entry's top-level app.start(callback) short-circuits instead of binding the env-configured port. The canonical entry shape becomes the unconditional app.start(cb); the per-app if (env !== 'test') guard goes away. Integration tests that genuinely want a real socket pass app.start({ force: true }) to opt back in. The lower-level server.start() (used by @tekir/testing's createTestApp) is unaffected, so request-fixture tests keep working without changes.
  • StartOptions.force?: boolean exported alongside the existing mode and callback fields, 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 HttpException subclass: the E_ prefix is removed in favour of plain UPPER_SNAKE_CASE matching the HTTP status name (NOT_FOUND, BAD_REQUEST, UNAUTHORIZED, ...). Aligns with gRPC, Google Cloud, AWS Cognito, and Stripe's snake_case conventions; error.code already implies "this is an error" so the prefix is redundant. **Breaking**: callers that branch on err.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.retry typed as number | string. The runtime path always coerces with String(data.retry) and strips newlines, so passing either shape is safe; the type now matches the implementation. Lets retry flow through configs that hold the value as a string without an intermediate cast.
v0.1.26May 6, 2026
  • **Breaking**: tekir start is removed. Use tekir serve for every long-running server invocation (dev, local prod, deploy targets like PM2/Docker/systemd). The two diverged historically only because start carried a buggy non-awaited auto-dispatch that double-bound the port when a user entry also called app.start(callback); collapsing to a single command means user code is identical across every launch shape. Migration is a one-line package.json edit: "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, and tekir --entry foo migrate all 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 early NODE_ENV setter and the environment detector, so all three layers agree on what the user actually asked for.
  • tekir serve (without --dev) defaults NODE_ENV to production when the shell did not set it, matching the tekir build precedent. The cli bin already exports NODE_ENV='development' before re-execing the watch child for --dev, so the dev path is preserved. Combined with the bun build banner shipped in 0.1.24, every prod entry path now sees the right env without an explicit NODE_ENV=production on the command line.
v0.1.24May 6, 2026
  • tekir build now defaults process.env.NODE_ENV to production at bundle-load time. Apps started with a raw bun ./dist/index.js (the shape PM2, Docker, and systemd typically use) no longer need an explicit NODE_ENV=production on 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 of process.env.NODE_ENV reads is unaffected.
v0.1.22May 5, 2026
  • Fix: production builds with router.registerDir, cron.registerDir, or emitter.registerDir now 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-detects appRoot by walking the call stack to find the file that called it and using that file's dirname. Same Error.captureStackTrace mechanism router.registerDir already uses for caller-relative resolution. Falls back to process.cwd() only when no user frame is recoverable. Eliminates the process.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 resolved appRoot instead of process.cwd() so the user's local @tekir/vite is 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 (notably app.port) before server.start() reads the value, which is what @tekir/vite 0.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') becomes Buffer.from('<base64>', 'base64'); readFile from fs/promises and Bun.file('./x').text() / .arrayBuffer() chains become Promise.resolve(<literal>). Detection covers named, aliased ({ readFileSync as rfs }), namespace (* as fs), and default imports from fs, node:fs, fs/promises, and node:fs/promises. Eliminates the postbuild scripts, manual file copying, and process.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.readFile are 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_pick helper. 0.1.18 wrapped the helper body in new Function("m", "<body>"), but Bun's bundler optimizer still parses the literal body and folds the call sites back to m.default ?? m because 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 through Object.prototype.hasOwnProperty.call(_m, "default"), blocks the static-shape analysis: the bundler cannot prove _m is an own-property holder for default purely from the namespace synthesis, so the body survives intact in the output. The fix has been verified on real production bundles (148 hasOwnProperty.call references survive in a typical sevk-shaped app, controllers and cron jobs all register).
v0.1.18May 4, 2026
  • Hardens the AST inliner's __tekir_pick helper 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 to m.default ?? m, which on a named-export controller (export class FooController, no default) collapsed back to the bare module namespace and crashed register(...) with Object is not a constructor. The picker is now built from a string literal via new 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, no default) 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 made register(...arr) call new <namespace>, producing Object is not a constructor at boot in production builds. Each rewritten registerDir/loadDir call now goes through an injected __tekir_pick(mod) helper that mirrors the runtime defaultPick (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/registerDir call sites), so the picker logic rides along inside the bundle without adding a new runtime dependency on @tekir/core for 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 imported loadDir and captureCallerFile inside the registerDir method, which placed the call across an await boundary, 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 a file:// URL, otherwise it is rejected. That blocks Bun's native (after the :1:11 suffix is stripped), Node's node: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, not process.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') from api/index.ts resolves to api/controllers regardless of the cwd. Base directory captured via Error.captureStackTrace. Pass options.from = process.cwd() to keep the old cwd-relative behavior for a specific call site.
  • New LoadDirOptions.from accepts a file:// URL or absolute path (typically import.meta.url) to set the resolution base explicitly when the auto-captured caller is not the right answer.
  • captureCallerFile(boundary) exported from @tekir/core so other registries (cron, emitter, custom) can apply the same caller-relative resolution. Both Bun and Node honor Error.captureStackTrace(obj, fn); the helper handles the format differences (Bun raw paths vs. Node file:// URLs) internally.
v0.1.14May 4, 2026
  • runBuild, parseBuildArgs, and BuildArgsError are now public exports of @tekir/core, so the new @tekir/cli package and any user driving Bun.build programmatically share one implementation. Same flag surface as the in-process bun run index.ts build dispatcher, plus optional extraPlugins / extraExternals / logger overrides 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 / --env values surface as clear errors instead of being silently dropped.
  • Forwarded flags expanded to match bun build more 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-syntax alone).
  • Refused with a clear error when invoked from inside a compiled binary. Detection is hybrid: Bun.main virtual-fs marker (~BUN) plus process.execPath basename check, two independent signals so a single Bun version drift does not break detection.
v0.1.13May 4, 2026
  • Pairs with the new @tekir/cli package. The CLI's tekir build command imports createInlinerPlugin from @tekir/core and runs Bun.build directly 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 for bun build api/index.ts --outdir ./dist [...] once you install @tekir/cli.
  • Default loadDir/registerDir picker handles export class FooController (named export, no default) automatically. The picker tries mod.default first, 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 custom pick: m => m.default ?? Object.values(m).find(...) for every registry call.
  • registerDir warnings now name the source file: [router.registerDir] core/controllers/typo.ts: skipped (unrecognized export shape: object). Same wording on cron.registerDir and emitter.registerDir. loadDirEntries(path, options) is exported alongside loadDir for 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.
  • createInlinerPlugin is exported from @tekir/core so plain bun build --outdir ./dist bundles (without --compile) can pick up the same inlining: Bun.build({ plugins: [await createInlinerPlugin()] }). Without it, runtime registerDir calls in those bundles can't see the source files and silently load nothing.
  • bun run index.ts build --outdir ./dist now runs a plain Bun bundle through the tekir CLI (no --compile required). The inliner plugin is auto-injected, so loadDir/registerDir calls are still followed by the bundler, and the existing --target / --minify / --sourcemap / --external / --define / --plugin / --splitting flags are all forwarded. CLI-only build setups can keep await router.registerDir('./controllers') instead of writing a Bun.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 custom pick callback to grab a named export, match / ignore regexes to filter, and recursive: true to walk subdirectories. Works on Bun and Node by routing through @tekir/runtime's readDirRecursive, which uses Bun.Glob on Bun for the directory scan and falls through to node:fs/promises on 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 via router.register), functional registrars (export default (router) => { ... }, invoked with the router), and classes with a register(router) method (a fresh instance is constructed and its register is called). Files whose default export does not match any pattern are skipped with a console.warn so misconfigured exports surface during boot.
  • bun build --compile now bundles the files referenced by loadDir('path') and *.registerDir('path') calls. The compile pipeline auto-injects an AST-based inliner (powered by oxc-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-parser is an optional peer dependency; install it with bun add -d oxc-parser to 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.$responseHeaders is 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-existent skipped the chain entirely and the browser saw a generic CORS error on what was actually a 404.
  • ctx.request exposes path, host, hostname, protocol, origin, and completeUrl as direct properties. Routes that touch any of these get a single upfront URL parse; routes that only read request.url / request.method skip the parse entirely.
  • Vary is appended (not overwritten) when both a handler and middleware set it, so a Vary: Accept-Encoding from the cache layer keeps living next to the Origin token CORS adds.
v0.1.10May 3, 2026
  • Server idle timeout default is now 120 seconds, 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 pass idleTimeout: 0 to 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.idleTimeout in your config (or pass idleTimeout to server.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. Pass envFile, configDir, and startDir explicitly 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 named env.ts (e.g. interactive .env setup 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 and cors() 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 the DOM.Iterable lib in the consumer's tsconfig. Some app tsconfigs only pull in DOM, which made the previous Headers.entries() call fail to type-check at the consumer side. Switched to Headers.forEach, available in plain DOM.
v0.1.6May 2, 2026
  • Body parser failures no longer crash routes with a generic 500. When the declared Content-Type does not match the actual payload (empty body with application/json, malformed urlencoded, etc.) the parse error is captured on ctx.bodyError so 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, so return response.unauthorized() works the way Express, Koa, and Hono users expect without remembering ctx.$result =.
v0.1.5April 30, 2026
  • tekir() accepts an inline routes callback so single-file apps can register routes without destructuring the router first. The callback runs after providers boot, so service() resolves to live instances inside it, and the methods passed in are pre-bound, so destructuring ({ get, post }) works without losing this.
v0.1.4April 29, 2026
  • response.redirect.back(fallback?) sends users back to the page they came from. Reads the Referer header 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.md and inline attribution comments crediting Elysia (MIT, Copyright 2022 saltyAom) for the implementation details that were adapted from its source: the AOT body parser's charCodeAt(12) content-type switch, the arrow-handler source separator, the query parser's bit-flag layout, the SSE helper, and the beforeHandle / afterHandle lifecycle hooks.
v0.1.2April 27, 2026
  • AppConfig interface added so config/app.ts can be authored with satisfies AppConfig for 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 a start/commands.ts to surface things like migrate or seed.
v0.1.1April 27, 2026
  • bun run index.ts build --compile now 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-artifacts to inspect the build output.
v0.1.0April 1, 2026
  • Initial release

Other Core packages