Skip to content

refactor: declare env variables once with envField helpers - #163

Open
devs-rootstrap wants to merge 5 commits into
masterfrom
refactor/env-field-helpers
Open

devs-rootstrap wants to merge 5 commits into
masterfrom
refactor/env-field-helpers

Conversation

@devs-rootstrap

Copy link
Copy Markdown
Collaborator

Jira board reference:


What does this do?

Adds a small envField interface to declare environment variables once, with their type, instead of declaring them twice (zod schema + parsed value in _clientEnv / _buildTimeEnv).

  • New env-fields.js module (CommonJS, next to env.js) exposing envField.string(), envField.number() and envField.boolean(). Each builder returns a zod schema that receives the raw process.env string, converts it to the declared type and validates it.
  • Every builder supports the optional and default options. Without options the variable is required, and an empty value (e.g. API_URL=) is treated as missing.
  • env.js now declares each .env variable only in clientEnvSchema / buildTimeEnvSchema. Values are read from process.env for every key declared in the schema (readEnv), and the static values defined in env.js (NAME, BUNDLE_ID, EXPO_ACCOUNT_OWNER, ...) are kept in clientStaticValues / buildTimeStaticValues and take precedence.
  • The parseString / parseNumber / parseBoolean calls and the hand-built _clientEnv / _buildTimeEnv entries are removed. Exports (Env, ClientEnv, withEnvSuffix), when validation runs and the error output are the same as before.
  • Unit tests for the builders and helpers (__tests__/env-fields.test.ts), plus tests that load env.js with a mocked process.env in both validation modes (__tests__/env.test.ts).
  • Docs: the environment variables page and the Sentry recipe now use envField.

Example:

const clientEnvSchema = z.object({
  // ...
  API_URL: envField.string(),
  VAR_NUMBER: envField.number({ default: 4321 }),
  VAR_BOOL: envField.boolean({ optional: true }),
});

Why did you do this?

Adding a variable meant writing its zod type in the schema and also remembering to add it to _clientEnv / _buildTimeEnv with the right parseX call. That repeated logic was easy to get out of sync. With envField, the schema is the only place to declare a variable, and the type conversion comes from the declared type.

Who/what does this impact?

  • env.js (used by app.config.ts and, through extra, by Env from @/lib/env in src). The variables and the exported types stay the same.
  • Projects created from the template: new variables are declared with envField.* instead of z.* plus a _clientEnv / _buildTimeEnv entry.
  • Stricter parsing (see Notes): invalid booleans and numbers now fail validation instead of being silently converted.

Layer 2: Risk Classification

Author applies a risk label when opening the PR. When in doubt, escalate.

Label When to use
risk:low Config, copy, minor UI tweaks. Low blast radius.
risk:medium Feature work, refactors touching multiple files.
risk:high Auth, payments, migrations, security-sensitive code.

Label applied: risk:medium (refactor of the shared env loading used by every build and run).

How did you test this?

  • Tested on iOS
  • Tested on Android
  • Tested on a small device
  • Tested on a real device
  • Tested all flows related with this PR changes
  • Tested accessibility
  • Added tests

Verification

Check Result
pnpm lint Passed
pnpm type-check Passed
pnpm lint:translations Passed
pnpm test Passed (30 suites, 164 tests)
Loading env.js with Node, with and without the variables set Env / ClientEnv built as expected; VAR_NUMBER and VAR_BOOL converted to number / boolean

Not verified on a device.

How to test locally

  1. pnpm install
  2. pnpm test __tests__/env-fields.test.ts __tests__/env.test.ts
  3. Copy .env.sample to .env.development, fill it in and run pnpm start. The app should start as before.
  4. Set VAR_BOOL=yes or VAR_NUMBER=abc (or remove API_URL) and run pnpm start again. It should fail with the usual Invalid environment variables message listing the variable.

Notes:

  • Scope: string / number / boolean builders with optional and default, as described in the card's Tasks. The card's context idea (build time vs app) is already covered by the existing split between clientEnvSchema and buildTimeEnvSchema.
  • Follow-ups (out of scope here):
    • An access option (public / secret), similar to Astro's envField.
    • Checking that every declared variable is also listed in .env.sample.
  • Behaviour changes:
    • Booleans: only true and false are accepted. Before, any other value (e.g. TRUE, 1, yes) was silently converted to false; now it fails validation.
    • Numbers: only plain decimal numbers are accepted (e.g. 42, -3.5). Before, whitespace-only values became 0 and forms like Infinity, 0x10 or 1e3 were accepted; now they fail validation.
    • When validation is skipped (scripts that don't depend on the environment), invalid values are now undefined instead of the raw or converted value.
  • Static values defined in env.js (e.g. NAME) take precedence over a variable with the same name in the .env file, as before.

Screenshots / Previews

N/A, no UI changes.

@devs-rootstrap devs-rootstrap added the risk:medium Feature work, refactors touching multiple files. label Nov 2, 2026
Copilot AI balanced review requested due to automatic review settings November 2, 2026 12:09
@devs-rootstrap
devs-rootstrap requested a review from a team as a code owner November 2, 2026 12:09
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

💯 Test Coverage

Lines Statements Branches Functions
Coverage: 56%
56.23% (347/617) 59.18% (174/294) 48.76% (118/242)

😎 Tests Results

Tests Skipped Failures Errors Time
164 0 💤 0 ❌ 0 🔥 37.347s ⏱️
👀 Tests Details • (56%)
File% Stmts% Branch% Funcs% LinesUncovered Line #s
All files56.2359.1848.7656.9 
report-only-changed-files is enabled. No files were changed in this commit :)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Hardcoded app-name assertions fail in generated projects because setup replaces the name in env.js but leaves the test expectations unchanged.

1 open issue
What changed in this PR

Overall Review: ai:serious

This PR centralizes environment-variable declarations in typed envField helpers, simplifying the template’s shared build and runtime configuration.

Changes:

  • Adds string, number, and boolean builders with optional values, defaults, and stricter parsing.
  • Refactors environment loading to read schema keys automatically while preserving static-value precedence.
  • Adds helper and integration tests and updates configuration documentation.
File Description
env.js Adopts schema-driven loading and separates static values.
env-fields.js Adds typed field builders and environment parsing helpers.
docs/​src/​content/​docs/​recipes/​sentry-setup.mdx Migrates Sentry configuration examples to envField.
docs/​src/​content/​docs/​getting-started/​environment-vars-config.mdx Documents single declarations, options, and parsing behavior.
__tests__/​env.test.ts Tests loading, validation modes, and static-value precedence.
__tests__/​env-fields.test.ts Tests builders, defaults, optional values, and parsing helpers.

Review effort: Balanced

Comment thread __tests__/env.test.ts
Comment on lines +49 to +58
NAME: 'RootstrapApp',
});
expect(ClientEnv.VAR_NUMBER).toBe(10);
expect(ClientEnv).not.toHaveProperty('SECRET_KEY');
});

it('keeps the static values over the ones in process.env', () => {
const { Env } = loadEnv({ ...VALID_ENV, NAME: 'OtherName' }, RUN_SCRIPT);

expect(Env.NAME).toBe('RootstrapApp');

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

risk:medium Feature work, refactors touching multiple files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants