Skip to content

Don't inject empty strings into process.env for unset schema items - #1038

Merged
theoephraim merged 6 commits into
mainfrom
unify-undefined-injection
Aug 27, 2026
Merged

Don't inject empty strings into process.env for unset schema items#1038
theoephraim merged 6 commits into
mainfrom
unify-undefined-injection

Conversation

@theoephraim

@theoephraim theoephraim commented Aug 27, 2026

Copy link
Copy Markdown
Member

What

A user migrating a Next.js app to varlock hit regressions because auto-load injected '' into process.env for schema items that resolved to undefined, breaking process.env.SOMEVAR ?? 'default' patterns. Meanwhile varlock run skipped those items entirely, so the two injection paths disagreed, and the docs promise that VAR= (undefined) and VAR="" are distinct.

This unifies on not injecting unset items, everywhere:

  • auto-load / initVarlockEnv(): items resolving to undefined are no longer injected as ''; a stale parent-injected echo for such a key is cleared (matching the masking varlock run already does). Explicit "" values are still injected as empty strings.
  • varlock load --format shell: skips unset items (export KEY= would set '' in the shell, misrepresenting undefined). --format env keeps its KEY= lines since those round-trip to undefined in the varlock dialect.
  • varlock run: unchanged (already skipped), but now honors the new decorator below.

@injectUndefinedAsEmpty

New root decorator that restores dotenv-style empty-string injection across all three surfaces, for code that relies on unset vars being ''. It travels in the __VARLOCK_ENV blob settings, and the blob provenance/drift checks are mode-aware: in skip mode an explicitly-set ambient '' now correctly reads as an override rather than a parent echo.

Generated TS types follow the setting: the process.env augmentation drops optionality when it is enabled, and any key that can be unset gets '' added to its union, so an optional enum(alpha, beta) types as "alpha" | "beta" | "". The import.meta.env augmentation keeps its optional keys: frameworks only expose prefixed keys through it, so the injection guarantee does not cover it. The ENV proxy typing keeps optional keys optional either way, since ENV.X still returns the real resolved value. As a side effect of the same fix (stripping undefined before the conditional with NonNullable), optional enums/booleans in default mode now keep their literal unions on process.env instead of widening to string. Because codegen reads the decorator, its value must be a static boolean (same rule as @disableProcessEnvInjection).

Testing

  • Unit tests for the runtime injection (skip, stale-echo cleanup, empty mode, reload transitions) and the type-generation output
  • New end-to-end smoke scenario covering auto-load, varlock run, and both load formats in both modes
  • Generated types verified by committed tsc --strict type tests in both modes (@ts-expect-error assertions pin process.env vs import.meta.env optionality)
  • Vite framework tests (v5-v8) build an SSR entry with ssrInjectMode: 'resolved-env' and execute it with plain node, asserting unset items are skipped from process.env by default, become '' under the decorator, and never appear on import.meta.env in either mode

Docs updated: root decorator reference, schema guide, dotenv migration guide, and load/run CLI reference.

Auto-load injected '' for items resolving to undefined while varlock run
skipped them. Unify on skipping everywhere (runtime injection, run, shell
format), matching the documented VAR= semantics, and add the
@injectUndefinedAsEmpty root decorator to opt back into dotenv-style
empty-string injection. Blob provenance/drift checks are now mode-aware.
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • @varlock/native-helper-darwin 1.17.1 → 1.18.0
  • @varlock/native-helper-linux-arm64 1.17.1 → 1.18.0
  • @varlock/native-helper-linux-x64 1.17.1 → 1.18.0
  • @varlock/native-helper-win32-x64 1.17.1 → 1.18.0
  • varlock 1.17.1 → 1.18.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size

⚠️ grows the bundle by 9.7 KB (+0.2%)

Metric main This PR Δ
Total dist 4084.1 KB 4093.7 KB +9.7 KB (+0.2%)
JS 1583.5 KB 1586.1 KB +2.6 KB (+0.2%)
Sourcemaps 2416.0 KB 2422.5 KB +6.5 KB (+0.3%)
Type defs 84.6 KB 85.1 KB +0.5 KB (+0.7%)
Other 0.0 KB 0.0 KB

dist/ only; native binaries are versioned separately and not counted here.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
varlock-website 7da34fb Commit Preview URL

Branch Preview URL
Aug 27 2026, 04:18 AM

@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

varlock

npm i https://pkg.pr.new/varlock@1038

@varlock/native-helper-darwin

npm i https://pkg.pr.new/@varlock/native-helper-darwin@1038

@varlock/native-helper-linux-arm64

npm i https://pkg.pr.new/@varlock/native-helper-linux-arm64@1038

@varlock/native-helper-linux-x64

npm i https://pkg.pr.new/@varlock/native-helper-linux-x64@1038

@varlock/native-helper-win32-x64

npm i https://pkg.pr.new/@varlock/native-helper-win32-x64@1038

commit: 5581d71

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The generated ImportMetaEnv contract becomes unsound when @injectUndefinedAsEmpty is enabled. This should be corrected before merge.

Reviewed changes in 823e159 across runtime injection, CLI output, blob provenance and reuse, generated TypeScript declarations, documentation, and focused coverage.

  • Undefined injection: Unset schema items are omitted from process.env, child environments, and shell output by default while explicit empty strings remain distinct.
  • Compatibility decorator: @injectUndefinedAsEmpty restores empty-string injection and is serialized for runtime reuse and provenance checks.
  • Type generation: String-form declarations preserve optional literal unions and attempt to reflect the selected injection mode.
  • Coverage and docs: Runtime and smoke tests exercise both modes, with guides and reference pages documenting the behavior.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using azure/gpt-5.6-sol𝕏

