Discuss what we should do if there are multiple `cz.*` files
Action Items
- Survey how other tools with multiple configuration files work when they detect multiple configuration files.
- For example, the tool
prettiercan be configured by.prettier.rc,.prettier.json, etc. (if I remembered it correctly)
- For example, the tool
- Decide next steps based on the survey and discussion.
Reference
I wonder should we throw a warning if there are multiple cz.* file. Not sure how other tools like eslint or prettier handles multiple configuration file edge case.
Originally posted by @bearomorphism in https://github.com/commitizen-tools/commitizen/pull/1657#discussion_r2573482065
Pasting the response from ChatGPT. Haven't validated yet.
They both use a “find nearest, then merge with clear precedence” strategy, but with some differences. I’ll break it down and then extract general design lessons you can copy for your own tool.
1. ESLint: how config resolution works
1.1 Where ESLint looks for config
ESLint looks for config starting from the directory of the file being linted and walking upward, in roughly this order:
-
eslint.config.js/eslint.config.mjs/eslint.config.cjs(new “flat config” system) -
Or older-style “eslintrc” files:
.eslintrc.js.eslintrc.cjs.eslintrc.yaml/.eslintrc.yml.eslintrc.jsonpackage.json→"eslintConfig"field
It stops when:
- It hits a config with
"root": true(in old config), or - It reaches the filesystem root.
1.2 Multiple configs: how they’re merged
In the old eslintrc system (still widely used):
- ESLint merges configs up the directory tree.
- The config closest to the file has highest precedence.
- Parents contribute defaults; children override.
Rough rules:
-
Scalars and simple keys (e.g.
parser, single rule values) → “nearest wins”. Child config overrides parent. -
Objects like
rules→ Keys are merged, with child keys overriding parent rules:// parent { "rules": { "eqeqeq": "error", "no-console": "warn" } } // child { "rules": { "no-console": "off" } } // effective { "rules": { "eqeqeq": "error", "no-console": "off" } } -
Arrays like
extends→ They’re effectively concatenated, but the later/nearest config has precedence for overlapping rules (since the presets are processed in order and later ones override earlier). -
Overrides / ignore patterns
overridescan be defined in multiple levels; ESLint computes whichfilespatterns match the file and merges all applicable override blocks.- Ignore files (
.eslintignoreorignorePatterns) are also combined, with more specific ones taking effect nearer to the file.
In the new flat config system (eslint.config.js), the mental model is:
- You export an array of config objects.
- ESLint walks that array in order and applies any configs whose
filesmatcher includes the current file. - Later entries override earlier ones. So precedence is array order, not directory walk order.
So: ESLint mostly does multi-source merging, not “first one wins and stop”.
2. Prettier: how config resolution works
Prettier’s behavior is simpler (and more “one file wins”).
2.1 Where Prettier looks
Prettier searches upward from the file’s directory for the first config file:
.prettierrc.prettierrc.json/.prettierrc.yml/.prettierrc.yaml.prettierrc.js/.prettierrc.cjsprettier.config.js/prettier.config.cjspackage.json→"prettier"field
It stops when it finds one and does not merge with configs above that.
So if you have:
repo/
.prettierrc // {"semi": true}
packages/
pkg-a/
.prettierrc // {"singleQuote": true}
src/file.ts
For src/file.ts, only pkg-a/.prettierrc is used. The root .prettierrc is ignored. If pkg-a/.prettierrc is missing, then it falls back to repo/.prettierrc.
2.2 Multiple config sources in one place
Within a single folder, you could theoretically have multiple valid locations (e.g. .prettierrc and package.json "prettier"), but Prettier picks them with a priority order and stops at the first match. It does not merge between them.
So Prettier is basically:
Nearest config file wins. No cross-file merging.
3. Design lessons for your own tool
When you “have multiple configuration files,” there are actually two different cases to clarify:
- Multiple config files in different directories on a path upward
- Multiple config files in the same directory / for the same scope
You should decide:
- Do you want deep merging (ESLint-style) or single winner (Prettier-style)?
- How do you define precedence?
Here are some commonly used patterns you can copy.
3.1 Directory-chain merging (ESLint-style)
Behavior: collect all configs from filesystem root down to file’s directory; merge them with “closest wins”.
Algorithm sketch:
- From file’s directory, walk up toward root.
- At each directory, see if there’s a config file (or
package.jsonwith your key). - Build a list:
[rootConfig, ..., nearestConfig]. - Optionally stop when a config has
root: true. - Merge them left-to-right.
Merging strategy:
-
Scalars: nearest overrides (last-writer wins).
-
Objects: shallow or deep merge with last-writer wins per key.
-
Arrays: either
- concatenate, or
- override (nearest replaces parent), depending on your semantics.
Pros:
- Lets teams define org-wide defaults and override per-package.
- Feels natural for monorepos and nested packages.
Cons:
- Harder for users to reason about if merging is complex.
- Debugging “where did this value come from?” can be tricky.
3.2 Nearest-only config (Prettier-style)
Behavior: search upwards, use the first config file, ignore others.
Algorithm sketch:
- From file’s directory, walk up.
- At each directory, check your priority list of config file names.
- Use the first file you find and stop.
Pros:
- Super easy to explain: “tool uses the closest config.”
- No complex merging logic; configs are self-contained.
Cons:
- No hierarchical defaults; you must repeat shared settings.
- Less flexible in large monorepos.
3.3 Same-directory conflicts
What if the same directory has multiple valid config files (e.g. tool.config.json and tool.config.js)?
You can:
-
Define a clear priority list, like:
tool.config.mjs > tool.config.cjs > tool.config.js > .toolrc > package.json:"tool" -
Pick the first one in that priority and ignore the rest.
-
Optionally, warn if multiple exist (“Found both
tool.config.jsand.toolrc. Usingtool.config.jsand ignoring.toolrc.”)
This is effectively what both ESLint and Prettier do: there’s an ordered list.
3.4 Per-file vs global cli options
Don’t forget how CLI flags interact:
-
Usually CLI flags have the highest precedence (override config).
-
If a user explicitly passes
--config path/to/custom.config, you normally:- Load only that config, or
- Treat it as the base and optionally still merge directory configs (but document clearly).
3.5 A practical design template you can steal
If you want something ergonomic & unsurprising, I’d suggest:
Option A – ESLint-ish (hierarchical, powerful)
-
Resolution:
- Start from file dir → walk up.
- In each dir, check for config files in priority order.
- Collect all found configs until root or
root: true.
-
Precedence:
- Merge configs from top → bottom.
- Later (closer) configs override earlier ones.
-
Merge rules:
-
Objects: deep merge, child overrides keys.
-
Arrays:
- For “list of plugins” / “rules”: concatenate.
- For things like
ignorePatterns: concatenate. - For special arrays, you can tag them as
override: trueif needed.
-
-
CLI:
--config= use only that config file (no searching), or document if you still merge.
Option B – Prettier-ish (simple, predictable)
-
Resolution:
- Start from file dir → walk up.
- Use the nearest config file (based on priority order).
- Don’t merge with parents.
-
CLI:
--config= use that config file and don't search upward.
-
If multiple config formats in same directory:
- Respect an explicit priority order; optionally warn on conflicts.
4. Edge cases to explicitly think about
Since you mentioned edge cases, here are the main ones worth deciding now:
-
Multiple config files in the same folder
- Exact priority order.
- Whether to log a warning.
-
Both project-level and user-level config
- Do you support a global config in
~/.toolrc? - Does project config override global config (usually yes)?
- Do you support a global config in
-
root/ “stop here” flag- Will you support a
root: trueor similar flag to stop upward search?
- Will you support a
-
Config for non-file inputs
- If your tool can run without specific files (e.g.
tool .), from which directory do you start the search? CWD? Repo root detection?
- If your tool can run without specific files (e.g.
-
Monorepos & workspaces
-
If a monorepo has
.toolrcat root and per-package configs, do you want:- root as default + per-package overrides (ESLint style), or
- per-package only (Prettier style)?
-
If you tell me:
- what your tool does, and
- whether you prefer “simple & explicit” vs “flexible & hierarchical”,
I can sketch a concrete config-resolution spec (including pseudo-code and examples) that’s very close to ESLint/Prettier semantics but tailored to your use case.