conventional-changelog-angular 版本冲突导致 CHANGELOG 格式差异分析
2026-04-17 当前状态补充:
- 本文分析的是 2026-04-09 根包仍依赖
conventional-changelog-cli时的依赖冲突事故。- 当前仓库现行根发版链路已经切换为
bumpp + changelogen,不再把conventional-changelog-cli或changelog:conventional-changelog用作正式根发版方案。- 因此,下文的依赖树、对照实验和修复建议仅作为历史排障背景保留,不再代表当前仓库的正式实现。
问题现象
两个项目使用完全相同的命令生成 CHANGELOG:
conventional-changelog -p angular -i CHANGELOG.md -s但产出的标题格式完全不同:
| 项目 | 实际输出 |
|---|---|
01s-11comm | ## <small>0.11.3 (2026-04-09)</small> |
eams-component-lib | ## [1.0.7](https://github.com/.../compare/v1.0.6...v1.0.7) (2026-04-09) |
根因结论
<small> 标签不来自任何版本的 conventional-changelog-angular。所有版本(6.0.0、7.0.0、8.3.0、8.3.1)的 headerPartial 模板完全相同,都是 link 链接格式。
<small> 来自 conventional-changelog-writer 包的内置默认 fallback 模板,在 angular preset 加载失败时被启用。
失败原因:pnpm shamefully-hoist=true 将旧版 angular@6.0.0(由 commit-and-tag-version 引入)提升到根 node_modules,遮蔽了 CLI 实际需要的 angular@8.3.0。新版 preset-loader 无法识别旧版 API,导致 preset 静默加载失败。
两个项目的完整依赖链路
01s-11comm 依赖树
@01s-11comm/root@0.11.3
│
├─ @01s-11comm/admin (workspace link)
│ └─ @commitlint/cli@19.8.1
│ └─ @commitlint/parse@19.8.1
│ └─ conventional-changelog-angular@7.0.0
│
├─ commit-and-tag-version@12.6.1 ← 引入旧版 angular 的元凶
│ ├─ conventional-changelog@4.0.0
│ │ ├─ conventional-changelog-angular@6.0.0 ← 旧版!被 hoist 到根
│ │ ├─ conventional-changelog-core@5.0.2
│ │ │ └─ conventional-changelog-writer@6.0.1
│ │ └─ conventional-changelog-preset-loader@3.0.0
│ └─ conventional-recommended-bump@7.0.1
│ └─ conventional-changelog-preset-loader@3.0.0
│
├─ commitlint@20.5.0
│ └─ @commitlint/cli@20.5.0
│ └─ @commitlint/parse@20.5.0
│ └─ conventional-changelog-angular@8.3.0
│
└─ conventional-changelog-cli@5.0.0 ← 用户执行的命令入口
└─ conventional-changelog@6.0.0
├─ conventional-changelog-angular@8.3.0 ← 期望使用的版本
├─ conventional-changelog-core@8.0.0
│ └─ conventional-changelog-writer@8.4.0 ← <small> 默认模板在这里
└─ conventional-changelog-preset-loader@5.0.0eams-component-lib 依赖树
@eams-monorepo/root@1.0.7
│
├─ @commitlint/cli@19.8.1
│ └─ @commitlint/parse@19.8.1
│ └─ conventional-changelog-angular@7.0.0
│
└─ conventional-changelog-cli@5.0.0 ← 用户执行的命令入口
└─ conventional-changelog@6.0.0
├─ conventional-changelog-angular@8.3.1 ← 唯一的高版本,被 hoist 到根
├─ conventional-changelog-core@8.0.0
│ └─ conventional-changelog-writer@8.4.0
└─ conventional-changelog-preset-loader@5.0.001s-11comm 的包混装现象
新旧链路交叉污染
01s-11comm 的核心问题不是"没有装 angular@8",而是"装了但 loader@5 运行时没命中它"。
仓库中同时存在两代 changelog 链路:
- 新链路:
conventional-changelog-cli@5→conventional-changelog@6→loader@5→ 期望angular@8.3.0 - 旧链路:
commit-and-tag-version@12.6.1→conventional-changelog@4→loader@3→angular@6.0.0
旧链路的 angular@6.0.0 被 pnpm shamefully-hoist 提升到根 node_modules,新链路的 loader@5 在运行时 import('conventional-changelog-angular') 向上解析,命中了根目录的旧版本。
包混装关系图
graph TB
subgraph ROOT["01s-11comm 根 node_modules (shamefully-hoist)"]
R1A["root conventional-changelog<br/>4.0.0"]
R1B["root angular preset<br/>6.0.0"]
end
subgraph NEW["新 changelog 链路 (用户执行的命令)"]
N1A["conventional-changelog-cli 5.0.0"]
N1B["conventional-changelog 6.0.0"]
N1C["preset-loader 5.0.0"]
N1D["expected angular 8.3.0"]
N1A --> N1B --> N1C --> N1D
end
subgraph OLD["旧 release 链路 (commit-and-tag-version)"]
O1A["commit-and-tag-version 12.6.1"]
O1B["conventional-changelog 4.0.0"]
O1C["preset-loader 3.0.0"]
O1D["legacy angular 6.0.0"]
O1A --> O1B
O1A --> O1C
O1A --> O1D
end
O1B -. "hoist source" .-> R1A
O1D -. "hoist source" .-> R1B
N1C -. "resolved at runtime" .-> R1B
N1D -. "not chosen" .-> R1E["runtime still picks root 6.0.0"]
R1E --> R1B
style R1B fill:#f96,stroke:#c60
style O1D fill:#f66,stroke:#900,color:#fff
style N1D fill:#6c6,stroke:#090,color:#fff
style R1E fill:#fce,stroke:#c66逐步解读
- 旧链路提供 hoist 源:
commit-and-tag-version@12.6.1依赖conventional-changelog@4.0.0,后者再依赖conventional-changelog-angular@6.0.0。由于shamefully-hoist=true,angular@6.0.0被提升到根node_modules/conventional-changelog-angular - 新链路期望使用 angular@8.3.0:
conventional-changelog-cli@5→conventional-changelog@6→angular@8.3.0存在于 pnpm 的.pnpm虚拟存储中,但未被提升到根 - 运行时解析指向旧版:
preset-loader@5.0.0执行import('conventional-changelog-angular')时,Node.js 模块解析从preset-loader的物理路径向上查找node_modules,最终命中根目录的angular@6.0.0 - API 不兼容:
angular@6.0.0导出 Promise(object),loader@5.0.0要求 function,类型检查失败
eams-component-lib 为什么没有此问题
graph TB
subgraph ROOT_E["eams 根 node_modules (shamefully-hoist)"]
RE1["root angular preset<br/>8.3.1"]
end
subgraph CHAIN_E["唯一的 changelog 链路"]
E1A["conventional-changelog-cli 5.0.0"]
E1B["conventional-changelog 6.0.0"]
E1C["preset-loader 5.0.0"]
E1D["angular 8.3.1"]
E1A --> E1B --> E1C --> E1D
end
E1D -. "hoist source" .-> RE1
E1C -. "resolved at runtime" .-> RE1
style RE1 fill:#9c6,stroke:#690
style E1D fill:#6c6,stroke:#090,color:#fffeams-component-lib 没有安装 commit-and-tag-version,不存在旧链路。唯一的 angular 来源是 conventional-changelog@6.0.0 依赖的 angular@8.3.1,它被正确 hoist 到根,loader@5 运行时命中的就是它——新 loader + 新 preset,API 兼容,加载成功。
两仓对照
graph TB
subgraph TOP["eams-component-lib"]
direction LR
EA1["loader 5"]
EA2["root angular 8.3.1"]
EA3["function export"]
EA4["writer override works"]
EA5["compare-link heading"]
EA1 --> EA2 --> EA3 --> EA4 --> EA5
end
subgraph BOTTOM["01s-11comm"]
direction LR
OB1["loader 5"]
OB2["root angular 6.0.0"]
OB3["promise object export"]
OB4["function check fails"]
OB5["fallback to writer default"]
OB1 --> OB2 --> OB3 --> OB4 --> OB5
end
EA5 ~~~ OB1
style EA2 fill:#6c6,stroke:#090,color:#fff
style EA4 fill:#6c6,stroke:#090,color:#fff
style EA5 fill:#9c6,stroke:#690
style OB2 fill:#f96,stroke:#c60
style OB4 fill:#f66,stroke:#900,color:#fff
style OB5 fill:#f66,stroke:#900,color:#fffMermaid 流程图:依赖解析与加载对比
图 1:两个项目的 angular 版本 hoist 对比
graph TB
subgraph "01s-11comm 根 node_modules (shamefully-hoist=true)"
ROOT_A["node_modules/conventional-changelog-angular<br/>→ <b>6.0.0</b> (被 hoist)"]
end
subgraph "01s-11comm 依赖来源"
CTV["commit-and-tag-version@12.6.1"]
CL4["conventional-changelog@4.0.0"]
A6["angular@6.0.0<br/>exports: Promise (object)"]
CTV --> CL4 --> A6
CLI5_A["conventional-changelog-cli@5.0.0"]
CL6_A["conventional-changelog@6.0.0"]
A8_A["angular@8.3.0<br/>exports: function"]
CLI5_A --> CL6_A --> A8_A
end
A6 -. "被 hoist 到根" .-> ROOT_A
A8_A -. "被遮蔽,未 hoist" .-x ROOT_A
subgraph "eams-component-lib 根 node_modules (shamefully-hoist=true)"
ROOT_B["node_modules/conventional-changelog-angular<br/>→ <b>8.3.1</b> (被 hoist)"]
end
subgraph "eams 依赖来源"
CLI5_B["conventional-changelog-cli@5.0.0"]
CL6_B["conventional-changelog@6.0.0"]
A8_B["angular@8.3.1<br/>exports: function"]
CLI5_B --> CL6_B --> A8_B
end
A8_B -. "唯一高版本,被 hoist" .-> ROOT_B
style A6 fill:#f66,stroke:#900,color:#fff
style A8_A fill:#6c6,stroke:#090,color:#fff
style A8_B fill:#6c6,stroke:#090,color:#fff
style ROOT_A fill:#f96,stroke:#c60
style ROOT_B fill:#9c6,stroke:#690图 2:preset-loader 加载流程与失败路径
flowchart TD
START["conventional-changelog -p angular"]
LOAD["preset-loader@5.0.0<br/>import('conventional-changelog-angular')"]
START --> LOAD
LOAD --> RESOLVE{"Node.js 模块解析<br/>从 preset-loader 目录开始<br/>向上查找 node_modules"}
subgraph "01s-11comm 路径"
RESOLVE -->|"找到根 node_modules"| FOUND_A["解析到 angular@6.0.0"]
FOUND_A --> CHECK_A{"typeof default export?"}
CHECK_A -->|"object (Promise)"| FAIL["❌ 不是 function<br/>抛出错误"]
FAIL --> FALLBACK["config = {} 空对象<br/>angular headerPartial 未注入"]
FALLBACK --> WRITER_DEFAULT["writer@8.4.0 使用自己的<br/>默认 headerPartial"]
WRITER_DEFAULT --> SMALL["输出: ## <small>version (date)</small>"]
end
subgraph "eams-component-lib 路径"
RESOLVE -->|"找到根 node_modules"| FOUND_B["解析到 angular@8.3.1"]
FOUND_B --> CHECK_B{"typeof default export?"}
CHECK_B -->|"function"| SUCCESS["✅ 调用 createPreset()<br/>获得 preset 配置"]
SUCCESS --> INJECT["preset.writer.headerPartial<br/>注入到 writerOpts"]
INJECT --> ANGULAR_TPL["writer 使用 angular 的<br/>headerPartial 模板"]
ANGULAR_TPL --> LINK["输出: ## [version](compare-url) (date)"]
end
style FAIL fill:#f66,stroke:#900,color:#fff
style SMALL fill:#f96,stroke:#c60
style SUCCESS fill:#6c6,stroke:#090,color:#fff
style LINK fill:#9c6,stroke:#690图 3:API 版本断代详解
graph LR
subgraph "旧 API (angular ≤ 7.0.0)"
OLD_EXPORT["module.exports = Promise.all([<br/> conventionalChangelog,<br/> parserOpts,<br/> recommendedBumpOpts,<br/> writerOpts<br/>])"]
OLD_LOADER["preset-loader@3.0.0<br/>能处理 Promise/Object"]
OLD_EXPORT --> OLD_LOADER
OLD_LOADER --> OLD_OK["✅ 正常工作"]
end
subgraph "新 API (angular ≥ 8.0.0)"
NEW_EXPORT["export default function createPreset() {<br/> return {<br/> commits, parser,<br/> writer, whatBump<br/> }<br/>}"]
NEW_LOADER["preset-loader@5.0.0<br/>只接受 function"]
NEW_EXPORT --> NEW_LOADER
NEW_LOADER --> NEW_OK["✅ 正常工作"]
end
subgraph "❌ 版本交叉冲突"
CROSS_OLD["angular@6.0.0<br/>exports: Promise (object)"]
CROSS_NEW["preset-loader@5.0.0<br/>期望: function"]
CROSS_OLD --> CROSS_NEW
CROSS_NEW --> CROSS_FAIL["❌ typeof !== function<br/>加载失败"]
end
style OLD_OK fill:#9c6,stroke:#690
style NEW_OK fill:#9c6,stroke:#690
style CROSS_FAIL fill:#f66,stroke:#900,color:#fff三个 headerPartial 模板对比
angular 的 headerPartial(所有版本相同,6.0.0 / 7.0.0 / 8.3.x)
渲染结果:## [1.0.7](https://github.com/.../compare/v1.0.6...v1.0.7) (2026-04-09)
writer@8.4.0 的默认 headerPartial(fallback 模板)
渲染结果:## <small>0.11.3 (2026-04-09)</small>
对比要点
| 特征 | angular headerPartial | writer 默认 headerPartial |
|---|---|---|
| patch 版本标记 | ##(h2) | ## <small>...</small> |
| 非 patch 版本标记 | #(h1) | ##(始终 h2) |
| 版本号格式 | [version](compare-url) link 链接 | 纯文本 version |
<small> 标签 | 无 | patch 版本时包裹 |
| commit 分组标题 | 有(Features / Bug Fixes 等) | 无 |
实验验证结果
在两个项目根目录分别执行:
node --input-type=module -e "
const m = await import('conventional-changelog-angular');
console.log(typeof m.default);
"| 项目 | 解析到的版本 | typeof m.default | preset 加载结果 |
|---|---|---|---|
01s-11comm | angular@6.0.0 | object | ❌ 失败 |
eams-component-lib | angular@8.3.1 | function | ✅ 成功 |
修复建议
方案 A:移除 commit-and-tag-version
当前项目已使用 bumpp + relizy 作为发版方案,commit-and-tag-version 已不再使用:
pnpm remove commit-and-tag-version -w移除后,angular@6.0.0 不再存在于依赖树中,pnpm 会将 angular@8.3.0 正确 hoist 到根目录。
方案 B:使用 pnpm overrides 强制版本
在根 package.json 中添加:
{
"pnpm": {
"overrides": {
"conventional-changelog-angular": "^8.3.0"
}
}
}强制所有依赖树中的 conventional-changelog-angular 使用 8.x 版本。
推荐
在本文分析所对应的历史阶段,方案 A 更彻底 —— 移除已废弃的工具,消除依赖冲突根源。
从当前仓库状态看,这条根因链已经被继续收敛:根包正式发版不再依赖 conventional-changelog-cli,而是改为 bumpp + changelogen。