Skip to content

fix(env): keep env.ts in Worker builds of apps without side effects - #745

Open
morinokami wants to merge 1 commit into
voidzero-dev:mainfrom
morinokami:fix/env-ts-side-effects
Open

morinokami wants to merge 1 commit into
voidzero-dev:mainfrom
morinokami:fix/env-ts-side-effects

Conversation

@morinokami

Copy link
Copy Markdown
Contributor

When an app's package.json declares "sideEffects": false, a production build leaves env.ts out of the Worker, so env from void/env returns raw values:

// env.ts
import { boolean, defineEnv, number } from 'void/env';

export default defineEnv({
  PORT: number().default(3000),
  DEBUG: boolean(),
});

With DEBUG=false and no PORT in the Worker's env, env.PORT is undefined and env.DEBUG is the string "false", so if (env.DEBUG) takes the debug branch. No schema is registered, so the boot validation passes any value, and a missing required key goes unnoticed. vp dev doesn't bundle, so it reads validated values, and the difference only shows after a build.

The generated entry imports env.ts only for its side effect, registering the schema, and uses none of its exports. The bundler follows the app's sideEffects field and drops the import. An array that doesn't match env.ts, such as ["**/*.css"], drops it too. An app whose code imports the env that env.ts exports keeps it.

The field is uncommon in apps, but some starters set it: most of TanStack Start's official examples declare "sideEffects": false.

Fix

In server builds, envSchemaPlugin returns moduleSideEffects: true for env.ts from a transform hook. Rolldown uses this value over the package's sideEffects field.

  • The hook matches the env file's path as found and as Vite resolves it, so a symlinked env.ts is covered whatever its target is named.
  • Client builds are unchanged. The browser variant of void/env reads import.meta.env and doesn't use the schema.

Tests

packages/void/test/integration/plugin-env-schema.test.ts builds an entry that imports env.ts for its side effect, in an app that declares "sideEffects": false:

  • A server build keeps env.ts, also when env.ts is a symlink to a file with another name. Both cases fail without this fix.
  • A client build still leaves it out.

Verification

  • vp test run: 781 files pass, 1 skipped (16,170 tests pass, 29 skipped).
  • vp run check: 33/33 pass.
  • By hand on a copy of playground/ssr with the env.ts above, "sideEffects": false, DEBUG=false in .env, and a route that returns both values: in vp preview, the route reads PORT as 3000 and DEBUG as false. Without this fix, the Worker bundle has no env.ts, and the route reads undefined and "false". vp dev reads the validated values without this fix too.
  • By hand with ["**/*.css"] instead: an app built with envSchemaPlugin and an entry that imports env.ts for its side effect drops env.ts without this fix and keeps it with it.
  • Not run: void deploy against Cloudflare.

🤖 Generated with Claude Code

When an app's package.json declares "sideEffects": false, a production
build dropped env.ts from the Worker. The generated entry imports env.ts
only for its side effect, registering the schema, so the bundler removed
that import: env read raw values, and boot validation passed anything.

envSchemaPlugin now marks env.ts as having side effects in server builds.
Rolldown honors the plugin's value over the package's sideEffects field.
The transform filter matches the env file's path as found and as Vite
resolves it, so a symlinked env.ts is covered whatever its target is
named. Client builds are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch has not been deployed

No deployments
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