Skip to content
Draft
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
7 changes: 6 additions & 1 deletion packages/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import { NetlifyDev } from '@netlify/dev'
const devServer = new NetlifyDev({
blobs: { enabled: true },
edgeFunctions: { enabled: true },
environmentVariables: { enabled: true },
environmentVariables: {
enabled: true,
// OPTIONAL: control whether user-defined environment variables are injected
// When false, only platform env vars (NETLIFY_LOCAL, CONTEXT, SITE_ID, etc.) are injected
injectUserEnv: true, // default: true
},
functions: { enabled: true },
redirects: { enabled: true },
staticFiles: {
Expand Down
22 changes: 22 additions & 0 deletions packages/dev/src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,26 @@ interface InjectEnvironmentVariablesOptions {
accountSlug?: string
baseVariables: Record<string, EnvironmentVariable>
envAPI: EnvironmentVariables
/**
* Whether to inject user-defined environment variables.
* When false, only platform environment variables (from 'general' and 'internal' sources)
* are injected. When true, all environment variables are injected.
* @default true
*/
injectUserEnv?: boolean
netlifyAPI?: NetlifyAPI
siteID?: string
}

/**
* Determines if an environment variable is a platform variable.
* Platform variables are from 'general' or 'internal' sources and include
* documented runtime variables like NETLIFY_LOCAL, CONTEXT, SITE_ID, etc.
*/
const isPlatformEnvironmentVariable = (variable: EnvironmentVariable): boolean => {
return variable.sources.includes('general') || variable.sources.includes('internal')
}

/**
* Inject user-defined environment variables (from various sources, see `@netlify/config`)
* into the provided `envAPI` (which may be a proxy to `process.env`, affecting the current proc),
Expand All @@ -61,6 +77,7 @@ export const injectEnvVariables = async ({
accountSlug,
baseVariables = {},
envAPI,
injectUserEnv = true,
netlifyAPI,
siteID,
}: InjectEnvironmentVariablesOptions) => {
Expand All @@ -80,6 +97,11 @@ export const injectEnvVariables = async ({
// Inject env vars which come from multiple `source`s and have been collected from
// `@netlify/config` and/or Envelope. These have not been populated on the actual env yet.
for (const [key, variable] of Object.entries(variables)) {
// If injectUserEnv is false, only inject platform env vars (from 'general' and 'internal' sources)
if (!injectUserEnv && !isPlatformEnvironmentVariable(variable)) {
continue
}

const existsInProcess = envAPI.has(key)
const [usedSource, ...overriddenSources] = existsInProcess ? ['process', ...variable.sources] : variable.sources
const isInternal = variable.sources.includes('internal')
Expand Down
81 changes: 81 additions & 0 deletions packages/dev/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1107,3 +1107,84 @@ describe('Handling requests', () => {
})
})
})

describe('Environment variable injection', () => {
afterEach(() => {
vi.unstubAllEnvs()
})

test('injectUserEnv option controls user-defined env var injection', async () => {
const fixture = new Fixture()
.withFile(
'netlify.toml',
`[build]
publish = "public"
[context.dev.environment]
MY_USER_VAR = "user value"
`,
)
.withFile(
'netlify/functions/env-test.mjs',
`export default async (req, context) => Response.json({
NETLIFY_LOCAL: Netlify.env.get("NETLIFY_LOCAL"),
CONTEXT: Netlify.env.get("CONTEXT"),
MY_USER_VAR: Netlify.env.get("MY_USER_VAR"),
});
export const config = { path: "/env-test" };`,
)
.withStateFile({ siteId: 'site_id' })
const directory = await fixture.create()

// Test with injectUserEnv: false - only platform vars should be injected
const devWithoutUserEnv = new NetlifyDev({
projectRoot: directory,
environmentVariables: {
enabled: true,
injectUserEnv: false,
},
edgeFunctions: { enabled: false },
geolocation: { enabled: false },
})

await devWithoutUserEnv.start()

const req1 = new Request('https://site.netlify/env-test')
const res1 = await devWithoutUserEnv.handle(req1)
const envVarsWithoutUserEnv = await res1?.json()

await devWithoutUserEnv.stop()

// Platform env vars should be present
expect(envVarsWithoutUserEnv).toHaveProperty('NETLIFY_LOCAL', 'true')
expect(envVarsWithoutUserEnv).toHaveProperty('CONTEXT', 'dev')

// User-defined env vars should NOT be present
expect(envVarsWithoutUserEnv.MY_USER_VAR).toBeUndefined()

// Test with injectUserEnv: true (default) - all vars should be injected
const devWithUserEnv = new NetlifyDev({
projectRoot: directory,
environmentVariables: {
enabled: true,
injectUserEnv: true,
},
edgeFunctions: { enabled: false },
geolocation: { enabled: false },
})

await devWithUserEnv.start()

const req2 = new Request('https://site.netlify/env-test')
const res2 = await devWithUserEnv.handle(req2)
const envVarsWithUserEnv = await res2?.json()

await devWithUserEnv.stop()

// Both platform and user-defined env vars should be present
expect(envVarsWithUserEnv).toHaveProperty('NETLIFY_LOCAL', 'true')
expect(envVarsWithUserEnv).toHaveProperty('CONTEXT', 'dev')
expect(envVarsWithUserEnv).toHaveProperty('MY_USER_VAR', 'user value')

await fixture.destroy()
})
})
12 changes: 12 additions & 0 deletions packages/dev/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export interface Features {
*/
environmentVariables?: {
enabled?: boolean
/**
* Whether to inject user-defined environment variables from the linked site.
* When false, only platform environment variables (NETLIFY_LOCAL, CONTEXT, SITE_ID, etc.)
* are injected. When true, all environment variables including user-defined ones
* from Netlify UI, account, and config files are injected.
*
* {@default} true
*/
injectUserEnv?: boolean
}

/**
Expand Down Expand Up @@ -181,6 +190,7 @@ export class NetlifyDev {
blobs: boolean
edgeFunctions: boolean
environmentVariables: boolean
environmentVariablesInjectUserEnv: boolean
functions: boolean
geolocation: boolean
headers: boolean
Expand Down Expand Up @@ -216,6 +226,7 @@ export class NetlifyDev {
blobs: options.blobs?.enabled !== false,
edgeFunctions: options.edgeFunctions?.enabled !== false,
environmentVariables: options.environmentVariables?.enabled !== false,
environmentVariablesInjectUserEnv: options.environmentVariables?.injectUserEnv !== false,
functions: options.functions?.enabled !== false,
geolocation: options.geolocation?.enabled !== false,
headers: options.headers?.enabled !== false,
Expand Down Expand Up @@ -517,6 +528,7 @@ export class NetlifyDev {
accountSlug: config?.siteInfo?.account_slug,
baseVariables: config?.env || {},
envAPI: runtime.env,
injectUserEnv: this.#features.environmentVariablesInjectUserEnv,
netlifyAPI: config?.api,
siteID,
})
Expand Down
29 changes: 29 additions & 0 deletions packages/vite-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ The plugin accepts the following options:
same way as the Netlify production environment
- `blobs`: Configure blob storage functionality
- `edgeFunctions`: Configure edge functions
- `environmentVariables`: Configure environment variable injection
- `enabled` (boolean, default: `true`): Enable environment variable injection
- `injectUserEnv` (boolean, default: `true`): Inject user-defined environment variables. When `false`, only platform
environment variables (like `NETLIFY_LOCAL`, `CONTEXT`, `SITE_ID`) are injected, excluding user-defined variables
from Netlify account settings, Netlify UI, config files, and addons
- `functions`: Configure serverless functions
- `headers`: Configure response headers
- `images`: Configure Image CDN functionality
Expand All @@ -49,3 +54,27 @@ export default defineConfig({
plugins: [netlify()],
})
```

### Environment Variables Configuration

By default, the plugin injects all environment variables including user-defined ones from your Netlify site. If you want
to only inject platform environment variables (useful for frameworks that manage their own environment variables):

```js
import { defineConfig } from 'vite'
import netlify from '@netlify/vite-plugin'

export default defineConfig({
plugins: [
netlify({
environmentVariables: {
enabled: true,
injectUserEnv: false,
},
}),
],
})
```

With `injectUserEnv: false`, only platform variables like `NETLIFY_LOCAL`, `CONTEXT`, and `SITE_ID` are injected,
which are required for platform features like `purgeCache()` to work correctly.
55 changes: 55 additions & 0 deletions packages/vite-plugin/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,61 @@ describe.for([['5.0.0'], ['6.0.0'], ['7.0.0']])('Vite %s', ([viteVersion]) => {
await fixture.destroy()
})

test('Populates platform env vars when injectUserEnv is false', async () => {
const fixture = new Fixture()
.withFile(
'vite.config.js',
`import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';

export default defineConfig({
plugins: [
netlify({
middleware: false,
environmentVariables: {
enabled: true,
injectUserEnv: false
}
})
]
});`,
)
.withFile(
'netlify.toml',
`[context.dev.environment]
MY_USER_VAR = "user value"`,
)
.withFile(
'index.html',
`<!DOCTYPE html>
<html>
<head><title>Hello World</title></head>
<body><h1>Hello from the browser</h1></body>
</html>`,
)
const directory = await fixture.create()
await fixture
.withPackages({
vite: viteVersion,
'@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
})
.create()

const { server } = await startTestServer({
root: directory,
})

// Platform env vars should be present
expect(process.env).toHaveProperty('NETLIFY_LOCAL', 'true')
expect(process.env).toHaveProperty('CONTEXT', 'dev')

// User-defined env vars should NOT be present when injectUserEnv is false
expect(process.env).not.toHaveProperty('MY_USER_VAR')

await server.close()
await fixture.destroy()
})

test('Prints a basic message on server start', async () => {
const fixture = new Fixture()
.withFile(
Expand Down
Loading