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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ This project follows [Keep a Changelog](https://keepachangelog.com/) and [Semant
-

### Fixed
-
-

## [2.3.10] - 2025-12-11
### Added
- More jsDOcs for better code documentation.
- t3-env integration to validate environment variable usage against T3 stack schema.

### Fixed
- Removed unused code for old --no-compare option.

## [2.3.9] - 2025-12-09
### Added
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# dotenv-diff

![Demo](./public/demo.gif)
![Demo](./public/demo2.png)

Scan your codebase to detect which environment variables are used in your code.

Expand Down Expand Up @@ -166,6 +166,22 @@ To disable this behavior, use the `--no-inconsistent-naming-warnings` flags resp
"inconsistentNamingWarnings": false
```

## t3-env integration

by default `dotenv-diff` will detect if your project uses t3-env and validate environment variable usage against the T3 stack schema.

This will give you warnings like this:

```bashT3-env validation issues:
- API_URL (src\index.ts:25) → Variable "API_URL" is not defined in t3-env schema. Add it to either server or client schema.
```

To disable this behavior, use the `--no-t3env` flag or set it to false in the config file:

```bash
"t3env": false
```

## Show unused variables

As default, `dotenv-diff` will list variables that are defined in `.env` but never used in your codebase.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dotenv-diff",
"version": "2.3.9",
"version": "2.3.10",
"type": "module",
"description": "Scan your codebase to find environment variables in use.",
"bin": {
Expand Down
Binary file added public/demo2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,10 @@ export function createProgram() {
'--no-inconsistent-naming-warnings',
'Disable inconsistent naming pattern warnings',
)
.option('--t3env', 'Warns about specifik Next.js t3env usage patterns')
.option(
'--no-t3env',
'Disables warnings about Next.js t3env usage patterns',
)
.option('--init', 'Create a sample dotenv-diff.config.json file');
}
62 changes: 31 additions & 31 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,36 @@ import { printErrorNotFound } from '../ui/compare/printErrorNotFound.js';
import { setupGlobalConfig } from '../ui/shared/setupGlobalConfig.js';
import { loadConfig } from '../config/loadConfig.js';

/**
* Run the CLI program
* @param program The commander program instance
* @returns void
*/
export async function run(program: Command): Promise<void> {
program.parse(process.argv);

// Load and normalize options
const cliOptions = program.opts();

// Handle --init flag
if (await handleInitFlag(cliOptions)) return;

// Merge CLI options with config file options
const mergedRawOptions = loadConfig(cliOptions);

// Normalize merged options
const opts = normalizeOptions(mergedRawOptions);

setupGlobalConfig(opts);

// Route to appropriate command
if (opts.compare) {
await runCompareMode(opts);
} else {
await runScanMode(opts);
}
}

/**
* Run scan-usage mode (default behavior)
* @param opts - Normalized options
Expand All @@ -41,10 +71,10 @@ async function runScanMode(opts: Options): Promise<void> {
secrets: opts.secrets,
strict: opts.strict ?? false,
ignoreUrls: opts.ignoreUrls ?? [],
noCompare: opts.noCompare ?? false,
uppercaseKeys: opts.uppercaseKeys ?? true,
expireWarnings: opts.expireWarnings,
inconsistentNamingWarnings: opts.inconsistentNamingWarnings,
t3env: opts.t3env,
...(opts.files ? { files: opts.files } : {}),
});

Expand Down Expand Up @@ -235,33 +265,3 @@ function outputResults(
}
process.exit(exitWithError ? 1 : 0);
}

/**
* Run the CLI program
* @param program The commander program instance
* @returns void
*/
export async function run(program: Command): Promise<void> {
program.parse(process.argv);

// Load and normalize options
const cliOptions = program.opts();

// Handle --init flag
if (await handleInitFlag(cliOptions)) return;

// Merge CLI options with config file options
const mergedRawOptions = loadConfig(cliOptions);

// Normalize merged options
const opts = normalizeOptions(mergedRawOptions);

setupGlobalConfig(opts);

// Route to appropriate command
if (opts.compare) {
await runCompareMode(opts);
} else {
await runScanMode(opts);
}
}
150 changes: 85 additions & 65 deletions src/commands/scanUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
ScanUsageOptions,
EnvUsage,
ScanResult,
T3EnvWarning,
} from '../config/types.js';
import { determineComparisonFile } from '../core/determineComparisonFile.js';
import { outputToConsole } from '../services/scanOutputToConsole.js';
Expand All @@ -13,71 +14,8 @@ import { printComparisonError } from '../ui/scan/printComparisonError.js';
import { hasIgnoreComment } from '../core/secretDetectors.js';
import { frameworkValidator } from '../core/frameworkValidator.js';
import { detectSecretsInExample } from '../core/exampleSecretDetector.js';

/**
* Filters out commented usages from the list.
* Skipping comments:
* // process.env.API_URL
* # process.env.API_URL
* /* process.env.API_URL
* * process.env.API_URL
* <!-- process.env.API_URL -->
* @param usages - List of environment variable usages
* @returns Filtered list of environment variable usages
*/
function skipCommentedUsages(usages: EnvUsage[]): EnvUsage[] {
let insideHtmlComment = false;
let insideIgnoreBlock = false;

return usages.filter((u) => {
if (!u.context) return true;
const line = u.context.trim();

if (line.includes('<!--')) insideHtmlComment = true;
if (line.includes('-->')) {
insideHtmlComment = false;
return false;
}

if (/<!--\s*dotenv[\s-]*diff[\s-]*ignore[\s-]*start\s*-->/i.test(line)) {
insideIgnoreBlock = true;
return false;
}

if (/<!--\s*dotenv[\s-]*diff[\s-]*ignore[\s-]*end\s*-->/i.test(line)) {
insideIgnoreBlock = false;
return false;
}

if (insideIgnoreBlock) return false;

return (
!insideHtmlComment &&
!/^\s*(\/\/|#|\/\*|\*|<!--|-->)/.test(line) &&
!hasIgnoreComment(line)
);
});
}

/**
* Recalculates statistics for a scan result after filtering usages.
* @param scanResult The current scan result
* @returns Updated scanResult with recalculated stats
*/
function calculateStats(scanResult: ScanResult): ScanResult {
const uniqueVariables = new Set(
scanResult.used.map((u: EnvUsage) => u.variable),
).size;

scanResult.stats = {
filesScanned: scanResult.stats.filesScanned,
totalUsages: scanResult.used.length,
uniqueVariables,
duration: scanResult.stats.duration,
};

return scanResult;
}
import { detectT3Env } from '../core/t3env/detectT3Env.js';
import { applyT3EnvRules } from '../core/t3env/t3EnvRules.js';

/**
* Scans the codebase for environment variable usage and compares it with
Expand Down Expand Up @@ -125,6 +63,22 @@ export async function scanUsage(
scanResult.frameworkWarnings = frameworkWarnings;
}

// T3-env validation if t3env option is enabled or auto-detected
if (opts.t3env) {
const t3Detection = await detectT3Env(opts.cwd);
if (t3Detection.detected && t3Detection.schema) {
const t3EnvWarnings: T3EnvWarning[] = [];

for (const usage of scanResult.used) {
applyT3EnvRules(usage, t3EnvWarnings, t3Detection.schema);
}

if (t3EnvWarnings.length > 0) {
scanResult.t3EnvWarnings = t3EnvWarnings;
}
}
}

// Determine which file to compare against
const compareFile = determineComparisonFile(opts);
let envVariables: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -208,6 +162,7 @@ export async function scanUsage(
(scanResult.secrets?.length ?? 0) > 0)) ||
(scanResult.exampleWarnings?.length ?? 0) > 0 ||
(scanResult.frameworkWarnings?.length ?? 0) > 0 ||
(scanResult.t3EnvWarnings?.length ?? 0) > 0 ||
(scanResult.logged?.length ?? 0) > 0 ||
(scanResult.uppercaseWarnings?.length ?? 0) > 0 ||
(scanResult.expireWarnings?.length ?? 0) > 0 ||
Expand All @@ -226,3 +181,68 @@ export async function scanUsage(

return { exitWithError: result.exitWithError || duplicatesFound };
}

/**
* Filters out commented usages from the list.
* Skipping comments:
* // process.env.API_URL
* # process.env.API_URL
* /* process.env.API_URL
* * process.env.API_URL
* <!-- process.env.API_URL -->
* @param usages - List of environment variable usages
* @returns Filtered list of environment variable usages
*/
function skipCommentedUsages(usages: EnvUsage[]): EnvUsage[] {
let insideHtmlComment = false;
let insideIgnoreBlock = false;

return usages.filter((u) => {
if (!u.context) return true;
const line = u.context.trim();

if (line.includes('<!--')) insideHtmlComment = true;
if (line.includes('-->')) {
insideHtmlComment = false;
return false;
}

if (/<!--\s*dotenv[\s-]*diff[\s-]*ignore[\s-]*start\s*-->/i.test(line)) {
insideIgnoreBlock = true;
return false;
}

if (/<!--\s*dotenv[\s-]*diff[\s-]*ignore[\s-]*end\s*-->/i.test(line)) {
insideIgnoreBlock = false;
return false;
}

if (insideIgnoreBlock) return false;

return (
!insideHtmlComment &&
!/^\s*(\/\/|#|\/\*|\*|<!--|-->)/.test(line) &&
!hasIgnoreComment(line)
);
});
}

/**
* Recalculates statistics for a scan result after filtering usages.
* @param scanResult The current scan result
* @returns Updated scanResult with recalculated stats
*/
function calculateStats(scanResult: ScanResult): ScanResult {
const uniqueVariables = new Set(
scanResult.used.map((u: EnvUsage) => u.variable),
).size;

scanResult.stats = {
filesScanned: scanResult.stats.filesScanned,
totalUsages: scanResult.used.length,
uniqueVariables,
duration: scanResult.stats.duration,
};

return scanResult;
}
5 changes: 2 additions & 3 deletions src/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,11 @@ export function normalizeOptions(raw: RawOptions): Options {
const excludeFiles = parseList(raw.excludeFiles);
const files = parseList(raw.files);

const noCompare = toBool(raw.noCompare);

const ignoreUrls = parseList(raw.ignoreUrls);
const uppercaseKeys = raw.uppercaseKeys !== false;
const expireWarnings = raw.expireWarnings !== false;
const inconsistentNamingWarnings = raw.inconsistentNamingWarnings !== false;
const t3env = raw.t3env !== false;

const cwd = process.cwd();
const envFlag =
Expand Down Expand Up @@ -139,9 +138,9 @@ export function normalizeOptions(raw: RawOptions): Options {
secrets,
strict,
ignoreUrls,
noCompare,
uppercaseKeys,
expireWarnings,
inconsistentNamingWarnings,
t3env,
};
}
Loading
Loading