@cldmv/fix-headers 1.0.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.
Files changed (47) hide show
  1. package/README.md +157 -0
  2. package/index.cjs +39 -0
  3. package/index.mjs +7 -0
  4. package/package.json +56 -0
  5. package/src/cli.mjs +216 -0
  6. package/src/constants.mjs +15 -0
  7. package/src/core/file-discovery.mjs +133 -0
  8. package/src/core/fix-headers.mjs +210 -0
  9. package/src/detect/project.mjs +175 -0
  10. package/src/detectors/css.mjs +54 -0
  11. package/src/detectors/go.mjs +52 -0
  12. package/src/detectors/html.mjs +54 -0
  13. package/src/detectors/index.mjs +160 -0
  14. package/src/detectors/node.mjs +63 -0
  15. package/src/detectors/php.mjs +53 -0
  16. package/src/detectors/python.mjs +48 -0
  17. package/src/detectors/rust.mjs +49 -0
  18. package/src/detectors/shared.mjs +32 -0
  19. package/src/fix-header.mjs +30 -0
  20. package/src/header/parser.mjs +76 -0
  21. package/src/header/syntax.mjs +54 -0
  22. package/src/header/template.mjs +44 -0
  23. package/src/utils/fs.mjs +125 -0
  24. package/src/utils/git.mjs +97 -0
  25. package/src/utils/time.mjs +31 -0
  26. package/types/index.d.mts +2 -0
  27. package/types/src/cli.d.mts +44 -0
  28. package/types/src/constants.d.mts +7 -0
  29. package/types/src/core/file-discovery.d.mts +22 -0
  30. package/types/src/core/fix-headers.d.mts +86 -0
  31. package/types/src/detect/project.d.mts +81 -0
  32. package/types/src/detectors/css.d.mts +25 -0
  33. package/types/src/detectors/go.d.mts +25 -0
  34. package/types/src/detectors/html.d.mts +25 -0
  35. package/types/src/detectors/index.d.mts +66 -0
  36. package/types/src/detectors/node.d.mts +25 -0
  37. package/types/src/detectors/php.d.mts +25 -0
  38. package/types/src/detectors/python.d.mts +23 -0
  39. package/types/src/detectors/rust.d.mts +25 -0
  40. package/types/src/detectors/shared.d.mts +14 -0
  41. package/types/src/fix-header.d.mts +12 -0
  42. package/types/src/header/parser.d.mts +43 -0
  43. package/types/src/header/syntax.d.mts +44 -0
  44. package/types/src/header/template.d.mts +52 -0
  45. package/types/src/utils/fs.d.mts +50 -0
  46. package/types/src/utils/git.d.mts +40 -0
  47. package/types/src/utils/time.d.mts +19 -0
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # fix-headers
2
+
3
+ Multi-language source header normalizer for Node.js projects.
4
+
5
+ `fix-headers` scans project files, auto-detects project metadata (language, root, project name, git author/email), and inserts or updates standard file headers.
6
+
7
+ [![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] <!-- [![GitHub release]][github_release_url] -->[![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] <!-- [![Release date]][release_date_url] -->[![npm last update]][npm_last_update_url] [![Coverage]][coverage_url]
8
+
9
+ [![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
10
+
11
+ ## Features
12
+
13
+ - Auto-detects project type by marker files (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `composer.json`)
14
+ - Auto-detects author and email from git config/commit history
15
+ - Supports per-run overrides for every detected value
16
+ - Supports folder inclusion and exclusion configuration
17
+ - Supports detector-based monorepo scanning with nearest config resolution per file
18
+ - Supports per-detector syntax overrides for line and block comment tokens
19
+ - Supports both ESM and CJS consumers
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm i fix-headers
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### ESM
30
+
31
+ ```js
32
+ import fixHeaders from "fix-headers";
33
+
34
+ const result = await fixHeaders({ dryRun: true });
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ After install, use the package binary:
40
+
41
+ ```bash
42
+ fix-headers --dry-run --include-folder src --exclude-folder dist
43
+ ```
44
+
45
+ Local development usage:
46
+
47
+ ```bash
48
+ npm run cli -- --dry-run --json
49
+ ```
50
+
51
+ Common CLI options:
52
+
53
+ - `--dry-run`
54
+ - `--json`
55
+ - `--cwd <path>`
56
+ - `--input <path>`
57
+ - `--include-folder <path>` (repeatable)
58
+ - `--exclude-folder <path>` (repeatable)
59
+ - `--include-extension <ext>` (repeatable)
60
+ - `--enable-detector <id>` / `--disable-detector <id>` (repeatable)
61
+ - `--project-name <name>`
62
+ - `--author-name <name>` / `--author-email <email>`
63
+ - `--company-name <name>`
64
+ - `--copyright-start-year <year>`
65
+ - `--config <json-file>`
66
+
67
+ ### CommonJS
68
+
69
+ ```js
70
+ const fixHeaders = require("fix-headers");
71
+
72
+ const result = await fixHeaders({ dryRun: true });
73
+ ```
74
+
75
+ ## API
76
+
77
+ ### `fixHeaders(options?)`
78
+
79
+ Runs header normalization. Project/language/author/email metadata is auto-detected internally on each run.
80
+
81
+ Important options:
82
+
83
+ - `cwd?: string` - start directory for project detection
84
+ - `input?: string` - explicit single file or folder path to process
85
+ - `dryRun?: boolean` - compute changes without writing files
86
+ - `configFile?: string` - load JSON options from file (resolved from `cwd`)
87
+ - `includeExtensions?: string[]` - file extensions to process
88
+ - `enabledDetectors?: string[]` - detector ids to enable (defaults to all)
89
+ - `disabledDetectors?: string[]` - detector ids to disable
90
+ - `detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }>` - override detector comment syntax tokens
91
+ - `includeFolders?: string[]` - project-relative folders to scan
92
+ - `excludeFolders?: string[]` - folder names or relative paths to exclude
93
+ - `projectName?: string`
94
+ - `language?: string`
95
+ - `projectRoot?: string`
96
+ - `marker?: string | null`
97
+ - `authorName?: string`
98
+ - `authorEmail?: string`
99
+ - `companyName?: string` (default: `Catalyzed Motivation Inc.`)
100
+ - `copyrightStartYear?: number` (default: current year)
101
+
102
+ Example:
103
+
104
+ ```js
105
+ const result = await fixHeaders({
106
+ cwd: process.cwd(),
107
+ dryRun: false,
108
+ configFile: "fix-headers.config.json",
109
+ includeFolders: ["src", "scripts"],
110
+ excludeFolders: ["src/generated", "dist"],
111
+ detectorSyntaxOverrides: {
112
+ node: {
113
+ blockStart: "/*",
114
+ blockLinePrefix: " * ",
115
+ blockEnd: " */"
116
+ },
117
+ python: {
118
+ linePrefix: ";;"
119
+ }
120
+ },
121
+ projectName: "@scope/my-package",
122
+ companyName: "Catalyzed Motivation Inc.",
123
+ copyrightStartYear: 2013
124
+ });
125
+ ```
126
+
127
+ ## Notes
128
+
129
+ - `excludeFolders` supports both folder-name and nested path matching.
130
+ - For monorepos, each file resolves metadata from the closest detector config in its parent tree.
131
+
132
+ ## License
133
+
134
+ Apache-2.0
135
+
136
+ <!-- Badge definitions -->
137
+ <!-- [github release]: https://img.shields.io/github/v/release/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
138
+ <!-- [github_release_url]: https://github.com/CLDMV/fix-headers/releases -->
139
+ <!-- [release date]: https://img.shields.io/github/release-date/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
140
+ <!-- [release_date_url]: https://github.com/CLDMV/fix-headers/releases -->
141
+
142
+ [npm version]: https://img.shields.io/npm/v/%40cldmv%2Ffix-headers.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
143
+ [npm_version_url]: https://www.npmjs.com/package/@cldmv/fix-headers
144
+ [npm downloads]: https://img.shields.io/npm/dm/%40cldmv%2Ffix-headers.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
145
+ [npm_downloads_url]: https://www.npmjs.com/package/@cldmv/fix-headers
146
+ [github downloads]: https://img.shields.io/github/downloads/CLDMV/fix-headers/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
147
+ [github_downloads_url]: https://github.com/CLDMV/fix-headers/releases
148
+ [last commit]: https://img.shields.io/github/last-commit/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
149
+ [last_commit_url]: https://github.com/CLDMV/fix-headers/commits
150
+ [npm last update]: https://img.shields.io/npm/last-update/%40cldmv%2Ffix-headers?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
151
+ [npm_last_update_url]: https://www.npmjs.com/package/@cldmv/fix-headers
152
+ [coverage]: https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FCLDMV%2Ffix-headers%2Fbadges%2Fcoverage.json&style=for-the-badge&logo=vitest&logoColor=white
153
+ [coverage_url]: https://github.com/CLDMV/fix-headers/blob/badges/coverage.json
154
+ [contributors]: https://img.shields.io/github/contributors/CLDMV/fix-headers.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
155
+ [contributors_url]: https://github.com/CLDMV/fix-headers/graphs/contributors
156
+ [sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor
157
+ [sponsor_url]: https://github.com/sponsors/shinrai
package/index.cjs ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /index.cjs
4
+ * @Date: 2026-03-01 13:29:45 -08:00 (1772400585)
5
+ * @Author: Nate Hyson <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Hyson <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01 16:29:36 -08:00 (1772411376)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
14
+ /**
15
+ * @fileoverview CJS shim for consuming the ESM-based fix-headers module.
16
+ * @module fix-headers/cjs-shim
17
+ */
18
+
19
+ "use strict";
20
+
21
+ /**
22
+ * Loads the ESM module implementation.
23
+ * @returns {Promise<import("./index.mjs")>} Loaded ESM module.
24
+ */
25
+ function loadEsmModule() {
26
+ return import("./index.mjs");
27
+ }
28
+
29
+ /**
30
+ * Runs header normalization with auto-detection and override options.
31
+ * @param {Record<string, unknown>} [options] - Runtime options.
32
+ * @returns {Promise<unknown>} Header update report.
33
+ */
34
+ async function fixHeaders(options) {
35
+ const mod = await loadEsmModule();
36
+ return mod.default(options);
37
+ }
38
+
39
+ module.exports = fixHeaders;
package/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview ESM shim for the fix-headers package API.
3
+ * @module fix-headers/esm-shim
4
+ */
5
+
6
+ export * from "./src/fix-header.mjs";
7
+ export { default } from "./src/fix-header.mjs";
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@cldmv/fix-headers",
3
+ "version": "1.0.0",
4
+ "description": "Multi-language project header normalizer with auto-detection and override support.",
5
+ "type": "module",
6
+ "main": "./index.cjs",
7
+ "module": "./index.mjs",
8
+ "types": "./types/index.d.mts",
9
+ "files": [
10
+ "index.mjs",
11
+ "index.cjs",
12
+ "src/",
13
+ "types/"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./types/index.d.mts",
18
+ "import": "./index.mjs",
19
+ "require": "./index.cjs"
20
+ }
21
+ },
22
+ "bin": {
23
+ "fix-headers": "./src/cli.mjs"
24
+ },
25
+ "scripts": {
26
+ "test": "vitest run --config .configs/vitest.config.mjs",
27
+ "test:watch": "vitest --config .configs/vitest.config.mjs",
28
+ "ci:coverage": "vitest run --coverage --reporter=dot --maxWorkers=1 --config .configs/vitest.ci.config.mjs",
29
+ "types:build": "tsc -p .configs/tsconfig.json --noCheck",
30
+ "types:check": "tsc -p .configs/tsconfig.json --noEmit",
31
+ "test:coverage": "vitest run --config .configs/vitest.config.mjs --coverage",
32
+ "cli": "node ./src/cli.mjs"
33
+ },
34
+ "keywords": [
35
+ "headers",
36
+ "copyright",
37
+ "automation",
38
+ "node-module"
39
+ ],
40
+ "peerDependencies": {
41
+ "vitest": ">=1.0.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "license": "Apache-2.0",
50
+ "devDependencies": {
51
+ "@types/node": "^25.3.0",
52
+ "typescript": "^5.9.3",
53
+ "@vitest/coverage-v8": "^3.2.4",
54
+ "vitest": "^3.2.4"
55
+ }
56
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { isAbsolute, join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import fixHeaders from "./fix-header.mjs";
7
+
8
+ /**
9
+ * @fileoverview CLI entry point for running fix-headers from terminal commands.
10
+ * @module fix-headers/cli
11
+ */
12
+
13
+ const HELP_TEXT = `fix-headers CLI\n\nUsage:\n fix-headers [options]\n\nOptions:\n -h, --help Show help\n --dry-run Compute changes without writing files\n --json Print JSON output\n --cwd <path> Working directory for project detection\n --input <path> Single file or folder input\n --include-folder <path> Include folder (repeatable)\n --exclude-folder <path> Exclude folder name/path (repeatable)\n --include-extension <ext> Include extension (repeatable)\n --enable-detector <id> Enable only specific detector (repeatable)\n --disable-detector <id> Disable detector by id (repeatable)\n --project-name <name> Override project name\n --language <id> Override language id\n --project-root <path> Override project root\n --marker <name|null> Override marker filename\n --author-name <name> Override author name\n --author-email <email> Override author email\n --company-name <name> Override company name\n --copyright-start-year <year> Override copyright start year\n --config <path> Load JSON options file\n\nExamples:\n fix-headers --dry-run --include-folder src\n fix-headers --project-name @scope/pkg --company-name "Catalyzed Motivation Inc."\n`;
14
+
15
+ /**
16
+ * Converts CLI flag token to camelCase key.
17
+ * @param {string} token - CLI token without leading dashes.
18
+ * @returns {string} CamelCase key.
19
+ */
20
+ function toCamelCase(token) {
21
+ return token.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
22
+ }
23
+
24
+ /**
25
+ * Parses CLI arguments into fixHeaders options and control flags.
26
+ * @param {string[]} argv - Process argument vector without node/script items.
27
+ * @returns {{
28
+ * options: Record<string, unknown>,
29
+ * help: boolean,
30
+ * json: boolean
31
+ * }} Parsed CLI payload.
32
+ */
33
+ export function parseCliArgs(argv) {
34
+ /** @type {Record<string, unknown>} */
35
+ const options = {};
36
+ const control = { help: false, json: false };
37
+ const multiMap = {
38
+ "include-folder": "includeFolders",
39
+ "exclude-folder": "excludeFolders",
40
+ "include-extension": "includeExtensions",
41
+ "enable-detector": "enabledDetectors",
42
+ "disable-detector": "disabledDetectors"
43
+ };
44
+ const scalarMap = {
45
+ cwd: "cwd",
46
+ input: "input",
47
+ "project-name": "projectName",
48
+ language: "language",
49
+ "project-root": "projectRoot",
50
+ marker: "marker",
51
+ "author-name": "authorName",
52
+ "author-email": "authorEmail",
53
+ "company-name": "companyName",
54
+ "copyright-start-year": "copyrightStartYear",
55
+ config: "config"
56
+ };
57
+
58
+ for (let index = 0; index < argv.length; index += 1) {
59
+ const arg = argv[index];
60
+ if (arg === "-h" || arg === "--help") {
61
+ control.help = true;
62
+ continue;
63
+ }
64
+ if (arg === "--json") {
65
+ control.json = true;
66
+ continue;
67
+ }
68
+ if (arg === "--dry-run") {
69
+ options.dryRun = true;
70
+ continue;
71
+ }
72
+ if (!arg.startsWith("--")) {
73
+ throw new Error(`Unexpected argument: ${arg}`);
74
+ }
75
+
76
+ const flag = arg.slice(2);
77
+ if (multiMap[flag]) {
78
+ const value = argv[index + 1];
79
+ if (!value || value.startsWith("--")) {
80
+ throw new Error(`Missing value for --${flag}`);
81
+ }
82
+ index += 1;
83
+ const key = multiMap[flag];
84
+ const list = Array.isArray(options[key]) ? options[key] : [];
85
+ options[key] = [...list, value];
86
+ continue;
87
+ }
88
+
89
+ if (scalarMap[flag]) {
90
+ const value = argv[index + 1];
91
+ if (!value || value.startsWith("--")) {
92
+ throw new Error(`Missing value for --${flag}`);
93
+ }
94
+ index += 1;
95
+ const key = scalarMap[flag];
96
+ if (key === "copyrightStartYear") {
97
+ options[key] = Number.parseInt(value, 10);
98
+ if (Number.isNaN(options[key])) {
99
+ throw new Error(`Invalid number for --${flag}: ${value}`);
100
+ }
101
+ } else if (key === "marker" && value === "null") {
102
+ options[key] = null;
103
+ } else {
104
+ options[key] = value;
105
+ }
106
+ continue;
107
+ }
108
+
109
+ const camelKey = toCamelCase(flag);
110
+ const value = argv[index + 1];
111
+ if (!value || value.startsWith("--")) {
112
+ throw new Error(`Unknown or malformed flag: --${flag}`);
113
+ }
114
+ index += 1;
115
+ options[camelKey] = value;
116
+ }
117
+
118
+ return {
119
+ options,
120
+ help: control.help,
121
+ json: control.json
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Loads extra options from a JSON config file.
127
+ * @param {Record<string, unknown>} options - Current options object.
128
+ * @returns {Promise<Record<string, unknown>>} Merged options object.
129
+ */
130
+ export async function applyConfigFile(options) {
131
+ if (typeof options.config !== "string" || options.config.trim().length === 0) {
132
+ return options;
133
+ }
134
+
135
+ const baseDir = typeof options.cwd === "string" && options.cwd.length > 0 ? options.cwd : process.cwd();
136
+ const configPath = isAbsolute(options.config) ? options.config : join(baseDir, options.config);
137
+ const raw = await readFile(configPath, "utf8");
138
+ const parsed = JSON.parse(raw);
139
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
140
+ throw new Error("Config file must contain a JSON object");
141
+ }
142
+
143
+ const merged = { ...parsed, ...options };
144
+ delete merged.config;
145
+ /** @type {Record<string, unknown>} */
146
+ const output = merged;
147
+ return output;
148
+ }
149
+
150
+ /**
151
+ * Executes CLI flow and returns process-like exit code.
152
+ * @param {string[]} argv - CLI arguments.
153
+ * @param {{
154
+ * runner?: (options: Record<string, unknown>) => Promise<unknown>,
155
+ * stdout?: (message: string) => void,
156
+ * stderr?: (message: string) => void
157
+ * }} [deps={}] - Dependency overrides for tests.
158
+ * @returns {Promise<number>} Exit code.
159
+ */
160
+ export async function runCli(argv, deps = {}) {
161
+ const runner = deps.runner || fixHeaders;
162
+ const stdout = deps.stdout || console.log;
163
+ const stderr = deps.stderr || console.error;
164
+
165
+ try {
166
+ const parsed = parseCliArgs(argv);
167
+ if (parsed.help) {
168
+ stdout(HELP_TEXT);
169
+ return 0;
170
+ }
171
+
172
+ const finalOptions = await applyConfigFile(parsed.options);
173
+ const result = await runner(finalOptions);
174
+
175
+ if (parsed.json) {
176
+ stdout(JSON.stringify(result, null, 2));
177
+ return 0;
178
+ }
179
+
180
+ if (result && typeof result === "object") {
181
+ const report = /** @type {{filesScanned?: number, filesUpdated?: number, dryRun?: boolean}} */ (result);
182
+ stdout(
183
+ `fix-headers complete: scanned=${report.filesScanned ?? 0}, updated=${report.filesUpdated ?? 0}, dryRun=${report.dryRun === true}`
184
+ );
185
+ } else {
186
+ stdout("fix-headers complete");
187
+ }
188
+ return 0;
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ stderr(`fix-headers failed: ${message}`);
192
+ return 1;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Executes CLI flow when the module is the process entrypoint.
198
+ * @param {string[]} [argv=process.argv] - Process argument vector.
199
+ * @param {string} [moduleUrl=import.meta.url] - Current module URL.
200
+ * @param {(args: string[]) => Promise<number>} [executor=runCli] - CLI executor.
201
+ * @returns {boolean} Whether the entrypoint branch was executed.
202
+ */
203
+ export function runCliAsMain(argv = process.argv, moduleUrl = import.meta.url, executor = runCli) {
204
+ const isMain = argv[1] && moduleUrl === pathToFileURL(argv[1]).href;
205
+ if (!isMain) {
206
+ return false;
207
+ }
208
+
209
+ executor(argv.slice(2)).then((code) => {
210
+ process.exitCode = code;
211
+ });
212
+
213
+ return true;
214
+ }
215
+
216
+ runCliAsMain();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Shared constants for project/language detection and header defaults.
3
+ * @module fix-headers/constants
4
+ */
5
+
6
+ export { DETECTOR_PROFILES, getAllowedExtensions, getEnabledDetectors } from "./detectors/index.mjs";
7
+
8
+ /** @type {string} */
9
+ export const DEFAULT_COMPANY_NAME = "Catalyzed Motivation Inc.";
10
+
11
+ /** @type {number} */
12
+ export const DEFAULT_MAX_HEADER_SCAN_LINES = 40;
13
+
14
+ /** @type {Set<string>} */
15
+ export const DEFAULT_IGNORE_FOLDERS = new Set([".git", "node_modules", "dist", "build", "coverage", "tmp", ".next", ".turbo"]);
@@ -0,0 +1,133 @@
1
+ import { join, relative, resolve } from "node:path";
2
+ import { DEFAULT_IGNORE_FOLDERS } from "../constants.mjs";
3
+ import { getAllowedExtensions } from "../detectors/index.mjs";
4
+ import { walkFiles } from "../utils/fs.mjs";
5
+
6
+ /**
7
+ * @fileoverview Resolves candidate source files for header updates based on language and options.
8
+ * @module fix-headers/core/file-discovery
9
+ */
10
+
11
+ /**
12
+ * Resolves extension set from enabled detectors and optional override.
13
+ * @param {{
14
+ * includeExtensions?: string[],
15
+ * enabledDetectors?: string[],
16
+ * disabledDetectors?: string[]
17
+ * }} options - Extension options.
18
+ * @returns {Set<string>} Effective extension set.
19
+ */
20
+ function resolveExtensions(options) {
21
+ return getAllowedExtensions(options);
22
+ }
23
+
24
+ const DEFAULT_IGNORED_FOLDERS = new Set(DEFAULT_IGNORE_FOLDERS);
25
+
26
+ /**
27
+ * Normalizes a user-provided folder path to a project-relative value.
28
+ * @param {string} folderPath - Folder path string.
29
+ * @returns {string} Normalized path.
30
+ */
31
+ function normalizeFolderPath(folderPath) {
32
+ return folderPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
33
+ }
34
+
35
+ /**
36
+ * Determines include roots from explicit include folders.
37
+ * @param {string[] | undefined} includeFolders - Include folder option.
38
+ * @returns {string[]} Effective include roots.
39
+ */
40
+ function resolveIncludeFolders(includeFolders) {
41
+ if (Array.isArray(includeFolders) && includeFolders.length > 0) {
42
+ return includeFolders;
43
+ }
44
+
45
+ return [];
46
+ }
47
+
48
+ /**
49
+ * Builds a directory exclusion matcher from path and folder-name lists.
50
+ * @param {string} projectRoot - Project root path.
51
+ * @param {string[] | undefined} excludeFolders - Folder exclusions.
52
+ * @returns {(targetPath: string, targetName: string) => boolean} Exclusion predicate.
53
+ */
54
+ function buildExclusionMatcher(projectRoot, excludeFolders) {
55
+ if (!Array.isArray(excludeFolders) || excludeFolders.length === 0) {
56
+ return () => false;
57
+ }
58
+
59
+ const absoluteRoot = resolve(projectRoot);
60
+ const pathExclusions = [];
61
+ const nameExclusions = new Set();
62
+
63
+ for (const entry of excludeFolders) {
64
+ if (typeof entry !== "string") {
65
+ continue;
66
+ }
67
+
68
+ const normalized = normalizeFolderPath(entry);
69
+ if (normalized.length === 0) {
70
+ continue;
71
+ }
72
+
73
+ if (normalized.includes("/")) {
74
+ pathExclusions.push(resolve(absoluteRoot, normalized));
75
+ continue;
76
+ }
77
+
78
+ nameExclusions.add(normalized);
79
+ }
80
+
81
+ return (targetPath, targetName) => {
82
+ if (nameExclusions.has(targetName)) {
83
+ return true;
84
+ }
85
+
86
+ for (const excludedPath of pathExclusions) {
87
+ if (targetPath === excludedPath || targetPath.startsWith(`${excludedPath}/`)) {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ const relPath = normalizeFolderPath(relative(absoluteRoot, targetPath));
93
+ return pathExclusions.some((excludedPath) => {
94
+ const relExcluded = normalizeFolderPath(relative(absoluteRoot, excludedPath));
95
+ return relPath === relExcluded || relPath.startsWith(`${relExcluded}/`);
96
+ });
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Discovers source files for processing.
102
+ * @param {{
103
+ * projectRoot: string,
104
+ * language?: string,
105
+ * includeExtensions?: string[],
106
+ * enabledDetectors?: string[],
107
+ * disabledDetectors?: string[],
108
+ * includeFolders?: string[],
109
+ * excludeFolders?: string[]
110
+ * }} options - File discovery options.
111
+ * @returns {Promise<string[]>} Absolute file paths.
112
+ */
113
+ export async function discoverFiles(options) {
114
+ const allowedExtensions = resolveExtensions(options);
115
+ const includeFolders = resolveIncludeFolders(options.includeFolders);
116
+ const excludeFolders = Array.isArray(options.excludeFolders) ? options.excludeFolders : [];
117
+ const exclusionMatcher = buildExclusionMatcher(options.projectRoot, excludeFolders);
118
+
119
+ const roots =
120
+ includeFolders.length > 0 ? includeFolders.map((relativePath) => join(options.projectRoot, relativePath)) : [options.projectRoot];
121
+
122
+ const files = [];
123
+ for (const rootPath of roots) {
124
+ const discovered = await walkFiles(rootPath, {
125
+ allowedExtensions,
126
+ ignoreFolders: DEFAULT_IGNORED_FOLDERS,
127
+ shouldSkipDirectory: exclusionMatcher
128
+ });
129
+ files.push(...discovered);
130
+ }
131
+
132
+ return files.filter((filePath) => !exclusionMatcher(filePath, filePath.split("/").at(-1) || ""));
133
+ }