Skip to content

feat: add selectConfig for config files that can't use top-level await#19

Closed
rqbazan wants to merge 1 commit into
mainfrom
feat/select-config
Closed

feat: add selectConfig for config files that can't use top-level await#19
rqbazan wants to merge 1 commit into
mainfrom
feat/select-config

Conversation

@rqbazan
Copy link
Copy Markdown
Member

@rqbazan rqbazan commented May 27, 2026

Why

loadConfig is async (it uses dynamic import(), which is irreducibly asynchronous). That's fine in app code, but it breaks in config files that tooling loads synchronously — files pulled in via require() or bundled to CJS, where a top-level await is rejected (ERR_REQUIRE_ASYNC_MODULE, or the esbuild "top-level await is not supported with the cjs output format"). There's no runtime-agnostic way to make loadConfig sync (a sync module load means require(), which only exists on CJS-capable runtimes and can't load .ts without a transpile hook).

What

selectConfig(configs) — a synchronous, runtime-agnostic counterpart. Pair it with static imports so the runtime/bundler resolves and transpiles each config at parse time; it picks the entry matching envName() and throws when there's none (the map is explicit, so a miss is almost always a typo).

import { defineEnv, selectConfig } from "@vlandoss/env";
import development from "./config/development.ts";
import production from "./config/production.ts";

export const env = defineEnv({
  schema: Env,
  config: selectConfig({ development, production }),
});

Changes

  • core: selectConfig (package/src/lib/select-config.ts) + export from the main entry.
  • tests: runtime tests (select-config.test.ts) + type tests in lib.test-d.ts (return type is T, never T | undefined; pipes into defineEnv).
  • example: examples/config-cjs/ (pinned to Node 22.22.3) — a node:test that require()s both a selectConfig config (loads ✓) and a loadConfig + top-level await config (throws ERR_REQUIRE_ASYNC_MODULE ✓). Registered in the root config_roots.
  • docs: new section in the loadConfig guide + a row in the core API reference + example table.
  • changeset: minor.

Node pin rationale: require(esm) is default from 22.12 and native TS stripping from 22.18, so 22.22.3 is the lowest LTS line where the reproduction (and fix) behave as described. Node 20 would throw ERR_REQUIRE_ESM — a different failure.

Verification

  • Package suite: 87 tests pass (node + browser projects).
  • examples/config-cjs test:e2e: both assertions pass.
  • rr check (package + docsite) + example check: clean.

🤖 Generated with Claude Code

@vland-bot
Copy link
Copy Markdown
Contributor

vland-bot Bot commented May 27, 2026

Preview release

Latest commit: 41bddaa

Some packages have been released:

Package Version Install
@vlandoss/env 0.2.2-git-41bddaa.0 @vlandoss/env@0.2.2-git-41bddaa.0

Note

Use the PR number as tag to install any package. For instance:

pnpm add @vlandoss/env@pr-19

`selectConfig(configs)` is a synchronous, runtime-agnostic counterpart to
`loadConfig`. It pairs with static `import`s and picks the entry matching
`envName()`, so config files that tooling loads via `require()` or bundles to
CJS — where top-level await is rejected (ERR_REQUIRE_ASYNC_MODULE) — can wire up
env without async.

- core: `selectConfig` + export from the main entry
- tests: runtime tests + type tests
- example: `config-cjs` (Node 22.22.3) asserting the require() failure mode and
  the selectConfig fix
- docs: loadConfig guide section + core API reference row
- changeset: minor

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rqbazan rqbazan force-pushed the feat/select-config branch from 2d0a24f to 41bddaa Compare May 27, 2026 22:42
@rqbazan
Copy link
Copy Markdown
Member Author

rqbazan commented May 27, 2026

Superseded by a sync loadConfig approach. After verifying that require(esm) + native TypeScript stripping work across Node ≥22.18, Bun, and Deno — including when a config is bundled to CJS — we're going with a loadConfigSync that shares a pure core with loadConfig and keeps auto-discovery, instead of a separate selectConfig that needs an explicit static-import map. Closing in favor of that work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant