关于 fix-knip-unused-exports
用于处理 Knip“未使用导出”告警的开发技能,先分类问题,再选择删除导出、移除重导出或抽取到新文件等修复方式。它还指导检查跨包引用、循环依赖及相关 lint 规则。
主要功能
- 01识别并分类未使用导出
- 02按类别选择对应修复策略
- 03将仅供测试使用的代码抽取到新文件
- 04检查 barrel 重导出与包子路径
- 05验证 Knip、类型检查和测试
适用场景
- 01修复 `npm run knip` 报告的未使用导出
- 02整理仅被测试引用的函数或类
- 03移除未使用的 `index.ts` 重导出
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ factory-ai/ factory-plugins --skill fix-knip-unused-exports安装后在 Claude Code 中输入/fix-knip-unused-exports确认已加载。
详细介绍
修复 Knip 未使用的导出
修复 Knip 的“未使用的导出”违规。有多种违规类别,每种都有不同的修复策略。
适用情况
npm run knip报告“未使用的导出”
不适用情况
- 该导出被另一个文件中的非测试生产代码使用——说明还有其他问题
工作流程
1. 找出违规项
npm run knip
输出如下:
Unused exports (3)
::error file=packages/foo/src/bar.ts,line=42,title=Unused exports::myFunction
2. 对每项违规分类
对于每个被标记的导出,使用 grep 搜索整个仓库(而不仅是当前包):
rg "myFunction"
确定其所属类别:
| 类别 | 调用方 | 修复方法 |
|---|---|---|
| 仅供测试使用的导出 | 仅在同一文件和测试文件中使用 | 提取到新文件 |
| 无效的 barrel 重新导出 | 从 index.ts 重新导出,但生产代码通过相对路径或其他子路径导入 | 从 barrel 中移除该重新导出 |
| 仅在内部使用的导出 | 仅在同一文件中使用,测试或其他文件均未使用 | 移除 export 关键字 |
| 死代码 | 没有任何调用方 | 删除该导出 |
| 存在生产代码使用方 | 被另一个文件中的非测试代码使用 | 不是 Knip 问题——进一步调查 |
重要提示:使用 grep 时,排除测试文件,以便找出生产代码使用方:
rg "myFunction" --glob '!**/*.test.*'
修复:仅供测试使用的导出(提取到新文件)
当某个函数仅为测试访问而导出,但也在同一文件中供内部使用时。
规划提取
编写代码前,先回答以下问题:
a) 哪些内容要移到新文件?
- 被标记的导出函数、类或常量
- 它所依赖的所有私有辅助函数
- 它所依赖的所有私有常量和类型
b) 是否有辅助函数会被保留在原文件中的函数共用?
- 如果有,该辅助函数必须从新文件导出,并由原文件导入
- 这意味着新文件会有 2 个或更多导出(对任何文件名与导出名称匹配的 lint 规则来说都没问题)
c) 新文件是否恰好导出一个函数?
- 如果项目强制执行
filename-match-exportlint 规则,文件名必须与该导出函数同名:myFunction.ts - 如果文件导出 2 个或更多函数,文件名可以灵活选择
d) 是否存在名称匹配的测试文件?
- 如果
bar.ts保留且存在bar.test.ts,测试必须仍从./bar导入内容(前提是项目强制执行test-imports-source规则) - 如果删除了
bar.ts(所有内容都已移出),该规则通常只适用于存在名称匹配的源文件时
e) 是否存在循环依赖风险?
- 绘制导入关系图:新文件 -> 原文件 -> 新文件会形成循环
- 修复方法:将共享依赖移到新文件或第三个文件中
f) 是否导出了常量?
- 如果项目强制执行
constants-file-organizationlint 规则,导出的常量必须放在名为constants.ts的文件中 - 如果提取出的函数依赖某个原文件中其他函数也会使用的常量,请勿从新文件导出该常量。应改为调用函数(例如,将
BUDGET[effort]替换为getBudget(effort)),以免需要单独的constants.ts
执行提取
在同一目录中创建新文件:
// myFunction.ts (new file)
import { SomeType } from '../types';
function privateHelper(): void { /* ... */ }
export function myFunction(): SomeType {
return privateHelper();
}
更新原文件,使其从新文件导入:
// bar.ts (original file, updated)
import { myFunction } from './myFunction';
function otherFunction() {
const result = myFunction(); // Now imports from new file
}
更新测试文件,使其从新文件导入:
// bar.test.ts (updated)
import { myFunction } from './myFunction';
// If bar.ts still exists, you may need to also import something from './bar'
// to satisfy any test-imports-source rule
留意连锁违规
提取后,再次运行 npm run knip。如果函数 A 与它调用的函数 B 一起被提取到新文件中,但 B 在外部也仅由测试使用,Knip 仍会标记 B。此时需要将 B 提取到自己的文件中,使 A 所在文件对 B 形成真正的生产代码导入。
例如,假设先将 throwMappedError 与 mapResponseFailure 一起提取到 error-mappers.ts。如果 throwMappedError 只在该文件内部被调用(由 mapResponseFailure 调用),它仍会被标记。修复方法:将它提取到 throwMappedError.ts,使 error-mappers.ts 对它的导入成为真正的生产代码使用。
修复:无效的 Barrel 再导出(从 index.ts 中移除)
如果 barrel index.ts 再导出了某项内容,但没有任何生产代码通过该 barrel 导入它,就会出现这种情况。具体包括:
- 同一软件包中的生产代码使用相对导入(例如,
import { x } from './source'),而不是通过 barrel 导入 - 其他软件包中的生产代码直接从子路径导入(例如,
@scope/pkg/feature/handlers),而不是通过 barrel 导入 - 再导出是推测性添加的,但从未被使用
如何识别
使用 grep 搜索,但排除测试文件。如果唯一的命中项是:
- barrel
index.ts本身 - 在同一软件包中使用相对导入的源文件
- 测试文件
那么该 barrel 再导出未被使用。只需将它从 index.ts 中移除。
跨软件包测试导入
如果另一个软件包中的测试通过 barrel 导入符号(例如,import { x } from '@scope/pkg/feature'),移除 barrel 再导出后,你需要提供替代导入路径:
-
在源软件包的
package.json中添加子路径导出:{ "exports": { "./feature": "./src/feature/index.ts", "./feature/doSomething": "./src/feature/doSomething.ts" } } -
更新测试,使其从新的子路径导入:
import { doSomething } from '@scope/pkg/feature/doSomething';
此模式遵循 monorepo 中常见的子路径导出约定。
修复:仅在内部使用的导出(取消导出)
如果某项导出只在同一文件内使用,且没有被其他任何内容(甚至测试)导入,只需移除 export 关键字:
// Before
export const MySchema = z.object({ ... });
// After
const MySchema = z.object({ ... });
对于仅在同一文件中作为其他 schema 的构建模块使用的 Zod schema,这种情况很常见。
验证
对受影响的软件包运行以下所有检查:
# Knip passes (the whole point)
npm run knip
# Types still compile
npm run typecheck
# Tests still pass
npm run test
# Lint passes (catches filename-match-export, test-imports-source, constants-file-organization, etc.)
npm run lint
如果存在跨软件包导入,也要验证使用该导入的软件包。
相关 Lint 规则
许多 TypeScript monorepo 会在 knip 之上叠加额外的自定义 lint 规则。请根据项目使用的规则调整以下修复方法。
filename-match-export(或类似规则)
如果一个文件恰好导出了一个函数(不是 React 组件),文件名必须与函数名一致。
loadConfig.ts中的export function loadConfig—— 通过helpers.ts中的export function loadConfig—— 不通过helpers.ts中有两个导出 —— 规则不适用(有多个导出)
test-imports-source(或类似规则)
如果 foo.test.ts 和 foo.ts 同时存在,测试必须从 ./foo 导入。
- 类似
import { x } from './foo'的导入满足此规则 - 通常也接受从
'.'或'./index'导入,前提是index.ts从foo.ts再导出 - 如果删除了
foo.ts,则此规则不适用
constants-file-organization(或类似规则)
导出的常量必须定义在名为 constants.ts 的文件中。
- 如果提取的函数依赖共享常量,请勿从函数所在文件导出该常量
- 应改用函数调用替代直接访问常量(例如,将
BUDGET[effort]改为getBudget(effort)) - 或将常量移至
constants.ts文件
Knip 如何追踪导出
- Knip 会忽略测试文件(
**/*.test.*、**/*.spec.*) knip.json中的ignoreIssues会抑制所列文件上的警告,但不会使源导出变为“已使用”- 带有
ignoreIssues的index.ts中的 barrel 再导出(export { x } from './source')不计作源导出的使用 - 只有来自项目中非测试、非忽略文件的实际导入才会被计为使用
includeEntryExports: true(如果已设置)表示也会检查入口文件中的导出,因此迁移、脚本等入口点风格的文件可能需要显式设置ignoreIssues
软件包子路径导出
移除跨软件包测试所依赖的 barrel 再导出时,请在 package.json 中添加子路径导出:
{
"exports": {
"./feature": "./src/feature/index.ts",
"./feature/doSomething": "./src/feature/doSomething.ts"
}
}
不要做的事
- 除非文件是真正的入口脚本(迁移脚本、CLI),否则不要将其添加到
knip.json的ignoreIssues中 - 不要为了减少导出而把所有函数合并到一个文件中——从 knip 的角度来看,在同一文件中使用导出不算作使用
- 如果测试需要
export关键字,不要将其移除——否则测试会失败 - 不要在新文件和原始文件之间创建循环导入
- 如果项目强制执行
constants-file-organizationlint 规则,不要从非constants.ts文件中导出常量