dependency-hygiene
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDependency hygiene
依赖治理
Every dependency is a permanent liability someone else may have to service. Optimise for resolves and builds years from now, not for latest. This skill governs pubspec/lock mechanics, the gate a new package must pass, and the escape hatch when a critical package rots.
Read the reference for the task at hand:
- — the refuse/accept gate, transitive auditing, licence recording, upgrade discipline.
references/dependency-gate-and-audit.md - — recording the SDK version and the silent-lint-disable trap on any SDK bump.
references/sdk-pin-and-lint-include.md - — vendoring a bus-factor-1 native plugin into
references/vendoring-behind-an-interface.mdwithout touchingthird_party/.lib/
Run before a PR that changes .
scripts/audit-deps.shpubspec.yaml每个 dependency 都是一项永久的负担,未来可能需要由他人来维护。优化目标应是多年后仍能解析并构建,而不是追求 latest。本 skill 管理 pubspec/lock 机制、新 package 必须通过的 gate,以及关键 package 出现问题时的应急方案。
针对当前任务阅读相应的 reference:
- —— 拒绝/接受 gate、传递依赖审计、许可证记录和升级纪律。
references/dependency-gate-and-audit.md - —— 记录 SDK 版本,以及 SDK 升级时可能静默禁用 lint 的陷阱。
references/sdk-pin-and-lint-include.md - —— 在不改动
references/vendoring-behind-an-interface.md的情况下,将 bus-factor-1 原生 plugin vendor 到lib/。third_party/
在提交会更改 的 PR 之前,运行 。
pubspec.yamlscripts/audit-deps.shNon-negotiable rules
不可妥协的规则
- Caret ranges in , exact pins only in
pubspec.yaml.pubspec.lock, neverdrift: ^2.31.0. The lock pins; ranges only keep resolution solvable. Exact pins in a pubspec manufacture unsolvable conflicts on the next SDK bump and buy nothing the lock is not already delivering.drift: 2.31.0 - Commit . This is an application, not a package — the asymmetry that flips the rule. The default Dart
pubspec.locktemplate lists.gitignore; delete that line. The committed lock is the only thing that makes a stranger'spubspec.lockresolve the exact versions that were tested on a real device.git clone - A diff without its
pubspec.yamldelta is incomplete. Runpubspec.lockand stage the lock in the same commit — add, upgrade, and remove alike.flutter pub get - Record the SDK version separately from the tool. Keep a real range so
environment: sdk:can solve; pin the exact tested Flutter version in a committed record CI reads (see reference). The record is what a stranger and CI read; do not confuse a version-manager tool with the file.pub - The lint include filename is coupled to the resolved SDK. A version-pinned that names a file absent from the resolved linter package emits
include:— fatal to defaultinclude_file_not_found/dart analyze, so standard CI catches it. It only goes silent-and-green where warnings are made non-fatal, and even then it is your ruleset's added/promoted rules that stop applying, not the analyzer's built-ins. Verify it after any SDK bump (see reference).flutter analyze - Audit the transitive tree before adding, not the pubspec. The second hop is exactly where a banned SDK arrives. Run (or
scripts/audit-deps.sh+ the audit script) before committing a new dependency.dart pub deps --json - Refuse by policy anything that opens a network path, reports crashes/usage, drags in a telemetry core, or collects device identifiers for an unshipped feature — directly or transitively. Green CI is not evidence a native capability works. See the gate reference.
- Grep after removing a dependency. A package deleted from
lib/but still transitively resolvable keeps compiling today and breaks on the clone that matters. The analyzer catches an unresolved import, not one that resolves by accident.pubspec.yaml - Wrap a bus-factor-1 native plugin behind an interface now, vendor later. The interface is cheap insurance; pre-emptive vendoring is a maintenance burden against a break that has not happened. See the vendoring reference.
- 在 中使用 caret range,仅在
pubspec.yaml中使用精确 pin。 使用pubspec.lock,绝不要写成drift: ^2.31.0。由 lock 负责 pin;range 只用于让依赖解析保持可解。在 pubspec 中使用精确 pin,会在下一次 SDK 升级时制造无法解决的冲突,而且 lock 已经提供了同等作用,精确 pin 没有任何额外收益。drift: 2.31.0 - 提交 。 这是一个 application,而不是 package——正是这种差异决定了规则相反。默认 Dart
pubspec.lock模板会列出.gitignore;请删除该行。已提交的 lock 是唯一能确保陌生开发者执行pubspec.lock后解析出真机上测试过的确切版本的机制。git clone - 只有 diff、却没有相应
pubspec.yamldelta 的变更是不完整的。 运行pubspec.lock,并在同一次 commit 中 stage lock——无论新增、升级还是移除。flutter pub get - 将 SDK 版本与工具分开记录。 保持 为实际 range,让
environment: sdk:能够求解;在 CI 会读取的已提交记录中,固定经过测试的确切 Flutter 版本。该记录才是陌生开发者和 CI 会读取的内容;不要把 version manager 工具与该文件混为一谈。pub - lint include 文件名与解析到的 SDK 耦合。 版本固定的 如果指定了 resolved linter package 中不存在的文件,会发出
include:——这会让默认include_file_not_found/dart analyze失败,因此标准 CI 能捕获。只有当 warning 被设为非致命时,它才会静默显示为绿色;即便如此,停止生效的也是规则集新增或提升的规则,而不是 analyzer 的内置规则。每次 SDK 升级后都要验证(见 reference)。flutter analyze - 添加前审计 transitive tree,而不是只审计 pubspec。 被禁用的 SDK 往往就在第二跳中。提交新 dependency 前运行 (或
scripts/audit-deps.sh加上 audit script)。dart pub deps --json - 按策略拒绝任何会开启 network path、上报 crash/usage、拖入 telemetry core,或为尚未发布的功能收集 device identifier 的依赖——无论是直接还是 transitive 引入。 绿色 CI 不能证明原生能力可以正常工作。
- 移除 dependency 后 grep 。 已从 pubspec 删除但仍可通过 transitive 方式解析的 package,今天可能仍能编译,却会在真正重要的 clone 中出问题。analyzer 能捕获无法解析的 import,却捕获不了偶然解析成功的 import。
lib/ - 现在就通过 interface 包装 bus-factor-1 原生 plugin,vendor 留到以后。 interface 是廉价的保险;在尚未发生的故障之前预先 vendor,反而会带来维护负担(见 vendoring reference)。
Ranges in pubspec, pins in the lock
pubspec 中使用 range,lock 中使用 pin
| Do | Never |
|---|---|
Caret range: | Exact pin: |
| An exact |
Commit | Gitignore |
The lock is authoritative. Ranges keep the solver able to move on an SDK upgrade; the lock keeps everyone on the versions that were actually verified.
| 应当 | 绝不应 |
|---|---|
Caret range: | 精确 pin: |
| 精确的 |
提交 | 将 |
lock 是权威来源。range 让 solver 在 SDK 升级时仍能继续解析;lock 则让所有人使用真正验证过的版本。
The gate every new dependency must pass
每个新 dependency 都必须通过的 gate
Refuse a package if it — or anything in its transitive tree — does any of:
- Opens a network path you did not choose (,
http, sockets, gRPC) when the product promise is no-network or offline-first.dio - Reports crashes or usage (Crashlytics, Sentry, any analytics or attribution SDK) against a no-telemetry policy.
- Drags in a telemetry/analytics core transitively — a crash SDK's core can worsen the store privacy label even when you never call it.
- Collects device identifiers for a feature you do not ship.
- Requires an flag — an abandoned repo that needs an experiment to build stops building.
--enable-experiment
Also weigh, and usually refuse: any bus-factor-1 package not behind an interface; anything whose function is a few hundred lines of first-party Dart; anything that only exists to save typing. Record the licence — a permissive one (MIT, BSD, Apache-2.0) is a precondition for the vendoring escape hatch; a copyleft dependency removes that option. Full checklist in .
references/dependency-gate-and-audit.md如果一个 package——或其 transitive tree 中的任何内容——出现以下任一情况,就应拒绝:
- 在你没有选择的情况下开启 network path(、
http、socket、gRPC),而产品承诺是无网络或 offline-first。dio - 上报 crash 或 usage(Crashlytics、Sentry、任何 analytics 或 attribution SDK),违反 no-telemetry 策略。
- Transitive 引入 telemetry/analytics core——即使你从不调用它,crash SDK 的 core 也可能让商店隐私标签更糟。
- 为不会发布的功能收集 device identifier。
- 要求传入 flag——一个需要 experiment 才能构建的废弃 repo,迟早会停止构建。
--enable-experiment
此外还要权衡,并且通常应拒绝:任何没有置于 interface 之后的 bus-factor-1 package;功能只需几百行第一方 Dart 就能实现的任何东西;仅仅为了少打几行字而存在的任何东西。记录许可证——宽松型许可证(MIT、BSD、Apache-2.0)是采用 vendoring escape hatch 的前提;copyleft dependency 会消除这一选项。完整清单见 。
references/dependency-gate-and-audit.mdAuditing the transitive tree
审计 transitive tree
sh
dart pub add --dry-run <package> # see what would resolve, resolve nothing
dart pub deps --json > /tmp/deps.json
scripts/audit-deps.sh # walks the resolved set, flags banned patternsThe audit walks the full resolved graph, matches each name against a policy pattern list, marks every hit or , and separates APK-shipping deps from build/test-only ones (a banned package reachable only from never reaches the binary and is not a shipping defect). Exit 1 means refuse the dependency or find one that does not pull those in. To learn who introduced a package: .
directTRANSITIVEdev_dependenciesdart pub deps | grep -B4 <name>Edit the / lists at the top of to match your policy; every entry needs a written justification beside it.
BANNEDALLOWscripts/audit_deps.pyALLOWsh
dart pub add --dry-run <package> # 查看将会解析到什么,但不实际解析
dart pub deps --json > /tmp/deps.json
scripts/audit-deps.sh # 遍历 resolved set,并标记被禁模式该 audit 会遍历完整的 resolved graph,将每个名称与策略 pattern 列表匹配,把每处命中标记为 或 ,并区分会随 APK 发布的 dependency 与仅用于 build/test 的 dependency(只能从 到达的被禁 package 永远不会进入 binary,因此不是 shipping defect)。退出码 1 意味着应拒绝该 dependency,或换用一个不会引入这些 dependency 的 package。要查明是谁引入了某个 package:。
directTRANSITIVEdev_dependenciesdart pub deps | grep -B4 <name>编辑 顶部的 / 列表,使其符合你的策略;每个 条目旁都需要有书面理由。
scripts/audit_deps.pyBANNEDALLOWALLOWUpgrading
升级
Run on your own schedule; there is no bot. Dependabot has never supported pub; Renovate raises PRs a solo repo has no workflow for. Keep their one useful idea by hand: never blind-upgrade a native plugin your app's core capability depends on.
dart pub outdatedGreen CI is not evidence of a native capability. No CI job on a headless emulator can prove the app plays audio, reads a sensor, or talks to a platform channel end-to-end — can assert a channel call was issued, nothing more. After any bump to a capability-critical native plugin or the SDK itself, run a manual pass on a real device. After any upgrade touching a codegen package (, , ), regenerate and commit — stale generated code is caught by CI, not the analyzer (see ).
integration_testdriftfreezedjson_serializablerun-codegen按自己的节奏运行 ;这里没有 bot。Dependabot 从未支持 pub;Renovate 会提交 PR,而单人 repo 没有相应 workflow 来处理。 手动保留它们唯一有用的思路:绝不要盲目升级应用核心能力所依赖的原生 plugin。
dart pub outdated绿色 CI 不能证明原生能力可用。 在 headless emulator 上运行的任何 CI job 都无法证明应用能播放音频、读取 sensor,或端到端地与 platform channel 通信—— 最多只能断言 channel call 已被发起。升级任何影响关键能力的原生 plugin 或 SDK 本身后,都要在真实设备上执行 manual pass。任何涉及 codegen package(、、)的升级后,都要重新生成并 commit——CI 而非 analyzer 才能发现过期的 generated code(见 )。
integration_testdriftfreezedjson_serializablerun-codegenVendoring a bus-factor-1 plugin
Vendor bus-factor-1 plugin
A single-maintainer native plugin whose failure means the app cannot build is the one dependency worth pre-planning for. The mitigation is a thin interface, authored on day one, so vendoring never touches :
lib/dart
// A port the app depends on; the plugin lives behind it. The `Gateway` suffix
// marks a thin wrapper over a SPECIFIC plugin (here flutter_secure_storage) —
// `naming-conventions` owns that Service-vs-Gateway distinction.
abstract interface class SecureStorageGateway {
Future<void> write(String key, String value);
Future<String?> read(String key);
}Only when the plugin actually breaks — stops building against a Flutter release, or ships a regression upstream will not fix — clone it at the last-good tag into , point the pubspec at , record the SHA and every changed line in a , confirm the licence permits redistribution, and patch, don't refactor. Full procedure and triggers in . See .
third_party/<plugin>/path:VENDORED.mdreferences/vendoring-behind-an-interface.mdexamples/vendored_plugin_behind_interface.dart由单一维护者维护、且一旦失效就会导致应用无法构建的原生 plugin,是唯一值得预先规划的 dependency。缓解方案是从第一天起编写一个轻量 interface,从而确保 vendor 时永远无需改动 :
lib/dart
// 应用依赖的 port;plugin 位于其后。`Gateway` 后缀
// 表示这是对某个特定 plugin 的轻量包装(此处为 flutter_secure_storage)——
// `naming-conventions` 负责区分 Service 与 Gateway。
abstract interface class SecureStorageGateway {
Future<void> write(String key, String value);
Future<String?> read(String key);
}只有在 plugin 真正损坏时——无法针对某个 Flutter release 构建,或发布了 upstream 不会修复的 regression——才将它在最后一个正常工作的 tag 处 clone 到 ,让 pubspec 指向 ,在 中记录 SHA 和每一处改动,确认许可证允许 redistribution,并且要 patch,不要 refactor。完整流程和触发条件见 。另见 。
third_party/<plugin>/path:VENDORED.mdreferences/vendoring-behind-an-interface.mdexamples/vendored_plugin_behind_interface.dartRemoving a dependency
移除 dependency
Delete the pubspec entry, run , commit the delta in the same commit — then . A transitively-resolvable import compiles today and breaks the clone that matters.
flutter pub getpubspec.lockgrep -r "package:<name>" lib/删除 pubspec 条目,运行 ,在同一个 commit 中提交 lock delta——然后运行 。一个仍能通过 transitive 方式解析的 import 今天或许能编译,却会在真正重要的 clone 中出问题。
flutter pub getgrep -r "package:<name>" lib/When multi-package (workspace)
多 package(workspace)场景
In a Dart pub workspace / monorepo, each package keeps its own with ranges, but the workspace resolves one shared lock at the root — audit and commit that root lock. Run the audit against the whole workspace's resolved tree, not one package's. A single package's pubspec cannot show what a sibling drags into the shared resolution. See for workspace toolchain mechanics.
pubspec.yamlcodegen-and-toolchain在 Dart pub workspace / monorepo 中,每个 package 都保留自己的 pubspec.yaml 并使用 range,但 workspace 会在根目录解析出一份共享 lock——应审计并提交根目录的 lock。针对整个 workspace 的 resolved tree 运行 audit,而不是只针对某个 package。单个 package 的 pubspec 无法显示 sibling package 给共享解析结果引入了什么。workspace 的 toolchain 机制见 。
codegen-and-toolchainAnti-patterns
反模式
- Exact pin in . Turns the next SDK upgrade into an unsolvable-conflict debugging session; the lock already pins.
pubspec.yaml - Gitignoring in an app. A fresh clone resolves whatever
pubspec.lockfeels like today, not what was tested.pub - Editing pubspec without committing the lock delta. The next machine resolves a different graph.
- Reading to decide if a dep is safe. The banned SDK is on the second hop, invisible there.
pubspec.yaml - Trusting green CI for a native capability. The emulator has no voice engine / real sensor; green proves the Dart compiled, not that the feature works.
- Pre-emptively vendoring a healthy plugin. You take on the maintenance burden before any break; the interface is the cheap insurance, not the fork.
- Vendoring by refactoring. Every line you touch in is a line you own forever — patch the break and stop.
third_party/ - Adding a bus-factor-1 package with no interface. When it rots you have no seam to vendor behind.
- 在 中使用精确 pin。 会把下一次 SDK 升级变成一场无法解决冲突的调试;lock 已经完成 pin。
pubspec.yaml - 在 app 中 Gitignore 。 新 clone 解析出的是 pub 今天“想”解析的版本,而不是经过测试的版本。
pubspec.lock - 编辑 pubspec 却不提交 lock delta。 下一台机器会解析出不同的 graph。
- 读取 来判断 dependency 是否安全。 被禁 SDK 位于第二跳,在这里看不见。
pubspec.yaml - 相信绿色 CI 可以验证原生能力。 Emulator 没有语音引擎或真实 sensor;绿色只证明 Dart 编译通过,不证明功能可用。
- 预先 vendor 一个健康的 plugin。 在任何故障发生前就背上维护负担;interface 才是廉价保险,fork 不是。
- 通过 refactor 来 vendor。 你在 中改动每一行,就等于永远拥有这一行——patch 故障然后停下。
third_party/ - 添加没有 interface 的 bus-factor-1 package。 它出现问题时,你没有可供 vendor 的隔离点。
Definition of done
完成定义
- New/changed deps use caret ranges; no exact pins in any .
pubspec.yaml - delta is staged in the same commit as the
pubspec.lockchange.pubspec.yaml - exits 0 (or every hit has a justified
scripts/audit-deps.shentry).ALLOW - On an SDK bump: the version record is updated and the pinned lint file is confirmed present in the resolved package (
include:).references/sdk-pin-and-lint-include.md - Licence recorded for any new dependency; permissive if it is a vendoring candidate.
- Capability-critical native plugin sits behind an interface; a real-device pass was run after its bump.
- After a removal: is empty.
grep -r "package:<name>" lib/
- 新增或变更的 dependency 使用 caret range;任何 中都没有精确 pin。
pubspec.yaml - delta 与
pubspec.lock变更在同一次 commit 中 stage。pubspec.yaml - 退出码为 0(或每处命中都有合理的
scripts/audit-deps.sh条目)。ALLOW - 升级 SDK 时:版本记录已更新,并确认 pinned lint 文件存在于 resolved package 中(
include:)。references/sdk-pin-and-lint-include.md - 已为所有新 dependency 记录许可证;如果是 vendoring 候选,则必须为宽松型许可证。
- 能力关键型原生 plugin 位于 interface 之后;升级后已在真实设备上执行验证。
- 移除 dependency 后:的结果为空。
grep -r "package:<name>" lib/
Related skills
相关 skills
- — workspace linking, SDK pinning at the toolchain level, generated-code commit-vs-gitignore.
codegen-and-toolchain - — the deterministic
run-codegenpass to run after upgrading a codegen package.build_runner - — the strict
lint-and-style-configthe version-pinnedanalysis_options.yamlfeeds.include: - — the injectable-interface-per-side-effect pattern the vendoring seam relies on.
service-boundary-and-native - — owns the
naming-conventions-vs-Serviceboundary-suffix rule; a plugin wrapper likeGatewaytakes theSecureStorageGatewaysuffix.Gateway - — wiring the audit and lock-freshness checks into CI.
ci-pipeline-and-gates
- —— workspace linking、toolchain 级别的 SDK pinning,以及 generated code 应 commit 还是 gitignore。
codegen-and-toolchain - —— 升级 codegen package 后运行的确定性
run-codegenpass。build_runner - —— version-pinned
lint-and-style-config所提供的严格include:。analysis_options.yaml - —— vendoring 隔离点所依赖的 injectable-interface-per-side-effect 模式。
service-boundary-and-native - —— 负责
naming-conventions与Service的边界后缀规则;像Gateway这样的 plugin wrapper 使用SecureStorageGateway后缀。Gateway - —— 将 audit 和 lock freshness 检查接入 CI。
ci-pipeline-and-gates
References
参考资料
- Dart — Package dependencies & version constraints: https://dart.dev/tools/pub/dependencies
- Dart — and glossary: https://dart.dev/tools/pub/glossary#lockfile
pubspec.lock - Dart — : https://dart.dev/tools/pub/cmd/pub-deps
dart pub deps - Dart — : https://dart.dev/tools/pub/cmd/pub-outdated
dart pub outdated - Dart — Pub workspaces: https://dart.dev/tools/pub/workspaces
- Flutter — Adding plugin packages / path dependencies: https://docs.flutter.dev/packages-and-plugins/using-packages
- Dart —— Package 依赖与版本约束:https://dart.dev/tools/pub/dependencies
- Dart —— 与术语表:https://dart.dev/tools/pub/glossary#lockfile
pubspec.lock - Dart —— :https://dart.dev/tools/pub/cmd/pub-deps
dart pub deps - Dart —— :https://dart.dev/tools/pub/cmd/pub-outdated
dart pub outdated - Dart —— Pub workspaces:https://dart.dev/tools/pub/workspaces
- Flutter —— 添加 plugin package / path dependency:https://docs.flutter.dev/packages-and-plugins/using-packages