Skip to content

Commit 495e282

Browse files
authored
fix: false warning + destructering env (#345)
1 parent 05cb52c commit 495e282

File tree

8 files changed

+136
-2
lines changed

8 files changed

+136
-2
lines changed

.changeset/slimy-jokes-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dotenv-diff': patch
3+
---
4+
5+
fixed false warning in sveltekit config files

.changeset/smart-planes-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dotenv-diff': patch
3+
---
4+
5+
sveltekit object destructering from env

docs/capabilities.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import.meta.env['MY_KEY']
2525
import { env } from '$env/dynamic/private';
2626
import { env } from '$env/dynamic/public';
2727
env.MY_KEY
28+
const { MY_KEY } = env
29+
const { MY_KEY: alias, OTHER_KEY = "fallback" } = env
2830

2931
// SvelteKit – static (named imports)
3032
import { MY_KEY } from '$env/static/private';

docs/frameworks/sveltekit_warnings.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,21 @@ import.meta.env.VITE_PUBLIC_URL
2323
## 2 `process.env` should only be used in server files
2424

2525
```ts
26-
process.env.VITE_SECRET
26+
// Warning in client file
27+
const apiUrl = process.env.API_URL;
28+
29+
// No warning in server file
30+
export async function load() {
31+
const secret = process.env.DATABASE_PASSWORD;
32+
}
2733
```
2834

2935
Warning:
3036

3137
`process.env should only be used in server files`
3238

39+
**Note:** `process.env` is allowed in configuration files like `svelte.config.js` or `svelte.config.ts`, as these are Node.js files that run during build time.
40+
3341
## 3 `$env/dynamic/private` cannot be used in client-side code
3442

3543
```svelte

packages/cli/src/core/frameworks/sveltekitRules.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export function applySvelteKitRules(
1818
return;
1919
}
2020

21+
// Config files are allowed to use process.env
22+
const isConfigFile = /svelte\.config\.(ts|js)$/.test(normalizedFile);
23+
2124
const isServerFile =
2225
// SvelteKit route server files
2326
normalizedFile.includes('/+server.') ||
@@ -50,7 +53,7 @@ export function applySvelteKitRules(
5053

5154
// process.env
5255
if (u.pattern === 'process.env') {
53-
if (!isServerFile) {
56+
if (!isServerFile && !isConfigFile) {
5457
warnings.push({
5558
variable: u.variable,
5659
reason: 'process.env should only be used in server files',

packages/cli/src/core/scan/patterns.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@ export const ENV_PATTERNS: Pattern[] = [
9494
regex: /(?<![.\w])env\.([A-Z_][A-Z0-9_]*)/g,
9595
},
9696

97+
// SvelteKit object destructuring from env
98+
// const { VAR1, VAR2 } = env; (destructured from $env/dynamic/* or $env/static/*)
99+
{
100+
name: 'sveltekit' as const,
101+
regex: /\{([^}]*)\}\s*=\s*env\b/g,
102+
processor: (match) => {
103+
const content = match[1];
104+
if (!content) return [];
105+
106+
return content
107+
.split(',')
108+
.map((part) => part.trim())
109+
.filter(Boolean)
110+
.map((part) => {
111+
// Handle aliases: VAR: alias
112+
// Handle defaults: VAR = "value"
113+
// We want the left-most identifier
114+
const [key] = part.split(/[:=]/);
115+
return key ? key.trim() : '';
116+
})
117+
.filter((key) => /^[A-Z_][A-Z0-9_]*$/.test(key));
118+
},
119+
},
120+
97121
// named import from dynamic is invalid in SvelteKit
98122
// import { env } from '$env/dynamic/private';
99123
{

packages/cli/test/unit/core/frameworks/sveltekitRules.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ describe('applySvelteKitRules', () => {
6666
expect(warnings).toHaveLength(0);
6767
});
6868

69+
it('allows process.env in svelte.config.js', () => {
70+
applySvelteKitRules({ ...baseUsage, file: 'svelte.config.js' }, warnings);
71+
expect(warnings).toHaveLength(0);
72+
});
73+
74+
it('allows process.env in svelte.config.ts', () => {
75+
applySvelteKitRules({ ...baseUsage, file: 'svelte.config.ts' }, warnings);
76+
expect(warnings).toHaveLength(0);
77+
});
78+
6979
// dynamic private
7080
it('warns dynamic/private in svelte file', () => {
7181
applySvelteKitRules(

packages/cli/test/unit/core/scan/patterns.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,81 @@ describe('scanFile - Pattern Detection', () => {
180180
});
181181
});
182182
});
183+
184+
describe('SvelteKit env Object Access', () => {
185+
it('detects env.VARIABLE_NAME access', () => {
186+
const code = 'const token = env.KEYCLOAK_SECRET;';
187+
const result = scanFile('test.ts', code, baseOpts);
188+
189+
expect(result).toHaveLength(1);
190+
expect(result[0]).toMatchObject({
191+
variable: 'KEYCLOAK_SECRET',
192+
pattern: 'sveltekit',
193+
});
194+
});
195+
196+
it('detects destructuring from env: const { VAR1, VAR2 } = env', () => {
197+
const code = 'const { KEYCLOAK_URL, KEYCLOAK_REALM } = env;';
198+
const result = scanFile('test.ts', code, baseOpts);
199+
200+
expect(result).toHaveLength(2);
201+
const variables = result.map((u) => u.variable).sort();
202+
expect(variables).toEqual(['KEYCLOAK_REALM', 'KEYCLOAK_URL']);
203+
});
204+
205+
it('detects destructuring with aliasing: const { VAR: alias } = env', () => {
206+
const code = 'const { KEYCLOAK_URL: url } = env;';
207+
const result = scanFile('test.ts', code, baseOpts);
208+
209+
expect(result).toHaveLength(1);
210+
expect(result[0]).toMatchObject({
211+
variable: 'KEYCLOAK_URL',
212+
pattern: 'sveltekit',
213+
});
214+
});
215+
216+
it('detects destructuring with default values: const { VAR = "default" } = env', () => {
217+
const code = 'const { KEYCLOAK_URL = "http://localhost:8080" } = env;';
218+
const result = scanFile('test.ts', code, baseOpts);
219+
220+
expect(result).toHaveLength(1);
221+
expect(result[0]).toMatchObject({
222+
variable: 'KEYCLOAK_URL',
223+
pattern: 'sveltekit',
224+
});
225+
});
226+
227+
it('detects multiple mixed destructuring from env', () => {
228+
const code =
229+
'const { KEYCLOAK_URL, KEYCLOAK_REALM: realm, CLIENT_SECRET = "default" } = env;';
230+
const result = scanFile('test.ts', code, baseOpts);
231+
232+
expect(result).toHaveLength(3);
233+
const variables = result.map((u) => u.variable).sort();
234+
expect(variables).toEqual([
235+
'CLIENT_SECRET',
236+
'KEYCLOAK_REALM',
237+
'KEYCLOAK_URL',
238+
]);
239+
});
240+
241+
it('detects multiline destructuring from env', () => {
242+
const code = `
243+
const {
244+
KEYCLOAK_URL,
245+
KEYCLOAK_REALM,
246+
CLIENT_ID
247+
} = env;
248+
`;
249+
const result = scanFile('test.ts', code, baseOpts);
250+
251+
expect(result).toHaveLength(3);
252+
const variables = result.map((u) => u.variable).sort();
253+
expect(variables).toEqual([
254+
'CLIENT_ID',
255+
'KEYCLOAK_REALM',
256+
'KEYCLOAK_URL',
257+
]);
258+
});
259+
});
183260
});

0 commit comments

Comments
 (0)