diff --git a/README.md b/README.md index 5a00a31..4a941ad 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,83 @@ # @cldmv/fix-headers -Multi-language source header normalizer for Node.js projects. +**@cldmv/fix-headers** is a multi-language source header normalizer for Node.js projects. It scans a project's files, works out which project each file belongs to from the manifests around it (`package.json`, `pyproject.toml`, `composer.json`, `Cargo.toml`, `go.mod`, …), detects the author from git, and inserts or updates a standard header at the top of every file in that file's own comment syntax. -`@cldmv/fix-headers` scans project files, auto-detects project metadata (language, root, project name, git author/email), and inserts or updates standard file headers. +Headers stay correct without hand-editing: `@Date` follows the file's real creation time, `@Last modified time` only moves when the header changes, the `@Copyright` holder and years come from the manifest and the file's history, and `--check` validates existing headers in CI without writing anything. It runs as a `fix-headers` command line tool or as a library from ESM and CommonJS, and one shared config can serve every repository in an organisation. + +> _One header format for every file in every repository, detected from the project itself and kept current by a single command._ [![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] [![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] [![npm last update]][npm_last_update_url] [![Coverage]][coverage_url] [![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url] +--- + ## ✨ What's New -### Latest: v2.1.3 (October 2026) +### Latest: v2.1.4 (October 2026) -- **CI and development-dependency maintenance, no runtime change** — the `✅ Required PR Check` mirror job in `ci.yml` no longer carries the required name while it is skipped, so a skipped `pull_request` run can no longer satisfy the branch ruleset and let an in-repo PR merge before the push run's tests have finished ([#111](https://github.com/CLDMV/fix-headers/pull/111)). Four development dependencies move to their current releases in the lockfile: `@cldmv/jsonv` 1.1.1, `@cldmv/eslint-plugin-jsonv` 1.0.13, `@cldmv/prettier-plugin-jsonv` 1.1.0 and `@cldmv/vitest-runner` 1.5.1 ([#109](https://github.com/CLDMV/fix-headers/pull/109), [#114](https://github.com/CLDMV/fix-headers/pull/114)). `ignore`, the only runtime dependency, is unchanged, and `dist/`, `bin/` and the API are the same as in v2.1.2. -- [View full v2.1.3 Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.3.md) +- **`require()` fails clearly where Node.js cannot load ES modules synchronously** — the CommonJS entry point (`dist/index.cjs`, a small wrapper around the ES module build) now checks `process.features.require_module` first. On a Node.js version without `require(esm)` it throws an `ERR_REQUIRE_ESM` error that names the package, the versions `require()` needs (^20.19.0 or >=22.12.0) and the running version, and points at `import()`, instead of Node's bare error from inside the package ([#117](https://github.com/CLDMV/fix-headers/pull/117)). New `node:test` checks run the built CommonJS entry point on every `npm test` and coverage run. On supported Node.js versions (`engines.node` is `>=22.12.0`) nothing changes. +- **Never breaks files it can't stamp** — strict JSON (`package.json` included), Markdown named with `--input`, and files with no or an unhandled extension are now skipped and reported instead of getting a JavaScript comment that broke them. Markdown gets a header only when forced with `--force-detector markdown`, as an HTML comment ([#124](https://github.com/CLDMV/fix-headers/pull/124)). +- **Repeatable `--input` and no more dependency folders** — every `--input` value is processed, not just the last ([#125](https://github.com/CLDMV/fix-headers/pull/125)), and `node_modules`, `bower_components`, `jspm_packages`, `.pnpm-store` and `.yarn` are never walked, at any depth, even without a `.gitignore` ([#126](https://github.com/CLDMV/fix-headers/pull/126)). +- [View full v2.1.4 Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.4.md) ### Recent Releases -- **v2.1.2** (October 2026) — no runtime change: the repository adopts the shared CLDMV fix-headers config (`.configs/fix-headers.json` extending `@cldmv/configs/fix-headers.json`, run with `npm run fix:headers`) and stamps uniform file headers across its own sources ([#107](https://github.com/CLDMV/fix-headers/pull/107)) ([Release](https://github.com/CLDMV/fix-headers/releases/tag/v2.1.2)) -- **v2.1.1** (October 2026) — a file that holds only a header now ends with the header instead of trailing `margin` blank lines ([#105](https://github.com/CLDMV/fix-headers/pull/105), fixes [#104](https://github.com/CLDMV/fix-headers/issues/104)); `esbuild` bumped to 0.28.2 to clear GHSA-g7r4-m6w7-qqqr ([#103](https://github.com/CLDMV/fix-headers/pull/103)) ([Release](https://github.com/CLDMV/fix-headers/releases/tag/v2.1.1)) -- **v2.1.0** (September 2026) — `spacing` and `margin` header layout options: every header is framed with empty comment lines and followed by two blank lines, and YAML and Python headers keep their padded `#` lines on the first run ([#101](https://github.com/CLDMV/fix-headers/pull/101)) ([Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.0.md)) -- **v2.0.0** (September 2026) — built package (`dist/` and `bin/` instead of `src/`), discovery without name-based skips, `@Project` and the copyright holder from the project manifest, `--check` date validation, `--diff`, `--timezone` and config `extends` ([Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.0.0.md)) +- **v2.1.3** (October 2026) — CI and development-dependency maintenance with no runtime change: a skipped PR run can no longer satisfy `✅ Required PR Check` and let a pull request merge before its tests finish ([Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.3.md)) +- **v2.1.2** (October 2026) — no runtime change: the repository adopts the shared CLDMV fix-headers config from `@cldmv/configs` and stamps uniform file headers across its own sources ([Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.2.md)) +- **v2.1.1** (October 2026) — a file that holds only a header now ends with the header instead of trailing `margin` blank lines; `esbuild` 0.28.2 clears GHSA-g7r4-m6w7-qqqr ([Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.1.md)) +- **v2.1.0** (September 2026) — `spacing` and `margin` header layout options: every header is framed with empty comment lines and followed by two blank lines ([Changelog](https://github.com/CLDMV/fix-headers/blob/master/docs/changelog/v2/v2.1.0.md)) -📚 For complete release notes, see the [docs/changelog/](https://github.com/CLDMV/fix-headers/tree/master/docs/changelog/) folder and the [GitHub Releases](https://github.com/CLDMV/fix-headers/releases). +📚 For complete release notes, see the [docs/changelog/](https://github.com/CLDMV/fix-headers/tree/master/docs/changelog/) folder. -## Features +--- + +## 🚀 Key Features - Finds the project each file belongs to from its manifest (`package.json`, `pyproject.toml` / `setup.cfg` / `setup.py`, `composer.json`, `Cargo.toml`, `go.mod`), whatever the file's type -- `@Project` is the name from that project's manifest, so a CSS, HTML, YAML or JSONC file in a Python, PHP, Rust or Go project gets that project's name too; the folder name is used only when no manifest provides one. Override it with `projectName`. See [Project name and root](#project-name-and-root) +- `@Project` is the name from that project's manifest, so a CSS, HTML, YAML or JSONC file in a Python, PHP, Rust or Go project gets that project's name too; the folder name is used only when no manifest provides one. Override it with `projectName`. See [Project name and root](#-project-name-and-root) - Auto-detects author and email from git config/commit history - Supports per-run overrides for every detected value - Supports folder inclusion and exclusion configuration; skips only what the project's ignore files (everything git honours) or your own exclusions say - Supports monorepos: every file resolves its own project from the nearest manifest in its parent tree -- Writes each header in the file's own comment syntax, and skips files that cannot carry one (strict JSON, plain text); Markdown gets a header only when you force it. See [Supported file types](#supported-file-types) +- Writes each header in the file's own comment syntax, and skips files that cannot carry one (strict JSON, plain text); Markdown gets a header only when you force it. See [Supported file types](#-supported-file-types) - Supports per-detector syntax overrides for line and block comment tokens -- Config files can use `extends` to build on a shared config (an https URL, an npm package path or a file path), so one organisation-wide config serves every repository. See [Shared configs](#shared-configs) +- Config files can use `extends` to build on a shared config (an https URL, an npm package path or a file path), so one organisation-wide config serves every repository. See [Shared configs](#-shared-configs) - Supports both ESM and CJS consumers -## Install +--- + +## 📦 Installation + +### Requirements + +- **Node.js 22.12.0 or later** (`engines.node` is `>=22.12.0`). +- The package is an ES module and loads with `import` on every supported version. `require("@cldmv/fix-headers")` loads the ES module build synchronously, which needs Node.js ^20.19.0 or >=22.12.0; on older Node.js, use `import()` instead. + +### Install ```bash npm i @cldmv/fix-headers ``` -## Usage +As a development dependency, for the CLI in `package.json` scripts: -### ESM +```bash +npm i -D @cldmv/fix-headers +``` + +--- + +## 🚀 Quick Start + +Preview what would change, then write it: + +```bash +fix-headers --dry-run --verbose +fix-headers +``` + +From ESM: ```js import fixHeaders from "@cldmv/fix-headers"; @@ -53,7 +85,19 @@ import fixHeaders from "@cldmv/fix-headers"; const result = await fixHeaders({ dryRun: true }); ``` -## CLI +From CommonJS: + +```js +const fixHeaders = require("@cldmv/fix-headers"); + +const result = await fixHeaders({ dryRun: true }); +``` + +`result` lists every scanned file and the changes a writing run makes; see [API](#-api) for the options and [Sample output](#-sample-output) for per-file detail. + +--- + +## 💻 CLI After install, use the package binary: @@ -70,11 +114,11 @@ npm run cli -- --dry-run --json Common CLI options: - `--dry-run` -- `--check` - validate header dates without writing; exits `1` on date drift (see [Date checks](#date-checks)) +- `--check` - validate header dates without writing; exits `1` on date drift (see [Date checks](#-date-checks)) - `--fix-created-date` - `--strict-created-date` - `--normalize-date-format` -- `--timezone ` - write new header dates in an IANA time zone (see [Time zone](#time-zone)) +- `--timezone ` - write new header dates in an IANA time zone (see [Time zone](#-time-zone)) - `--convert-timezone` - with `--timezone`, also rewrite existing header dates into that zone - `--json` - `--verbose` - list updated files; together with `--sample-output` or `--diff`, also list each file's field differences (`authorName: found "X", expected "Y"`) @@ -84,29 +128,23 @@ Common CLI options: - `--force-last-modified-author-update` - `--use-gpg-signer-author` (the signing key's UID name, with the OpenPGP UID comment dropped) - `--cwd ` -- `--input ` (repeatable) - process these files and folders instead of the whole project: the union of every value, each file once (`--input src/a.mjs --input src/b.mjs --input scripts`). A named file whose type cannot carry a header (see [Supported file types](#supported-file-types)) is reported as `skipped: ()` and left unchanged -- `--include-folder ` (repeatable) - naming a folder inside a dependency folder (`--include-folder node_modules/pkg`) processes it, although discovery otherwise skips dependency folders (see [Which files are processed](#which-files-are-processed)) +- `--input ` (repeatable) - process these files and folders instead of the whole project: the union of every value, each file once (`--input src/a.mjs --input src/b.mjs --input scripts`). A named file whose type cannot carry a header (see [Supported file types](#-supported-file-types)) is reported as `skipped: ()` and left unchanged +- `--include-folder ` (repeatable) - naming a folder inside a dependency folder (`--include-folder node_modules/pkg`) processes it, although discovery otherwise skips dependency folders (see [Which files are processed](#-which-files-are-processed)) - `--include-folder-non-recursive ` (repeatable) - include only that folder's own files, not its subfolders - `--exclude-folder ` (repeatable) - `--include-extension ` (repeatable) - `--enable-detector ` / `--disable-detector ` (repeatable) -- `--force-detector ` (repeatable) - turn on a force-only detector; `--force-detector markdown` gives `.md` / `.markdown` files an HTML-comment header (see [Supported file types](#supported-file-types)) +- `--force-detector ` (repeatable) - turn on a force-only detector; `--force-detector markdown` gives `.md` / `.markdown` files an HTML-comment header (see [Supported file types](#-supported-file-types)) - `--project-name ` - `--author-name ` / `--author-email ` -- `--company-name ` - the `@Copyright` holder for every file, instead of the one the manifests provide (see [Copyright holder](#copyright-holder)) +- `--company-name ` - the `@Copyright` holder for every file, instead of the one the manifests provide (see [Copyright holder](#-copyright-holder)) - `--copyright-start-year ` - the `@Copyright` start year for every file (default: the year of each file's `@Date`) -- `--spacing ` / `--margin ` - the header's layout: empty comment lines inside the header's edges (default `1`) and blank lines after it (default `2`), see [Header layout](#header-layout) -- `--config ` - load options from a JSON file, which may use `extends` to build on shared configs (see [Shared configs](#shared-configs)); flags on the command line win over the file +- `--spacing ` / `--margin ` - the header's layout: empty comment lines inside the header's edges (default `1`) and blank lines after it (default `2`), see [Header layout](#-header-layout) +- `--config ` - load options from a JSON file, which may use `extends` to build on shared configs (see [Shared configs](#-shared-configs)); flags on the command line win over the file -### CommonJS +--- -```js -const fixHeaders = require("@cldmv/fix-headers"); - -const result = await fixHeaders({ dryRun: true }); -``` - -## API +## 🔧 API ### `fixHeaders(options?)` @@ -115,24 +153,24 @@ Runs header normalization. Project/language/author/email metadata is auto-detect Important options: - `cwd?: string` - start directory for project detection -- `input?: string | string[]` - file or folder paths to process instead of the whole project. Every path is processed: the files named plus the files discovered under the folders named, each file once, in the order given. A path that does not exist throws; an empty list means no input. A file whose type cannot carry a header is listed in the result's `skipped` instead of being changed (see [Supported file types](#supported-file-types)) +- `input?: string | string[]` - file or folder paths to process instead of the whole project. Every path is processed: the files named plus the files discovered under the folders named, each file once, in the order given. A path that does not exist throws; an empty list means no input. A file whose type cannot carry a header is listed in the result's `skipped` instead of being changed (see [Supported file types](#-supported-file-types)) - `dryRun?: boolean` - compute changes without writing files -- `check?: boolean` - validate each existing header's dates and write nothing (implies `dryRun`). Each result entry gets `dateIssues`, and the result gets `filesWithDateDrift` and `dateAdvisories`. See [Date checks](#date-checks) -- `fixCreatedDate?: boolean` - move an existing `@Date` back to the oldest of itself, the file's git first commit, and its filesystem creation time (see [Creation date](#creation-date)). It only ever moves `@Date` earlier. Off by default: an existing `@Date` is kept as written +- `check?: boolean` - validate each existing header's dates and write nothing (implies `dryRun`). Each result entry gets `dateIssues`, and the result gets `filesWithDateDrift` and `dateAdvisories`. See [Date checks](#-date-checks) +- `fixCreatedDate?: boolean` - move an existing `@Date` back to the oldest of itself, the file's git first commit, and its filesystem creation time (see [Creation date](#-creation-date)). It only ever moves `@Date` earlier. Off by default: an existing `@Date` is kept as written - `strictCreatedDate?: boolean` - with `check`, count a `@Date` later than the file's git first commit or filesystem creation time as drift (fails the run). Off by default, where it is an advisory - `normalizeDateFormat?: boolean` - write every header date in the git `%aI` form (`2026-03-01T17:59:32-08:00`), keeping each date's offset and instant. Off by default; the first run rewrites (and restamps) every managed file whose dates use the space form (`2026-03-01 17:59:32 -08:00`) -- `timezone?: string` - an IANA time zone name (`America/Los_Angeles`, `UTC`, `Asia/Kolkata`, ...). Every date fix-headers writes (a new header's `@Date`, and `@Last modified time`) is expressed in that zone; the instant and its epoch never change. Unset (the default): dates are written as today. An unknown zone name throws. See [Time zone](#time-zone) -- `convertTimezone?: boolean` - with `timezone`, also rewrite the existing `@Date` and `@Last modified time` values of every header into that zone, keeping each instant. Throws when `timezone` is not set. See [Time zone](#time-zone) -- `sampleOutput?: boolean` - include a `sample` for each changed file: previous/new header text, a unified `diff`, per-field `issues`, and `detectedValues` (see [Sample output](#sample-output)) -- `configFile?: string` - load JSON options from file (resolved from `cwd`). The file may use `extends` to build on shared configs by URL, npm package path or file path (see [Shared configs](#shared-configs)); options passed in the call win over everything from files +- `timezone?: string` - an IANA time zone name (`America/Los_Angeles`, `UTC`, `Asia/Kolkata`, ...). Every date fix-headers writes (a new header's `@Date`, and `@Last modified time`) is expressed in that zone; the instant and its epoch never change. Unset (the default): dates are written as today. An unknown zone name throws. See [Time zone](#-time-zone) +- `convertTimezone?: boolean` - with `timezone`, also rewrite the existing `@Date` and `@Last modified time` values of every header into that zone, keeping each instant. Throws when `timezone` is not set. See [Time zone](#-time-zone) +- `sampleOutput?: boolean` - include a `sample` for each changed file: previous/new header text, a unified `diff`, per-field `issues`, and `detectedValues` (see [Sample output](#-sample-output)) +- `configFile?: string` - load JSON options from file (resolved from `cwd`). The file may use `extends` to build on shared configs by URL, npm package path or file path (see [Shared configs](#-shared-configs)); options passed in the call win over everything from files - `includeExtensions?: string[]` - file extensions to process - `enabledDetectors?: string[]` - detector ids to enable (defaults to every detector that is not force-only) - `disabledDetectors?: string[]` - detector ids to disable -- `forcedDetectors?: string[]` - force-only detector ids to turn on (currently only `"markdown"`). A forced detector is used for `input` and for discovery alike, even when `enabledDetectors` does not list it; `disabledDetectors` still turns it off. An id that is unknown or does not need forcing throws. See [Supported file types](#supported-file-types) +- `forcedDetectors?: string[]` - force-only detector ids to turn on (currently only `"markdown"`). A forced detector is used for `input` and for discovery alike, even when `enabledDetectors` does not list it; `disabledDetectors` still turns it off. An id that is unknown or does not need forcing throws. See [Supported file types](#-supported-file-types) - `detectorSyntaxOverrides?: Record` - override detector comment syntax tokens - `includeFolders?: Array` - project-relative folders to scan. A string entry is scanned recursively; `{ path, recursive: false }` includes only that folder's own files (for example `{ path: ".", recursive: false }` for the project-root files without the whole tree). Overlapping entries are collapsed, so every file is scanned once however the folders nest or are spelled (`"."` next to `"src"`, `"src"` next to `"src/core"`, `"./src"` next to `"src/"`) -- `excludeFolders?: string[]` - folder names or relative paths to exclude, on top of what the ignore files exclude. Dependency folders are always excluded: `node_modules`, `bower_components`, `jspm_packages`, `.pnpm-store` and `.yarn` at any depth, and a `vendor` folder holding Composer's `autoload.php` or Go's `modules.txt` (see [Which files are processed](#which-files-are-processed)); list a path inside one in `includeFolders` (or pass it as `input`) to process it anyway -- `gitignore?: boolean | string | string[]` - which ignore files decide what discovery skips. Omitted (or `true`): every ignore file git honours (see [Which files are processed](#which-files-are-processed)). `false`: no ignore files, every file is processed except those in dependency folders. A path or array of paths (relative to the project root): exactly those files, parsed with `.gitignore` syntax, without asking git. +- `excludeFolders?: string[]` - folder names or relative paths to exclude, on top of what the ignore files exclude. Dependency folders are always excluded: `node_modules`, `bower_components`, `jspm_packages`, `.pnpm-store` and `.yarn` at any depth, and a `vendor` folder holding Composer's `autoload.php` or Go's `modules.txt` (see [Which files are processed](#-which-files-are-processed)); list a path inside one in `includeFolders` (or pass it as `input`) to process it anyway +- `gitignore?: boolean | string | string[]` - which ignore files decide what discovery skips. Omitted (or `true`): every ignore file git honours (see [Which files are processed](#-which-files-are-processed)). `false`: no ignore files, every file is processed except those in dependency folders. A path or array of paths (relative to the project root): exactly those files, parsed with `.gitignore` syntax, without asking git. - `projectName?: string` - the `@Project` value for every file, instead of the manifest name - `language?: string` - the reported `language` for every file (does not change comment syntax or project resolution) - `projectRoot?: string` - the project root for every file (the base of `@Filename` and of git history lookups) and the scan root @@ -143,10 +181,10 @@ Important options: - `forceAuthorUpdate?: boolean` - force update `@Author`/`@Email` to detected or overridden current values - `forceLastModifiedAuthorUpdate?: boolean` - force update `@Last modified by` to detected or overridden current values. Without this, an existing header's recorded `@Last modified by` identity is preserved and does not by itself trigger an update just because the running author differs (e.g. a different `git config user.name` than whoever last touched the file) - `useGpgSignerAuthor?: boolean` - take the detected `@Author` name from the user ID of the OpenPGP key git signs commits with (`user.signingkey`, read through `gpg.openpgp.program` / `gpg.program` / `gpg`; the first user ID that is not revoked or expired). The OpenPGP UID comment is dropped, so `Nate Corcoran (2023 PC) ` becomes `Nate Corcoran` (with `company: "CLDMV"`: `Nate Corcoran `). It describes whoever runs the tool, whatever the last commit is — a squash merge made by GitHub or a bot has no locally verifiable signer. With no readable OpenPGP signing key (none configured, `gpg.format` is `ssh`/`x509`, or gpg is unavailable) it falls back to the last commit's signer (`%GS`), then `git config user.name`, then the last commit's author -- `companyName?: string` - the `@Copyright` holder for every file, instead of the one the project's manifests provide. There is no built-in default: unset, the holder comes from the manifests, and with none it is left out of the line (see [Copyright holder](#copyright-holder)) -- `copyrightStartYear?: number` - the `@Copyright` start year for every file. Unset (the default): each file's start year is the year of its own `@Date`, see [Copyright years](#copyright-years) -- `spacing?: number` - empty comment lines just inside the header's opening and just before its closing. Default `1`, see [Header layout](#header-layout) -- `margin?: number` - blank lines between the header and the file's next content. Default `2`, see [Header layout](#header-layout) +- `companyName?: string` - the `@Copyright` holder for every file, instead of the one the project's manifests provide. There is no built-in default: unset, the holder comes from the manifests, and with none it is left out of the line (see [Copyright holder](#-copyright-holder)) +- `copyrightStartYear?: number` - the `@Copyright` start year for every file. Unset (the default): each file's start year is the year of its own `@Date`, see [Copyright years](#-copyright-years) +- `spacing?: number` - empty comment lines just inside the header's opening and just before its closing. Default `1`, see [Header layout](#-header-layout) +- `margin?: number` - blank lines between the header and the file's next content. Default `2`, see [Header layout](#-header-layout) Example: @@ -174,7 +212,9 @@ const result = await fixHeaders({ }); ``` -## Shared configs +--- + +## 🧩 Shared configs A config file (`--config ` on the CLI, `configFile` in the API) can extend other configs with `extends`, so an organisation keeps its header settings in one place instead of copying them into every repository. `extends` is a string or an array of strings, and each entry is one of: @@ -213,7 +253,9 @@ A shared config can also be served from a URL, and mixed with local files: } ``` -## Project name and root +--- + +## 🔎 Project name and root Which project a file belongs to depends on the manifests around it, not on the file's type. Comment syntax is the only thing the file's type decides. @@ -239,7 +281,7 @@ For each file: A folder holding `.git` is a repository boundary: neither the root search nor the climb goes past it, so a nested repository without a manifest is a project of its own. With no manifest up to the repository root, that repository root is the project root and its folder name is the name. With neither a manifest nor a repository anywhere above the file, the file's own folder is the project root: `@Project` is that folder's name and `@Filename` is `/`. -`projectName`, `projectRoot`, `language` and `marker` override the detected values for every file. The copyright holder is resolved from the same manifests by the same rules, see [Copyright holder](#copyright-holder). +`projectName`, `projectRoot`, `language` and `marker` override the detected values for every file. The copyright holder is resolved from the same manifests by the same rules, see [Copyright holder](#-copyright-holder). For example, scanning `repo/`: @@ -260,7 +302,9 @@ repo/ To support another ecosystem, add a module to `src/drivers/` that exports a `driver` with `id`, `languages` (the file-type detector ids native to it), `manifests` (filenames that claim a folder, in reading order), `detect(dirPath)` (returns `detectManifests(dirPath, manifests)` from `src/drivers/shared.mjs`) and `read(detection)` (returns `{ name, company }`, each `undefined` when the manifest has none), then add it to `MANIFEST_DRIVERS` in `src/drivers/index.mjs` at its place in the fixed order. -## Copyright holder +--- + +## 📜 Copyright holder The holder on the `@Copyright` line (`companyName`) comes from the manifest of the project the file belongs to, read by the same drivers and with the same rules as the project name: the nearest claimed folder, the file's native driver first, the per-field fallback, and climbing up to the scan root (never past a `.git` folder). The holder climbs on its own, so a sub-package whose `package.json` has a name but no author takes the repository's author while keeping its own `@Project`. The field each driver reads is in the table above. @@ -286,7 +330,9 @@ With `companyName: "Example Co"` every file gets `Copyright (c) 2026-2026 Exampl With `sampleOutput`, `detectedValues.companyName` is the resolved holder (`null` when there is none) and `detectedValues.companyNameSource` says where it came from: `{ from: "manifest", driver, manifest, dir }`, `{ from: "option" }` (`companyName`) or `{ from: "none" }`. `result.metadata` carries the same two values for the scan root. The `@Author` suffix set by `company` (`Name `) is a separate option and does not affect the holder. -## Creation date +--- + +## 📅 Creation date `@Date` is "oldest wins": a file cannot have been created later than its first commit or than the time the filesystem first saw it, and an `@Date` older than both (a file brought in from elsewhere, or dated before it was committed) is kept. @@ -296,7 +342,9 @@ With `sampleOutput`, `detectedValues.companyName` is the resolved holder (`null` The filesystem creation time is the earlier of the file's birth time and its modification time. Content last written at the modification time existed by then, so it bounds creation even when the birth time is later (an extracted archive or a `cp -p` copy keeps the source's modification time). Where the platform reports no birth time, the modification time is used. A fresh clone or CI checkout gives every file a current filesystem time, so there the git first commit decides. -## Copyright years +--- + +## 📆 Copyright years `@Copyright: Copyright (c) - All rights reserved.` @@ -305,7 +353,9 @@ The filesystem creation time is the earlier of the file's birth time and its mod A file created in 2019 therefore gets `2019-2026` when fix-headers runs in 2026, not `2026-2026`. -## Header layout +--- + +## 📐 Header layout Two options set the shape of the header, and both apply to every file type. @@ -344,7 +394,9 @@ With `spacing: 0, margin: 1` the header is compact, with no empty comment lines An existing header is restyled in place to match, so changing either option rewrites the layout of every header on the next run, and a run with unchanged options finds nothing to update. A line-comment header always keeps at least one blank line after it, even with `margin: 0`, so a comment that follows the file's header is not read as part of it. -## Date checks +--- + +## ✅ Date checks `check: true` / `--check` validates the `@Date` and `@Last modified time` values of every existing header and writes nothing. It compares instants, not strings, so the same moment written with another offset or in the space/`T` form is not drift. It is independent of the rendered-header diff: an author, identity, copyright, or other content difference never fails the check. Files without a header are skipped. @@ -377,11 +429,13 @@ Advisories are counted in the summary and listed with `--verbose`; `--json` prin In a normal (writing) run, an epoch that disagrees with its datetime text is recomputed from the text; the datetime text itself is left as written. `created-newer-than-source` is corrected only with `fixCreatedDate` / `--fix-created-date`. -## Time zone +--- + +## 🌍 Time zone A header date is correct in any zone: the offset is only how the instant is shown, and the epoch in parentheses is the instant. So nothing is converted by default. `timezone` / `--timezone ` is for projects that want every date shown in one zone: -- **Dates fix-headers writes** are expressed in the zone: a new header's `@Date` (from the git first commit or the filesystem, see [Creation date](#creation-date)), an `@Date` moved by `fixCreatedDate`, and the `@Last modified time` stamped on every changed file. +- **Dates fix-headers writes** are expressed in the zone: a new header's `@Date` (from the git first commit or the filesystem, see [Creation date](#-creation-date)), an `@Date` moved by `fixCreatedDate`, and the `@Last modified time` stamped on every changed file. - **Dates already in a header** are kept as written, unless `convertTimezone` / `--convert-timezone` is also set. That sweep rewrites existing `@Date` and `@Last modified time` values into the zone. A value whose datetime is not recognised is left alone, and so is one already in the zone's offset at that instant. Only the wall-clock time and the offset change; the instant and the epoch stay the same. The offset is the zone's offset at that instant, from the time zone data built into Node (`Intl`), so daylight saving time is applied per date: with `America/Los_Angeles`, a January date is written at `-08:00` and a July date at `-07:00`. Half-hour and other zones work the same way (`Asia/Kolkata` is `+05:30`, `Pacific/Kiritimati` is `+14:00`). The zone name is validated with `Intl`, and an unknown name fails the run. @@ -406,7 +460,9 @@ $ fix-headers --timezone America/Los_Angeles --convert-timezone The `@Date` instant is unchanged; `@Last modified time` is restamped with the time of the run, in the zone. -## Supported file types +--- + +## 📑 Supported file types Each file type is handled by a detector, which decides the header's comment syntax. A file is given a header only when an enabled detector handles its extension: @@ -437,9 +493,11 @@ fix-headers complete: scanned=0, updated=0, skipped=1, dryRun=false skipped: package.json (no enabled detector handles .json files) ``` -## Which files are processed +--- -By default every file with a supported extension (see [Supported file types](#supported-file-types)) is processed. Build output is not skipped by name: `dist`, `build`, `coverage`, `tmp` and the like are processed unless something excludes them. Files are skipped only when: +## 📁 Which files are processed + +By default every file with a supported extension (see [Supported file types](#-supported-file-types)) is processed. Build output is not skipped by name: `dist`, `build`, `coverage`, `tmp` and the like are processed unless something excludes them. Files are skipped only when: - they are inside a dependency folder (below), - the project's ignore files ignore them, or @@ -461,15 +519,19 @@ A path inside a dependency folder that you name explicitly is still processed: a `gitignore: false` turns all of this off, and `gitignore: ""` / `["", ...]` replaces it with exactly the listed files. -## Notes +--- + +## 📝 Notes - `excludeFolders` supports both folder-name and nested path matching. - `includeFolders` entries never double-count a file. A folder that lies inside another recursive include is not walked a second time; the exception is a folder the outer walk never enters because it is a dependency folder or `excludeFolders` excludes it (for example `node_modules/pkg` listed explicitly), which keeps being walked on its own because it was named explicitly. An `includeFolders` entry does not override the ignore files: a folder they ignore contributes no files. -- File discovery is described in [Which files are processed](#which-files-are-processed). -- For monorepos, each file resolves its project from the nearest manifest in its parent tree (see [Project name and root](#project-name-and-root)). +- File discovery is described in [Which files are processed](#-which-files-are-processed). +- For monorepos, each file resolves its project from the nearest manifest in its parent tree (see [Project name and root](#-project-name-and-root)). - With `sampleOutput` enabled, each changed file includes `previousValue`, `newValue`, `diff`, `issues`, and `detectedValues` in results. -## Sample output +--- + +## 🔍 Sample output With `sampleOutput: true` (CLI: `--sample-output` or `--diff`), every changed entry in `result.changes` carries a `sample` object. It costs nothing when the option is off. @@ -477,7 +539,7 @@ With `sampleOutput: true` (CLI: `--sample-output` or `--diff`), every changed en - `newValue` - the header block this run writes. - `diff` - a ready-to-print unified diff of the header block. The `---`/`+++` lines name the file (`a/` / `b/`, or `/dev/null` when there was no previous header, in which case the whole new header shows as added), and hunk line numbers are file line numbers. - `issues` - one `{ field, previous, detected }` entry per header field whose written value differs from the existing header, in header order. Fields: `projectName`, `filename`, `createdAt`, `authorName`, `authorEmail`, `lastModifiedByName`, `lastModifiedByEmail`, `lastModifiedAt`, `copyrightStartYear`, `copyrightEndYear`, `companyName`. Values are the field text as written in the header (dates keep their `date (timestamp)` form); `previous` is `null` when the field was missing. -- `detectedValues` - the metadata resolved for the file. `projectNameSource` says where `projectName` came from: `{ from: "manifest", driver, manifest, dir }` (the driver, its manifest and the folder it sits in), `{ from: "folder", dir }` (the project root's folder name) or `{ from: "option" }` (`projectName`). `copyrightStartYear` is the start year written for the file, and `copyrightStartYearSource` says where it came from: `"option"` (`copyrightStartYear`) or `"created-date"` (the year of the file's `@Date`, see [Copyright years](#copyright-years)). The run-level `result.metadata.copyrightStartYear` is the `copyrightStartYear` option, or `null` when it is not set. +- `detectedValues` - the metadata resolved for the file. `projectNameSource` says where `projectName` came from: `{ from: "manifest", driver, manifest, dir }` (the driver, its manifest and the folder it sits in), `{ from: "folder", dir }` (the project root's folder name) or `{ from: "option" }` (`projectName`). `copyrightStartYear` is the start year written for the file, and `copyrightStartYearSource` says where it came from: `"option"` (`copyrightStartYear`) or `"created-date"` (the year of the file's `@Date`, see [Copyright years](#-copyright-years)). The run-level `result.metadata.copyrightStartYear` is the `copyrightStartYear` option, or `null` when it is not set. `issues` compares the existing header against what is actually written, not against the raw detected metadata. fix-headers preserves an existing `@Author`/`@Email` and `@Last modified by` identity unless `forceAuthorUpdate` / `forceLastModifiedAuthorUpdate` is set, so those fields only appear when they really change. An updated file always gets a fresh `@Last modified time`, so `lastModifiedAt` is listed for every changed file that already had a header. @@ -512,9 +574,40 @@ issues: src/cli.mjs */ ``` -## License +--- + +## 📚 Documentation + +- **[Changelog](https://github.com/CLDMV/fix-headers/tree/master/docs/changelog/)** — release notes for every version (v1 and v2) +- **[Release notes on GitHub](https://github.com/CLDMV/fix-headers/releases)** — the same notes attached to each release tag + +[![CodeFactor]][codefactor_url] [![OpenSSF Scorecard]][ossf_scorecard_url] [![npms.io score]][npms_url] [![npm unpacked size]][npm_size_url] [![Repo size]][repo_size_url] + +--- + +## 🤝 Contributing + +Bug reports and pull requests are welcome on [GitHub](https://github.com/CLDMV/fix-headers/issues). Pull requests target the `next` branch; releases ship from `next` to `master`. + +[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url] + +--- + +## 🔗 Links + +- **npm**: [@cldmv/fix-headers](https://www.npmjs.com/package/@cldmv/fix-headers) +- **GitHub**: [CLDMV/fix-headers](https://github.com/CLDMV/fix-headers) +- **Issues**: [GitHub Issues](https://github.com/CLDMV/fix-headers/issues) +- **Changelog**: [docs/changelog/](https://github.com/CLDMV/fix-headers/tree/master/docs/changelog/) +- **Releases**: [GitHub Releases](https://github.com/CLDMV/fix-headers/releases) + +--- + +## 📄 License + +[![GitHub license]][github_license_url] [![npm license]][npm_license_url] -Apache-2.0 +Apache-2.0 © Shinrai / CLDMV @@ -538,3 +631,17 @@ Apache-2.0 [contributors_url]: https://github.com/CLDMV/fix-headers/graphs/contributors [sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor [sponsor_url]: https://github.com/sponsors/shinrai +[codefactor]: https://img.shields.io/codefactor/grade/github/CLDMV/fix-headers?style=for-the-badge&logo=codefactor&logoColor=white&labelColor=F44A6A +[codefactor_url]: https://www.codefactor.io/repository/github/cldmv/fix-headers +[openssf scorecard]: https://img.shields.io/ossf-scorecard/github.com/CLDMV/fix-headers?style=for-the-badge&label=OpenSSF%20Scorecard +[ossf_scorecard_url]: https://scorecard.dev/viewer/?uri=github.com/CLDMV/fix-headers +[npms.io score]: https://img.shields.io/npms-io/final-score/%40cldmv%2Ffix-headers?style=for-the-badge&logo=npms&logoColor=white&labelColor=0B5D57 +[npms_url]: https://npms.io/search?q=%40cldmv%2Ffix-headers +[npm unpacked size]: https://img.shields.io/npm/unpacked-size/%40cldmv%2Ffix-headers.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_size_url]: https://www.npmjs.com/package/@cldmv/fix-headers +[repo size]: https://img.shields.io/github/repo-size/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[repo_size_url]: https://github.com/CLDMV/fix-headers +[github license]: https://img.shields.io/github/license/CLDMV/fix-headers.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[github_license_url]: https://github.com/CLDMV/fix-headers/blob/HEAD/LICENSE +[npm license]: https://img.shields.io/npm/l/%40cldmv%2Ffix-headers.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_license_url]: https://www.npmjs.com/package/@cldmv/fix-headers diff --git a/docs/changelog/v1/v1.0.0.md b/docs/changelog/v1/v1.0.0.md new file mode 100644 index 0000000..7247eb5 --- /dev/null +++ b/docs/changelog/v1/v1.0.0.md @@ -0,0 +1,77 @@ +# @cldmv/fix-headers v1.0.0 Changelog + +**Release Date**: March 2026 +**Release Type**: Major + +--- + +## Overview + +Version 1.0.0 is the initial release of `@cldmv/fix-headers`, a multi-language source header normalizer for Node.js projects. It scans a project, detects the project's language, root, name and git author, and inserts or refreshes a standard file header at the top of each source file. It ships as a library (ESM and CommonJS) and as a `fix-headers` command line tool. + +Later releases extend this surface: [v1.1.0](./v1.1.0.md) adds YAML support and author-preservation options, [v1.2.0](./v1.2.0.md) adds a JSON-family detector, and the [v2.0.0](../v2/v2.0.0.md) line builds on it further. + +--- + +## ✨ Features + +### Standard header + +Every processed file receives a header with these fields, rendered in the file's own comment syntax: + +- `@Project` (the detected project name), `@Filename` (project-relative path with a leading `/`), `@Date` (creation time with a Unix timestamp), `@Author` and `@Email`. +- `@Last modified by` and `@Last modified time`, in a second block between `-----` separators. +- `@Copyright: Copyright (c) - . All rights reserved.` + +Creation and modification times come from git history when available and fall back to filesystem times. + +### Project detection + +Metadata is auto-detected per file by walking up to the nearest marker file, so monorepos resolve each file against its closest project. Seven language detectors are enabled by default: + +- `node`: marker `package.json`; extensions `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx`, `.jsonv`, `.jsonc`, `.json5`. The project name is the `name` field. +- `python`: markers `pyproject.toml`, `setup.py`, `requirements.txt`; extension `.py`; `#` line comments. +- `go`: marker `go.mod`; extension `.go`; the project name is the `module` path. +- `rust`: marker `Cargo.toml`; extension `.rs`; the project name is the package `name`. +- `php`: marker `composer.json`; extension `.php`. +- `css`: markers `package.json` and the PostCSS config files; extension `.css`. +- `html`: markers `index.html`, `vite.config.*` and `next.config.*`; extensions `.html`, `.htm`; `` comments. + +Author name and email come from `git config user.name` and `user.email`. Every detected value (project name, language, project root, marker, author name and email, company name, copyright start year) can be overridden per run. Detectors can be selected with `enabledDetectors` and `disabledDetectors`, and comment tokens can be customized per detector with `detectorSyntaxOverrides`. + +### Library API + +`fixHeaders(options?)` is the default export of the package, available through `import` and `require`. It returns a report with `metadata`, `detectedProjects`, `filesScanned`, `filesUpdated`, `dryRun` and a per-file `changes` list. Notable options: + +- `cwd`, `input` (a single file or folder), `dryRun`, and `configFile` (a JSON file of options, merged under explicit options). +- `includeFolders`, `excludeFolders` (folder names or nested relative paths) and `includeExtensions`. +- `enabledDetectors`, `disabledDetectors`, `detectorSyntaxOverrides`. +- `projectName`, `language`, `projectRoot`, `marker`, `authorName`, `authorEmail`, `companyName` and `copyrightStartYear`. + +`companyName` defaults to `Catalyzed Motivation Inc.` and `copyrightStartYear` defaults to the current year. The default ignored folders are `.git`, `node_modules`, `dist`, `build`, `coverage`, `tmp`, `.next` and `.turbo`. + +```js +import fixHeaders from "@cldmv/fix-headers"; + +const result = await fixHeaders({ dryRun: true, includeFolders: ["src"] }); +``` + +### Command line + +The package installs a `fix-headers` binary. Flags map one to one onto the options above: `--dry-run`, `--json`, `--cwd`, `--input`, `--include-folder`, `--exclude-folder`, `--include-extension`, `--enable-detector` and `--disable-detector` (all repeatable), `--project-name`, `--language`, `--project-root`, `--marker`, `--author-name`, `--author-email`, `--company-name`, `--copyright-start-year`, `--config` and `--help`. + +```bash +fix-headers --dry-run --include-folder src --exclude-folder dist +``` + +By default the CLI prints a one-line summary (`scanned`, `updated`, `dryRun`); `--json` prints the full report. + +## 🧪 Tests + +The release ships with a vitest suite covering the parser, detectors, CLI, git and time utilities and file discovery, run through `npm test` and `npm run ci:coverage`. CI publishes a coverage badge. + +--- + +## Upgrade notes + +- First release; there is nothing to upgrade from. Install with `npm i @cldmv/fix-headers`. diff --git a/docs/changelog/v1/v1.1.0.md b/docs/changelog/v1/v1.1.0.md new file mode 100644 index 0000000..f3f404d --- /dev/null +++ b/docs/changelog/v1/v1.1.0.md @@ -0,0 +1,65 @@ +# @cldmv/fix-headers v1.1.0 Changelog + +**Release Date**: March 2026 +**Release Type**: Minor + +--- + +## Overview + +Version 1.1.0 makes header updates much less destructive and adds the first batch of new options. An existing header's creation date and original author are now preserved, `@Last modified time` only moves when the header actually changes, a leading shebang line is kept above the header, and YAML files are supported. The CLI gains `--verbose`, `--sample-output`, `--force-author-update` and `--use-gpg-signer-author`. + +Runtime behaviour changes for existing headers; see the notes below. The previous release is [v1.0.0](./v1.0.0.md). + +--- + +## ✨ Features + +### YAML detector + +A new `yaml` detector handles `.yaml` and `.yml` files with `#` line comments. Its markers are `package.json` and `.git`; the project name is taken from `package.json` when that is the marker, otherwise from the directory name. + +### Shebang preservation + +The `node` detector (shebangs mentioning `node`, `bun`, `deno`, `tsx` or `ts-node`) and the `python` detector (`python`, `python3`, `python3.12` and so on) now report a preserved prefix. A leading `#!` line is left at the very top of the file and the header is inserted below it, instead of the header being placed above the shebang and breaking the script. + +### Preserved dates and original author + +When a file already has a header, its `@Date` (creation time) and its `@Author` / `@Email` are reused rather than recomputed from git. `@Last modified by` and `@Last modified time` are only refreshed when something else in the header would change; a file that is already up to date keeps its existing modified time, so repeated runs are stable. `forceAuthorUpdate` (`--force-author-update`) overrides the preservation and rewrites `@Author` / `@Email` to the detected or overridden values. + +### Signer identity from signed commits + +`useGpgSignerAuthor` (`--use-gpg-signer-author`) takes the detected author from the user ID of the latest signed commit (`git log -1 --format=%GS`). The signer's name replaces the git config name; the signer's email is used only if no `user.email` is configured. + +### Sample output + +`sampleOutput` (`--sample-output`) adds a `sample` object to each changed entry in `changes`, holding `previousValue` (the old header, or `null`), `newValue`, and `detectedValues` (project, language, author, company, the resolved created and modified times and where each was sourced from). The CLI prints these as `sample:`, `previous:`, `new:` and `detected-values:` blocks. + +### `--verbose` and `lineSeparator` + +`--verbose` prints an `updated: ` line for each changed file in the default summary mode. Line-comment syntaxes accept a new `lineSeparator` in `detectorSyntaxOverrides` (default is a tab) to control the text between the comment prefix and the header text, for example `{ python: { linePrefix: ";;", lineSeparator: " " } }`. + +## 🐛 Bug Fixes + +### Doubled period in the copyright line + +The generated line was `Copyright (c) 2013-2026 Catalyzed Motivation Inc.. All rights reserved.` because the template appended a period to a company name that already ends in one. The template no longer adds the period, so the default company renders as `... Catalyzed Motivation Inc. All rights reserved.`. A custom `companyName` without a trailing period is now rendered without one; include it in the name if you want it. + +### Stacked headers collapse into one + +The header finder now consumes consecutive header blocks that contain `@Project:` instead of only the first, so a file that accumulated duplicate headers is rewritten with a single header. + +### Repeated CLI values + +Repeatable flags such as `--include-folder` now de-duplicate their values. + +## 🧪 Tests + +Test coverage grows substantially, with new suites for the CLI, git edge cases and project metadata, and expanded core, header and detector tests. + +--- + +## Upgrade notes + +- No breaking changes to the API or CLI flags. Output changes in two visible ways: files with existing headers keep their `@Date`, `@Author` and (when unchanged) `@Last modified time`, and the copyright line no longer has a doubled period, so the first run after upgrading rewrites headers that carried `Inc..`. +- Use `--force-author-update` if you relied on the previous behaviour of rewriting `@Author` on every run. diff --git a/docs/changelog/v1/v1.1.1.md b/docs/changelog/v1/v1.1.1.md new file mode 100644 index 0000000..e81427a --- /dev/null +++ b/docs/changelog/v1/v1.1.1.md @@ -0,0 +1,28 @@ +# @cldmv/fix-headers v1.1.1 Changelog + +**Release Date**: March 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.1.1 fixes the `fix-headers` binary doing nothing when it is started through a symlink. The previous release is [v1.1.0](./v1.1.0.md); the next is [v1.2.0](./v1.2.0.md). + +--- + +## 🐛 Bug Fixes + +### CLI did not run when launched through a symlinked bin + +`src/cli.mjs` decides whether it is being run as the main script by comparing its own module URL with `process.argv[1]`. Package managers install `node_modules/.bin/fix-headers` as a symlink, so the two paths differed and the entry point silently exited without running. `runCliAsMain` now compares the real paths of both (`realpathSync`) and only falls back to the old URL comparison if resolving either path throws. + +## 🧪 Tests + +A CLI test covers the symlinked entry point. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.1.0. Anyone who saw `fix-headers` exit with no output when run from `node_modules/.bin` or via `npx` should upgrade. diff --git a/docs/changelog/v1/v1.2.0.md b/docs/changelog/v1/v1.2.0.md new file mode 100644 index 0000000..19b8d30 --- /dev/null +++ b/docs/changelog/v1/v1.2.0.md @@ -0,0 +1,43 @@ +# @cldmv/fix-headers v1.2.0 Changelog + +**Release Date**: March 2026 +**Release Type**: Minor + +--- + +## Overview + +Version 1.2.0 adds a dedicated `json` detector for the JSON-with-comments family and a `company` option that appends an organization to the `@Author` line. The previous release is [v1.1.1](./v1.1.1.md). + +--- + +## ✨ Features + +### `json` detector + +`.jsonv`, `.jsonc` and `.json5` files are now handled by a new `json` detector (marker `package.json`, project name from its `name` field, `/** */` block header) instead of the `node` detector. Plain `.json` files are still not processed. + +### `company` option + +`company` (`--company ` on the CLI) appends a company suffix to the detected author in the form `Name `, so `@Author` becomes for example `Nate Corcoran `. The suffix is skipped when the value is blank or when the author name already contains an `<...>` part, which keeps repeated runs from stacking suffixes. This is separate from `companyName` / `--company-name`, which still sets the company in the copyright line. + +## 🐛 Bug Fixes + +### JSON-family extensions are claimed by `json`, not `node` + +The `node` detector no longer lists `.jsonv`, `.jsonc` and `.json5` in its extensions. Default runs behave the same, but a run restricted with `enabledDetectors: ["node"]` (or `--enable-detector node`) no longer touches those files, and `disabledDetectors: ["json"]` now turns them off. + +## 📚 Documentation + +- The README documents `company`. + +## 🧪 Tests + +New tests cover the JSON detector, `company` handling (including the blank-value case) and constants. + +--- + +## Upgrade notes + +- No breaking changes for default configurations — drop-in for v1.1.1. +- If you pass `enabledDetectors` explicitly and want JSON-family files processed, add `json` to the list. diff --git a/docs/changelog/v1/v1.2.1.md b/docs/changelog/v1/v1.2.1.md new file mode 100644 index 0000000..ee7a136 --- /dev/null +++ b/docs/changelog/v1/v1.2.1.md @@ -0,0 +1,23 @@ +# @cldmv/fix-headers v1.2.1 Changelog + +**Release Date**: March 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.2.1 adds one test and bumps the package version; no runtime code changed. This version was committed on `master` but never tagged or published to npm; its changes first reached npm in [v1.2.2](./v1.2.2.md). The previous release is [v1.2.0](./v1.2.0.md). + +--- + +## 🧪 Tests + +A coverage test in `tests/project-metadata-edge.test.vitest.mjs` exercises the blank `company` branch of the author metadata code (a whitespace-only value leaves `@Author` unchanged). + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.2.0. +- No runtime code changed. diff --git a/docs/changelog/v1/v1.2.2.md b/docs/changelog/v1/v1.2.2.md new file mode 100644 index 0000000..785ccbd --- /dev/null +++ b/docs/changelog/v1/v1.2.2.md @@ -0,0 +1,36 @@ +# @cldmv/fix-headers v1.2.2 Changelog + +**Release Date**: March 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.2.2 stops a missing `--include-folder` from crashing a run, and updates the README to use the scoped package name. It also carries the changes from [v1.2.1](./v1.2.1.md), which was never published to npm (a test-only change). The previous published release is [v1.2.0](./v1.2.0.md). + +--- + +## 🐛 Bug Fixes + +### Missing include folders are skipped instead of crashing + +`discoverFiles` walked every entry of `includeFolders` and let the resulting `ENOENT` abort the whole run, so a single folder that did not exist (for example `src` in a repo without one, or a path shared across several config files) made `fixHeaders` throw. Each include folder is now checked first: a folder that does not exist, or a path that is not a directory, is skipped with a `fix-headers: skipped include folder "" (...)` warning on `console.warn`, and the remaining folders are processed. Any other filesystem error is still thrown. + +## 🔧 CI & tooling + +- The coverage badge calculation in `ci.yml` treats a metric with zero total (for example no branches) as 100% instead of using its reported percentage, so such runs no longer drag the badge down. + +## 📚 Documentation + +- The README title, install command (`npm i @cldmv/fix-headers`) and the ESM and CommonJS examples now use `@cldmv/fix-headers`; they previously showed the unscoped `fix-headers` name. + +## 🧪 Tests + +New tests cover missing and non-directory include folders and adjust detector and branch tests. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.2.0. diff --git a/docs/changelog/v1/v1.2.3.md b/docs/changelog/v1/v1.2.3.md new file mode 100644 index 0000000..7b28f51 --- /dev/null +++ b/docs/changelog/v1/v1.2.3.md @@ -0,0 +1,30 @@ +# @cldmv/fix-headers v1.2.3 Changelog + +**Release Date**: June 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.2.3 fixes a data-loss bug: replacing an existing block-comment header could delete the code that followed it. The previous release is [v1.2.2](./v1.2.2.md). + +--- + +## 🐛 Bug Fixes + +### Replacing a block-comment header could delete file content + +To find an existing header, the parser matched from the opening comment token to the closing token _as rendered by the detector_, which is ` */` with a leading space. A real header whose closer was written differently, such as `**/`, a tab-indented `*/` or a `*/` at column 0, was therefore not recognized as ending there. The lazy match ran on to the next ` */` further down the file, inside a `//` line comment, a string literal or another block comment, and everything in between was removed when the header was replaced. + +The matcher now follows the real block-comment grammar: a header opens with `/*` (so both `/*` and `/**` openers are recognized) and ends at the first `*/` after it, with the styled leading space ignored. Headers closed with `**/`, a tabbed `*/` or a column-0 `*/` are replaced cleanly, and code, strings and comments below the header are left untouched. Line-comment and HTML headers are unaffected. + +## 🧪 Tests + +A regression group in `tests/header.test.vitest.mjs` covers `**/` closers followed by a `*/` inside a `//` comment or a string literal, a `/*` single-asterisk opener, tab-indented and column-0 closers, and a `//` comment containing `/** */` below the header. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.2.2. Upgrading is recommended for anyone running `fix-headers` on files whose existing headers use non-standard block-comment closers; review any earlier run on such files with `git diff`. diff --git a/docs/changelog/v1/v1.3.0.md b/docs/changelog/v1/v1.3.0.md new file mode 100644 index 0000000..9f034d0 --- /dev/null +++ b/docs/changelog/v1/v1.3.0.md @@ -0,0 +1,77 @@ +# @cldmv/fix-headers v1.3.0 Changelog + +**Release Date**: June 2026 +**Release Type**: Minor + +--- + +## Overview + +Version 1.3.0 changes how file discovery decides what to skip. Build and cache directories are now ignored only at the project root, and discovery respects the project's `.gitignore` by default through a new `gitignore` option. The only runtime dependency, `ignore`, is added to support this. + +This release is published to npm. Its `exports` map exposes only `"."`; `./package.json` is not exported (that arrives in [v1.3.1](./v1.3.1.md)). + +--- + +## 💥 Breaking Changes + +Despite being a minor release, v1.3.0 changes which files a run processes without any change to the caller's options: + +- **Nested build-named folders are now scanned.** A `dist`, `build`, `coverage`, `tmp`, `.next` or `.turbo` folder below the project root used to be skipped by name and now gets headers written into its files. Add such folders to `excludeFolders` (or to `.gitignore`) to keep them out. +- **`.gitignore` is honoured by default.** Files matched by `/.gitignore` used to be processed and are now skipped. Pass `gitignore: false` to process them again. + +Preview the effect with `--dry-run --verbose` before the first writing run. + +## ✨ Features + +### Root-anchored build and cache ignores + +Previously `dist`, `build`, `coverage`, `tmp`, `.next` and `.turbo` were skipped at any depth, which silently dropped source directories that merely shared a name (for example `tools/build`). Discovery now splits the built-in ignores in two: + +- `ALWAYS_IGNORE_FOLDERS` (`.git`, `node_modules`) are skipped at any depth. +- `ROOT_IGNORE_FOLDERS` (`dist`, `build`, `coverage`, `tmp`, `.next`, `.turbo`) are skipped only when they sit directly under the project root. + +Both sets are exported from `src/constants.mjs`. `DEFAULT_IGNORE_FOLDERS` remains as the union of the two for backward compatibility. A nested directory with one of the root-only names is now scanned; add it to `excludeFolders` if it should still be skipped. + +### `.gitignore` support via the `gitignore` option + +`fixHeaders()` accepts `gitignore?: boolean | string | string[]`, forwarded to file discovery: + +- Omitted (or any other value): auto-detect `/.gitignore`. A missing file is silently ignored. +- `false`: disable `.gitignore` handling. +- A path or an array of paths: load those ignore files, resolved against the project root. + +Matched files and directories are skipped, so generated output is excluded by the project's own rules without hard-coding folder names. Matching uses the new `ignore` dependency. Because auto-detection is on by default, files that a project's `.gitignore` covers are no longer processed unless `gitignore: false` is passed. + +```js +await fixHeaders({ projectRoot: ".", gitignore: false }); +await fixHeaders({ projectRoot: ".", gitignore: [".gitignore", ".headersignore"] }); +``` + +### Detector override in comment-syntax lookup + +`getCommentSyntaxForFile()` in `src/detectors/index.mjs` accepts a `detectors` array in its options, which replaces the enabled-detector set in the same way `detectProjectFromMarkers` already does. + +--- + +## 🧪 Tests + +- New `tests/file-discovery-gitignore.test.vitest.mjs` covers `.gitignore` auto-detection, explicit paths and `false`. +- New `tests/branch-coverage-edge.test.vitest.mjs` adds edge-case branch coverage. + +## 📚 Documentation + +- README documents the `gitignore` option and the scoped built-in ignores. + +## 🔧 Dependencies + +- `ignore` added at ^7.0.5 (runtime) +- `vitest` ^3.2.4 → ^4.1.8 (dev) +- `@vitest/coverage-v8` ^3.2.4 → ^4.1.8 (dev) + +--- + +## Upgrade notes + +- No API was removed, but two defaults change (see [Breaking Changes](#-breaking-changes)): nested `dist`/`build`/`coverage`/`tmp`/`.next`/`.turbo` directories are now scanned, and files matched by the project's `.gitignore` are now skipped. Pass `gitignore: false` and/or `excludeFolders` to restore the previous behavior. +- Next: [v1.3.1](./v1.3.1.md). diff --git a/docs/changelog/v1/v1.3.1.md b/docs/changelog/v1/v1.3.1.md new file mode 100644 index 0000000..403db61 --- /dev/null +++ b/docs/changelog/v1/v1.3.1.md @@ -0,0 +1,26 @@ +# @cldmv/fix-headers v1.3.1 Changelog + +**Release Date**: June 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.1 adds `./package.json` to the package `exports` map. No source code changed. + +This version was committed on `master` but never tagged or published to npm; its changes first reached npm in [v1.3.4](./v1.3.4.md). + +--- + +## 🐛 Bug Fixes + +### `./package.json` is now an exported subpath + +[v1.3.0](./v1.3.0.md) exported only `"."`, so `require("@cldmv/fix-headers/package.json")` and `import ... from "@cldmv/fix-headers/package.json"` failed with `ERR_PACKAGE_PATH_NOT_EXPORTED`. Tools that resolve a package's metadata through its `package.json` subpath can now do so. The entry is `"./package.json": "./package.json"`. + +--- + +## Upgrade notes + +- No breaking changes: drop-in for [v1.3.0](./v1.3.0.md). No runtime code changed. diff --git a/docs/changelog/v1/v1.3.10.md b/docs/changelog/v1/v1.3.10.md new file mode 100644 index 0000000..d9809d0 --- /dev/null +++ b/docs/changelog/v1/v1.3.10.md @@ -0,0 +1,37 @@ +# @cldmv/fix-headers v1.3.10 Changelog + +**Release Date**: August 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.10 is a test-infrastructure release. The test fixtures no longer leak into the directory above the repository, and a global setup step removes stale fixtures. No runtime code changed: `src/`, `bin/` and the published API are identical to [v1.3.9](./v1.3.9.md). Release PR [#30](https://github.com/CLDMV/fix-headers/pull/30). + +--- + +## 🧪 Tests + +### Fixtures live in the repository's `tmp/` + +`tests/helpers/workspace.mjs` used to create fixtures at `/../tmp-fix-headers-tests`, which is the parent of the repository. Cleanup only ran on the success path, so failed or interrupted runs left many fixture git repositories behind in the parent directory. `createWorkspace` now builds under `FIXTURE_ROOT`, the repository's gitignored `tmp/fix-headers-tests`, resolved from the helper's own location rather than the working directory. + +### Stale fixtures are reaped + +`reapStaleWorkspaces(maxAgeMs = 3600000)` deletes fixture directories older than one hour and ignores a missing root or a directory removed mid-sweep. The age guard keeps it from touching fixtures that a concurrent run is using. A new `.configs/vitest.globalSetup.mjs` calls it before and after each run and is registered as `test.globalSetup` in `.configs/vitest.config.mjs`. + +### Fallback-path tests use an isolated workspace + +Fixtures under `tmp/` now always sit below the repository's own `package.json` and `.git`, which defeats tests of the "nothing detected" fallbacks. `createIsolatedWorkspace(name)` creates a workspace with `mkdtemp` in the OS temp directory, and the unknown-language and unknown-author tests in `tests/integration.module.test.vitest.mjs` and `tests/project-metadata-edge.test.vitest.mjs` use it. + +## 🔧 Dependencies + +- `@types/node` 26.1.2 → 26.2.0 (dev, lockfile only) + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.3.9. +- No runtime code changed. diff --git a/docs/changelog/v1/v1.3.11.md b/docs/changelog/v1/v1.3.11.md new file mode 100644 index 0000000..b47dfa8 --- /dev/null +++ b/docs/changelog/v1/v1.3.11.md @@ -0,0 +1,25 @@ +# @cldmv/fix-headers v1.3.11 Changelog + +**Release Date**: September 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.11 is a CI-only release. The release and feature-PR caller workflows now forward the bot identity secrets to the reusable `@v4` workflows. No runtime code, test or dependency changed relative to [v1.3.10](./v1.3.10.md). Release PR [#37](https://github.com/CLDMV/fix-headers/pull/37). + +--- + +## 🔧 CI & tooling + +Each caller maps repository secrets onto the names the reusable workflows expect: + +- `feature-pr.yml`, `hotfixes-release.yml` and `next-release.yml` pass `BOT_NAME` (from `CLDMV_BOT_NAME`) and `BOT_EMAIL` (from `CLDMV_BOT_EMAIL`). +- `hotfix-redirector.yml` passes the same two and also `BOT_GPG_PRIVATE_KEY` and `BOT_GPG_PASSPHRASE` (from `CLDMV_BOT_GPG_PRIVATE_KEY` and `CLDMV_BOT_GPG_PASSPHRASE`), so the cherry-pick it makes when redirecting a security PR to `hotfixes` can be committed under the bot identity and signed. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.3.10. No runtime code changed. diff --git a/docs/changelog/v1/v1.3.12.md b/docs/changelog/v1/v1.3.12.md new file mode 100644 index 0000000..6c25d80 --- /dev/null +++ b/docs/changelog/v1/v1.3.12.md @@ -0,0 +1,43 @@ +# @cldmv/fix-headers v1.3.12 Changelog + +**Release Date**: September 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.12 raises the minimum supported Node.js version to 22.12.0 and moves the test toolchain to vitest 5, in a release whose title only mentions an `ignore` bump. Release PR [#42](https://github.com/CLDMV/fix-headers/pull/42). No file under `src/` changed. + +Despite being a patch, `engines.node` changes from `>=20.19.0` to `>=22.12.0`. The later [v2.0.0](../v2/v2.0.0.md) changelog refers to this release as the point where `engines` began to require Node 22.12 or newer, and that is accurate. + +--- + +## 💥 Breaking Changes + +### `engines.node` is now `>=22.12.0` + +`package.json` declares `"node": ">=22.12.0"`, up from `">=20.19.0"`. Installing on Node 20.x or on 22.0 through 22.11 produces an `EBADENGINE` warning, and an error under `engine-strict`. The package code itself did not change, so it may still run on those versions, but they are no longer supported or tested. + +Upgrade: use Node 22.12.0 or later (22.12+, 24 or 26), or pin `@cldmv/fix-headers` to 1.3.11 on older runtimes. + +--- + +## 🔧 CI & tooling + +- The default CI matrix floor in `ci.yml` and `publish.yml` moves from Node `20` to `22.12.0`, the oldest version vitest 5 runs on, and the matrix ceiling `max_node_major` from `22` to `26`. +- `.github/dependabot.yml` adds three update groups, placed before the existing `security`, patch and minor groups because Dependabot uses the first matching group: `vitest` (`vitest` and `@vitest/*`, which peer each other exactly), `eslint` (`eslint`, `@eslint/*`, `@cldmv/eslint-plugin-*`) and `prettier` (`prettier`, `@cldmv/prettier-plugin-*`). + +## 🔧 Dependencies + +- `ignore` 7.0.6 → 7.0.8 (runtime, lockfile only; the `^7.0.5` range is unchanged) +- `vitest` `^4.1.8` → `^5.0.0` (dev) +- `@vitest/coverage-v8` `^4.1.8` → `^5.0.0` (dev) +- `@types/node` 26.2.0 → 26.4.1, `vite` 8.1.5 → 8.3.0 and `rolldown` 1.1.5 → 1.2.8 (dev, lockfile only) + +--- + +## Upgrade notes + +- Breaking for Node 20.x and 22.0 to 22.11 despite being a patch: run on Node 22.12.0 or later, or stay on [v1.3.11](./v1.3.11.md). +- No runtime code changed and no option was added or removed. diff --git a/docs/changelog/v1/v1.3.2.md b/docs/changelog/v1/v1.3.2.md new file mode 100644 index 0000000..1a46e5a --- /dev/null +++ b/docs/changelog/v1/v1.3.2.md @@ -0,0 +1,33 @@ +# @cldmv/fix-headers v1.3.2 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.2 corrects the declared Node.js floor in `engines.node` and onboards the repository onto the CLDMV v4 staging-branch release workflows. No source code changed. + +This version was tagged on `master` but never published to npm; its changes first reached npm in [v1.3.4](./v1.3.4.md). Release PR [#3](https://github.com/CLDMV/fix-headers/pull/3). + +--- + +## 💥 Breaking Changes + +### `engines.node` raised from `>=18.0.0` to `>=20.19.0` + +Despite being a patch, this raises the declared Node.js floor. The test toolchain moved to `vitest` 4 in [v1.3.0](./v1.3.0.md), whose `vite`/`rolldown` dependencies require Node `^20.19.0 || >=22.12.0`, so the previous `>=18.0.0` claim was no longer tested. No runtime code changed, but installing on Node.js older than 20.19 now prints an `EBADENGINE` warning, and fails outright with `engine-strict=true`. + +## 🔧 CI & tooling + +- `.github/workflows/` is rebuilt on the CLDMV v4 flow: `ci.yml` is replaced by a caller of the shared `workflow-ci.yml@v4` with an LTS-only Node test matrix, and the release pipeline (`feature-pr`, `next-release`, `hotfixes-release`, `next-reset`, `hotfix-redirector`, `publish`) is added alongside `v4-bootstrap`. +- Repository-hygiene workflows are added: CodeQL, dependency review, OpenSSF Scorecard, Dependabot auto-merge, labeler, PR-title normalizer, CLA, stale, welcome, tag health and branch retention, plus `.github/dependabot.yml`. +- A `build:ci` script is added as a no-op (`echo 'no build step: ...'`) because the package ships source directly. + +--- + +## Upgrade notes + +- No API change and no runtime code changed. The declared `engines.node` floor is now `>=20.19.0`: upgrade Node.js to 20.19 or later, or stay on v1.3.0. +- Previous: [v1.3.1](./v1.3.1.md). Next: [v1.3.3](./v1.3.3.md). diff --git a/docs/changelog/v1/v1.3.3.md b/docs/changelog/v1/v1.3.3.md new file mode 100644 index 0000000..81d9ebc --- /dev/null +++ b/docs/changelog/v1/v1.3.3.md @@ -0,0 +1,27 @@ +# @cldmv/fix-headers v1.3.3 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.3 is a CI-only release: `security-events: write` is dropped from the OpenSSF Scorecard workflow. No runtime code changed. + +This version was tagged on `master` but never published to npm; its changes first reached npm in [v1.3.4](./v1.3.4.md). Release PR [#5](https://github.com/CLDMV/fix-headers/pull/5), carrying the fix from [#4](https://github.com/CLDMV/fix-headers/pull/4). + +--- + +## 🔧 CI & tooling + +### Scorecard no longer requests `security-events: write` + +`scorecard.yml` publishes results to the public OpenSSF transparency log (`publish_results: true`). The publish step rejects a workflow whose token has `security-events` write access ("workflow verification failed: global perm is set to write"). The permission is removed from the workflow, and a comment records the trade-off: the SARIF upload to the Security tab does not run in this configuration, while the public badge does. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for [v1.3.2](./v1.3.2.md). No runtime code changed. +- Next: [v1.3.4](./v1.3.4.md). diff --git a/docs/changelog/v1/v1.3.4.md b/docs/changelog/v1/v1.3.4.md new file mode 100644 index 0000000..55db30e --- /dev/null +++ b/docs/changelog/v1/v1.3.4.md @@ -0,0 +1,31 @@ +# @cldmv/fix-headers v1.3.4 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.4 adds package metadata to `package.json` so npm provenance can link the published tarball to its repository. It is the first version on npm since [v1.3.0](./v1.3.0.md), so installing it also delivers everything from [v1.3.1](./v1.3.1.md), [v1.3.2](./v1.3.2.md) and [v1.3.3](./v1.3.3.md): the `./package.json` export, the `engines.node` floor of `>=20.19.0`, and the CI changes. + +No source code changed. Release PR [#7](https://github.com/CLDMV/fix-headers/pull/7), carrying [#6](https://github.com/CLDMV/fix-headers/pull/6). + +--- + +## 🐛 Bug Fixes + +### `repository`, `bugs` and `homepage` fields for provenance + +`package.json` gains `repository` (`git+https://github.com/CLDMV/fix-headers.git`), `bugs` (`https://github.com/CLDMV/fix-headers/issues`) and `homepage` (`https://github.com/CLDMV/fix-headers#readme`). Provenance publishing requires the repository URL to match the build source. + +### Standard author and funding metadata + +`author`, `contributors` and `funding` are added, along with `"sideEffects": false`, which lets bundlers tree-shake the package. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for [v1.3.3](./v1.3.3.md). No runtime code changed. +- Coming from [v1.3.0](./v1.3.0.md) on npm: note the `engines.node` floor is now `>=20.19.0` (see [v1.3.2](./v1.3.2.md)). diff --git a/docs/changelog/v1/v1.3.5.md b/docs/changelog/v1/v1.3.5.md new file mode 100644 index 0000000..f6bb7f8 --- /dev/null +++ b/docs/changelog/v1/v1.3.5.md @@ -0,0 +1,24 @@ +# @cldmv/fix-headers v1.3.5 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.5 is a CI-only release that moves the Scorecard workflow's permissions from workflow level to job level. No runtime code changed. Release PR [#9](https://github.com/CLDMV/fix-headers/pull/9). + +--- + +## 🔧 CI & tooling + +### Scorecard permissions scoped to the job + +[v1.3.3](./v1.3.3.md) removed `security-events: write` but left `id-token: write`, `contents: read` and `actions: read` at workflow level, and scorecard-action's publish check still rejected the run because it expects write permissions granted job-scoped, as in OpenSSF's own example workflow. The workflow-level `permissions:` block is removed and the same three permissions are granted on the `analyze` job. The explanatory comment is updated to match. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for [v1.3.4](./v1.3.4.md). No runtime code changed. diff --git a/docs/changelog/v1/v1.3.6.md b/docs/changelog/v1/v1.3.6.md new file mode 100644 index 0000000..ff32468 --- /dev/null +++ b/docs/changelog/v1/v1.3.6.md @@ -0,0 +1,24 @@ +# @cldmv/fix-headers v1.3.6 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.6 switches the vitest reporter to `dot` to shorten CI logs. No runtime code changed. Release PR [#11](https://github.com/CLDMV/fix-headers/pull/11), carrying [#10](https://github.com/CLDMV/fix-headers/pull/10). + +--- + +## 🔧 CI & tooling + +### Quieter test output + +`.configs/vitest.config.mjs` sets `reporters: ["dot"]`. In a non-interactive terminal, vitest's default reporter reprints a per-file pass/fail block for every test file in addition to the final summary; `dot` prints one character per test file. The final `Test Files` / `Tests` summary is unchanged. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for [v1.3.5](./v1.3.5.md). No runtime code changed. diff --git a/docs/changelog/v1/v1.3.7.md b/docs/changelog/v1/v1.3.7.md new file mode 100644 index 0000000..bfc195f --- /dev/null +++ b/docs/changelog/v1/v1.3.7.md @@ -0,0 +1,45 @@ +# @cldmv/fix-headers v1.3.7 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.7 fixes a header-duplication bug for files whose existing header block is longer than 40 lines, and refreshes the CI and release workflows and the development toolchain. Release PR [#17](https://github.com/CLDMV/fix-headers/pull/17). + +The runtime change is one constant: `DEFAULT_MAX_HEADER_SCAN_LINES` goes from `40` to `200`. The parser only looks at the first N lines of a file when it searches for an existing header block, so a header with many `@Field` lines whose closing `*/` fell beyond line 40 was not recognized as a header. A new header was then inserted on top of the old one. The only runtime dependency, `ignore`, keeps its `^7.0.5` range. + +--- + +## 🐛 Bug Fixes + +### Long header blocks are replaced instead of duplicated + +`src/constants.mjs` raises `DEFAULT_MAX_HEADER_SCAN_LINES` from `40` to `200`, and `src/header/parser.mjs` uses it to bound the scan for an existing header. A metadata block that legitimately runs past 40 lines now has its closing `*/` found, so `replaceOrInsertHeader` replaces the block in place. A new test in `tests/header.test.vitest.mjs` replaces a block with 45 extra fields and asserts that exactly one `@Project:` line remains and the old content is gone. `tests/constants.test.vitest.mjs` now expects `200`. + +## 🔧 CI & tooling + +- The v4 release-flow workflows `feature-pr.yml`, `hotfix-redirector.yml`, `hotfixes-release.yml`, `next-release.yml`, `next-reset.yml` and `pr-title-normalizer.yml` are re-synced from the `CLDMV/.github` templates. Most of them shrink to thin callers of the reusable `@v4` workflows, with the job logic living upstream instead of inline. +- `hotfix-redirector.yml` changes how a Dependabot security PR is recognized: a Dependabot PR whose base is not Dependabot's routine target branch (`next`) is treated as a security update and redirected to `hotfixes`, replacing the previous check for a GHSA reference in the PR body. The redirector job now needs `contents: write` for its cherry-pick path. +- `package.json` gains a `build` script, `echo 'no build step - stopgap for CI coverage-badge; see tracking issue'`, so the coverage-badge job's default `npm run build` succeeds. It does not build anything. + +## 📚 Documentation + +- JSDoc for `getCommentSyntaxForFile` in `src/detectors/index.mjs` types the `detectors` option as `DetectorProfile[]` instead of `object[]`. + +## 🔧 Dependencies + +Development dependencies only; the `package.json` ranges for the first two change and the rest move through the lockfile. + +- `@types/node` `^25.3.0` → `^26.1.1` (resolved 25.3.3 → 26.1.1, dev) +- `typescript` `^5.9.3` → `^7.0.2` (resolved 5.9.3 → 7.0.2, dev) +- `vitest` and `@vitest/coverage-v8` 4.1.8 → 4.1.10 (dev) +- `ignore` 7.0.5 → 7.0.6 (runtime, lockfile only) + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.3.6. Files with a header block longer than 40 lines, which previously received a second header, are now updated in place. A file that already carries a duplicated header from an earlier run needs the extra block removed by hand. diff --git a/docs/changelog/v1/v1.3.8.md b/docs/changelog/v1/v1.3.8.md new file mode 100644 index 0000000..16fb192 --- /dev/null +++ b/docs/changelog/v1/v1.3.8.md @@ -0,0 +1,28 @@ +# @cldmv/fix-headers v1.3.8 Changelog + +**Release Date**: August 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.8 is a CI-only release: the `ci.yml` concurrency group no longer lets a newer run cancel release-relevant runs. This version was released on `master` but never published to npm; its changes first reached npm in [v1.3.9](./v1.3.9.md). Release PR [#20](https://github.com/CLDMV/fix-headers/pull/20). + +No source, test or dependency changed, so the library and CLI behave exactly as in [v1.3.7](./v1.3.7.md). + +--- + +## 🔧 CI & tooling + +### Release-relevant CI runs are never superseded + +The `concurrency` block in `.github/workflows/ci.yml` previously cancelled superseded runs on every branch except `master` and `main`. During the burst of pushes a release produces on `next` or `hotfixes` (the feature squash, the post-hotfix sync merge, the bot's `chore: bump version` commit), the middle runs were cancelled and the release PR showed a red cancelled check. + +The group now appends `github.run_id` for pushes to the release base branch (the `CLDMV_RELEASE_BASE` repository variable, falling back to the repository's default branch), for pushes to `next` and `hotfixes`, and for the `next` and `hotfixes` release PRs. Each of those runs gets its own group, so none is cancelled and each posts a real result. Feature branches keep the per-ref group, so a newer push still cancels an older run there. `cancel-in-progress` is now a plain `true`; with a unique group on release contexts it only affects feature refs. + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.3.7. No runtime code changed. diff --git a/docs/changelog/v1/v1.3.9.md b/docs/changelog/v1/v1.3.9.md new file mode 100644 index 0000000..c586c95 --- /dev/null +++ b/docs/changelog/v1/v1.3.9.md @@ -0,0 +1,55 @@ +# @cldmv/fix-headers v1.3.9 Changelog + +**Release Date**: August 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 1.3.9 changes how an existing header's `@Last modified by` line is treated and adds an option to opt back in to the old behavior. Release PR [#23](https://github.com/CLDMV/fix-headers/pull/23). + +Despite being a patch, this release changes a default. Before, every update that rewrote a header replaced `@Last modified by` with the current author identity. Now the identity already recorded in the header is kept, and the new `forceLastModifiedAuthorUpdate` option (CLI flag `--force-last-modified-author-update`) restores the replacing behavior. This is also the first release of the 1.3.8 CI change to reach npm, since [v1.3.8](./v1.3.8.md) was not published. + +--- + +## 💥 Breaking Changes + +### `@Last modified by` is preserved by default + +In `src/core/fix-headers.mjs`, the `lastModifiedByName` and `lastModifiedByEmail` values used to build the header always came from the detected or configured author. They now come from the existing header's `@Last modified by: Name (email)` line when one is present, and fall back to the detected author for files with no header or no parseable line. Two consequences: + +- Running with a different `git config user.name` (or `authorName` / `authorEmail`) than whoever last touched a file no longer rewrites the line and no longer makes the file count as changed on its own. +- Pipelines that relied on `@Last modified by` tracking the most recent runner must pass the new option. + +Upgrade: add `forceLastModifiedAuthorUpdate: true` to the `fixHeaders` options, set it in the JSON file passed to `--config`, or pass `--force-last-modified-author-update` on the CLI. `forceAuthorUpdate` still controls only `@Author` and `@Email`, and it does not cascade into `@Last modified by`. + +--- + +## ✨ Features + +### `forceLastModifiedAuthorUpdate` option and `--force-last-modified-author-update` flag + +- API: `forceLastModifiedAuthorUpdate?: boolean` on `fixHeaders` options. When `true`, `@Last modified by` is set to the detected or overridden current author, as in v1.3.8 and earlier. Default: unset, so the existing identity is preserved. +- CLI: `--force-last-modified-author-update`, listed in the `--help` output. +- The README option lists describe both, and the generated declarations in `types/` include the new option. + +## 🧪 Tests + +- `tests/cli.test.vitest.mjs` covers parsing the new flag. +- `tests/core-edge.test.vitest.mjs` asserts that an unchanged header keeps its `@Author`, `@Email`, `@Last modified by` and `@Last modified time` and reports `filesUpdated` of `0`; that the option replaces only the last-modified identity; and that `forceAuthorUpdate` alone leaves `@Last modified by` untouched. + +## 🔧 Dependencies + +- `@types/node` 26.1.1 → 26.1.2 (dev, lockfile only) + +## 📚 Documentation + +- The generated `.d.mts` files in `types/` are regenerated. Besides the new option they now describe the `ALWAYS_IGNORE_FOLDERS` and `ROOT_IGNORE_FOLDERS` constants, the `gitignore` discovery option (`false` disables, a path or array loads those ignore files, otherwise `/.gitignore` is auto-detected) and the `detectors` option of `getCommentSyntaxForFile`. The runtime code for these was already in v1.3.8; only the published type declarations were out of date. + +--- + +## Upgrade notes + +- Behavior change despite being a patch: `@Last modified by` is no longer overwritten. Pass `forceLastModifiedAuthorUpdate: true` or `--force-last-modified-author-update` to keep the previous behavior. +- Otherwise a drop-in for v1.3.7. diff --git a/docs/changelog/v2/v2.1.1.md b/docs/changelog/v2/v2.1.1.md new file mode 100644 index 0000000..be54ff7 --- /dev/null +++ b/docs/changelog/v2/v2.1.1.md @@ -0,0 +1,37 @@ +# @cldmv/fix-headers v2.1.1 Changelog + +**Release Date**: October 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 2.1.1 fixes the layout of a file that holds nothing but its header. Since v2.1.0 frames every header with a configurable `margin` of blank lines before the file's own content, an empty file (or one with only blank lines after the header) ended with those margin lines and nothing after them. Such a file now ends with the header and a single newline. It also moves the build-only `esbuild` to 0.28.2 to clear a security advisory. + +The fix is a small change in the header writer; nothing else in the runtime changed, and the `fixHeaders` API and options are the same as in [v2.1.0](./v2.1.0.md). + +--- + +## 🐛 Bug Fixes + +### A header-only file ends with the header, not trailing margin lines ([#105](https://github.com/CLDMV/fix-headers/pull/105), fixes [#104](https://github.com/CLDMV/fix-headers/issues/104)) + +`replaceOrInsertHeader` always joined the header and the rest of the file with `margin + 1` newlines. When the rest was empty or only whitespace, the file ended with the header followed by a run of blank lines, and with the default `margin` of 2 every newly stamped empty file carried them. The header writer now ends the file with the header and one newline whenever nothing follows it, for both a newly inserted header and a replaced one, whatever `margin` is set to. Re-running on such a file is a no-op, and a file that already has trailing blank lines after its header is trimmed to one newline on the next writing run. + +## 🔒 Security & supply chain + +### `esbuild` 0.28.2 ([#103](https://github.com/CLDMV/fix-headers/pull/103)) + +`esbuild` is pulled in by `tsup` to build `dist/` and `bin/`; it is not shipped in the package. It is now a direct development dependency at `^0.28.2` with an `overrides` entry (`"esbuild": "$esbuild"`) so `tsup`'s copy resolves to the same patched version, which clears [GHSA-g7r4-m6w7-qqqr](https://github.com/advisories/GHSA-g7r4-m6w7-qqqr). + +## 🔧 Dependencies + +- `esbuild` → 0.28.2 (dev only, via `tsup`; [#103](https://github.com/CLDMV/fix-headers/pull/103)). +- `ignore`, the only runtime dependency, is unchanged. + +--- + +## Upgrade notes + +- No breaking changes: drop-in for v2.1.0. A file whose header is its only content loses its trailing blank lines on the next writing run. diff --git a/docs/changelog/v2/v2.1.2.md b/docs/changelog/v2/v2.1.2.md new file mode 100644 index 0000000..10c2b2e --- /dev/null +++ b/docs/changelog/v2/v2.1.2.md @@ -0,0 +1,31 @@ +# @cldmv/fix-headers v2.1.2 Changelog + +**Release Date**: October 2026 +**Release Type**: Patch + +--- + +## Overview + +Version 2.1.2 is a repository-maintenance release with no runtime change. fix-headers now maintains its own file headers with the shared CLDMV configuration from `@cldmv/configs`, and every source, test, config and type file was re-stamped with that uniform layout. The only source edits are those comment headers, so `dist/`, `bin/` and the `fixHeaders` API behave exactly as in [v2.1.1](./v2.1.1.md). + +--- + +## 🔧 CI & tooling + +### The repository adopts the shared CLDMV fix-headers config ([#107](https://github.com/CLDMV/fix-headers/pull/107)) + +- A new `.configs/fix-headers.json` contains only `{ "extends": "@cldmv/configs/fix-headers.json" }`, using the `extends` support added in [v2.0.0](./v2.0.0.md), and a new `fix:headers` script runs the in-repo CLI against it (`node ./src/cli.mjs --config .configs/fix-headers.json`). +- `@cldmv/configs` is added as a development dependency to provide the shared config. +- Running it rewrote the header of 100+ files: ISO 8601 `@Date` and `@Last modified time` values, framing lines inside the comment, a uniform `@Author`, and a `@Copyright` range starting in 2013. + +## 🔧 Dependencies + +- `@cldmv/configs` added (dev only). +- `ignore`, the only runtime dependency, is unchanged. + +--- + +## Upgrade notes + +- No breaking changes: drop-in for v2.1.1. No runtime code changed. diff --git a/docs/changelog/v2/v2.1.3.md b/docs/changelog/v2/v2.1.3.md index 77f5ead..b516a9e 100644 --- a/docs/changelog/v2/v2.1.3.md +++ b/docs/changelog/v2/v2.1.3.md @@ -10,7 +10,7 @@ Version 2.1.3 is a CI and tooling release with no runtime change. The `✅ Required PR Check` mirror job in `ci.yml` no longer satisfies the branch ruleset while it is skipped, which closes a window in which an in-repo pull request could be merged before its tests had finished. Four development dependencies move to their current releases through the lockfile; `ignore`, the only runtime dependency, is unchanged. -No source file changes, so `dist/`, `bin/` and the `fixHeaders` API are the same as in v2.1.2. The two patch releases before this one, [v2.1.1](https://github.com/CLDMV/fix-headers/releases/tag/v2.1.1) (a header-only file ends with the header instead of trailing margin lines; `esbuild` 0.28.2) and [v2.1.2](https://github.com/CLDMV/fix-headers/releases/tag/v2.1.2) (the repository adopts the shared CLDMV fix-headers config and stamps uniform file headers), shipped without a changelog file; their notes are on their GitHub Releases. +No source file changes, so `dist/`, `bin/` and the `fixHeaders` API are the same as in v2.1.2. The two patch releases before this one, [v2.1.1](./v2.1.1.md) (a header-only file ends with the header instead of trailing margin lines; `esbuild` 0.28.2) and [v2.1.2](./v2.1.2.md) (the repository adopts the shared CLDMV fix-headers config and stamps uniform file headers), originally shipped without a changelog file; their changelogs were added later. --- diff --git a/docs/changelog/v2/v2.1.4.md b/docs/changelog/v2/v2.1.4.md new file mode 100644 index 0000000..cf0ee13 --- /dev/null +++ b/docs/changelog/v2/v2.1.4.md @@ -0,0 +1,79 @@ +# @cldmv/fix-headers v2.1.4 Changelog + +**Release Date**: October 2026 +**Release Type**: Patch +**Branch**: `release/2.1.4` + +--- + +## Overview + +Version 2.1.4 stops fix-headers from damaging files it cannot safely stamp, and fixes two CLI and discovery problems found while rolling it out across the CLDMV repositories: + +- Files whose format has no usable comment syntax are never given a header. Strict JSON (`package.json` included) used to get a JavaScript `/** … */` block that made it invalid, and Markdown named with `--input` got one that rendered as visible text, including in release notes built from changelog files. Markdown now gets a header only when the new `markdown` detector is explicitly forced, and then as an HTML comment. +- `--input` can be repeated, and every value is processed. Before, only the last one was. +- Dependency folders (`node_modules` and friends) are never walked, at any depth, whatever the ignore files say. In a project without a `.gitignore`, a default run used to stamp headers into installed packages. +- `require("@cldmv/fix-headers")` fails with a clear, actionable error on a Node.js version that cannot `require()` an ES module, instead of Node's bare `ERR_REQUIRE_ESM`. + +The header format, the date logic and the `fixHeaders` result for files that do get a header are unchanged. See the upgrade notes for the few behaviour changes that could affect an existing setup. + +--- + +## 🐛 Bug Fixes + +### Never write a comment into a file that cannot carry one; Markdown headers only when forced ([#124](https://github.com/CLDMV/fix-headers/pull/124), fixes [#120](https://github.com/CLDMV/fix-headers/issues/120) and [#122](https://github.com/CLDMV/fix-headers/issues/122)) + +A file now gets a header only when an enabled detector handles its extension. Every other file was previously given a JavaScript `/** … */` block, whatever its format, and is now skipped and left byte-for-byte unchanged, even when named with `--input` or added through `includeExtensions`: + +- **Strict JSON (`.json`).** JSON has no comment syntax, so the old header made the file invalid; on `package.json` every `npm` command in the folder then failed with `ERR_INVALID_PACKAGE_CONFIG`. JSONC, JSON5 and JSONV still get headers. +- **Markdown (`.md`, `.markdown`).** A repo-wide run already skipped Markdown, but naming a file with `--input` stamped it with a JS comment that showed up as text, and in a per-version changelog it would have ended up in the release notes. Markdown is now handled by a new `markdown` detector that never runs unless forced with `--force-detector markdown` (`forcedDetectors: ["markdown"]` in the API or a config file). A forced header is an HTML comment (``), placed below any YAML front matter and updated in place on later runs. Naming a file with `--input`, or listing `markdown` in `--enable-detector`, does not force it. +- **Files with no extension, or an extension no enabled detector handles** (`.txt`, `.toml`, a disabled detector's extensions). These used to get a guessed JS comment, which in an extensionless script could land above the shebang. + +Skipped files are reported: the API result gains `filesSkipped` and `skipped: [{ file, reason }]` (skipped files no longer count in `filesScanned`), and the CLI summary adds `skipped=` plus one `skipped: ()` line per file. The README gains a **Supported file types** table that matches the detectors. + +### Repeated `--input` processes every value ([#125](https://github.com/CLDMV/fix-headers/pull/125), fixes [#119](https://github.com/CLDMV/fix-headers/issues/119)) + +`--input a.mjs --input b.mjs` used to process only `b.mjs` and report `scanned=1`, silently skipping the rest. `--input` is now repeatable and the `input` option accepts a string or an array: every file and folder given is processed, each file once, in the order given. A path that does not exist throws an error naming it. + +### Dependency folders are never walked, at any depth ([#126](https://github.com/CLDMV/fix-headers/pull/126), fixes [#123](https://github.com/CLDMV/fix-headers/issues/123)) + +The discovery walker only ever skipped `.git` by name and relied on the project's ignore files for everything else. In a project with no `.gitignore`, or for a sub-package's own `node_modules`, a default run processed installed packages. `node_modules`, `bower_components`, `jspm_packages`, `.pnpm-store` and `.yarn` are now skipped at any depth regardless of the ignore files or `gitignore: false`; `vendor` is skipped only when Composer (`autoload.php`) or Go (`modules.txt`) created it, since the name is often used for a project's own code. A path inside one of these folders named explicitly with `--include-folder` or `--input` is still processed. This reverses part of [#71](https://github.com/CLDMV/fix-headers/issues/71) (v2.0.0), which had stopped skipping `node_modules` by name; build output (`dist`, `build`, `coverage`) is still processed unless something excludes it. + +### `require()` fails clearly on Node.js without `require(esm)` ([#117](https://github.com/CLDMV/fix-headers/pull/117)) + +Since [v2.0.0](./v2.0.0.md), `dist/index.cjs` is a small wrapper that loads `dist/index.mjs` through Node's synchronous `require(esm)` and returns its default export. On a Node.js version without that feature (before 20.19 on the 20.x line, or before 22.12), the `require()` call inside the wrapper threw a generic `ERR_REQUIRE_ESM` that pointed at the package's own internals and gave no hint of what to do. + +The wrapper now checks `process.features.require_module` before loading the ES module build. When it is missing, it throws an error with the same `ERR_REQUIRE_ESM` code and a message that names the package, the required Node.js versions and the running version, and points at `import()`: + +```text +@cldmv/fix-headers: require() needs Node.js ^20.19.0 or >=22.12.0 (this is v20.18.0). On older Node.js, load the package with import() instead. +``` + +Code that catches `ERR_REQUIRE_ESM` by `code` keeps working. On supported Node.js versions the check passes and `require()` returns the `fixHeaders` function as before. + +## 🧪 Tests + +- New suites for each fix, written to fail before it: non-JS formats and forced Markdown (`tests/non-js-formats.test.vitest.mjs`), repeated `--input` (`tests/repeatable-input.test.vitest.mjs`) and dependency folders (`tests/file-discovery-dependency-folders.test.vitest.mjs`). Coverage stays at 100%. +- New `tests/cjs/entry.test.cjs`, run with `node --test` by a new `test:cjs` script after a fresh `npm run build`. It checks that `require()` of the built package returns the same default export as `import`, and that the version check throws the new error when `require(esm)` is unavailable. +- `npm test` and `npm run coverage` (and so `ci:coverage`) now run `test:cjs` after the Vitest suite, so CI exercises the published CommonJS entry point, not only the source. + +## 📚 Documentation + +- **NEW:** [docs/changelog/v2/v2.1.4.md](./v2.1.4.md): this changelog. +- **NEW:** changelog files for every earlier release that shipped without one: v1.0.0 to v1.3.12 in [docs/changelog/v1/](../v1/), plus [v2.1.1](./v2.1.1.md) and [v2.1.2](./v2.1.2.md). +- README restructured to the CLDMV layout: badges, **What's New**, Key Features, Installation with Node.js requirements, Quick Start, then the existing usage, API and configuration reference. + +## 🔧 Dependencies + +_No dependency updates._ `ignore`, the only runtime dependency, is unchanged. + +--- + +## Upgrade notes + +No changes are needed for the usual setup (source files plus a config). The behaviour changes that could affect an existing setup: + +- **Files that used to get a JS comment are now skipped:** strict `.json`, Markdown named with `--input`, files with no extension, and extensions no enabled detector handles (for example a `.txt` added through `includeExtensions`). If you relied on that, the header was being written in the wrong comment syntax; for Markdown, use `--force-detector markdown`. +- **Dependency folders are no longer processed**, even with `gitignore: false`. To stamp something inside one on purpose, name it with `--include-folder` or `--input`. +- **`filesScanned` no longer counts skipped files**; read `filesSkipped` and `skipped` for those. +- On Node.js versions without `require(esm)`, `require()` fails with a clearer message and the same `ERR_REQUIRE_ESM` code.