envdoc-scan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/envdoc.js +80 -0
- package/lib/dotenv.js +53 -0
- package/lib/format.js +90 -0
- package/lib/scan.js +147 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 takeaseatventure
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# envdoc-scan
|
|
2
|
+
|
|
3
|
+
**Scan your codebase for environment-variable references and generate a `.env.example` template or a documentation table. Never let your `.env.example` drift again.**
|
|
4
|
+
|
|
5
|
+
[](#)
|
|
6
|
+
[](https://npmjs.com/package/envdoc-scan)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Every project has a `.env.example` that's supposed to list all the environment variables the app needs. In practice, it's always out of date — someone adds `process.env.NEW_THING` to the code and forgets to update the template. New contributors hit cryptic errors because they're missing a var.
|
|
13
|
+
|
|
14
|
+
`envdoc` scans your source files for every environment-variable reference and tells you exactly:
|
|
15
|
+
|
|
16
|
+
- **Which vars your code actually uses** (and where)
|
|
17
|
+
- **Which vars are in your `.env.example` but no longer referenced** (dead config)
|
|
18
|
+
- **Which vars are used in code but missing from `.env.example`** (undocumented)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g envdoc-scan
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or use it directly with `npx`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx envdoc-scan
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Print a .env.example template (default)
|
|
36
|
+
envdoc-scan
|
|
37
|
+
|
|
38
|
+
# Print a markdown documentation table
|
|
39
|
+
envdoc-scan --markdown
|
|
40
|
+
|
|
41
|
+
# Merge with your existing .env.example (preserves comments + defaults)
|
|
42
|
+
envdoc-scan --existing .env.example
|
|
43
|
+
|
|
44
|
+
# Flag vars in .env.example that are no longer in source
|
|
45
|
+
envdoc-scan --existing .env.example --mark-unused
|
|
46
|
+
|
|
47
|
+
# Output JSON (for CI checks or tooling)
|
|
48
|
+
envdoc-scan src/ --json
|
|
49
|
+
|
|
50
|
+
# Scan a specific directory
|
|
51
|
+
envdoc-scan src/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Output formats
|
|
55
|
+
|
|
56
|
+
**`.env.example` (default):**
|
|
57
|
+
```bash
|
|
58
|
+
# Database connection string
|
|
59
|
+
DATABASE_URL=postgres://localhost/mydb
|
|
60
|
+
|
|
61
|
+
# Auth
|
|
62
|
+
# (unused — not found in source)
|
|
63
|
+
SECRET_KEY=changeme
|
|
64
|
+
|
|
65
|
+
API_KEY=
|
|
66
|
+
|
|
67
|
+
REDIS_URL=
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Markdown table:**
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Used | Location |
|
|
73
|
+
|----------|---------|------|----------|
|
|
74
|
+
| `DATABASE_URL` | postgres://localhost/mydb | yes | `sample.js:3` (+1) |
|
|
75
|
+
| `SECRET_KEY` | changeme | no | — |
|
|
76
|
+
| `API_KEY` | — | yes | `sample.js:2` |
|
|
77
|
+
|
|
78
|
+
**JSON:**
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"DATABASE_URL": [{ "file": "config.js", "line": 12, "col": 25 }]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Supported patterns
|
|
86
|
+
|
|
87
|
+
`envdoc` detects environment-variable access across multiple languages:
|
|
88
|
+
|
|
89
|
+
| Language | Pattern |
|
|
90
|
+
|----------|---------|
|
|
91
|
+
| **Node / Bun** | `process.env.NAME`, `process.env['NAME']` |
|
|
92
|
+
| **Python** | `os.environ.get('NAME')`, `os.getenv('NAME')`, `os.environ['NAME']` |
|
|
93
|
+
| **Java** | `System.getenv("NAME")` |
|
|
94
|
+
| **Ruby** | `ENV['NAME']` |
|
|
95
|
+
| **Rust** | `env::var("NAME")` |
|
|
96
|
+
| **Go** | `os.LookupEnv("NAME")` |
|
|
97
|
+
|
|
98
|
+
## What it scans
|
|
99
|
+
|
|
100
|
+
By default, `envdoc` scans these file types:
|
|
101
|
+
|
|
102
|
+
`.js`, `.mjs`, `.cjs`, `.ts`, `.jsx`, `.tsx`, `.py`, `.java`, `.rb`, `.rs`, `.go`, `.sh`, `.bash`, `.yml`, `.yaml`
|
|
103
|
+
|
|
104
|
+
It automatically skips: `node_modules`, `.git`, `vendor`, `dist`, `build`, `.next`, `target`, `__pycache__`, `.venv`, `venv`, `coverage`, `.cache`, and hidden directories.
|
|
105
|
+
|
|
106
|
+
## CI integration
|
|
107
|
+
|
|
108
|
+
Use `--json` to fail CI if your `.env.example` is missing vars:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# In your CI pipeline:
|
|
112
|
+
envdoc --json > /tmp/scanned.json
|
|
113
|
+
# ... diff against your committed .env.example
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT © takeaseatventure
|
package/bin/envdoc.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { scanDirectory } = require('../lib/scan');
|
|
5
|
+
const { readFile } = require('../lib/dotenv');
|
|
6
|
+
const { merge, formatEnvExample, formatMarkdown } = require('../lib/format');
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = {
|
|
10
|
+
_: [],
|
|
11
|
+
format: 'env', // 'env' | 'markdown' | 'json'
|
|
12
|
+
root: '.',
|
|
13
|
+
existing: null, // path to existing .env.example to merge
|
|
14
|
+
markUnused: false,
|
|
15
|
+
help: false,
|
|
16
|
+
};
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === '-h' || a === '--help') args.help = true;
|
|
20
|
+
else if (a === '--markdown' || a === '-m') args.format = 'markdown';
|
|
21
|
+
else if (a === '--json') args.format = 'json';
|
|
22
|
+
else if (a === '--env' || a === '-e') args.format = 'env';
|
|
23
|
+
else if (a === '--mark-unused') args.markUnused = true;
|
|
24
|
+
else if (a === '--existing') args.existing = argv[++i];
|
|
25
|
+
else if (a.startsWith('--root')) args.root = argv[++i];
|
|
26
|
+
else if (a.startsWith('-')) { /* ignore unknown */ }
|
|
27
|
+
else args._.push(a);
|
|
28
|
+
}
|
|
29
|
+
if (args._.length) args.root = args._[0];
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HELP = `envdoc — scan your codebase for environment-variable references
|
|
34
|
+
and generate a .env.example template or a documentation table.
|
|
35
|
+
|
|
36
|
+
USAGE
|
|
37
|
+
envdoc [options] [<path>]
|
|
38
|
+
|
|
39
|
+
OPTIONS
|
|
40
|
+
-e, --env Output a .env.example template (default)
|
|
41
|
+
-m, --markdown Output a markdown documentation table
|
|
42
|
+
--json Output JSON (name -> locations[])
|
|
43
|
+
--existing <file> Merge with an existing .env / .env.example
|
|
44
|
+
(preserves comments and known default values)
|
|
45
|
+
--mark-unused Flag vars in the existing file that are no longer
|
|
46
|
+
referenced in source
|
|
47
|
+
<path> Root directory to scan (default: current dir)
|
|
48
|
+
-h, --help Show this help
|
|
49
|
+
|
|
50
|
+
EXAMPLES
|
|
51
|
+
envdoc # scan . and print a .env.example
|
|
52
|
+
envdoc --markdown # print a markdown doc table
|
|
53
|
+
envdoc --existing .env.example # merge with your current template
|
|
54
|
+
envdoc src/ --json # scan only src/, emit JSON
|
|
55
|
+
|
|
56
|
+
Never let your .env.example drift again.`;
|
|
57
|
+
|
|
58
|
+
function main() {
|
|
59
|
+
const args = parseArgs(process.argv.slice(2));
|
|
60
|
+
if (args.help) { process.stdout.write(HELP + '\n'); return 0; }
|
|
61
|
+
|
|
62
|
+
const scanned = scanDirectory(args.root);
|
|
63
|
+
const existing = args.existing ? readFile(args.existing) : [];
|
|
64
|
+
const entries = merge(scanned, existing);
|
|
65
|
+
|
|
66
|
+
if (args.format === 'json') {
|
|
67
|
+
const obj = {};
|
|
68
|
+
for (const e of entries) obj[e.key] = e.locations || [];
|
|
69
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (args.format === 'markdown') {
|
|
73
|
+
process.stdout.write(formatMarkdown(entries) + '\n');
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(formatEnvExample(entries, { markUnused: args.markUnused }));
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.exit(main());
|
package/lib/dotenv.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// dotenv.js — minimal .env / .env.example parser.
|
|
3
|
+
// Reads KEY=VALUE lines and any leading comments so we can preserve them
|
|
4
|
+
// when regenerating a template.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a .env-format file into an ordered list of entries.
|
|
10
|
+
* @param {string} content
|
|
11
|
+
* @returns {Array<{key:string, value:string, comment:string, present:boolean}>}
|
|
12
|
+
*/
|
|
13
|
+
function parse(content) {
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
const out = [];
|
|
16
|
+
let pendingComment = '';
|
|
17
|
+
for (const raw of lines) {
|
|
18
|
+
const line = raw.trim();
|
|
19
|
+
if (!line) { pendingComment = ''; continue; }
|
|
20
|
+
if (line.startsWith('#')) {
|
|
21
|
+
// Accumulate comment lines; attach to the next key.
|
|
22
|
+
const text = line.replace(/^#\s?/, '');
|
|
23
|
+
pendingComment = pendingComment ? pendingComment + ' ' + text : text;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const eq = line.indexOf('=');
|
|
27
|
+
if (eq === -1) { pendingComment = ''; continue; }
|
|
28
|
+
const key = line.slice(0, eq).trim();
|
|
29
|
+
let value = line.slice(eq + 1).trim();
|
|
30
|
+
// Strip surrounding quotes.
|
|
31
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
32
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
33
|
+
value = value.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
if (!/^[A-Z0-9_]+$/i.test(key)) { pendingComment = ''; continue; }
|
|
36
|
+
out.push({ key, value, comment: pendingComment, present: true });
|
|
37
|
+
pendingComment = '';
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read a .env file from disk, returning entries (or [] if missing).
|
|
44
|
+
*/
|
|
45
|
+
function readFile(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
return parse(fs.readFileSync(filePath, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { parse, readFile };
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// format.js — turn a scan result into a .env.example or a markdown table.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merge scanned vars with an existing .env.example's entries so comments
|
|
6
|
+
* and known defaults are preserved.
|
|
7
|
+
*
|
|
8
|
+
* @param {Map<string, Array>} scanned - name -> locations[]
|
|
9
|
+
* @param {Array} existing - [{key, value, comment, present}]
|
|
10
|
+
* @returns {Array} merged entries (ordered: existing first, then new scanned)
|
|
11
|
+
*/
|
|
12
|
+
function merge(scanned, existing) {
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const merged = [];
|
|
15
|
+
// Existing entries first (preserve order + comments).
|
|
16
|
+
for (const e of existing) {
|
|
17
|
+
const locs = scanned.get(e.key) || [];
|
|
18
|
+
merged.push({
|
|
19
|
+
key: e.key,
|
|
20
|
+
value: e.value,
|
|
21
|
+
comment: e.comment,
|
|
22
|
+
used: locs.length > 0,
|
|
23
|
+
locations: locs,
|
|
24
|
+
});
|
|
25
|
+
seen.add(e.key);
|
|
26
|
+
}
|
|
27
|
+
// Then newly-discovered vars not in the existing file.
|
|
28
|
+
const sorted = Array.from(scanned.keys()).sort();
|
|
29
|
+
for (const key of sorted) {
|
|
30
|
+
if (seen.has(key)) continue;
|
|
31
|
+
merged.push({
|
|
32
|
+
key,
|
|
33
|
+
value: '',
|
|
34
|
+
comment: '',
|
|
35
|
+
used: true,
|
|
36
|
+
locations: scanned.get(key),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render merged entries as a .env.example file.
|
|
44
|
+
* @param {Array} entries
|
|
45
|
+
* @param {object} opts - { markUnused:true }
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function formatEnvExample(entries, opts) {
|
|
49
|
+
opts = opts || {};
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
if (e.comment) {
|
|
53
|
+
lines.push('# ' + e.comment);
|
|
54
|
+
}
|
|
55
|
+
if (opts.markUnused && e.used === false) {
|
|
56
|
+
lines.push('# (unused — not found in source)');
|
|
57
|
+
}
|
|
58
|
+
const value = e.value !== '' ? e.value : '';
|
|
59
|
+
lines.push(`${e.key}=${value}`);
|
|
60
|
+
lines.push('');
|
|
61
|
+
}
|
|
62
|
+
return lines.join('\n').replace(/\n+$/, '\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render merged entries as a markdown documentation table.
|
|
67
|
+
* @param {Array} entries
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function formatMarkdown(entries) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
lines.push('# Environment Variables');
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push('| Variable | Default | Used | Location |');
|
|
75
|
+
lines.push('|----------|---------|------|----------|');
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
const def = e.value || '—';
|
|
78
|
+
const used = e.used === false ? 'no' : 'yes';
|
|
79
|
+
const loc = e.locations && e.locations.length
|
|
80
|
+
? '`' + e.locations[0].file + ':' + e.locations[0].line + '`' +
|
|
81
|
+
(e.locations.length > 1 ? ` (+${e.locations.length - 1})` : '')
|
|
82
|
+
: '—';
|
|
83
|
+
const row = `| \`${e.key}\` | ${def} | ${used} | ${loc} |`;
|
|
84
|
+
lines.push(row);
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { merge, formatEnvExample, formatMarkdown };
|
package/lib/scan.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scan.js — find environment-variable references in source files.
|
|
3
|
+
//
|
|
4
|
+
// Supports common patterns across languages:
|
|
5
|
+
// process.env.NAME (Node/Bun)
|
|
6
|
+
// process.env['NAME'] (Node, bracket form)
|
|
7
|
+
// os.environ['NAME'] (Python)
|
|
8
|
+
// os.environ.get('NAME') (Python)
|
|
9
|
+
// os.getenv('NAME') (Python)
|
|
10
|
+
// System.getenv("NAME") (Java)
|
|
11
|
+
// ENV['NAME'] (Ruby)
|
|
12
|
+
// env::var("NAME") (Rust)
|
|
13
|
+
// os.LookupEnv("NAME") (Go)
|
|
14
|
+
//
|
|
15
|
+
// Also reads an existing .env / .env.example to carry over comments and
|
|
16
|
+
// mark which vars are already documented.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
// (accessor prefix) (optional quote) NAME (closing bracket or word boundary)
|
|
22
|
+
const PATTERNS = [
|
|
23
|
+
// process.env.NAME and process.env['NAME']
|
|
24
|
+
/process\.env(?:\[['"]([A-Z0-9_]{2,})['"]\]|\s*\.\s*([A-Z0-9_]{2,}))/g,
|
|
25
|
+
// os.environ['NAME'] / os.environ.get('NAME') / os.getenv('NAME')
|
|
26
|
+
/os\.environ(?:\.get)?\(\s*['"]([A-Z0-9_]{2,})['"]\s*\)|os\.environ\[['"]([A-Z0-9_]{2,})['"]\]/g,
|
|
27
|
+
/os\.getenv\(\s*['"]([A-Z0-9_]{2,})['"]\s*\)/g,
|
|
28
|
+
// System.getenv("NAME")
|
|
29
|
+
/System\.getenv\(\s*"([A-Z0-9_]{2,})"\s*\)/g,
|
|
30
|
+
// ENV['NAME'] (Ruby)
|
|
31
|
+
/\bENV\[['"]([A-Z0-9_]{2,})['"]\]/g,
|
|
32
|
+
// env::var("NAME") (Rust)
|
|
33
|
+
/env::var\(\s*"([A-Z0-9_]{2,})"\s*\)/g,
|
|
34
|
+
// os.LookupEnv("NAME") (Go)
|
|
35
|
+
/os\.LookupEnv\(\s*"([A-Z0-9_]{2,})"\s*\)/g,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// File extensions we scan, by default.
|
|
39
|
+
const DEFAULT_EXTENSIONS = new Set([
|
|
40
|
+
'.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx',
|
|
41
|
+
'.py',
|
|
42
|
+
'.java',
|
|
43
|
+
'.rb',
|
|
44
|
+
'.rs',
|
|
45
|
+
'.go',
|
|
46
|
+
'.sh', '.bash',
|
|
47
|
+
'.yml', '.yaml',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// Directories we never descend into.
|
|
51
|
+
const IGNORE_DIRS = new Set([
|
|
52
|
+
'node_modules', '.git', 'vendor', 'dist', 'build', '.next', '.nuxt',
|
|
53
|
+
'target', '__pycache__', '.venv', 'venv', 'env', '.env',
|
|
54
|
+
'coverage', '.cache', '.turbo', '.svelte-kit',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Walk a directory tree, returning source file paths to scan.
|
|
59
|
+
* @param {string} root
|
|
60
|
+
* @param {object} opts - { extensions?: Set<string> }
|
|
61
|
+
* @returns {string[]}
|
|
62
|
+
*/
|
|
63
|
+
function listSourceFiles(root, opts) {
|
|
64
|
+
opts = opts || {};
|
|
65
|
+
const exts = opts.extensions || DEFAULT_EXTENSIONS;
|
|
66
|
+
const out = [];
|
|
67
|
+
if (!fs.existsSync(root)) return out;
|
|
68
|
+
const stat = fs.statSync(root);
|
|
69
|
+
if (stat.isFile()) return [root];
|
|
70
|
+
_walk(root, exts, out);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _walk(dir, exts, out) {
|
|
75
|
+
let entries;
|
|
76
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
77
|
+
catch { return; }
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
if (e.isDirectory()) {
|
|
80
|
+
if (IGNORE_DIRS.has(e.name)) continue;
|
|
81
|
+
// Skip hidden directories.
|
|
82
|
+
if (e.name.startsWith('.')) continue;
|
|
83
|
+
_walk(path.join(dir, e.name), exts, out);
|
|
84
|
+
} else if (e.isFile()) {
|
|
85
|
+
if (exts.has(path.extname(e.name).toLowerCase())) {
|
|
86
|
+
out.push(path.join(dir, e.name));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Scan a single file's content for env-var references.
|
|
94
|
+
* @param {string} content
|
|
95
|
+
* @returns {Array<{name:string, line:number, col:number}>}
|
|
96
|
+
*/
|
|
97
|
+
function scanContent(content) {
|
|
98
|
+
const hits = [];
|
|
99
|
+
const lines = content.split('\n');
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
for (const re of PATTERNS) {
|
|
103
|
+
re.lastIndex = 0;
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = re.exec(line)) !== null) {
|
|
106
|
+
// Pick the first defined capture group (patterns have alternations).
|
|
107
|
+
const name = m[1] || m[2] || m[3] || null;
|
|
108
|
+
if (name) {
|
|
109
|
+
hits.push({ name, line: i + 1, col: m.index + 1 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return hits;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scan an entire directory, returning a map of varName -> locations[].
|
|
119
|
+
* @param {string} root
|
|
120
|
+
* @param {object} opts
|
|
121
|
+
* @returns {Map<string, Array<{file:string, line:number, col:number}>>}
|
|
122
|
+
*/
|
|
123
|
+
function scanDirectory(root, opts) {
|
|
124
|
+
opts = opts || {};
|
|
125
|
+
const files = listSourceFiles(root, opts);
|
|
126
|
+
const result = new Map();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
let content;
|
|
129
|
+
try { content = fs.readFileSync(file, 'utf8'); }
|
|
130
|
+
catch { continue; }
|
|
131
|
+
const hits = scanContent(content);
|
|
132
|
+
for (const h of hits) {
|
|
133
|
+
if (!result.has(h.name)) result.set(h.name, []);
|
|
134
|
+
result.get(h.name).push({ file: path.relative(root, file), line: h.line, col: h.col });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
scanContent,
|
|
142
|
+
scanDirectory,
|
|
143
|
+
listSourceFiles,
|
|
144
|
+
PATTERNS,
|
|
145
|
+
DEFAULT_EXTENSIONS,
|
|
146
|
+
IGNORE_DIRS,
|
|
147
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "envdoc-scan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scan your codebase for environment-variable references and generate a .env.example template and/or a documentation table. Never let your .env.example drift again.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "takeaseatventure",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"env",
|
|
9
|
+
"environment",
|
|
10
|
+
"variables",
|
|
11
|
+
"dotenv",
|
|
12
|
+
"example",
|
|
13
|
+
"documentation",
|
|
14
|
+
"cli",
|
|
15
|
+
"scanner",
|
|
16
|
+
"audit",
|
|
17
|
+
"envdoc"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/takeaseatventure/envdoc-scan#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/takeaseatventure/envdoc-scan.git"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"envdoc-scan": "./bin/envdoc.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"lib/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "node --test test/*.test.js"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/takeaseatventure/envdoc-scan/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|