diff --git a/packages/devtools/README.md b/packages/devtools/README.md index 67006d67..24320681 100644 --- a/packages/devtools/README.md +++ b/packages/devtools/README.md @@ -19,6 +19,8 @@ npm install @json-render/devtools This package is pulled in automatically by every framework adapter. Install it directly only if you're building your own adapter or using the stream tap utilities on the server. +Adapters detect production through the bundler's `process.env.NODE_ENV` replacement, including in browsers without a `process` global. + ## What you get - **Event store** — capped ring buffer with `push`, `subscribe`, `snapshot`, `clear`. diff --git a/packages/devtools/src/prod-guard.test.ts b/packages/devtools/src/prod-guard.test.ts index 2799373d..4fb3c08e 100644 --- a/packages/devtools/src/prod-guard.test.ts +++ b/packages/devtools/src/prod-guard.test.ts @@ -1,10 +1,19 @@ -import { afterEach, describe, expect, it } from "vitest"; +// @vitest-environment node +import { afterEach, describe, expect, it, vi } from "vitest"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runInNewContext } from "node:vm"; +import { build } from "tsup"; import { isProduction } from "./prod-guard"; describe("isProduction", () => { const original = process.env.NODE_ENV; afterEach(() => { - process.env.NODE_ENV = original; + vi.unstubAllGlobals(); + if (original === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = original; }); it("returns false when NODE_ENV is not production", () => { @@ -21,4 +30,31 @@ describe("isProduction", () => { delete process.env.NODE_ENV; expect(isProduction()).toBe(false); }); + + it("returns false in an unbundled browser without process", () => { + vi.stubGlobal("process", undefined); + expect(isProduction()).toBe(false); + }); + + it("detects a production bundle without a browser process global", async () => { + const outDir = await mkdtemp(join(tmpdir(), "json-render-prod-guard-")); + try { + await build({ + entry: [fileURLToPath(new URL("./prod-guard.ts", import.meta.url))], + outDir, + format: ["cjs"], + config: false, + silent: true, + define: { "process.env.NODE_ENV": '"production"' }, + }); + const source = await readFile(join(outDir, "prod-guard.js"), "utf8"); + expect( + runInNewContext(`${source}\nmodule.exports.isProduction()`, { + module: { exports: {} }, + }), + ).toBe(true); + } finally { + await rm(outDir, { recursive: true, force: true }); + } + }); }); diff --git a/packages/devtools/src/prod-guard.ts b/packages/devtools/src/prod-guard.ts index 098a23cb..20abc6e7 100644 --- a/packages/devtools/src/prod-guard.ts +++ b/packages/devtools/src/prod-guard.ts @@ -4,12 +4,13 @@ * nothing in production builds. * * Tree-shaken bundlers will fold constant `process.env.NODE_ENV` checks. - * Consumers that run in browsers without `process` also work because the - * typeof check guards the access. + * Check the expression directly so bundler replacement still works when + * browsers have no `process` global. Unbundled browsers fall back to false. */ export function isProduction(): boolean { - if (typeof process !== "undefined" && process.env) { + try { return process.env.NODE_ENV === "production"; + } catch { + return false; } - return false; }