Skip to content

Commit df79cf1

Browse files
yamcodesCopilot
andcommitted
fix: tackle issue #1837
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 98707c2 commit df79cf1

5 files changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@arkenv/vite-plugin": patch
3+
---
4+
5+
#### Guarantee Vite startup validation
6+
7+
The Vite plugin now has regression coverage for validating `env.ts` during
8+
config resolution and revalidating environment changes during HMR. The
9+
documented plugin contract makes startup failures explicit:
10+
11+
```ts
12+
export default defineConfig({
13+
plugins: [arkenvPlugin()],
14+
});
15+
```
16+
17+
With the plugin registered, invalid environment variables abort startup before
18+
Vite is ready. Without it, validation remains import-driven.

apps/www/content/docs/frameworks/tanstack-start.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ or `@arkenv/rsbuild-plugin`, depending on your underlying bundler. ArkEnv
88
inlines public keys (`VITE_` or `PUBLIC_`) into the client bundle, keeps server
99
secrets inside server functions, and throws if client code reads them.
1010

11+
When the Vite plugin is registered, it discovers and validates `env.ts` during
12+
Vite config resolution. Missing or invalid values abort the dev server or
13+
production build before it is ready, and relevant `.env` or schema changes are
14+
revalidated during HMR. Without the plugin, validation is import-driven and
15+
starts when a module first imports `env.ts`.
16+
17+
This is the existing fail-fast contract, not a new lazy-validation option.
18+
ArkEnv does not add a default-off flag for lazy validation.
19+
1120
For high-level architectural trade-offs, see
1221
[Frameworks](/docs/frameworks).
1322

apps/www/content/docs/frameworks/vite.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ description: Learn how to use ArkEnv in a Vite project.
77
Transform mode inlines public `VITE_` keys into the client bundle and
88
keeps server secrets out.
99

10+
## When does validation run?
11+
12+
When `@arkenv/vite-plugin` is registered, it discovers and validates `env.ts`
13+
during Vite config resolution. An invalid or missing value aborts the dev
14+
server or production build before Vite reports that it is ready. Relevant
15+
`.env` and schema changes are validated again during HMR.
16+
17+
Without the plugin, validation is import-driven: `env.ts` runs when an
18+
application module imports it. This distinction applies to both server and
19+
client graphs; the plugin is what lets Vite validate the schema before the
20+
graph is ready and transform client imports safely. See the
21+
[`@arkenv/vite-plugin` reference](/docs/reference/vite-plugin) for the
22+
plugin contract.
23+
24+
This documents the existing fail-fast behavior. ArkEnv does not add a
25+
default-off lazy-validation flag; a separate lazy mode would be an
26+
intentionally scoped feature.
27+
1028
For high-level architectural trade-offs, see
1129
[Frameworks](/docs/frameworks).
1230

apps/www/content/docs/reference/vite-plugin.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ Call `arkenvPlugin()` with no args, or an options object that includes
4444
transform fields (`schemaPath`, `clientPrefix`, logging). Do not pass a
4545
schema map or compiled `type()` to the plugin.
4646

47+
## Startup validation
48+
49+
When the plugin is registered, it resolves `env.ts` and validates it during
50+
Vite config resolution. Missing or invalid environment variables therefore
51+
abort the dev server or production build before Vite is ready. The plugin
52+
revalidates the schema when a relevant `.env` file or schema module changes
53+
during HMR.
54+
55+
Without the plugin, validation is import-driven and runs when a module imports
56+
`env.ts`. Startup fail-fast is the documented behavior of the existing plugin
57+
contract; there is no default-off lazy-validation flag.
58+
4759
## Next steps
4860

4961
<Cards>

packages/vite-plugin/src/env-module.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,119 @@ describe("transform mode plugin", () => {
170170
}
171171
});
172172

