Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .bumpy/_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"@varlock/cloudflare-integration": {
"cascadeFrom": ["@varlock/vite-integration"]
},
"@varlock/nuxt-integration": {
"cascadeFrom": ["@varlock/vite-integration"]
},
"varlock": {
"cascadeFrom": {
"@env-spec/parser": {
Expand Down
5 changes: 5 additions & 0 deletions .bumpy/add-nuxt-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@varlock/nuxt-integration": minor
---

Add the new Nuxt integration package and docs references.
5 changes: 5 additions & 0 deletions .bumpy/vite-ssr-init-code-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@varlock/vite-integration": minor
---

Add a `rootDir` option so integrations can point varlock at the project root when the framework sets vite's `root` to a source subdirectory, and export `buildVarlockSsrInitCode` for build pipelines vite does not own.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Varlock is built on top of @env-spec, a new DSL for attaching a schema and addit
| [@varlock/cloudflare-integration](packages/integrations/cloudflare) | [![npm version](https://img.shields.io/npm/v/@varlock/cloudflare-integration.svg)](https://npmx.dev/package/@varlock/cloudflare-integration) |
| [@varlock/expo-integration](packages/integrations/expo) | [![npm version](https://img.shields.io/npm/v/@varlock/expo-integration.svg)](https://npmx.dev/package/@varlock/expo-integration) |
| [@varlock/nextjs-integration](packages/integrations/nextjs) | [![npm version](https://img.shields.io/npm/v/@varlock/nextjs-integration.svg)](https://npmx.dev/package/@varlock/nextjs-integration) |
| [@varlock/nuxt-integration](packages/integrations/nuxt) | [![npm version](https://img.shields.io/npm/v/@varlock/nuxt-integration.svg)](https://npmx.dev/package/@varlock/nuxt-integration) |
| [@varlock/vite-integration](packages/integrations/vite) | [![npm version](https://img.shields.io/npm/v/@varlock/vite-integration.svg)](https://npmx.dev/package/@varlock/vite-integration) |


Expand Down
1,314 changes: 1,290 additions & 24 deletions bun.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineNuxtConfig({
modules: ['@varlock/nuxt-integration'],
compatibilityDate: '2025-01-01',
varlock: { ssrInjectMode: 'auto-load' },
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineNuxtConfig({
modules: ['@varlock/nuxt-integration'],
compatibilityDate: '2025-01-01',
varlock: { ssrInjectMode: 'resolved-env' },
});
4 changes: 4 additions & 0 deletions framework-tests/frameworks/nuxt/files/configs/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default defineNuxtConfig({
modules: ['@varlock/nuxt-integration'],
compatibilityDate: '2025-01-01',
});
17 changes: 17 additions & 0 deletions framework-tests/frameworks/nuxt/files/pages/basic-page.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { ENV } from 'varlock/env';

const publicVar = ENV.PUBLIC_VAR;
const publicNum = ENV.PUBLIC_NUM;
const envSpecific = ENV.ENV_SPECIFIC_VAR;
const hasSecret = ENV.SENSITIVE_VAR ? 'yes' : 'no';
</script>

<template>
<div>
<p id="public-var">{{ publicVar }}</p>
<p id="public-num">{{ publicNum }}/{{ typeof publicNum }}</p>
<p id="env-specific">{{ envSpecific }}</p>
<p id="has-secret">{{ hasSecret }}</p>
</div>
</template>
10 changes: 10 additions & 0 deletions framework-tests/frameworks/nuxt/files/routes/env-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ENV } from 'varlock/env';

// Nitro server route. These are built by nitro's own rollup pass and are never
// part of vite's SSR module graph, so ENV here is only available if the module
// injected varlock init into the nitro bundle.
export default defineEventHandler(() => ({
PUBLIC_VAR: ENV.PUBLIC_VAR,
ENV_SPECIFIC_VAR: ENV.ENV_SPECIFIC_VAR,
HAS_SECRET: ENV.SENSITIVE_VAR ? 'yes' : 'no',
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ENV } from 'varlock/env';

// Returns a sensitive value straight to the client — varlock's patched response
// objects should catch this before it reaches the network.
export default defineEventHandler(() => `token is ${ENV.SENSITIVE_VAR}`);
6 changes: 6 additions & 0 deletions framework-tests/frameworks/nuxt/files/routes/log-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ENV } from 'varlock/env';

export default defineEventHandler(() => {
console.log('secret-log-test:', ENV.SENSITIVE_VAR);
return { ok: true };
});
1 change: 1 addition & 0 deletions framework-tests/frameworks/nuxt/files/schemas/.env.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENV_SPECIFIC_VAR=env-specific-var--dev
1 change: 1 addition & 0 deletions framework-tests/frameworks/nuxt/files/schemas/.env.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENV_SPECIFIC_VAR=env-specific-var--prod
15 changes: 15 additions & 0 deletions framework-tests/frameworks/nuxt/files/schemas/.env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @defaultSensitive=false @defaultRequired=infer
# @generateTypes(lang="ts", path="env.d.ts")
# @currentEnv=$APP_ENV
# ---

# @type=enum(dev, prod, test)
APP_ENV=dev

PUBLIC_VAR=public-var-value
# @type=number
PUBLIC_NUM=1234
ENV_SPECIFIC_VAR=env-specific-var--default

# @sensitive
SENSITIVE_VAR=super-secret-value
233 changes: 233 additions & 0 deletions framework-tests/frameworks/nuxt/nuxt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
Nuxt framework tests.

Two things make Nuxt different from a plain vite app, and both are covered here:

1. Nuxt points vite's `root` at the srcDir (`app/` by default in Nuxt 4) while
`.env.schema` lives at the project root. The module has to tell the vite
plugin where the project root actually is, or every build-time `ENV.*`
replacement is silently dropped. The default layout below (app/app.vue) is
the regression guard.
2. Nitro rebuilds the server with its own rollup pass, so the init module
injected into vite's SSR entry never reaches `.output/server` — and server
routes are never in vite's module graph at all. The module registers the
same init as a nitro plugin; the server-route and leak scenarios cover it.
*/
import {
describe, beforeAll, afterAll,
} from 'vitest';
import { FrameworkTestEnv } from '../../harness/index';

describe('Nuxt', () => {
const env = new FrameworkTestEnv({
testDir: import.meta.dirname,
framework: 'nuxt',
packageManager: 'pnpm',
dependencies: {
nuxt: '^4',
vue: '^3',
'vue-router': '^4',
varlock: 'will-be-replaced',
'@varlock/nuxt-integration': 'will-be-replaced',
},
packageJsonMerge: {
packageManager: 'pnpm@10.17.0',
},
templateFiles: {
'.env.schema': 'schemas/.env.schema',
'.env.dev': 'schemas/.env.dev',
'.env.prod': 'schemas/.env.prod',
// default Nuxt 4 layout — srcDir is `app/`, one level below the env files
'app/app.vue': 'pages/basic-page.vue',
'nuxt.config.ts': 'configs/nuxt.config.ts',
},
});

beforeAll(() => env.setup(), 300_000);
afterAll(() => env.teardown());

env.describeScenario('build: non-sensitive inlined, sensitive not leaked', {
command: 'nuxt build',
expectSuccess: true,
timeout: 300_000,
fileAssertions: [
{
description: 'client bundle inlines non-sensitive values',
fileGlob: '.output/public/**/*.js',
shouldContain: ['public-var-value', 'env-specific-var--dev'],
},
{
description: 'sensitive value is absent from all build output',
fileGlob: '.output/**/*.{js,mjs,html}',
shouldNotContain: ['super-secret-value'],
},
{
description: 'nitro server bundle carries the varlock init',
fileGlob: '.output/server/chunks/nitro/*.mjs',
shouldContain: ['initVarlockEnv'],
},
],
});

env.describeScenario('build: env-specific vars use prod environment', {
command: 'nuxt build',
expectSuccess: true,
timeout: 300_000,
env: { APP_ENV: 'prod' },
fileAssertions: [
{
description: 'prod-specific value is inlined',
fileGlob: '.output/public/**/*.js',
shouldContain: ['env-specific-var--prod'],
shouldNotContain: ['env-specific-var--dev', 'env-specific-var--default'],
},
],
});

// `nuxt build --cwd <dir>` does not chdir, so process.cwd() is not the
// project root. Everything that reads varlock config before vite's `config`
// hook runs — notably the nitro init template — has to be told the real root,
// or a `resolved-env` build bakes an empty env and every route 500s.
env.describeScenario('build: --cwd from a different working directory', {
// run from the parent dir so process.cwd() is not the project root. The
// nuxt binary is invoked directly rather than via `pnpm exec`, which would
// walk up from the parent to a different package root.
command: "sh -c 'cd .. && node nuxt/node_modules/nuxt/bin/nuxt.mjs build --cwd nuxt'",
expectSuccess: true,
timeout: 300_000,
templateFiles: {
'nuxt.config.ts': 'configs/nuxt.config.resolved-env.ts',
},
fileAssertions: [
{
description: 'client bundle still inlines non-sensitive values',
fileGlob: '.output/public/**/*.js',
shouldContain: ['public-var-value'],
},
{
description: 'nitro server bundle carries a populated resolved env',
fileGlob: '.output/server/chunks/nitro/*.mjs',
shouldContain: ['__varlockLoadedEnv', 'PUBLIC_VAR'],
},
],
});

// TODO: `nuxt dev` produces no output at all when spawned by this harness
// (detached + shell), so the ready pattern never matches — the process stays
// alive but silent for the full timeout. The same spawn (same command, cwd,
// env, pnpm version, `--no-fork`, stdin from /dev/null) streams output
// normally outside vitest, so this is harness plumbing rather than an
// integration bug: dev mode has been verified by hand to serve `ENV` in both
// pages and server routes, redact secrets from logs, and block leaked values
// in responses. The build and production-server scenarios below cover the
// same code paths through the nitro plugin.
env.describeDevScenario('dev: ENV available in pages and server routes', {
skip: true,
command: 'nuxt dev --no-fork --port 14930 < /dev/null',
readyPattern: /localhost:14930/,
readyTimeout: 120_000,
timeout: 300_000,
templateFiles: {
'server/api/env.get.ts': 'routes/env-endpoint.ts',
'server/api/log.get.ts': 'routes/log-endpoint.ts',
},
requests: [
{
path: '/api/env',
bodyAssertions: {
shouldContain: ['"PUBLIC_VAR":"public-var-value"', '"HAS_SECRET":"yes"'],
shouldNotContain: ['super-secret-value'],
},
},
{
path: '/',
bodyAssertions: {
shouldContain: ['public-var-value', '1234/number', 'env-specific-var--dev'],
shouldNotContain: ['super-secret-value'],
},
},
{ path: '/api/log' },
],
outputAssertions: [
{
description: 'secret is redacted from server logs',
shouldContain: ['secret-log-test:'],
shouldNotContain: ['super-secret-value'],
},
],
});

// `init-only` (the default) expects the env to already be in the server
// process, which is what `varlock run` does.
env.describeDevScenario('production server: init-only under `varlock run`', {
command: 'nuxt build < /dev/null && pnpm exec varlock run -- node .output/server/index.mjs',
env: { PORT: '14931', HOST: '127.0.0.1' },
readyPattern: /Listening on/,
readyTimeout: 300_000,
timeout: 420_000,
templateFiles: {
'server/api/env.get.ts': 'routes/env-endpoint.ts',
},
requests: [
{
path: '/api/env',
bodyAssertions: {
shouldContain: ['"PUBLIC_VAR":"public-var-value"', '"HAS_SECRET":"yes"'],
shouldNotContain: ['super-secret-value'],
},
},
],
});

// `auto-load` has to survive nitro's rollup pass, which treats external
// modules as side-effect free — a bare `import 'varlock/auto-load'` gets
// tree-shaken away and the server starts with no env at all.
env.describeDevScenario('production server: auto-load runs without `varlock run`', {
command: 'nuxt build < /dev/null && node .output/server/index.mjs',
env: { PORT: '14932', HOST: '127.0.0.1' },
readyPattern: /Listening on/,
readyTimeout: 300_000,
timeout: 420_000,
templateFiles: {
'nuxt.config.ts': 'configs/nuxt.config.auto-load.ts',
'server/api/env.get.ts': 'routes/env-endpoint.ts',
},
requests: [
{
path: '/api/env',
bodyAssertions: {
shouldContain: ['"PUBLIC_VAR":"public-var-value"', '"HAS_SECRET":"yes"'],
shouldNotContain: ['super-secret-value'],
},
},
],
});

env.describeDevScenario('production server: leaked secret is blocked in the response', {
command: 'nuxt build < /dev/null && pnpm exec varlock run -- node .output/server/index.mjs',
env: { PORT: '14933', HOST: '127.0.0.1' },
readyPattern: /Listening on/,
readyTimeout: 300_000,
timeout: 420_000,
templateFiles: {
'server/api/leak.get.ts': 'routes/leaky-endpoint.ts',
},
requests: [
{
path: '/api/leak',
// the patched response throws instead of writing the body, so the
// request never completes — only the absence of the secret matters
allowRequestFailure: true,
bodyAssertions: {
shouldNotContain: ['super-secret-value'],
},
},
],
outputAssertions: [
{
description: 'server reports the leak',
shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'],
},
],
});
});
1 change: 1 addition & 0 deletions framework-tests/harness/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const PACKAGE_DIRS: Record<string, string> = {
'@varlock/vite-integration': 'packages/integrations/vite',
'@varlock/expo-integration': 'packages/integrations/expo',
'@varlock/cloudflare-integration': 'packages/integrations/cloudflare',
'@varlock/nuxt-integration': 'packages/integrations/nuxt',
};

/**
Expand Down
1 change: 1 addition & 0 deletions framework-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test:cloudflare": "vitest run frameworks/cloudflare",
"test:expo": "vitest run frameworks/expo",
"test:nextjs": "vitest run frameworks/nextjs",
"test:nuxt": "vitest run frameworks/nuxt",
"test:sveltekit": "vitest run frameworks/sveltekit",
"test:vanilla-node": "vitest run frameworks/vanilla-node",
"test:tanstack-start": "vitest run frameworks/tanstack-start",
Expand Down
37 changes: 37 additions & 0 deletions packages/integrations/nuxt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# @varlock/nuxt-integration

[![npm version](https://img.shields.io/npm/v/@varlock/nuxt-integration.svg)](https://npmx.dev/package/@varlock/nuxt-integration) [![GitHub stars](https://img.shields.io/github/stars/dmno-dev/varlock.svg?style=social&label=Star)](https://github.com/dmno-dev/varlock) [![license](https://img.shields.io/npm/l/@varlock/nuxt-integration.svg)](https://github.com/dmno-dev/varlock/blob/main/LICENSE)

This package helps you integrate [varlock](https://varlock.dev) into a [Nuxt](https://nuxt.com) project.

> See [our docs site](https://varlock.dev/integrations/nuxt/) for complete installation and usage instructions.

It is designed as a [Nuxt module](https://nuxt.com/docs/guide/concepts/modules) that adds varlock to both the Vite config and the Nitro server build, so `.env` files are loaded and validated by varlock across dev, build, and the production server.

Compared to the [default Nuxt behavior](https://nuxt.com/docs/guide/going-further/runtime-config), this package provides:

- Validation of your env vars against your `.env.schema`
- Type-generation and type-safe env var access with built-in docs
- Redaction of sensitive values from logs during build and dev time
- Automatic leak prevention of sensitive items at build and runtime
- More flexible multi-env handling

## Installation

```bash
npm install @varlock/nuxt-integration varlock
# or
bun add @varlock/nuxt-integration varlock
```

## Setup

Add the module to your `nuxt.config.ts`:

```ts title="nuxt.config.ts"
export default defineNuxtConfig({
modules: ['@varlock/nuxt-integration'],
})
```

Then run `varlock init` to set up your `.env.schema` file based on your existing `.env` files.
Loading
Loading