Skip to content
Merged
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
17 changes: 15 additions & 2 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ export const SVELTEKIT_ENV_PATTERN = /(?<![.\w])env\.([A-Z_][A-Z0-9_]*)/g;
*/
export const SVELTEKIT_ENV_IMPORT_PATTERN = /from\s+['"](\$env\/)/;

/**
* Matches SvelteKit env imports that use `env` as a binding name (direct or aliased).
* Handles both dynamic and static $env imports where `env` is used as an object.
* Captures the alias name if present (group 1), otherwise the local name defaults to 'env'.
*
* Examples:
* import { env } from '$env/dynamic/private' → group 1: undefined
* import { env as privateEnv } from '$env/dynamic/private' → group 1: 'privateEnv'
* import { env as publicEnv } from "$env/dynamic/public" → group 1: 'publicEnv'
* import { env } from '$env/static/public' → group 1: undefined
*/
export const SVELTEKIT_DYNAMIC_ENV_IMPORT_PATTERN =
/import\s*\{[^}]*\benv(?:\s+as\s+([A-Za-z_$][\w$]*))?[^}]*\}\s*from\s*['"]\$env\/(?:dynamic|static)\/(?:private|public)['"]/g;

/**
* Matches object destructuring assignments from SvelteKit's env object.
* Captures the full object pattern between braces for further parsing.
Expand All @@ -108,8 +122,7 @@ export const SVELTEKIT_ENV_IMPORT_PATTERN = /from\s+['"](\$env\/)/;
* const { MY_KEY } = env
* const { MY_KEY: alias, OTHER_KEY = "fallback" } = env
*/
export const SVELTEKIT_ENV_DESTRUCTURING_PATTERN =
/\{([^}]*)\}\s*=\s*\benv\b/g;
export const SVELTEKIT_ENV_DESTRUCTURING_PATTERN = /\{([^}]*)\}\s*=\s*\benv\b/g;

/**
* Matches SvelteKit static env named imports.
Expand Down
58 changes: 47 additions & 11 deletions src/core/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {
PROCESS_ENV_PATTERN,
PROCESS_ENV_DESTRUCTURING_PATTERN,
IMPORT_META_ENV_PATTERN,
SVELTEKIT_ENV_IMPORT_PATTERN,
SVELTEKIT_ENV_PATTERN,
SVELTEKIT_ENV_DESTRUCTURING_PATTERN,
SVELTEKIT_STATIC_ENV_IMPORT_PATTERN,
SVELTEKIT_DYNAMIC_ENV_IMPORT_PATTERN,
} from "./constants";

/**
Expand Down Expand Up @@ -44,9 +44,18 @@ export function scanForEnvUsages(sourceText: string): EnvUsage[] {
// Build patterns array fresh on each call
const patterns = [PROCESS_ENV_PATTERN, IMPORT_META_ENV_PATTERN];

// Only scan for env.KEY if file imports from SvelteKit's $env module
if (SVELTEKIT_ENV_IMPORT_PATTERN.test(sourceText)) {
patterns.push(SVELTEKIT_ENV_PATTERN);
// For each dynamic SvelteKit env import, add a pattern matching the local binding name.
// Handles both direct usage (env.KEY) and aliased usage (privateEnv.KEY).
const dynamicEnvNames = collectSvelteKitDynamicEnvNames(sourceText);
for (const name of dynamicEnvNames) {
const pattern =
name === "env"
? SVELTEKIT_ENV_PATTERN
: new RegExp(
`(?<![.\\w])${escapeRegExp(name)}\.([A-Z_][A-Z0-9_]*)`,
"g",
);
patterns.push(pattern);
}

for (const pattern of patterns) {
Expand All @@ -72,8 +81,8 @@ export function scanForEnvUsages(sourceText: string): EnvUsage[] {

usages.push(...scanProcessEnvDestructuringUsages(sourceText));

if (SVELTEKIT_ENV_IMPORT_PATTERN.test(sourceText)) {
usages.push(...scanSvelteKitEnvDestructuringUsages(sourceText));
for (const name of dynamicEnvNames) {
usages.push(...scanSvelteKitEnvDestructuringUsages(sourceText, name));
}

return usages;
Expand Down Expand Up @@ -200,20 +209,47 @@ function scanProcessEnvDestructuringUsages(sourceText: string): EnvUsage[] {
return usages;
}

/**
* Collects local binding names for all dynamic SvelteKit env imports in the source.
* Handles both direct (env) and aliased (env as alias) imports.
* @param sourceText The full source code text to scan
* @return An array of local names (e.g. ['env', 'privateEnv', 'publicEnv'])
*/
function collectSvelteKitDynamicEnvNames(sourceText: string): string[] {
const names: string[] = [];
let match: RegExpExecArray | null;

while (
(match = SVELTEKIT_DYNAMIC_ENV_IMPORT_PATTERN.exec(sourceText)) !== null
) {
names.push(match[1] ?? "env");
}

SVELTEKIT_DYNAMIC_ENV_IMPORT_PATTERN.lastIndex = 0;
return names;
}

