From add960cf3f6aa5db381afdb0f3917076c21796a4 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Sun, 10 May 2026 19:56:39 +0100 Subject: [PATCH 1/2] fix(nitro): scope runtime config probes to active major version --- .changeset/nitro-bridge-active-runtime.md | 11 +++ packages/evlog/src/nitro-v3/plugin.ts | 3 +- packages/evlog/src/nitro/plugin.ts | 3 +- .../evlog/src/shared/nitroConfigBridge.ts | 73 +++++++++++++++- .../test/shared/nitroConfigBridge.test.ts | 86 +++++++++++++++++++ 5 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 .changeset/nitro-bridge-active-runtime.md create mode 100644 packages/evlog/test/shared/nitroConfigBridge.test.ts diff --git a/.changeset/nitro-bridge-active-runtime.md b/.changeset/nitro-bridge-active-runtime.md new file mode 100644 index 00000000..319153de --- /dev/null +++ b/.changeset/nitro-bridge-active-runtime.md @@ -0,0 +1,11 @@ +--- +'evlog': patch +--- + +Fix a runtime crash on Vercel + Bun + Nitro v3 where evlog probed `nitropack/runtime/internal/config` even though only Nitro v3 was installed. Bun's auto-install kicked in for the missing dependency and tried to write `node_modules/.cache`, which crashes on Vercel's read-only function filesystem with `bun is unable to write files: ReadOnlyFileSystem`. + +The Nitro plugins now declare their major version once (via the new internal `setActiveNitroRuntime` helper) and the shared config bridge probes only the matching runtime — `nitro/runtime-config` for v3, `nitropack/...` for v2. Adapters resolving config through `runtimeConfig.evlog.` benefit from the same restriction, so `createPostHogDrain()` (and any adapter using `resolveAdapterConfig`) no longer triggers the cross-version probe. + +No public-API change. The `process.env.__EVLOG_CONFIG` fast path remains the highest-priority lookup. + +Closes [#312](https://github.com/HugoRCD/evlog/issues/312). diff --git a/packages/evlog/src/nitro-v3/plugin.ts b/packages/evlog/src/nitro-v3/plugin.ts index 865e33ac..c1558f70 100644 --- a/packages/evlog/src/nitro-v3/plugin.ts +++ b/packages/evlog/src/nitro-v3/plugin.ts @@ -5,7 +5,7 @@ import { parseURL } from 'ufo' import { createRequestLogger, getGlobalPluginRunner, initLogger, isEnabled } from '../logger' import { shouldLog, getServiceForPath, extractErrorStatus } from '../nitro' import { normalizeRedactConfig } from '../redact' -import { resolveEvlogConfigForNitroPlugin } from '../shared/nitroConfigBridge' +import { resolveEvlogConfigForNitroPlugin, setActiveNitroRuntime } from '../shared/nitroConfigBridge' import type { EnrichContext, RequestLogger, TailSamplingContext, WideEvent } from '../types' import { filterSafeHeaders } from '../utils' @@ -150,6 +150,7 @@ async function callEnrichAndDrain( * ``` */ export default definePlugin(async (nitroApp) => { + setActiveNitroRuntime('v3') const evlogConfig = await resolveEvlogConfigForNitroPlugin() const redact = normalizeRedactConfig(evlogConfig?.redact as boolean | Record | undefined) diff --git a/packages/evlog/src/nitro/plugin.ts b/packages/evlog/src/nitro/plugin.ts index 0c2047fa..260fcaf1 100644 --- a/packages/evlog/src/nitro/plugin.ts +++ b/packages/evlog/src/nitro/plugin.ts @@ -8,7 +8,7 @@ import { getHeaders } from 'h3' import { createRequestLogger, getGlobalPluginRunner, initLogger, isEnabled } from '../logger' import { shouldLog, getServiceForPath, extractErrorStatus } from '../nitro' import { normalizeRedactConfig } from '../redact' -import { resolveEvlogConfigForNitroPlugin } from '../shared/nitroConfigBridge' +import { resolveEvlogConfigForNitroPlugin, setActiveNitroRuntime } from '../shared/nitroConfigBridge' import { startStreamServer, type StreamServerOptions } from '../stream' import type { EnrichContext, RequestLogger, ServerEvent, TailSamplingContext, WideEvent } from '../types' import { filterSafeHeaders } from '../utils' @@ -120,6 +120,7 @@ async function callEnrichAndDrain( } export default defineNitroPlugin(async (nitroApp) => { + setActiveNitroRuntime('v2') const evlogConfig = await resolveEvlogConfigForNitroPlugin() const redact = normalizeRedactConfig(evlogConfig?.redact as boolean | Record | undefined) diff --git a/packages/evlog/src/shared/nitroConfigBridge.ts b/packages/evlog/src/shared/nitroConfigBridge.ts index 288cd4d3..1f8bcb34 100644 --- a/packages/evlog/src/shared/nitroConfigBridge.ts +++ b/packages/evlog/src/shared/nitroConfigBridge.ts @@ -13,8 +13,11 @@ * modules; preferred in production Workers builds). * 2. Computed module IDs — `['a','b'].join('/')` passed to `import()` so emitted * JS does not contain a static `import("a/b")`. - * 3. Plugin resolution tries Nitro v3 first, then nitropack internal config (v2). - * 4. Adapter resolution keeps historical order: nitropack runtime barrel, then v3. + * 3. Plugins call {@link setActiveNitroRuntime} so adapters never probe modules + * from the other major version (e.g. Bun on Vercel triggers a package-cache + * write when probing a missing dynamic import — see issue #312). + * 4. When the active runtime is unknown (standalone use outside a Nitro + * plugin), the bridge falls back to the historical probe order. * * Not exported from `evlog/toolkit` — package-internal only. */ @@ -25,6 +28,29 @@ type EvlogConfig = NitroPluginEvlogConfig const EVLOG_NITRO_ENV = '__EVLOG_CONFIG' as const +type NitroMajor = 'v2' | 'v3' + +let activeNitroRuntime: NitroMajor | undefined + +/** + * Declare the active Nitro major version so adapters never probe the other + * version's modules at runtime. The evlog Nitro plugins call this in their + * first synchronous statement. + * + * Bun's auto-install behavior writes to `node_modules/.cache` whenever a + * dynamic import targets a missing package, which crashes on Vercel's + * read-only function filesystem. Restricting probes to the runtime that is + * actually installed avoids that path entirely. + */ +export function setActiveNitroRuntime(version: NitroMajor): void { + activeNitroRuntime = version +} + +/** @internal Reset the active runtime declaration. Used by tests only. */ +export function resetActiveNitroRuntime(): void { + activeNitroRuntime = undefined +} + type NitroRuntimeConfigModule = { useRuntimeConfig: () => Record } @@ -102,12 +128,36 @@ function evlogSlice(config: Record): EvlogConfig | undefined { /** * Options for evlog Nitro plugins (nitropack v2 and Nitro v3). - * Env bridge first; then Nitro v3 `runtime-config`; then nitropack internal config. + * + * Lookup order: + * 1. `process.env.__EVLOG_CONFIG` + * 2. The active runtime declared by {@link setActiveNitroRuntime} — either + * Nitro v3 `runtime-config` or nitropack internal config, never both. + * 3. When no active runtime has been declared (standalone use): probe v3 then + * nitropack v2 as a best-effort fallback. */ export async function resolveEvlogConfigForNitroPlugin(): Promise { const fromEnv = readEvlogConfigFromNitroEnv() if (fromEnv !== undefined) return fromEnv + if (activeNitroRuntime === 'v3') { + const v3 = await getNitroV3Runtime() + if (v3) { + const slice = evlogSlice(v3.useRuntimeConfig()) + if (slice !== undefined) return slice + } + return undefined + } + + if (activeNitroRuntime === 'v2') { + const internal = await getNitropackInternalRuntimeConfig() + if (internal) { + const slice = evlogSlice(internal.useRuntimeConfig()) + if (slice !== undefined) return slice + } + return undefined + } + const v3 = await getNitroV3Runtime() if (v3) { const slice = evlogSlice(v3.useRuntimeConfig()) @@ -124,9 +174,24 @@ export async function resolveEvlogConfigForNitroPlugin(): Promise | undefined> { + if (activeNitroRuntime === 'v3') { + const v3 = await getNitroV3Runtime() + return v3 ? v3.useRuntimeConfig() : undefined + } + + if (activeNitroRuntime === 'v2') { + const nitropack = await getNitropackRuntime() + return nitropack ? nitropack.useRuntimeConfig() : undefined + } + const nitropack = await getNitropackRuntime() if (nitropack) return nitropack.useRuntimeConfig() diff --git a/packages/evlog/test/shared/nitroConfigBridge.test.ts b/packages/evlog/test/shared/nitroConfigBridge.test.ts new file mode 100644 index 00000000..01cad7af --- /dev/null +++ b/packages/evlog/test/shared/nitroConfigBridge.test.ts @@ -0,0 +1,86 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +beforeEach(() => { + vi.resetModules() + vi.unstubAllEnvs() + delete process.env.__EVLOG_CONFIG +}) + +afterEach(() => { + vi.resetModules() + vi.doUnmock(['nitro', 'runtime-config'].join('/')) + vi.doUnmock(['nitropack', 'runtime', 'internal', 'config'].join('/')) + vi.doUnmock(['nitropack', 'runtime'].join('/')) +}) + +async function loadBridgeWithMocks() { + const importSpy = vi.fn<(specifier: string) => void>() + vi.doMock(['nitro', 'runtime-config'].join('/'), () => { + importSpy('nitro/runtime-config') + return { useRuntimeConfig: () => ({ evlog: { env: { service: 'svc-v3' } }, posthog: { apiKey: 'phc-v3' } }) } + }) + vi.doMock(['nitropack', 'runtime', 'internal', 'config'].join('/'), () => { + importSpy('nitropack/runtime/internal/config') + return { useRuntimeConfig: () => ({ evlog: { env: { service: 'svc-v2' } }, posthog: { apiKey: 'phc-v2' } }) } + }) + vi.doMock(['nitropack', 'runtime'].join('/'), () => { + importSpy('nitropack/runtime') + return { useRuntimeConfig: () => ({ evlog: { env: { service: 'svc-v2-barrel' } }, posthog: { apiKey: 'phc-v2-barrel' } }) } + }) + const bridge = await import('../../src/shared/nitroConfigBridge') + return { bridge, importSpy } +} + +describe('nitroConfigBridge — active runtime', () => { + it('only probes Nitro v3 modules when v3 is the active runtime', async () => { + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.setActiveNitroRuntime('v3') + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + const record = await bridge.getNitroRuntimeConfigRecord() + + expect(config).toEqual({ env: { service: 'svc-v3' } }) + expect(record).toEqual({ evlog: { env: { service: 'svc-v3' } }, posthog: { apiKey: 'phc-v3' } }) + + const probed = importSpy.mock.calls.map(call => call[0]) + expect(probed).toContain('nitro/runtime-config') + expect(probed).not.toContain('nitropack/runtime') + expect(probed).not.toContain('nitropack/runtime/internal/config') + }) + + it('only probes nitropack v2 modules when v2 is the active runtime', async () => { + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.setActiveNitroRuntime('v2') + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + const record = await bridge.getNitroRuntimeConfigRecord() + + expect(config).toEqual({ env: { service: 'svc-v2' } }) + expect(record).toEqual({ evlog: { env: { service: 'svc-v2-barrel' } }, posthog: { apiKey: 'phc-v2-barrel' } }) + + const probed = importSpy.mock.calls.map(call => call[0]) + expect(probed).not.toContain('nitro/runtime-config') + }) + + it('reads __EVLOG_CONFIG from env without probing any Nitro module', async () => { + process.env.__EVLOG_CONFIG = JSON.stringify({ env: { service: 'svc-env' } }) + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.setActiveNitroRuntime('v3') + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + + expect(config).toEqual({ env: { service: 'svc-env' } }) + expect(importSpy).not.toHaveBeenCalled() + }) + + it('falls back to historical probe order when no runtime is declared', async () => { + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.resetActiveNitroRuntime() + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + + expect(config).toEqual({ env: { service: 'svc-v3' } }) + const probed = importSpy.mock.calls.map(call => call[0]) + expect(probed).toContain('nitro/runtime-config') + }) +}) From 984193d4fbd15011f352d1900278b8c66fd231be Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Thu, 21 May 2026 20:57:19 +0100 Subject: [PATCH 2/2] fix(nitro): inline evlog config via nitro.options.replace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nitro plugin's runtime probe of `nitro/runtime-config` transitively imports `#nitro/virtual/runtime-config`, a build-only virtual module that does not exist in deployed bundles. On Vercel + Bun the missing virtual triggered Bun's package auto-installer and crashed every request with `ReadOnlyFileSystem`. The previous `setActiveNitroRuntime` fix only narrowed which runtime to probe — the v3 probe itself still ran and still crashed. Bake the evlog config into the bundle as a literal via `nitro.options.replace.__EVLOG_CONFIG__`. The shared bridge reads that literal first and short-circuits all runtime probing — both for the plugin (`resolveEvlogConfigForNitroPlugin`) and for adapters resolving through `useRuntimeConfig().evlog.` (`getNitroRuntimeConfigRecord` returns a synthetic record). No dynamic import, no env propagation guesswork. --- .changeset/nitro-bridge-active-runtime.md | 8 ++-- packages/evlog/src/nitro-v3/module.ts | 9 ++++ packages/evlog/src/nitro/module.ts | 8 ++++ .../evlog/src/shared/nitroConfigBridge.ts | 47 +++++++++++++++---- .../test/shared/nitroConfigBridge.test.ts | 43 +++++++++++++++++ 5 files changed, 102 insertions(+), 13 deletions(-) diff --git a/.changeset/nitro-bridge-active-runtime.md b/.changeset/nitro-bridge-active-runtime.md index 319153de..b8c8caa3 100644 --- a/.changeset/nitro-bridge-active-runtime.md +++ b/.changeset/nitro-bridge-active-runtime.md @@ -2,10 +2,12 @@ 'evlog': patch --- -Fix a runtime crash on Vercel + Bun + Nitro v3 where evlog probed `nitropack/runtime/internal/config` even though only Nitro v3 was installed. Bun's auto-install kicked in for the missing dependency and tried to write `node_modules/.cache`, which crashes on Vercel's read-only function filesystem with `bun is unable to write files: ReadOnlyFileSystem`. +Fix a runtime crash on Vercel + Bun + Nitro v3 where every request failed with `bun is unable to write files: ReadOnlyFileSystem`. The Nitro plugin probed `nitro/runtime-config` at runtime to read evlog's config; that module transitively imports the build-only `#nitro/virtual/runtime-config`, which doesn't exist in deployed bundles. On Vercel + Bun the missing virtual triggered Bun's package auto-installer, which tried to write `node_modules/.cache` and crashed on the read-only function filesystem. -The Nitro plugins now declare their major version once (via the new internal `setActiveNitroRuntime` helper) and the shared config bridge probes only the matching runtime — `nitro/runtime-config` for v3, `nitropack/...` for v2. Adapters resolving config through `runtimeConfig.evlog.` benefit from the same restriction, so `createPostHogDrain()` (and any adapter using `resolveAdapterConfig`) no longer triggers the cross-version probe. +The Nitro modules now bake the evlog config into the bundle as a literal via `nitro.options.replace.__EVLOG_CONFIG__`. The shared config bridge reads that build-time literal first and skips all runtime probing — no `import('nitro/runtime-config')`, no env propagation guesswork. The bridge also exposes the inlined value as a synthetic `{ evlog: }` record, so drain adapters resolving `runtimeConfig.evlog.` never trigger the probe either. -No public-API change. The `process.env.__EVLOG_CONFIG` fast path remains the highest-priority lookup. +For defense-in-depth, the bridge additionally scopes its dynamic-import fallback to the major version declared by the plugin (new internal `setActiveNitroRuntime` helper) — `nitro/runtime-config` for v3, `nitropack/...` for v2 — so standalone use outside a plugin (e.g. adapters called from non-Nitro code) doesn't probe both versions. + +No public-API change. Closes [#312](https://github.com/HugoRCD/evlog/issues/312). diff --git a/packages/evlog/src/nitro-v3/module.ts b/packages/evlog/src/nitro-v3/module.ts index 5f311b3e..9b7218fc 100644 --- a/packages/evlog/src/nitro-v3/module.ts +++ b/packages/evlog/src/nitro-v3/module.ts @@ -38,6 +38,15 @@ export default function evlog(options?: NitroModuleOptions) { nitro.options.runtimeConfig = nitro.options.runtimeConfig || {} nitro.options.runtimeConfig.evlog = options || {} + // Bake the config into the bundle as a literal so the plugin never has + // to do a runtime `import('nitro/runtime-config')` to discover it. That + // dynamic import resolves to a module which itself imports the build-only + // virtual `#nitro/virtual/runtime-config` — fine inside the Nitro build, + // but on Vercel + Bun the missing virtual triggers Bun's auto-installer + // and crashes with `ReadOnlyFileSystem` (see issue #312). + nitro.options.replace = nitro.options.replace || {} + nitro.options.replace.__EVLOG_CONFIG__ = JSON.stringify(options || {}) + // In dev mode, Nitro loads plugins externally (not bundled), so the // virtual runtime-config module is unreachable and useRuntimeConfig() // returns a stub without our values. process.env is inherited by the diff --git a/packages/evlog/src/nitro/module.ts b/packages/evlog/src/nitro/module.ts index fb99b224..ea4083f9 100644 --- a/packages/evlog/src/nitro/module.ts +++ b/packages/evlog/src/nitro/module.ts @@ -31,6 +31,14 @@ export default function evlog(options?: NitroModuleOptions) { nitro.options.runtimeConfig = nitro.options.runtimeConfig || {} nitro.options.runtimeConfig.evlog = options || {} + // Bake the config into the bundle as a literal so the plugin never has + // to do a runtime `import('nitropack/runtime/internal/config')` to + // discover it. The dynamic probe transitively imports a build-only + // virtual module; on Vercel + Bun the missing virtual triggers Bun's + // auto-installer and crashes with `ReadOnlyFileSystem` (issue #312). + nitro.options.replace = nitro.options.replace || {} + nitro.options.replace.__EVLOG_CONFIG__ = JSON.stringify(options || {}) + // In dev mode, Nitro loads plugins externally (not bundled), so the // virtual runtime-config module is unreachable and useRuntimeConfig() // returns a stub without our values. process.env is inherited by the diff --git a/packages/evlog/src/shared/nitroConfigBridge.ts b/packages/evlog/src/shared/nitroConfigBridge.ts index 1f8bcb34..63e98e07 100644 --- a/packages/evlog/src/shared/nitroConfigBridge.ts +++ b/packages/evlog/src/shared/nitroConfigBridge.ts @@ -9,14 +9,16 @@ * * **Strategy** * - * 1. `process.env.__EVLOG_CONFIG` — JSON set by evlog Nitro modules (no virtual - * modules; preferred in production Workers builds). - * 2. Computed module IDs — `['a','b'].join('/')` passed to `import()` so emitted + * 1. `__EVLOG_CONFIG__` — build-time literal baked in by the evlog Nitro + * modules via `nitro.options.replace`. When present, all runtime probing is + * skipped (see issue #312: Vercel + Bun crashes if the v3 probe runs). + * 2. `process.env.__EVLOG_CONFIG` — JSON set by evlog Nitro modules during + * build; survives into runtime on platforms that propagate build env vars. + * 3. Computed module IDs — `['a','b'].join('/')` passed to `import()` so emitted * JS does not contain a static `import("a/b")`. - * 3. Plugins call {@link setActiveNitroRuntime} so adapters never probe modules - * from the other major version (e.g. Bun on Vercel triggers a package-cache - * write when probing a missing dynamic import — see issue #312). - * 4. When the active runtime is unknown (standalone use outside a Nitro + * 4. Plugins call {@link setActiveNitroRuntime} so adapters never probe modules + * from the other major version. + * 5. When the active runtime is unknown (standalone use outside a Nitro * plugin), the bridge falls back to the historical probe order. * * Not exported from `evlog/toolkit` — package-internal only. @@ -28,6 +30,19 @@ type EvlogConfig = NitroPluginEvlogConfig const EVLOG_NITRO_ENV = '__EVLOG_CONFIG' as const +// Replaced at build time by `nitro.options.replace` in the evlog Nitro +// modules. Outside of a Nitro build, this identifier is undeclared and the +// `typeof` guard below evaluates safely. +// eslint-disable-next-line @typescript-eslint/naming-convention +declare const __EVLOG_CONFIG__: EvlogConfig | undefined + +/** Build-time inlined config, or `undefined` if the bundle was not produced by an evlog Nitro module. */ +export function readEvlogConfigFromInline(): EvlogConfig | undefined { + if (typeof __EVLOG_CONFIG__ === 'undefined') return undefined + if (__EVLOG_CONFIG__ === null || typeof __EVLOG_CONFIG__ !== 'object') return undefined + return __EVLOG_CONFIG__ +} + type NitroMajor = 'v2' | 'v3' let activeNitroRuntime: NitroMajor | undefined @@ -130,13 +145,18 @@ function evlogSlice(config: Record): EvlogConfig | undefined { * Options for evlog Nitro plugins (nitropack v2 and Nitro v3). * * Lookup order: - * 1. `process.env.__EVLOG_CONFIG` - * 2. The active runtime declared by {@link setActiveNitroRuntime} — either + * 1. `__EVLOG_CONFIG__` — inlined at build time by the evlog Nitro module. + * Hits in every deployed bundle and skips runtime probing entirely. + * 2. `process.env.__EVLOG_CONFIG` + * 3. The active runtime declared by {@link setActiveNitroRuntime} — either * Nitro v3 `runtime-config` or nitropack internal config, never both. - * 3. When no active runtime has been declared (standalone use): probe v3 then + * 4. When no active runtime has been declared (standalone use): probe v3 then * nitropack v2 as a best-effort fallback. */ export async function resolveEvlogConfigForNitroPlugin(): Promise { + const fromInline = readEvlogConfigFromInline() + if (fromInline !== undefined) return fromInline + const fromEnv = readEvlogConfigFromNitroEnv() if (fromEnv !== undefined) return fromEnv @@ -180,8 +200,15 @@ export async function resolveEvlogConfigForNitroPlugin(): Promise }` record so adapters can read `runtimeConfig.evlog.*` + * without triggering the dynamic import (issue #312). */ export async function getNitroRuntimeConfigRecord(): Promise | undefined> { + const inline = readEvlogConfigFromInline() + if (inline !== undefined) return { evlog: inline } + if (activeNitroRuntime === 'v3') { const v3 = await getNitroV3Runtime() return v3 ? v3.useRuntimeConfig() : undefined diff --git a/packages/evlog/test/shared/nitroConfigBridge.test.ts b/packages/evlog/test/shared/nitroConfigBridge.test.ts index 01cad7af..4ae1faba 100644 --- a/packages/evlog/test/shared/nitroConfigBridge.test.ts +++ b/packages/evlog/test/shared/nitroConfigBridge.test.ts @@ -1,9 +1,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +declare global { + // eslint-disable-next-line @typescript-eslint/naming-convention + var __EVLOG_CONFIG__: unknown +} + beforeEach(() => { vi.resetModules() vi.unstubAllEnvs() delete process.env.__EVLOG_CONFIG + delete (globalThis as { __EVLOG_CONFIG__?: unknown }).__EVLOG_CONFIG__ }) afterEach(() => { @@ -11,6 +17,7 @@ afterEach(() => { vi.doUnmock(['nitro', 'runtime-config'].join('/')) vi.doUnmock(['nitropack', 'runtime', 'internal', 'config'].join('/')) vi.doUnmock(['nitropack', 'runtime'].join('/')) + delete (globalThis as { __EVLOG_CONFIG__?: unknown }).__EVLOG_CONFIG__ }) async function loadBridgeWithMocks() { @@ -83,4 +90,40 @@ describe('nitroConfigBridge — active runtime', () => { const probed = importSpy.mock.calls.map(call => call[0]) expect(probed).toContain('nitro/runtime-config') }) + + it('returns the build-time inlined __EVLOG_CONFIG__ without probing', async () => { + globalThis.__EVLOG_CONFIG__ = { env: { service: 'svc-inline' } } + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.setActiveNitroRuntime('v3') + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + const record = await bridge.getNitroRuntimeConfigRecord() + + expect(config).toEqual({ env: { service: 'svc-inline' } }) + expect(record).toEqual({ evlog: { env: { service: 'svc-inline' } } }) + expect(importSpy).not.toHaveBeenCalled() + }) + + it('prefers __EVLOG_CONFIG__ over process.env.__EVLOG_CONFIG', async () => { + globalThis.__EVLOG_CONFIG__ = { env: { service: 'svc-inline' } } + process.env.__EVLOG_CONFIG = JSON.stringify({ env: { service: 'svc-env' } }) + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.setActiveNitroRuntime('v3') + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + + expect(config).toEqual({ env: { service: 'svc-inline' } }) + expect(importSpy).not.toHaveBeenCalled() + }) + + it('ignores __EVLOG_CONFIG__ when it is not an object literal', async () => { + globalThis.__EVLOG_CONFIG__ = 'not-an-object' + const { bridge, importSpy } = await loadBridgeWithMocks() + bridge.setActiveNitroRuntime('v3') + + const config = await bridge.resolveEvlogConfigForNitroPlugin() + + expect(config).toEqual({ env: { service: 'svc-v3' } }) + expect(importSpy.mock.calls.map(c => c[0])).toContain('nitro/runtime-config') + }) })