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
25 changes: 25 additions & 0 deletions packages/cli/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ export interface FrameworkWarning {
framework: SupportedFramework;
}

/**
* Information about the file an environment variable usage was found in
*/
export interface FrameworkRuleContext {
/** The file path, normalized to forward slashes */
file: string;
/** The full content of the file (if available) */
content?: string | undefined;
}

/**
* A supported framework: how to detect it and which rules apply to it
*/
export interface Framework {
/** Unique name of the framework */
name: SupportedFramework;
/** npm package whose presence in package.json identifies the framework */
packageName: string;
/**
* Checks a single environment variable usage against the framework rules
* @returns The reason for a warning, or undefined if the usage is fine
*/
check(usage: EnvUsage, context: FrameworkRuleContext): string | undefined;
}

/**
* Type representing a duplicate entry
*/
Expand Down
5 changes: 0 additions & 5 deletions packages/cli/src/core/frameworks/constants.ts

This file was deleted.

70 changes: 31 additions & 39 deletions packages/cli/src/core/frameworks/frameworkDetector.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,55 @@
import fs from 'fs';
import path from 'path';
import type { DetectedFramework } from '../../config/types.js';
import type { Framework } from '../../config/types.js';
import { FRAMEWORKS } from './registry.js';

/**
* Interface representing the detected framework and its version (if applicable)
* Interface representing the detected framework and its version
*/
interface FrameworkDetection {
/** The detected framework (e.g., 'sveltekit', 'nextjs', or 'unknown') */
framework: DetectedFramework;
/** The version of the detected framework (if available) */
version?: string;
/** The detected framework */
framework: Framework;
/** The version range of the framework from package.json */
version: string;
}

/**
* Detects the framework being used in the project
* by checking package.json and file structure
* Detects the framework being used in the project by checking package.json
* @param cwd The current working directory of the project
* @returns Detected framework and its version (if applicable)
* @returns The detected framework and its version, or undefined if none is supported
*/
export function detectFramework(cwd: string): FrameworkDetection {
export function detectFramework(cwd: string): FrameworkDetection | undefined {
const deps = readDependencies(cwd);

for (const framework of FRAMEWORKS) {
const version = deps[framework.packageName];
if (version) {
return { framework, version };
}
}

return undefined;
}

/**
* Reads dependencies and devDependencies from package.json
* @param cwd The current working directory of the project
* @returns Map of package names to version ranges (empty if package.json is missing or invalid)
*/
function readDependencies(cwd: string): Record<string, string> {
try {
const packageJsonPath = path.join(cwd, 'package.json');

if (!fs.existsSync(packageJsonPath)) {
return { framework: 'unknown' };
return {};
}

const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const deps = {
return {
...packageJson.dependencies,
...packageJson.devDependencies,
};

// Check for SvelteKit
if (deps['@sveltejs/kit']) {
return {
framework: 'sveltekit',
version: deps['@sveltejs/kit'],
};
}

// Check for Next.js
if (deps['next']) {
return {
framework: 'nextjs',
version: deps['next'],
};
}

// Check for Nuxt
if (deps['nuxt']) {
return {
framework: 'nuxt',
version: deps['nuxt'],
};
}

return { framework: 'unknown' };
} catch {
return { framework: 'unknown' };
return {};
}
}
61 changes: 49 additions & 12 deletions packages/cli/src/core/frameworks/frameworkValidator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { EnvUsage, FrameworkWarning } from '../../config/types.js';
import type {
EnvUsage,
Framework,
FrameworkWarning,
} from '../../config/types.js';
import { normalizePath } from '../helpers/normalizePath.js';
import { detectFramework } from './frameworkDetector.js';
import {
applySvelteKitRules,
applyNextJsRules,
applyNuxtRules,
} from './index.js';

/**
* Validates environment variable usages against framework-specific rules
* Validates environment variable usages against the rules of the project's framework
* @param usages - Array of environment variable usages
* @param cwd - Current working directory to detect framework
* @param fileContentMap - Map of file paths to their content for detecting client components
Expand All @@ -17,14 +17,51 @@ export function frameworkValidator(
usages: EnvUsage[],
cwd: string,
fileContentMap?: Map<string, string>,
): FrameworkWarning[] {
const detection = detectFramework(cwd);
if (!detection) {
return [];
}

return validateUsages(usages, detection.framework, fileContentMap);
}

/**
* Validates environment variable usages against the rules of a given framework
* @param usages - Array of environment variable usages
* @param framework - The framework whose rules to apply
* @param fileContentMap - Map of file paths to their content for detecting client components
* @returns Array of framework-specific warnings
*/
export function validateUsages(
usages: EnvUsage[],
framework: Framework,
fileContentMap?: Map<string, string>,
): FrameworkWarning[] {
const warnings: FrameworkWarning[] = [];
const { framework } = detectFramework(cwd);

for (const u of usages) {
if (framework === 'sveltekit') applySvelteKitRules(u, warnings);
if (framework === 'nextjs') applyNextJsRules(u, warnings, fileContentMap);
if (framework === 'nuxt') applyNuxtRules(u, warnings);
for (const usage of usages) {
// Normalize path separators for cross-platform consistency
const file = normalizePath(usage.file);

if (file.includes('/node_modules/')) {
continue;
}

const reason = framework.check(usage, {
file,
content: fileContentMap?.get(usage.file),
});

if (reason) {
warnings.push({
variable: usage.variable,
reason,
file,
line: usage.line,
framework: framework.name,
});
}
}

return warnings;
Expand Down
3 changes: 0 additions & 3 deletions packages/cli/src/core/frameworks/index.ts

This file was deleted.

81 changes: 0 additions & 81 deletions packages/cli/src/core/frameworks/nextJsRules.ts

This file was deleted.

54 changes: 54 additions & 0 deletions packages/cli/src/core/frameworks/nextjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Framework } from '../../config/types.js';
import { EXPOSED_SECRET_REASON, isExposedSecret } from './shared.js';

/**
* Next.js environment variable rules.
*
* Only variables prefixed with `NEXT_PUBLIC_` are inlined into client code.
*/
export const nextjs: Framework = {
name: 'nextjs',
packageName: 'next',

check(usage, { file, content }) {
if (
isClientFile(file, content) &&
!usage.variable.startsWith('NEXT_PUBLIC_')
) {
return 'Server-only variable accessed from client code';
}

if (usage.pattern === 'import.meta.env') {
return 'Next.js uses process.env, not import.meta.env (Vite syntax)';
}

if (isExposedSecret(usage.variable, ['NEXT_PUBLIC_'])) {
return EXPOSED_SECRET_REASON;
}

return undefined;
},
};

/**
* Checks whether a file runs in the browser:
* - App Router: files with a "use client" directive
* - Pages Router: everything in `pages/` except `pages/api/`
*/
function isClientFile(file: string, content?: string): boolean {
return hasUseClientDirective(content) || isPagesRouterPage(file);
}

function hasUseClientDirective(content?: string): boolean {
if (!content) return false;

// The directive must appear at the top of the file
const firstLines = content.split('\n').slice(0, 10).join('\n');
return (
/['"]use client['"]/.test(firstLines) || /^use client;?$/m.test(firstLines)
);
}

function isPagesRouterPage(file: string): boolean {
return /(^|\/)pages\//.test(file) && !/(^|\/)pages\/api\//.test(file);
}
Loading
Loading