@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
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/index.mjs
4
+ * @Date: 2026-03-01 16:34:41 -08:00 (1772411681)
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 17:57:59 -08:00 (1772416679)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
14
+ import { readdir } from "node:fs/promises";
15
+ import { extname } from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+ import { dirname, join } from "node:path";
18
+
19
+ /**
20
+ * @fileoverview Detector registry and shared selector helpers.
21
+ * @module fix-headers/detectors
22
+ */
23
+
24
+ /**
25
+ * @typedef {{
26
+ * id: string,
27
+ * markers: string[],
28
+ * extensions: string[],
29
+ * enabledByDefault: boolean,
30
+ * findNearestConfig: (startPath: string) => Promise<{root: string, marker: string} | null>,
31
+ * parseProjectName: (marker: string, markerContent: string, rootDirName: string) => string,
32
+ * resolveCommentSyntax: (filePath: string) => ({kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string} | null),
33
+ * priority?: number
34
+ * }} DetectorProfile
35
+ */
36
+
37
+ /**
38
+ * Loads detector modules from the current directory.
39
+ * @returns {Promise<DetectorProfile[]>} Loaded detectors.
40
+ */
41
+ async function loadDetectorsFromDirectory() {
42
+ const directoryPath = dirname(fileURLToPath(import.meta.url));
43
+ const files = await readdir(directoryPath);
44
+ const detectorFiles = files
45
+ .filter((fileName) => fileName.endsWith(".mjs"))
46
+ .filter((fileName) => fileName !== "index.mjs" && fileName !== "shared.mjs")
47
+ .sort((left, right) => left.localeCompare(right));
48
+
49
+ const modules = await Promise.all(
50
+ detectorFiles.map((fileName) => {
51
+ const fileUrl = pathToFileURL(join(directoryPath, fileName)).href;
52
+ return import(fileUrl);
53
+ })
54
+ );
55
+
56
+ return modules.map((moduleExports) => moduleExports.detector).filter((entry) => entry && typeof entry.id === "string");
57
+ }
58
+
59
+ export const DETECTOR_PROFILES = await loadDetectorsFromDirectory();
60
+
61
+ /** @type {Map<string, typeof DETECTOR_PROFILES[number]>} */
62
+ const detectorMap = new Map(DETECTOR_PROFILES.map((detector) => [detector.id, detector]));
63
+
64
+ /**
65
+ * Applies runtime syntax overrides to a detector-provided syntax descriptor.
66
+ * @param {{kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} syntax - Base syntax descriptor.
67
+ * @param {{ linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string } | undefined} override - Override descriptor.
68
+ * @returns {{kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Effective descriptor.
69
+ */
70
+ function applySyntaxOverride(syntax, override) {
71
+ if (!override || typeof override !== "object") {
72
+ return syntax;
73
+ }
74
+
75
+ if (syntax.kind === "line") {
76
+ return {
77
+ ...syntax,
78
+ linePrefix: typeof override.linePrefix === "string" && override.linePrefix.length > 0 ? override.linePrefix : syntax.linePrefix
79
+ };
80
+ }
81
+
82
+ return {
83
+ ...syntax,
84
+ blockStart: typeof override.blockStart === "string" && override.blockStart.length > 0 ? override.blockStart : syntax.blockStart,
85
+ blockLinePrefix:
86
+ typeof override.blockLinePrefix === "string" && override.blockLinePrefix.length > 0
87
+ ? override.blockLinePrefix
88
+ : syntax.blockLinePrefix,
89
+ blockEnd: typeof override.blockEnd === "string" && override.blockEnd.length > 0 ? override.blockEnd : syntax.blockEnd
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Gets enabled detector profiles based on include/exclude options.
95
+ * @param {{ enabledDetectors?: string[], disabledDetectors?: string[] }} [options={}] - Runtime options.
96
+ * @returns {typeof DETECTOR_PROFILES} Enabled detector list.
97
+ */
98
+ export function getEnabledDetectors(options = {}) {
99
+ const explicitEnabled = new Set(Array.isArray(options.enabledDetectors) ? options.enabledDetectors : []);
100
+ const explicitDisabled = new Set(Array.isArray(options.disabledDetectors) ? options.disabledDetectors : []);
101
+
102
+ return DETECTOR_PROFILES.filter((detector) => {
103
+ if (explicitEnabled.size > 0) {
104
+ return explicitEnabled.has(detector.id);
105
+ }
106
+
107
+ if (explicitDisabled.has(detector.id)) {
108
+ return false;
109
+ }
110
+
111
+ return detector.enabledByDefault;
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Gets allowed file extensions for enabled detectors.
117
+ * @param {{ enabledDetectors?: string[], disabledDetectors?: string[], includeExtensions?: string[] }} [options={}] - Runtime options.
118
+ * @returns {Set<string>} Allowed extensions.
119
+ */
120
+ export function getAllowedExtensions(options = {}) {
121
+ if (Array.isArray(options.includeExtensions) && options.includeExtensions.length > 0) {
122
+ return new Set(options.includeExtensions.map((extension) => extension.toLowerCase()));
123
+ }
124
+
125
+ const detectors = getEnabledDetectors(options);
126
+ return new Set(detectors.flatMap((detector) => detector.extensions));
127
+ }
128
+
129
+ /**
130
+ * Gets a detector by id.
131
+ * @param {string} id - Detector id.
132
+ * @returns {typeof DETECTOR_PROFILES[number] | undefined} Detector.
133
+ */
134
+ export function getDetectorById(id) {
135
+ return detectorMap.get(id);
136
+ }
137
+
138
+ /**
139
+ * Resolves comment syntax for a file path using detector-specific templates.
140
+ * @param {string} filePath - File path.
141
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [options={}] - Runtime options.
142
+ * @returns {{kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Syntax descriptor.
143
+ */
144
+ export function getCommentSyntaxForFile(filePath, options = {}) {
145
+ const extension = extname(filePath).toLowerCase();
146
+ const detectors = getEnabledDetectors(options);
147
+ for (const detector of detectors) {
148
+ if (!detector.extensions.includes(extension)) {
149
+ continue;
150
+ }
151
+ const resolved = detector.resolveCommentSyntax(filePath);
152
+ if (resolved) {
153
+ const overrides =
154
+ options.detectorSyntaxOverrides && typeof options.detectorSyntaxOverrides === "object" ? options.detectorSyntaxOverrides : {};
155
+ return applySyntaxOverride(resolved, overrides[detector.id]);
156
+ }
157
+ }
158
+
159
+ return { kind: "block", blockStart: "/**", blockLinePrefix: " *\t", blockEnd: " */" };
160
+ }
@@ -0,0 +1,63 @@
1
+ import { extname } from "node:path";
2
+ import { findNearestMarker } from "./shared.mjs";
3
+
4
+ /**
5
+ * @fileoverview Node.js detector implementation.
6
+ * @module fix-headers/detectors/node
7
+ */
8
+
9
+ const markers = ["package.json"];
10
+ const extensions = [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".jsonv", ".jsonc", ".json5"];
11
+
12
+ /**
13
+ * Parses Node project name from package marker content.
14
+ * @param {string} markerContent - Marker file content.
15
+ * @param {string} rootDirName - Fallback root directory name.
16
+ * @returns {string} Project name.
17
+ */
18
+ function parseNodeProjectName(markerContent, rootDirName) {
19
+ try {
20
+ const parsed = JSON.parse(markerContent);
21
+ if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
22
+ return parsed.name.trim();
23
+ }
24
+ return rootDirName;
25
+ } catch {
26
+ return rootDirName;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Resolves comment syntax for Node-handled file extensions.
32
+ * @param {string} filePath - File path.
33
+ * @returns {{kind: "block", blockStart: string, blockLinePrefix: string, blockEnd: string} | null} Syntax descriptor.
34
+ */
35
+ function resolveNodeCommentSyntax(filePath) {
36
+ const extension = extname(filePath).toLowerCase();
37
+ if (extensions.includes(extension)) {
38
+ return {
39
+ kind: "block",
40
+ blockStart: "/**",
41
+ blockLinePrefix: " *\t",
42
+ blockEnd: " */"
43
+ };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export const detector = {
49
+ id: "node",
50
+ priority: 100,
51
+ markers,
52
+ extensions,
53
+ enabledByDefault: true,
54
+ findNearestConfig(startPath) {
55
+ return findNearestMarker(startPath, markers);
56
+ },
57
+ parseProjectName(_marker, markerContent, rootDirName) {
58
+ return parseNodeProjectName(markerContent, rootDirName);
59
+ },
60
+ resolveCommentSyntax(filePath) {
61
+ return resolveNodeCommentSyntax(filePath);
62
+ }
63
+ };
@@ -0,0 +1,53 @@
1
+ import { extname } from "node:path";
2
+ import { findNearestMarker } from "./shared.mjs";
3
+
4
+ /**
5
+ * @fileoverview PHP detector implementation.
6
+ * @module fix-headers/detectors/php
7
+ */
8
+
9
+ const markers = ["composer.json"];
10
+ const extensions = [".php"];
11
+
12
+ /**
13
+ * Resolves PHP comment syntax.
14
+ * @param {string} filePath - File path.
15
+ * @returns {{kind: "block", blockStart: string, blockLinePrefix: string, blockEnd: string} | null} Syntax descriptor.
16
+ */
17
+ function resolvePhpCommentSyntax(filePath) {
18
+ const extension = extname(filePath).toLowerCase();
19
+ if (extensions.includes(extension)) {
20
+ return {
21
+ kind: "block",
22
+ blockStart: "/**",
23
+ blockLinePrefix: " *\t",
24
+ blockEnd: " */"
25
+ };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ export const detector = {
31
+ id: "php",
32
+ priority: 80,
33
+ markers,
34
+ extensions,
35
+ enabledByDefault: true,
36
+ findNearestConfig(startPath) {
37
+ return findNearestMarker(startPath, markers);
38
+ },
39
+ parseProjectName(_marker, markerContent, rootDirName) {
40
+ try {
41
+ const parsed = JSON.parse(markerContent);
42
+ if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
43
+ return parsed.name.trim();
44
+ }
45
+ return rootDirName;
46
+ } catch {
47
+ return rootDirName;
48
+ }
49
+ },
50
+ resolveCommentSyntax(filePath) {
51
+ return resolvePhpCommentSyntax(filePath);
52
+ }
53
+ };
@@ -0,0 +1,48 @@
1
+ import { extname } from "node:path";
2
+ import { findNearestMarker } from "./shared.mjs";
3
+
4
+ /**
5
+ * @fileoverview Python detector implementation.
6
+ * @module fix-headers/detectors/python
7
+ */
8
+
9
+ const markers = ["pyproject.toml", "setup.py", "requirements.txt"];
10
+ const extensions = [".py"];
11
+
12
+ /**
13
+ * Parses Python project name from pyproject marker content when possible.
14
+ * @param {string} markerContent - Marker content.
15
+ * @param {string} rootDirName - Fallback root directory name.
16
+ * @returns {string} Project name.
17
+ */
18
+ function parsePythonProjectName(markerContent, rootDirName) {
19
+ const pyprojectMatch = markerContent.match(/^name\s*=\s*["']([^"']+)["']/m);
20
+ if (pyprojectMatch?.[1]) {
21
+ return pyprojectMatch[1];
22
+ }
23
+ return rootDirName;
24
+ }
25
+
26
+ export const detector = {
27
+ id: "python",
28
+ priority: 80,
29
+ markers,
30
+ extensions,
31
+ enabledByDefault: true,
32
+ findNearestConfig(startPath) {
33
+ return findNearestMarker(startPath, markers);
34
+ },
35
+ parseProjectName(_marker, markerContent, rootDirName) {
36
+ return parsePythonProjectName(markerContent, rootDirName);
37
+ },
38
+ resolveCommentSyntax(filePath) {
39
+ const extension = extname(filePath).toLowerCase();
40
+ if (extensions.includes(extension)) {
41
+ return {
42
+ kind: "line",
43
+ linePrefix: "#"
44
+ };
45
+ }
46
+ return null;
47
+ }
48
+ };
@@ -0,0 +1,49 @@
1
+ import { extname } from "node:path";
2
+ import { findNearestMarker } from "./shared.mjs";
3
+
4
+ /**
5
+ * @fileoverview Rust detector implementation.
6
+ * @module fix-headers/detectors/rust
7
+ */
8
+
9
+ const markers = ["Cargo.toml"];
10
+ const extensions = [".rs"];
11
+
12
+ /**
13
+ * Resolves Rust comment syntax.
14
+ * @param {string} filePath - File path.
15
+ * @returns {{kind: "block", blockStart: string, blockLinePrefix: string, blockEnd: string} | null} Syntax descriptor.
16
+ */
17
+ function resolveRustCommentSyntax(filePath) {
18
+ const extension = extname(filePath).toLowerCase();
19
+ if (extensions.includes(extension)) {
20
+ return {
21
+ kind: "block",
22
+ blockStart: "/**",
23
+ blockLinePrefix: " *\t",
24
+ blockEnd: " */"
25
+ };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ export const detector = {
31
+ id: "rust",
32
+ priority: 80,
33
+ markers,
34
+ extensions,
35
+ enabledByDefault: true,
36
+ findNearestConfig(startPath) {
37
+ return findNearestMarker(startPath, markers);
38
+ },
39
+ parseProjectName(_marker, markerContent, rootDirName) {
40
+ const cargoNameMatch = markerContent.match(/^name\s*=\s*["']([^"']+)["']/m);
41
+ if (cargoNameMatch?.[1]) {
42
+ return cargoNameMatch[1];
43
+ }
44
+ return rootDirName;
45
+ },
46
+ resolveCommentSyntax(filePath) {
47
+ return resolveRustCommentSyntax(filePath);
48
+ }
49
+ };
@@ -0,0 +1,32 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { pathExists } from "../utils/fs.mjs";
3
+
4
+ /**
5
+ * @fileoverview Shared detector helpers for nearest marker search.
6
+ * @module fix-headers/detectors/shared
7
+ */
8
+
9
+ /**
10
+ * Finds the closest marker file by walking up parent directories.
11
+ * @param {string} startPath - Starting directory or file path.
12
+ * @param {string[]} markers - Marker filenames to search.
13
+ * @returns {Promise<{root: string, marker: string} | null>} Closest located marker.
14
+ */
15
+ export async function findNearestMarker(startPath, markers) {
16
+ let currentDir = resolve(startPath);
17
+
18
+ while (true) {
19
+ for (const marker of markers) {
20
+ const markerPath = join(currentDir, marker);
21
+ if (await pathExists(markerPath)) {
22
+ return { root: currentDir, marker };
23
+ }
24
+ }
25
+
26
+ const parent = dirname(currentDir);
27
+ if (parent === currentDir) {
28
+ return null;
29
+ }
30
+ currentDir = parent;
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/fix-header.mjs
4
+ * @Date: 2026-03-01 13:34:00 -08:00 (1772400840)
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 14:39:14 -08:00 (1772404754)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
14
+ import { fixHeaders as fixHeadersCore } from "./core/fix-headers.mjs";
15
+
16
+ /**
17
+ * @fileoverview Public module API exposing a single function for header updates.
18
+ * @module fix-headers
19
+ */
20
+
21
+ /**
22
+ * Runs header fix-up using automatic metadata detection and optional overrides.
23
+ * @param {Parameters<typeof fixHeadersCore>[0]} [options] - Runtime options.
24
+ * @returns {ReturnType<typeof fixHeadersCore>} Update report.
25
+ */
26
+ export function fixHeaders(options) {
27
+ return fixHeadersCore(options);
28
+ }
29
+
30
+ export default fixHeaders;
@@ -0,0 +1,76 @@
1
+ import { DEFAULT_MAX_HEADER_SCAN_LINES } from "../constants.mjs";
2
+ import { getHeaderSyntaxForFile } from "./syntax.mjs";
3
+
4
+ /**
5
+ * @fileoverview Header parser utilities for extracting and replacing top-of-file headers.
6
+ * @module fix-headers/header/parser
7
+ */
8
+
9
+ /**
10
+ * Escapes special regex characters in literal text.
11
+ * @param {string} text - Literal text.
12
+ * @returns {string} Regex-safe text.
13
+ */
14
+ function escapeRegex(text) {
15
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16
+ }
17
+
18
+ /**
19
+ * Finds the first top-level project header block in a file.
20
+ * @param {string} content - File content.
21
+ * @param {string} [filePath=""] - File path used for syntax selection.
22
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax resolution options.
23
+ * @returns {{start: number, end: number} | null} Header location.
24
+ */
25
+ export function findProjectHeader(content, filePath = "", syntaxOptions = {}) {
26
+ const scanLines = content.split("\n").slice(0, DEFAULT_MAX_HEADER_SCAN_LINES).join("\n");
27
+ const syntax = getHeaderSyntaxForFile(filePath, syntaxOptions);
28
+
29
+ let match = null;
30
+ if (syntax.kind === "html") {
31
+ const blockStart = escapeRegex(syntax.blockStart || "<!--");
32
+ const blockEnd = escapeRegex(syntax.blockEnd || "-->");
33
+ match = scanLines.match(new RegExp(`^${blockStart}[\\s\\S]*?${blockEnd}\\n*`));
34
+ } else if (syntax.kind === "line") {
35
+ const linePrefix = escapeRegex(syntax.linePrefix || "#");
36
+ match = scanLines.match(new RegExp(`^(?:${linePrefix}.*\\n)+\\n*`));
37
+ } else {
38
+ const blockStart = escapeRegex(syntax.blockStart || "/**");
39
+ const blockEnd = escapeRegex(syntax.blockEnd || " */");
40
+ match = scanLines.match(new RegExp(`^${blockStart}[\\s\\S]*?${blockEnd}\\n*`));
41
+ }
42
+
43
+ if (!match) {
44
+ return null;
45
+ }
46
+
47
+ if (!match[0].includes("@Project:")) {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ start: 0,
53
+ end: match[0].length
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Replaces or inserts a project header block.
59
+ * @param {string} content - Original file content.
60
+ * @param {string} newHeader - Generated header text.
61
+ * @param {string} [filePath=""] - File path used for syntax selection.
62
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax resolution options.
63
+ * @returns {{nextContent: string, changed: boolean}} Updated content result.
64
+ */
65
+ export function replaceOrInsertHeader(content, newHeader, filePath = "", syntaxOptions = {}) {
66
+ const existing = findProjectHeader(content, filePath, syntaxOptions);
67
+ if (!existing) {
68
+ const nextContent = `${newHeader}\n\n${content.replace(/^\n+/, "")}`;
69
+ return { nextContent, changed: nextContent !== content };
70
+ }
71
+
72
+ const before = content.slice(0, existing.start);
73
+ const after = content.slice(existing.end).replace(/^\n+/, "");
74
+ const nextContent = `${before}${newHeader}\n\n${after}`;
75
+ return { nextContent, changed: nextContent !== content };
76
+ }
@@ -0,0 +1,54 @@
1
+ import { extname } from "node:path";
2
+ import { getCommentSyntaxForFile } from "../detectors/index.mjs";
3
+
4
+ /**
5
+ * @fileoverview Header comment syntax helpers for mapping file extensions to comment styles.
6
+ * @module fix-headers/header/syntax
7
+ */
8
+
9
+ /**
10
+ * @typedef {{
11
+ * kind: "block" | "line" | "html",
12
+ * linePrefix?: string,
13
+ * blockStart?: string,
14
+ * blockLinePrefix?: string,
15
+ * blockEnd?: string
16
+ * }} HeaderSyntax
17
+ */
18
+
19
+ /**
20
+ * Resolves comment syntax for a file path based on extension.
21
+ * @param {string} filePath - Absolute or relative file path.
22
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [options={}] - Syntax resolution options.
23
+ * @returns {HeaderSyntax} Header syntax descriptor.
24
+ */
25
+ export function getHeaderSyntaxForFile(filePath, options = {}) {
26
+ const extension = extname(filePath).toLowerCase();
27
+ if (extension.length === 0) {
28
+ return { kind: "block", blockStart: "/**", blockLinePrefix: " *\t", blockEnd: " */" };
29
+ }
30
+
31
+ return getCommentSyntaxForFile(filePath, options);
32
+ }
33
+
34
+ /**
35
+ * Renders header body lines using a chosen syntax.
36
+ * @param {HeaderSyntax} syntax - Header syntax descriptor.
37
+ * @param {string[]} lines - Header lines without comment wrappers.
38
+ * @returns {string} Formatted header block.
39
+ */
40
+ export function renderHeaderLines(syntax, lines) {
41
+ if (syntax.kind === "line") {
42
+ return lines.map((line) => `${syntax.linePrefix || "#"}\t${line}`).join("\n");
43
+ }
44
+
45
+ const blockStart = syntax.blockStart || (syntax.kind === "html" ? "<!--" : "/**");
46
+ const blockLinePrefix = syntax.blockLinePrefix || (syntax.kind === "html" ? "\t" : " *\t");
47
+ const blockEnd = syntax.blockEnd || (syntax.kind === "html" ? "-->" : " */");
48
+
49
+ if (syntax.kind === "html") {
50
+ return `${blockStart}\n${lines.map((line) => `${blockLinePrefix}${line}`).join("\n")}\n${blockEnd}`;
51
+ }
52
+
53
+ return `${blockStart}\n${lines.map((line) => `${blockLinePrefix}${line}`).join("\n")}\n${blockEnd}`;
54
+ }
@@ -0,0 +1,44 @@
1
+ import { relative } from "node:path";
2
+ import { getHeaderSyntaxForFile, renderHeaderLines } from "./syntax.mjs";
3
+
4
+ /**
5
+ * @fileoverview Header template builder used to generate normalized file headers.
6
+ * @module fix-headers/header/template
7
+ */
8
+
9
+ /**
10
+ * Builds a normalized header block for a source file.
11
+ * @param {{
12
+ * absoluteFilePath: string,
13
+ * language?: string,
14
+ * syntaxOptions?: { language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> },
15
+ * projectRoot: string,
16
+ * projectName: string,
17
+ * authorName: string,
18
+ * authorEmail: string,
19
+ * createdAt: {date: string, timestamp: number},
20
+ * lastModifiedAt: {date: string, timestamp: number},
21
+ * copyrightStartYear: number,
22
+ * companyName: string,
23
+ * currentYear: number
24
+ * }} data - Header data.
25
+ * @returns {string} Header block text.
26
+ */
27
+ export function buildHeader(data) {
28
+ const relativePath = `/${relative(data.projectRoot, data.absoluteFilePath).replace(/\\/g, "/")}`;
29
+ const syntax = getHeaderSyntaxForFile(data.absoluteFilePath, data.syntaxOptions || { language: data.language });
30
+ const headerLines = [
31
+ `@Project: ${data.projectName}`,
32
+ `@Filename: ${relativePath}`,
33
+ `@Date: ${data.createdAt.date} (${data.createdAt.timestamp})`,
34
+ `@Author: ${data.authorName}`,
35
+ `@Email: <${data.authorEmail}>`,
36
+ "-----",
37
+ `@Last modified by: ${data.authorName} (${data.authorEmail})`,
38
+ `@Last modified time: ${data.lastModifiedAt.date} (${data.lastModifiedAt.timestamp})`,
39
+ "-----",
40
+ `@Copyright: Copyright (c) ${data.copyrightStartYear}-${data.currentYear} ${data.companyName}. All rights reserved.`
41
+ ];
42
+
43
+ return renderHeaderLines(syntax, headerLines);
44
+ }