Runner modes (TEKIR_RUNNER)

A cross-package convention so libraries with eager side effects can short-circuit during bundling and testing without every project re-declaring an env flag in its scripts.

Introduction

Some libraries do real work as soon as they are imported: a Redis client opens a TCP connection, a queue worker starts polling, a scheduler arms its first timer, a file watcher binds an inotify handle. That work is fine when the process exists to serve traffic, and harmful when it exists for any other reason — most visibly during tekir build, where every such connection is dead weight that slows the bundler and can hang the build outright if the remote dependency is unreachable.

Tekir publishes a contract: process.env.TEKIR_RUNNER is set to a known value for the two non-serving modes ('build' and 'test') and left unset for everything else. The framework sets it; libraries read it. No project-level glue.

Values

// Set automatically by the framework — never override unless you have a reason

TEKIR_RUNNER=test     // `tekir test` (cli runs the runtime's native test command)
TEKIR_RUNNER=build    // `tekir build` or `bun run index.ts build` (bundle is the goal,
                      //  not serving traffic)
// (unset)            // Every other invocation: `tekir serve`, raw `bun ./dist/index.js`,
                      //  compiled binary, REPL, custom commands.

The framework only sets TEKIR_RUNNER when it recognises the command — and uses ??= assignment so an outer caller (CI scripts, custom wrappers) can pin a different value first. tekir serve, bun ./dist/index.js, compiled binaries, and any custom tekir <command> path leave the variable unset; library code defaults to its normal eager behaviour there.

Library pattern

A three-line guard at the top of any module that opens a connection, starts a worker, or hooks into the OS:

// my-redis-lib/src/index.ts
// Eager-side-effect modules read TEKIR_RUNNER and short-circuit when the
// process is for bundling or testing. Keeping the gate inside the
// library means downstream users never have to remember to set anything
// in their package.json scripts — `tekir test` and `tekir build` are
// the two contracts the framework guarantees.
import { Redis } from 'ioredis'

const isBuildOrTest = process.env.TEKIR_RUNNER === 'build' ||
  process.env.TEKIR_RUNNER === 'test'

const client = new Redis(process.env.REDIS_URL!, {
  // `lazyConnect` keeps the constructor side-effect-free until the
  // first command. Pair it with a guarded `connect()` call below so
  // the bundler does not pay for a TCP handshake it never uses.
  lazyConnect: true,
})

export function startSubscriber(channels: string[]) {
  if (isBuildOrTest) return // bundle/test path: register no listeners
  client.subscribe(...channels)
  client.on('message', handle)
}

export { client }

Both branches are needed:

  • build guards against a doomed connection during bundling. The bundler imports every module to follow its static graph; a Redis client that connects on import would force the bundler to talk to your Redis just to learn the module's exports.
  • test guards against the equivalent in test runners. Tests typically point at fakes or in-memory stores; letting the production client try to connect during a unit test wastes time and surfaces flakes.

Examples

Project scripts

// Before — every project's package.json had to set the flag manually:
{
  "scripts": {
    "build": "TEKIR_RUNNER=build tekir build --outdir ./dist",
    "test":  "TEKIR_RUNNER=test bun test"
  }
}

// After — the framework sets it for you. Project scripts stay clean:
{
  "scripts": {
    "build": "tekir build --outdir ./dist",
    "test":  "tekir test"
  }
}

Framework-side gates that already use the flag

// Inside a tekir() entry — the same flag drives the framework's own
// build-time and test-time skip paths. There is nothing to opt into;
// these short-circuits fire as soon as `TEKIR_RUNNER` is set.

import { tekir } from '@tekir/core'

const app = await tekir({
  config: { app: { port: 20000 } },
  frontend: { type: 'vite' },     // dev gateway is skipped under TEKIR_RUNNER=test
})

await app.router.registerDir('./controllers')

app.start(() => console.log('ready'))   // no-op under TEKIR_RUNNER=test
                                        // (use `{ force: true }` to opt back in
                                        //  for integration tests that need a
                                        //  real socket).

Guarantees

  • TEKIR_RUNNER is set before the user entry is imported, so module-top-level code observes the right value.
  • The framework uses ??=: an outer process can pin a value first and Tekir will not overwrite it.
  • Library authors can rely on the variable being a stable string ('build', 'test', or unset). New values will only land in major releases.
  • Custom workflows that want library code to behave as if it were a real run can simply delete process.env.TEKIR_RUNNER before importing the entry.