Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/www/content/docs/frameworks/tanstack-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ or `@arkenv/rsbuild-plugin`, depending on your underlying bundler. ArkEnv
inlines public keys (`VITE_` or `PUBLIC_`) into the client bundle, keeps server
secrets inside server functions, and throws if client code reads them.

When the Vite plugin is registered, it discovers and validates `env.ts` during
Vite config resolution. Missing or invalid values abort the dev server or
production build before it is ready, and relevant `.env` or schema changes are
revalidated during HMR. Without the plugin, validation is import-driven and
starts when a module first imports `env.ts`.

This is the existing fail-fast contract, not a new lazy-validation option.
ArkEnv does not add a default-off flag for lazy validation.

For high-level architectural trade-offs, see
[Frameworks](/docs/frameworks).

Expand Down
18 changes: 18 additions & 0 deletions apps/www/content/docs/frameworks/vite.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ description: Learn how to use ArkEnv in a Vite project.
Transform mode inlines public `VITE_` keys into the client bundle and
keeps server secrets out.

## When does validation run?

When `@arkenv/vite-plugin` is registered, it discovers and validates `env.ts`
during Vite config resolution. An invalid or missing value aborts the dev
server or production build before Vite reports that it is ready. Relevant
`.env` and schema changes are validated again during HMR.

Without the plugin, validation is import-driven: `env.ts` runs when an
application module imports it. This distinction applies to both server and
client graphs; the plugin is what lets Vite validate the schema before the
graph is ready and transform client imports safely. See the
[`@arkenv/vite-plugin` reference](/docs/reference/vite-plugin) for the
plugin contract.

This documents the existing fail-fast behavior. ArkEnv does not add a
default-off lazy-validation flag; a separate lazy mode would be an
intentionally scoped feature.

For high-level architectural trade-offs, see
[Frameworks](/docs/frameworks).

Expand Down
12 changes: 12 additions & 0 deletions apps/www/content/docs/reference/vite-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ Call `arkenvPlugin()` with no args, or an options object that includes
transform fields (`schemaPath`, `clientPrefix`, logging). Do not pass a
schema map or compiled `type()` to the plugin.

## Startup validation

When the plugin is registered, it resolves `env.ts` and validates it during
Vite config resolution. Missing or invalid environment variables therefore
abort the dev server or production build before Vite is ready. The plugin
revalidates the schema when a relevant `.env` file or schema module changes
during HMR.

Without the plugin, validation is import-driven and runs when a module imports
`env.ts`. Startup fail-fast is the documented behavior of the existing plugin
contract; there is no default-off lazy-validation flag.

## Next steps

<Cards>
Expand Down
113 changes: 113 additions & 0 deletions packages/vite-plugin/src/env-module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,119 @@ describe("transform mode plugin", () => {
}
});

it("fails during config resolution when the environment is invalid", async () => {
const root = mkdtempSync(join(tmpdir(), "arkenv-vite-invalid-startup-"));
temps.push(root);
writeFileSync(
join(root, "env.ts"),
'import arkenv from "@arkenv/core";\n\nexport const env = arkenv({ REQUIRED_TOKEN: "string" });\n',
);
const plugin = arkenvPlugin() as any;
const context = {} as any;

if (plugin.config && typeof plugin.config === "function") {
plugin.config.call(
context,
{ root, envDir: root },
{ mode: "test", command: "serve" },
);
}

await expect(
Promise.resolve().then(() =>
plugin.configResolved?.call(context, {
root,
envDir: root,
envPrefix: "VITE_",
} as any),
),
).rejects.toMatchObject({ name: "ArkEnvError" });
expect(process.env.REQUIRED_TOKEN).toBeUndefined();
});

it("revalidates valid schema and dotenv changes during HMR", async () => {
const root = mkdtempSync(join(tmpdir(), "arkenv-vite-hmr-"));
temps.push(root);
const schemaPath = join(root, "env.ts");
const dotenvPath = join(root, ".env.test");
writeFileSync(
schemaPath,
'import arkenv from "@arkenv/core";\n\nexport const env = arkenv({ VITE_API_URL: "string" });\n',
);
writeFileSync(dotenvPath, "VITE_API_URL=https://example.com\n");
const plugin = arkenvPlugin({ schemaPath }) as any;
const server = {
moduleGraph: {
getModulesByFile: () => new Set([{ id: schemaPath }]),
invalidateModule: () => {},
},
} as any;
const context = {} as any;

plugin.config?.call(
context,
{ root, envDir: root },
{ mode: "test", command: "serve" },
);
await plugin.configResolved?.call(context, {
root,
envDir: root,
envPrefix: "VITE_",
} as any);

const schemaUpdate = plugin.handleHotUpdate?.call(context, {
file: schemaPath,
server,
} as any);
expect(schemaUpdate).toHaveLength(1);
const dotenvUpdate = plugin.handleHotUpdate?.call(context, {
file: dotenvPath,
server,
} as any);
expect(dotenvUpdate).toHaveLength(1);
Comment thread
yamcodes marked this conversation as resolved.
});

it("propagates invalid dotenv values during HMR", async () => {
const root = mkdtempSync(join(tmpdir(), "arkenv-vite-invalid-hmr-"));
temps.push(root);
const schemaPath = join(root, "env.ts");
const dotenvPath = join(root, ".env.test");
writeFileSync(
schemaPath,
'import arkenv from "@arkenv/core";\n\nexport const env = arkenv({ VITE_PORT: "number" });\n',
);
writeFileSync(dotenvPath, "VITE_PORT=8080\n");
const plugin = arkenvPlugin({ schemaPath }) as any;
const server = {
moduleGraph: {
getModulesByFile: () => new Set(),
invalidateModule: () => {},
},
} as any;
const context = {} as any;

plugin.config?.call(
context,
{ root, envDir: root },
{ mode: "test", command: "serve" },
);
await plugin.configResolved?.call(context, {
root,
envDir: root,
envPrefix: "VITE_",
} as any);
writeFileSync(dotenvPath, "VITE_PORT=not-a-number\n");

await expect(
Promise.resolve().then(() =>
plugin.handleHotUpdate?.call(context, {
file: dotenvPath,
server,
} as any),
),
).rejects.toMatchObject({ name: "ArkEnvError" });
});

it("passes through the env module unchanged in the SSR graph", async () => {
const fixtureDir = join(__dirname, "__fixtures__", "transform-env");
const plugin = arkenvPlugin({ schemaPath: join(fixtureDir, "env.ts") });
Expand Down
Loading