173+
it("fails during config resolution when the environment is invalid", async () => {
174+
const root = mkdtempSync(join(tmpdir(), "arkenv-vite-invalid-startup-"));
175+
temps.push(root);
176+
writeFileSync(
177+
join(root, "env.ts"),
178+
'import arkenv from "@arkenv/core";\n\nexport const env = arkenv({ REQUIRED_TOKEN: "string" });\n',
179+
);
180+
const plugin = arkenvPlugin() as any;
181+
const context = {} as any;
182+
183+
if (plugin.config && typeof plugin.config === "function") {
184+
plugin.config.call(
185+
context,
186+
{ root, envDir: root },
187+
{ mode: "test", command: "serve" },
188+
);
189+
}
190+
191+
await expect(
192+
Promise.resolve().then(() =>
193+
plugin.configResolved?.call(context, {
194+
root,
195+
envDir: root,
196+
envPrefix: "VITE_",
197+
} as any),
198+
),
199+
).rejects.toMatchObject({ name: "ArkEnvError" });
200+
expect(process.env.REQUIRED_TOKEN).toBeUndefined();
201+
});
202+
203+
it("revalidates valid schema and dotenv changes during HMR", async () => {
204+
const root = mkdtempSync(join(tmpdir(), "arkenv-vite-hmr-"));
205+
temps.push(root);
206+
const schemaPath = join(root, "env.ts");
207+
const dotenvPath = join(root, ".env.test");
208+
writeFileSync(
209+
schemaPath,
210+
'import arkenv from "@arkenv/core";\n\nexport const env = arkenv({ VITE_API_URL: "string" });\n',
211+
);
212+
writeFileSync(dotenvPath, "VITE_API_URL=https://example.com\n");
213+
const plugin = arkenvPlugin({ schemaPath }) as any;
214+
const server = {
215+
moduleGraph: {
216+
getModulesByFile: () => new Set([{ id: schemaPath }]),
217+
invalidateModule: () => {},
218+
},
219+
} as any;
220+
const context = {} as any;
221+
222+
plugin.config?.call(
223+
context,
224+
{ root, envDir: root },
225+
{ mode: "test", command: "serve" },
226+
);
227+
await plugin.configResolved?.call(context, {
228+
root,
229+
envDir: root,
230+
envPrefix: "VITE_",
231+
} as any);
232+
233+
const schemaUpdate = plugin.handleHotUpdate?.call(context, {
234+
file: schemaPath,
235+
server,
236+
} as any);
237+
expect(schemaUpdate).toHaveLength(1);
238+
const dotenvUpdate = plugin.handleHotUpdate?.call(context, {
239+
file: dotenvPath,
240+
server,
241+
} as any);
242+
expect(dotenvUpdate).toHaveLength(1);
243+
});
244+
245+
it("propagates invalid dotenv values during HMR", async () => {
246+
const root = mkdtempSync(join(tmpdir(), "arkenv-vite-invalid-hmr-"));
247+
temps.push(root);
248+
const schemaPath = join(root, "env.ts");
249+
const dotenvPath = join(root, ".env.test");
250+
writeFileSync(
251+
schemaPath,
252+
'import arkenv from "@arkenv/core";\n\nexport const env = arkenv({ VITE_PORT: "number" });\n',
253+
);
254+
writeFileSync(dotenvPath, "VITE_PORT=8080\n");
255+
const plugin = arkenvPlugin({ schemaPath }) as any;
256+
const server = {
257+
moduleGraph: {
258+
getModulesByFile: () => new Set(),
259+
invalidateModule: () => {},
260+
},
261+
} as any;
262+
const context = {} as any;
263+
264+
plugin.config?.call(
265+
context,
266+
{ root, envDir: root },
267+
{ mode: "test", command: "serve" },
268+
);
269+
await plugin.configResolved?.call(context, {
270+
root,
271+
envDir: root,
272+
envPrefix: "VITE_",
273+
} as any);
274+
writeFileSync(dotenvPath, "VITE_PORT=not-a-number\n");
275+
276+
await expect(
277+
Promise.resolve().then(() =>
278+
plugin.handleHotUpdate?.call(context, {
279+
file: dotenvPath,
280+
server,
281+
} as any),
282+
),
283+
).rejects.toMatchObject({ name: "ArkEnvError" });
284+
});
285+
173286
it("passes through the env module unchanged in the SSR graph", async () => {
174287
const fixtureDir = join(__dirname, "__fixtures__", "transform-env");
175288
const plugin = arkenvPlugin({ schemaPath: join(fixtureDir, "env.ts") });

0 commit comments

Comments
 (0)