Comment thread packages/varlock/src/env-graph/lib/type-generation/emitters/ts.ts Outdated
The empty-string injection guarantee only covers process.env; frameworks
expose only prefixed keys through import.meta.env, so its augmentation
must keep optional keys. Split the string-form type: ProcessEnv extends
a required variant with '' unions, ImportMetaEnv keeps the optional one.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The generated ProcessEnv type is still unsound for consumers that disable strictNullChecks. This should be corrected before merge.

Reviewed changes since the prior Pullfrog review, covering the target-specific TypeScript alias fix and its documentation and test updates.

  • Clarified loader scope: Documented that blob-based non-TypeScript loaders retain their language-specific absent representation regardless of @injectUndefinedAsEmpty.
  • Separated environment declarations: Kept ImportMetaEnv on the optional string schema while applying required empty-string-aware fields only to ProcessEnv.
  • Expanded source assertions: Checked that generated declarations select distinct aliases for ProcessEnv and ImportMetaEnv.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using azure/gpt-5.6-sol𝕏

Comment thread packages/varlock/src/env-graph/lib/type-generation/emitters/ts.ts Outdated
…SSR runtime

Smoke tests now typecheck the generated env.d.ts in both modes with tsc
--strict (ts-expect-error assertions pin process.env vs import.meta.env
optionality). Vite framework tests build an SSR entry with resolved-env
inject mode and execute it with plain node, asserting unset items are
skipped from process.env by default, become '' under
@injectUndefinedAsEmpty, and never appear on import.meta.env.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues found in the added coverage. The existing open type-soundness thread still applies.

Reviewed changes since the prior Pullfrog review, covering the new generated-type smoke checks and Vite SSR runtime coverage.

  • Added compiled type checks: Generated declarations for default and empty-string injection modes are now checked with tsc --strict, including the distinct process.env, import.meta.env, and ENV contracts.
  • Added Vite SSR runtime checks: Vite 5 through 8 now build and execute both injection modes to verify unset values across process.env, import.meta.env, and the ENV proxy.

Pullfrog  | Fix it ➔View workflow run | Using azure/gpt-5.6-sol𝕏

'undefined extends T' is evaluated under the consumer's compiler settings,
so with strictNullChecks disabled it is true for every type and required
literal unions would gain ''. Use '{} extends Pick<...>' instead, and add
a non-strict tsc pass to the smoke type tests covering required/optional
enum and boolean fields.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes since the prior Pullfrog review, covering the structural optionality fix and its compiler-level regression coverage.

  • Corrected non-strict generated types: Replaced the strictNullChecks-dependent conditional with structural optional-key detection, preserving required literal unions while adding '' only to optional fields.
  • Added non-strict compiler coverage: Extended the smoke schema with required and optional literal fields and verified the generated declaration without strictNullChecks.

Pullfrog  | View workflow run | Using azure/gpt-5.6-sol𝕏

The type-test fixtures previously shadowed the process global with a local
declaration, bypassing the merge between the generated ProcessEnv
augmentation and @types/node's own ProcessEnv. Use the real global instead
and assert that named schema props win over the index signature (definite
string in empty mode) while non-schema keys keep string | undefined.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes since the prior Pullfrog review, covering the generated-type fixtures' switch to the real Node environment declarations.

  • Exercised declaration merging: Replaced local process stand-ins with @types/node, so schema-key guarantees are now checked against Node's actual ProcessEnv index signature.
  • Pinned unaffected keys: Added compiler assertions that ordinary Node and unknown environment keys remain string | undefined while named schema keys retain their mode-specific types.

Pullfrog  | View workflow run | Using azure/gpt-5.6-sol𝕏

@theoephraim
theoephraim merged commit 188b726 into main Aug 27, 2026
28 checks passed
theoephraim added a commit that referenced this pull request Sep 1, 2026
…napshot (#1055)

When a server boots from the env blob baked into build output (e.g. a
Next.js standalone container where the varlock CLI is unreachable),
initVarlockEnv treated the blob like a fresh resolution. Items that
resolved to undefined at build time triggered the stale-echo cleanup
from #1038, DELETING the corresponding runtime-provided values from
process.env. A schema item like `REDIS_URL=` would actively clear a
`docker run -e REDIS_URL=...` value at boot, silently breaking the
service. Shipped only in varlock@1.17.1.

That cleanup assumes a resolution happened in this process (a genuine
ambient value would have acted as an override and resolved to it),
which is false for a blob resolved on a build machine.

The injection preludes (nextjs webpack + turbopack, vite resolved-env
SSR entry) now bake an `injectedAtBuild: true` flag INSIDE the
serialized payload, before encryption where applicable, and
initVarlockEnv skips the cleanup for a flagged blob. Provenance lives
in the payload so it travels with the blob to child processes and
through encryption round-trips, and can never outlive it: any fresh
resolution produces an unflagged blob, so no marker clearing is needed
anywhere.

Otherwise unchanged: baked values stay authoritative for ENV, and
runtime values are still not applied to a baked snapshot (they cannot
be validated or coerced against it). Making that contract explicit and
enforceable belongs with `varlock freeze` (#1049).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant