From 90f4874cc1a3ba276d885039ed1c0b0332502411 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:15:37 -0700 Subject: [PATCH 1/3] refactor(kernel-utils): replace vat bundler NODE_ENV define with a plugin Move `process.env.NODE_ENV` injection out of the `bundleVat` build config and into a dedicated, auto-detecting Rolldown transform plugin (`replaceNodeEnvPlugin`). The plugin inlines `process.env.NODE_ENV` as the literal `"production"` in any module that references it and no-ops on modules that don't, matching the previous `define` behavior while making the logic a first-class, unit-tested plugin instead of a build-config detail. Closes #812 Co-Authored-By: Claude Opus 4.8 --- .../src/vite-plugins/bundle-vat.ts | 8 +-- .../kernel-utils/src/vite-plugins/index.ts | 1 + .../replace-node-env-plugin.test.ts | 58 +++++++++++++++++++ .../vite-plugins/replace-node-env-plugin.ts | 38 ++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts create mode 100644 packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts diff --git a/packages/kernel-utils/src/vite-plugins/bundle-vat.ts b/packages/kernel-utils/src/vite-plugins/bundle-vat.ts index 49e2c5c3fc..6ed2ff6de1 100644 --- a/packages/kernel-utils/src/vite-plugins/bundle-vat.ts +++ b/packages/kernel-utils/src/vite-plugins/bundle-vat.ts @@ -7,6 +7,7 @@ import { build } from 'vite'; import type { VatBundle } from '../vat-bundle.ts'; import { exportMetadataPlugin } from './export-metadata-plugin.ts'; +import { replaceNodeEnvPlugin } from './replace-node-env-plugin.ts'; import { stripCommentsPlugin } from './strip-comments-plugin.ts'; export type { VatBundle } from '../vat-bundle.ts'; @@ -88,12 +89,6 @@ export async function bundleVat(sourcePath: string): Promise { result = await build({ configFile: false, logLevel: 'silent', - // TODO: Remove this define block and add a process shim to VatSupervisor - // workerEndowments instead. This injects into ALL bundles but is only needed - // for libraries like immer that check process.env.NODE_ENV. - define: { - 'process.env.NODE_ENV': JSON.stringify('production'), - }, build: { write: false, minify: false, @@ -109,6 +104,7 @@ export async function bundleVat(sourcePath: string): Promise { }, plugins: [ removeDynamicImportsPlugin(), + replaceNodeEnvPlugin(), stripCommentsPlugin(), metadataPlugin, ], diff --git a/packages/kernel-utils/src/vite-plugins/index.ts b/packages/kernel-utils/src/vite-plugins/index.ts index e4e15fc068..111edb5500 100644 --- a/packages/kernel-utils/src/vite-plugins/index.ts +++ b/packages/kernel-utils/src/vite-plugins/index.ts @@ -4,6 +4,7 @@ import { bundleVat } from './bundle-vat.ts'; export { bundleVat, removeDynamicImportsPlugin } from './bundle-vat.ts'; export type { VatBundle } from './bundle-vat.ts'; +export { replaceNodeEnvPlugin } from './replace-node-env-plugin.ts'; type VatEntry = { /** Absolute path to the vat source file */ diff --git a/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts new file mode 100644 index 0000000000..b89e3a6ef0 --- /dev/null +++ b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest'; + +import { replaceNodeEnvPlugin } from './replace-node-env-plugin.ts'; + +type TransformFn = ( + code: string, + id: string, +) => { code: string; map: null } | null; + +describe('replaceNodeEnvPlugin', () => { + const plugin = replaceNodeEnvPlugin(); + const transform = plugin.transform as TransformFn; + + it.each([ + ['bare reference', `process.env.NODE_ENV`, `"production"`], + [ + 'reference in a conditional', + `if (process.env.NODE_ENV !== 'production') { warn(); }`, + `if ("production" !== 'production') { warn(); }`, + ], + [ + 'multiple references in one file', + `const a = process.env.NODE_ENV;\nconst b = process.env.NODE_ENV;`, + `const a = "production";\nconst b = "production";`, + ], + [ + 'reference surrounded by other code', + `const x = 1;\nexport const mode = process.env.NODE_ENV;\nconst y = 2;`, + `const x = 1;\nexport const mode = "production";\nconst y = 2;`, + ], + ])( + 'replaces %s with the "production" string literal', + (_name, code, expected) => { + const result = transform(code, 'test.ts'); + expect(result).toStrictEqual({ code: expected, map: null }); + }, + ); + + it('injects a quoted string literal, not the bare word', () => { + const result = transform(`const mode = process.env.NODE_ENV;`, 'test.ts'); + expect(result?.code).toContain(`"production"`); + expect(result?.code).not.toContain(`process.env.NODE_ENV`); + expect(result?.code).not.toContain(`= production;`); + }); + + it.each([ + ['no process reference', 'const x = 1;'], + ['unrelated process.env key', `const port = process.env.PORT;`], + ['empty code', ''], + ])('returns null for %s', (_name, code) => { + expect(transform(code, 'test.ts')).toBeNull(); + }); + + it('returns null when the substring matches but the word boundary does not', () => { + const code = `const x = process.env.NODE_ENVIRONMENT;`; + expect(transform(code, 'test.ts')).toBeNull(); + }); +}); diff --git a/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts new file mode 100644 index 0000000000..89c793f499 --- /dev/null +++ b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts @@ -0,0 +1,38 @@ +import type { Plugin as RolldownPlugin } from 'rolldown'; + +/** + * A Rolldown plugin that inlines `process.env.NODE_ENV` as the literal + * `"production"` in any module that references it. + * + * This replaces the former build-config `define` (issue #812). Libraries such + * as immer branch on `process.env.NODE_ENV`, and vats have no `process` global + * at runtime, so the reference must be resolved at bundle time. The plugin + * auto-detects the need: modules without the reference are left untouched. + * + * This is a textual replacement (the same class of approach as the sibling + * {@link removeDynamicImportsPlugin}) and handles the dotted + * `process.env.NODE_ENV` form, consistent with the `define` it replaces. + * + * @returns A Rolldown plugin. + */ +export function replaceNodeEnvPlugin(): RolldownPlugin { + return { + name: 'ocap-kernel:replace-node-env', + transform(code) { + if (!code.includes('process.env.NODE_ENV')) { + return null; + } + + const transformed = code.replace( + /\bprocess\.env\.NODE_ENV\b/gu, + JSON.stringify('production'), + ); + + if (transformed === code) { + return null; + } + + return { code: transformed, map: null }; + }, + }; +} From a8c05898729911abf6e77ce7ddf73b4544fc29ee Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:16:34 -0700 Subject: [PATCH 2/3] docs(kernel-utils): add changelog entry for replaceNodeEnvPlugin Co-Authored-By: Claude Opus 4.8 --- packages/kernel-utils/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kernel-utils/CHANGELOG.md b/packages/kernel-utils/CHANGELOG.md index 56c5e767b0..ba0b4fb883 100644 --- a/packages/kernel-utils/CHANGELOG.md +++ b/packages/kernel-utils/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add an optional `required` field to `MethodSchema` (mirroring `required` on object `JsonSchema`) naming which arguments are required, and a `{ required }` option on `methodArgsToStruct` that validates unlisted arguments as optional, so a method's argument schema can faithfully represent the optional trailing arguments its guard already allows ([#958](https://github.com/MetaMask/ocap-kernel/pull/958)) - Add `getLibp2pRelayHome()` to the `./nodejs` exports, returning the libp2p relay's bookkeeping directory (default `~/.libp2p-relay`, overridable via `$LIBP2P_RELAY_HOME`) — kept separate from `$OCAP_HOME` so one relay can serve daemons with different OCAP_HOMEs ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) - `startRelay()` accepts an optional `publicIp` that is fed to libp2p's `appendAnnounce`, so a relay running on a NAT-backed host can announce its publicly-reachable IPv4 alongside its bound NIC addresses ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) +- Add `replaceNodeEnvPlugin` to the `./vite-plugins` exports, a Rolldown transform plugin that inlines `process.env.NODE_ENV` as the literal `"production"` in any module referencing it — used by `bundleVat` in place of a build-config `define`, so vats that pull in libraries like immer resolve the reference at bundle time ([#967](https://github.com/MetaMask/ocap-kernel/pull/967)) ## [0.5.0] From 2ad555b3d243e9074ae0869179f42472c2d21142 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:22:48 -0700 Subject: [PATCH 3/3] feat(kernel-utils): make replaceNodeEnvPlugin NODE_ENV value configurable Accept an options bag `{ value }` on `replaceNodeEnvPlugin`, defaulting to `'production'`, so callers can inline a different `process.env.NODE_ENV` value. Export the `ReplaceNodeEnvPluginOptions` type. Co-Authored-By: Claude Opus 4.8 --- packages/kernel-utils/CHANGELOG.md | 2 +- .../kernel-utils/src/vite-plugins/index.ts | 1 + .../replace-node-env-plugin.test.ts | 13 ++++++++++++ .../vite-plugins/replace-node-env-plugin.ts | 21 +++++++++++++++---- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/kernel-utils/CHANGELOG.md b/packages/kernel-utils/CHANGELOG.md index ba0b4fb883..88f885e05e 100644 --- a/packages/kernel-utils/CHANGELOG.md +++ b/packages/kernel-utils/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add an optional `required` field to `MethodSchema` (mirroring `required` on object `JsonSchema`) naming which arguments are required, and a `{ required }` option on `methodArgsToStruct` that validates unlisted arguments as optional, so a method's argument schema can faithfully represent the optional trailing arguments its guard already allows ([#958](https://github.com/MetaMask/ocap-kernel/pull/958)) - Add `getLibp2pRelayHome()` to the `./nodejs` exports, returning the libp2p relay's bookkeeping directory (default `~/.libp2p-relay`, overridable via `$LIBP2P_RELAY_HOME`) — kept separate from `$OCAP_HOME` so one relay can serve daemons with different OCAP_HOMEs ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) - `startRelay()` accepts an optional `publicIp` that is fed to libp2p's `appendAnnounce`, so a relay running on a NAT-backed host can announce its publicly-reachable IPv4 alongside its bound NIC addresses ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) -- Add `replaceNodeEnvPlugin` to the `./vite-plugins` exports, a Rolldown transform plugin that inlines `process.env.NODE_ENV` as the literal `"production"` in any module referencing it — used by `bundleVat` in place of a build-config `define`, so vats that pull in libraries like immer resolve the reference at bundle time ([#967](https://github.com/MetaMask/ocap-kernel/pull/967)) +- Add `replaceNodeEnvPlugin` to the `./vite-plugins` exports, a Rolldown transform plugin that inlines `process.env.NODE_ENV` as a string literal (configurable via `{ value }`, defaulting to `'production'`) in any module referencing it — used by `bundleVat` in place of a build-config `define`, so vats that pull in libraries like immer resolve the reference at bundle time ([#967](https://github.com/MetaMask/ocap-kernel/pull/967)) ## [0.5.0] diff --git a/packages/kernel-utils/src/vite-plugins/index.ts b/packages/kernel-utils/src/vite-plugins/index.ts index 111edb5500..ce52fb000f 100644 --- a/packages/kernel-utils/src/vite-plugins/index.ts +++ b/packages/kernel-utils/src/vite-plugins/index.ts @@ -5,6 +5,7 @@ import { bundleVat } from './bundle-vat.ts'; export { bundleVat, removeDynamicImportsPlugin } from './bundle-vat.ts'; export type { VatBundle } from './bundle-vat.ts'; export { replaceNodeEnvPlugin } from './replace-node-env-plugin.ts'; +export type { ReplaceNodeEnvPluginOptions } from './replace-node-env-plugin.ts'; type VatEntry = { /** Absolute path to the vat source file */ diff --git a/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts index b89e3a6ef0..b10a581bcf 100644 --- a/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts +++ b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.test.ts @@ -55,4 +55,17 @@ describe('replaceNodeEnvPlugin', () => { const code = `const x = process.env.NODE_ENVIRONMENT;`; expect(transform(code, 'test.ts')).toBeNull(); }); + + it('inlines a configured value instead of the default', () => { + const devTransform = replaceNodeEnvPlugin({ value: 'development' }) + .transform as TransformFn; + const result = devTransform( + `const mode = process.env.NODE_ENV;`, + 'test.ts', + ); + expect(result).toStrictEqual({ + code: `const mode = "development";`, + map: null, + }); + }); }); diff --git a/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts index 89c793f499..5245c1caff 100644 --- a/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts +++ b/packages/kernel-utils/src/vite-plugins/replace-node-env-plugin.ts @@ -1,8 +1,15 @@ import type { Plugin as RolldownPlugin } from 'rolldown'; +export type ReplaceNodeEnvPluginOptions = { + /** + * The value to inline for `process.env.NODE_ENV`. Defaults to `'production'`. + */ + value?: string; +}; + /** - * A Rolldown plugin that inlines `process.env.NODE_ENV` as the literal - * `"production"` in any module that references it. + * A Rolldown plugin that inlines `process.env.NODE_ENV` as a string literal in + * any module that references it. * * This replaces the former build-config `define` (issue #812). Libraries such * as immer branch on `process.env.NODE_ENV`, and vats have no `process` global @@ -13,9 +20,15 @@ import type { Plugin as RolldownPlugin } from 'rolldown'; * {@link removeDynamicImportsPlugin}) and handles the dotted * `process.env.NODE_ENV` form, consistent with the `define` it replaces. * + * @param options - Plugin options. + * @param options.value - The value to inline for `process.env.NODE_ENV`. + * Defaults to `'production'`. * @returns A Rolldown plugin. */ -export function replaceNodeEnvPlugin(): RolldownPlugin { +export function replaceNodeEnvPlugin({ + value = 'production', +}: ReplaceNodeEnvPluginOptions = {}): RolldownPlugin { + const replacement = JSON.stringify(value); return { name: 'ocap-kernel:replace-node-env', transform(code) { @@ -25,7 +38,7 @@ export function replaceNodeEnvPlugin(): RolldownPlugin { const transformed = code.replace( /\bprocess\.env\.NODE_ENV\b/gu, - JSON.stringify('production'), + replacement, ); if (transformed === code) {