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
4 changes: 3 additions & 1 deletion packages/cli/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export interface Options {
inconsistentNamingWarnings: boolean;
}

export type EnvPatternName = 'process.env' | 'import.meta.env' | 'sveltekit';

/**
* Represents a single usage of an environment variable in the codebase.
*/
Expand All @@ -150,7 +152,7 @@ export interface EnvUsage {
/** The column number where the usage was detected */
column: number;
/** The pattern used to access the environment variable */
pattern: 'process.env' | 'import.meta.env' | 'sveltekit';
pattern: EnvPatternName;
/** List of imported env modules (for sveltekit) */
imports?: string[];
/** The actual line content where the usage was detected */
Expand Down
16 changes: 9 additions & 7 deletions packages/cli/src/core/scan/patterns.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { EnvPatternName } from '../../config/types.js';

type Pattern = {
name: 'process.env' | 'import.meta.env' | 'sveltekit';
name: EnvPatternName;
regex: RegExp;
processor?: (match: RegExpExecArray) => string[];
processor?: (match: RegExpMatchArray) => string[];
};

/**
Expand Down Expand Up @@ -82,22 +84,22 @@ export const ENV_PATTERNS: Pattern[] = [
// import { SECRET } from '$env/static/private';
// import { PUBLIC_URL } from '$env/static/public';
{
name: 'sveltekit' as const,
name: 'sveltekit',
regex:
/import\s*\{\s*([A-Z_][A-Z0-9_]*)\s*\}\s*from\s*['"]\$env\/static\/(?:private|public)['"]/g,
},

// SvelteKit dynamic env object
// env.SECRET (Only matches .env variables accessed via env.VAR syntax)
{
name: 'sveltekit' as const,
name: 'sveltekit',
regex: /(?<![.\w])env\.([A-Z_][A-Z0-9_]*)/g,
},

// SvelteKit object destructuring from env
// const { VAR1, VAR2 } = env; (destructured from $env/dynamic/* or $env/static/*)
{
name: 'sveltekit' as const,
name: 'sveltekit',
regex: /\{([^}]*)\}\s*=\s*env\b/g,
processor: (match) => {
const content = match[1];
Expand All @@ -121,15 +123,15 @@ export const ENV_PATTERNS: Pattern[] = [
// named import from dynamic is invalid in SvelteKit
// import { env } from '$env/dynamic/private';
{
name: 'sveltekit' as const,
name: 'sveltekit',
regex:
/import\s*\{\s*([A-Z_][A-Z0-9_]*)\s*\}\s*from\s*['"]\$env\/dynamic\/(?:private|public)['"]/g,
},

// default import from any $env module is invalid in SvelteKit
// import SECRET from '$env/...';
{
name: 'sveltekit' as const,
name: 'sveltekit',
regex:
/import\s+([A-Z_][A-Z0-9_]*)\s+from\s+['"]\$env\/(?:static|dynamic)\/(?:private|public)['"]/g,
},
Expand Down
21 changes: 8 additions & 13 deletions packages/cli/src/core/scan/scanFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,39 @@ export function scanFile(
const relativePath = normalizePath(path.relative(opts.cwd, filePath));

// Collect all $env imports used in this file
const envImports: string[] = [];

const importRegex =
/import\s+(?:\{[^}]*\}|\w+)\s+from\s+['"](\$env\/(?:static|dynamic)\/(?:private|public))['"]/g;

let importMatch: RegExpExecArray | null;

while ((importMatch = importRegex.exec(content)) !== null) {
envImports.push(importMatch[1]!);
}
const envImports = [...content.matchAll(importRegex)]
.map((m) => m[1])
.filter((m): m is string => m != null);

for (const pattern of ENV_PATTERNS) {
let match: RegExpExecArray | null;
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);

while ((match = regex.exec(content)) !== null) {
for (const match of content.matchAll(regex)) {
const variables = pattern.processor
? pattern.processor(match)
: [match[1]!];
: [match[1] ?? ''];

for (const variable of variables) {
if (!variable) continue;

const matchIndex = match.index;
const matchIndex = match.index!;

// Find line and column
// Note: For destructured variables, this points to the start of the destructuring block
// not the specific variable location. Ideally we'd search within the match.
const beforeMatch = content.substring(0, matchIndex);
const lineNumber = beforeMatch.split('\n').length;
const lineNumber = (beforeMatch.match(/\n/g)?.length ?? 0) + 1;
const lastNewlineIndex = beforeMatch.lastIndexOf('\n');
const column =
lastNewlineIndex === -1
? matchIndex + 1
: matchIndex - lastNewlineIndex;

// Get the context (the actual line)
const contextLine = lines[lineNumber - 1]!;
const contextLine = lines[lineNumber - 1] ?? '';

// Ignore likely minified / bundled lines to avoid scan false positives
if (isLikelyMinified(contextLine)) continue;
Expand Down