/**
* Scans object destructuring assignments from SvelteKit's env object and extracts env keys.
* Only called when a SvelteKit $env import is detected in the file.
* Supports direct keys, aliases, and default values.
* @param sourceText The full source code text to scan
* @param envName The local binding name of the env object (defaults to 'env')
* @return A list of environment variable usages found in destructuring patterns
*/
function scanSvelteKitEnvDestructuringUsages(sourceText: string): EnvUsage[] {
function scanSvelteKitEnvDestructuringUsages(
sourceText: string,
envName = "env",
): EnvUsage[] {
const usages: EnvUsage[] = [];
let match: RegExpExecArray | null;

while (
(match = SVELTEKIT_ENV_DESTRUCTURING_PATTERN.exec(sourceText)) !== null
) {
const pattern =
envName === "env"
? SVELTEKIT_ENV_DESTRUCTURING_PATTERN
: new RegExp(`\\{([^}]*)\\}\\s*=\\s*\\b${escapeRegExp(envName)}\\b`, "g");

while ((match = pattern.exec(sourceText)) !== null) {
const objectPattern = match[1] ?? "";
const objectStartInMatch = match[0].indexOf("{") + 1;
const objectStartInSource = match.index + objectStartInMatch;
Expand All @@ -232,6 +268,6 @@ function scanSvelteKitEnvDestructuringUsages(sourceText: string): EnvUsage[] {
}
}

SVELTEKIT_ENV_DESTRUCTURING_PATTERN.lastIndex = 0;
pattern.lastIndex = 0;
return usages;
}
22 changes: 22 additions & 0 deletions src/test/fixtures/sveltekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,25 @@ export const dynamicDestructuringWithoutImport = `
const env = {};
const { SECRET_KEY } = env;
`;

export const dynamicAliasedPrivateImport = `
import { env as privateEnv } from '$env/dynamic/private';
const key = privateEnv.SUPABASE_SERVICE_ROLE_KEY;
`;

export const dynamicAliasedPublicImport = `
import { env as publicEnv } from '$env/dynamic/public';
const url = publicEnv.PUBLIC_API_URL;
`;

export const dynamicMultipleAliasedImports = `
import { env as publicEnv } from '$env/dynamic/public';
import { env as privateEnv } from '$env/dynamic/private';
const url = publicEnv.PUBLIC_SUPABASE_URL;
const key = privateEnv.SUPABASE_SERVICE_ROLE_KEY;
`;

export const dynamicAliasedDestructuring = `
import { env as privateEnv } from '$env/dynamic/private';
const { SECRET_KEY } = privateEnv;
`;
31 changes: 31 additions & 0 deletions src/test/unit/scanner/scanner.sveltekit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
dynamicDestructuringSingleKey,
dynamicDestructuringAliasAndFallback,
dynamicDestructuringWithoutImport,
dynamicAliasedPrivateImport,
dynamicAliasedPublicImport,
dynamicMultipleAliasedImports,
dynamicAliasedDestructuring,
} from "../../fixtures/sveltekit";

suite("scanner (sveltekit)", () => {
Expand Down Expand Up @@ -70,4 +74,31 @@ suite("scanner (sveltekit)", () => {
const usages = scanForEnvUsages(dynamicDestructuringWithoutImport);
assert.strictEqual(usages.length, 0);
});

test("finds aliased private env import usage (privateEnv.KEY)", () => {
const usages = scanForEnvUsages(dynamicAliasedPrivateImport);
assert.strictEqual(usages.length, 1);
assert.strictEqual(usages[0].key, "SUPABASE_SERVICE_ROLE_KEY");
});

test("finds aliased public env import usage (publicEnv.KEY)", () => {
const usages = scanForEnvUsages(dynamicAliasedPublicImport);
assert.strictEqual(usages.length, 1);
assert.strictEqual(usages[0].key, "PUBLIC_API_URL");
});

test("finds usages from multiple aliased imports", () => {
const usages = scanForEnvUsages(dynamicMultipleAliasedImports);
const keys = usages.map((u) => u.key).sort();
assert.deepStrictEqual(keys, [
"PUBLIC_SUPABASE_URL",
"SUPABASE_SERVICE_ROLE_KEY",
]);
});

test("finds key in destructuring from aliased env (privateEnv)", () => {
const usages = scanForEnvUsages(dynamicAliasedDestructuring);
assert.strictEqual(usages.length, 1);
assert.strictEqual(usages[0].key, "SECRET_KEY");
});
});