dependency-hygiene

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dependency 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:
  • references/dependency-gate-and-audit.md
    — the refuse/accept gate, transitive auditing, licence recording, upgrade discipline.
  • references/sdk-pin-and-lint-include.md
    — recording the SDK version and the silent-lint-disable trap on any SDK bump.
  • references/vendoring-behind-an-interface.md
    — vendoring a bus-factor-1 native plugin into
    third_party/
    without touching
    lib/
    .
Run
scripts/audit-deps.sh
before a PR that changes
pubspec.yaml
.
每个 dependency 都是一项永久的负担,未来可能需要由他人来维护。优化目标应是多年后仍能解析并构建,而不是追求 latest。本 skill 管理 pubspec/lock 机制、新 package 必须通过的 gate,以及关键 package 出现问题时的应急方案。
针对当前任务阅读相应的 reference:
  • references/dependency-gate-and-audit.md
    —— 拒绝/接受 gate、传递依赖审计、许可证记录和升级纪律。
  • references/sdk-pin-and-lint-include.md
    —— 记录 SDK 版本,以及 SDK 升级时可能静默禁用 lint 的陷阱。
  • references/vendoring-behind-an-interface.md
    —— 在不改动
    lib/
    的情况下,将 bus-factor-1 原生 plugin vendor 到
    third_party/
    。
在提交会更改
pubspec.yaml
的 PR 之前,运行
scripts/audit-deps.sh
。

Non-negotiable rules

不可妥协的规则

  1. Caret ranges in
    pubspec.yaml
    , exact pins only in
    pubspec.lock
    .
    drift: ^2.31.0
    , never
    drift: 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.
  2. Commit
    pubspec.lock
    .
    This is an application, not a package — the asymmetry that flips the rule. The default Dart
    .gitignore
    template lists
    pubspec.lock
    ; delete that line. The committed lock is the only thing that makes a stranger's
    git clone
    resolve the exact versions that were tested on a real device.
  3. A
    pubspec.yaml
    diff without its
    pubspec.lock
    delta is incomplete.
    Run
    flutter pub get
    and stage the lock in the same commit — add, upgrade, and remove alike.
  4. Record the SDK version separately from the tool. Keep
    environment: sdk:
    a real range so
    pub
    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.
  5. The lint include filename is coupled to the resolved SDK. A version-pinned
    include:
    that names a file absent from the resolved linter package emits
    include_file_not_found
    — fatal to default
    dart analyze
    /
    flutter 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).
  6. Audit the transitive tree before adding, not the pubspec. The second hop is exactly where a banned SDK arrives. Run
    scripts/audit-deps.sh
    (or
    dart pub deps --json
    + the audit script) before committing a new dependency.
  7. 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.
  8. Grep
    lib/
    after removing a dependency.
    A package deleted from
    pubspec.yaml
    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.
  9. 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.
  1. 在
    pubspec.yaml
    中使用 caret range,仅在
    pubspec.lock
    中使用精确 pin。
    使用
    drift: ^2.31.0
    ,绝不要写成
    drift: 2.31.0
    。由 lock 负责 pin;range 只用于让依赖解析保持可解。在 pubspec 中使用精确 pin,会在下一次 SDK 升级时制造无法解决的冲突,而且 lock 已经提供了同等作用,精确 pin 没有任何额外收益。
  2. 提交
    pubspec.lock
    。
    这是一个 application,而不是 package——正是这种差异决定了规则相反。默认 Dart
    .gitignore
    模板会列出
    pubspec.lock
    ;请删除该行。已提交的 lock 是唯一能确保陌生开发者执行
    git clone
    后解析出真机上测试过的确切版本的机制。
  3. 只有
    pubspec.yaml
    diff、却没有相应
    pubspec.lock
    delta 的变更是不完整的。
    运行
    flutter pub get
    ,并在同一次 commit 中 stage lock——无论新增、升级还是移除。
  4. 将 SDK 版本与工具分开记录。 保持
    environment: sdk:
    为实际 range,让
    pub
    能够求解;在 CI 会读取的已提交记录中,固定经过测试的确切 Flutter 版本。该记录才是陌生开发者和 CI 会读取的内容;不要把 version manager 工具与该文件混为一谈。
  5. lint include 文件名与解析到的 SDK 耦合。 版本固定的
    include:
    如果指定了 resolved linter package 中不存在的文件,会发出
    include_file_not_found
    ——这会让默认
    dart analyze
    /
    flutter analyze
    失败,因此标准 CI 能捕获。只有当 warning 被设为非致命时,它才会静默显示为绿色;即便如此,停止生效的也是规则集新增或提升的规则,而不是 analyzer 的内置规则。每次 SDK 升级后都要验证(见 reference)。
  6. 添加前审计 transitive tree,而不是只审计 pubspec。 被禁用的 SDK 往往就在第二跳中。提交新 dependency 前运行
    scripts/audit-deps.sh
    (或
    dart pub deps --json
    加上 audit script)。
  7. 按策略拒绝任何会开启 network path、上报 crash/usage、拖入 telemetry core,或为尚未发布的功能收集 device identifier 的依赖——无论是直接还是 transitive 引入。 绿色 CI 不能证明原生能力可以正常工作。
  8. 移除 dependency 后 grep
    lib/
    。
    已从 pubspec 删除但仍可通过 transitive 方式解析的 package,今天可能仍能编译,却会在真正重要的 clone 中出问题。analyzer 能捕获无法解析的 import,却捕获不了偶然解析成功的 import。
  9. 现在就通过 interface 包装 bus-factor-1 原生 plugin,vendor 留到以后。 interface 是廉价的保险;在尚未发生的故障之前预先 vendor,反而会带来维护负担(见 vendoring reference)。

Ranges in pubspec, pins in the lock

pubspec 中使用 range,lock 中使用 pin

DoNever
Caret range:
drift: ^2.31.0
Exact pin:
drift: 2.31.0
environment: sdk: ^3.6.0
— a real range
An exact
sdk:
version
Commit
pubspec.lock
Gitignore
pubspec.lock
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:
drift: ^2.31.0
精确 pin:
drift: 2.31.0
environment: sdk: ^3.6.0
—— 实际 range
精确的
sdk:
版本
提交
pubspec.lock
将
pubspec.lock
加入 Gitignore
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
    ,
    dio
    , sockets, gRPC) when the product promise is no-network or offline-first.
  • 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
    --enable-experiment
    flag
    — an abandoned repo that needs an experiment to build stops building.
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
    、
    dio
    、socket、gRPC),而产品承诺是无网络或 offline-first。
  • 上报 crash 或 usage(Crashlytics、Sentry、任何 analytics 或 attribution SDK),违反 no-telemetry 策略。
  • Transitive 引入 telemetry/analytics core——即使你从不调用它,crash SDK 的 core 也可能让商店隐私标签更糟。
  • 为不会发布的功能收集 device identifier。
  • 要求传入
    --enable-experiment
    flag
    ——一个需要 experiment 才能构建的废弃 repo,迟早会停止构建。
此外还要权衡,并且通常应拒绝:任何没有置于 interface 之后的 bus-factor-1 package;功能只需几百行第一方 Dart 就能实现的任何东西;仅仅为了少打几行字而存在的任何东西。记录许可证——宽松型许可证(MIT、BSD、Apache-2.0)是采用 vendoring escape hatch 的前提;copyleft dependency 会消除这一选项。完整清单见
references/dependency-gate-and-audit.md
。

Auditing 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 patterns
The audit walks the full resolved graph, matches each name against a policy pattern list, marks every hit
direct
or
TRANSITIVE
, and separates APK-shipping deps from build/test-only ones (a banned package reachable only from
dev_dependencies
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:
dart pub deps | grep -B4 <name>
.
Edit the
BANNED
/
ALLOW
lists at the top of
scripts/audit_deps.py
to match your policy; every
ALLOW
entry needs a written justification beside it.
sh
dart pub add --dry-run <package>     # 查看将会解析到什么,但不实际解析
dart pub deps --json > /tmp/deps.json
scripts/audit-deps.sh                 # 遍历 resolved set,并标记被禁模式
该 audit 会遍历完整的 resolved graph,将每个名称与策略 pattern 列表匹配,把每处命中标记为
direct
或
TRANSITIVE
,并区分会随 APK 发布的 dependency 与仅用于 build/test 的 dependency(只能从
dev_dependencies
到达的被禁 package 永远不会进入 binary,因此不是 shipping defect)。退出码 1 意味着应拒绝该 dependency,或换用一个不会引入这些 dependency 的 package。要查明是谁引入了某个 package:
dart pub deps | grep -B4 <name>
。
编辑
scripts/audit_deps.py
顶部的
BANNED
/
ALLOW
列表,使其符合你的策略;每个
ALLOW
条目旁都需要有书面理由。

Upgrading

升级

Run
dart pub outdated
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.
Green 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 —
integration_test
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 (
drift
,
freezed
,
json_serializable
), regenerate and commit — stale generated code is caught by CI, not the analyzer (see
run-codegen
).
按自己的节奏运行
dart pub outdated
;这里没有 bot。Dependabot 从未支持 pub;Renovate 会提交 PR,而单人 repo 没有相应 workflow 来处理。 手动保留它们唯一有用的思路:绝不要盲目升级应用核心能力所依赖的原生 plugin。
绿色 CI 不能证明原生能力可用。 在 headless emulator 上运行的任何 CI job 都无法证明应用能播放音频、读取 sensor,或端到端地与 platform channel 通信——
integration_test
最多只能断言 channel call 已被发起。升级任何影响关键能力的原生 plugin 或 SDK 本身后,都要在真实设备上执行 manual pass。任何涉及 codegen package(
drift
、
freezed
、
json_serializable
)的升级后,都要重新生成并 commit——CI 而非 analyzer 才能发现过期的 generated code(见
run-codegen
)。

Vendoring 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
third_party/<plugin>/
, point the pubspec at
path:
, record the SHA and every changed line in a
VENDORED.md
, confirm the licence permits redistribution, and patch, don't refactor. Full procedure and triggers in
references/vendoring-behind-an-interface.md
. See
examples/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 到
third_party/<plugin>/
,让 pubspec 指向
path:
,在
VENDORED.md
中记录 SHA 和每一处改动,确认许可证允许 redistribution,并且要 patch,不要 refactor。完整流程和触发条件见
references/vendoring-behind-an-interface.md
。另见
examples/vendored_plugin_behind_interface.dart
。

Removing a dependency

移除 dependency

Delete the pubspec entry, run
flutter pub get
, commit the
pubspec.lock
delta in the same commit — then
grep -r "package:<name>" lib/
. A transitively-resolvable import compiles today and breaks the clone that matters.
删除 pubspec 条目,运行
flutter pub get
,在同一个 commit 中提交 lock delta——然后运行
grep -r "package:<name>" lib/
。一个仍能通过 transitive 方式解析的 import 今天或许能编译,却会在真正重要的 clone 中出问题。

When multi-package (workspace)

多 package(workspace)场景

In a Dart pub workspace / monorepo, each package keeps its own
pubspec.yaml
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
codegen-and-toolchain
for workspace toolchain mechanics.
在 Dart pub workspace / monorepo 中,每个 package 都保留自己的 pubspec.yaml 并使用 range,但 workspace 会在根目录解析出一份共享 lock——应审计并提交根目录的 lock。针对整个 workspace 的 resolved tree 运行 audit,而不是只针对某个 package。单个 package 的 pubspec 无法显示 sibling package 给共享解析结果引入了什么。workspace 的 toolchain 机制见
codegen-and-toolchain
。

Anti-patterns

反模式

  • Exact pin in
    pubspec.yaml
    .
    Turns the next SDK upgrade into an unsolvable-conflict debugging session; the lock already pins.
  • Gitignoring
    pubspec.lock
    in an app.
    A fresh clone resolves whatever
    pub
    feels like today, not what was tested.
  • Editing pubspec without committing the lock delta. The next machine resolves a different graph.
  • Reading
    pubspec.yaml
    to decide if a dep is safe.
    The banned SDK is on the second hop, invisible there.
  • 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
    third_party/
    is a line you own forever — patch the break and stop.
  • Adding a bus-factor-1 package with no interface. When it rots you have no seam to vendor behind.
  • 在
    pubspec.yaml
    中使用精确 pin。
    会把下一次 SDK 升级变成一场无法解决冲突的调试;lock 已经完成 pin。
  • 在 app 中 Gitignore
    pubspec.lock
    。
    新 clone 解析出的是 pub 今天“想”解析的版本,而不是经过测试的版本。
  • 编辑 pubspec 却不提交 lock delta。 下一台机器会解析出不同的 graph。
  • 读取
    pubspec.yaml
    来判断 dependency 是否安全。
    被禁 SDK 位于第二跳,在这里看不见。
  • 相信绿色 CI 可以验证原生能力。 Emulator 没有语音引擎或真实 sensor;绿色只证明 Dart 编译通过,不证明功能可用。
  • 预先 vendor 一个健康的 plugin。 在任何故障发生前就背上维护负担;interface 才是廉价保险,fork 不是。
  • 通过 refactor 来 vendor。 你在
    third_party/
    中改动每一行,就等于永远拥有这一行——patch 故障然后停下。
  • 添加没有 interface 的 bus-factor-1 package。 它出现问题时,你没有可供 vendor 的隔离点。

Definition of done

完成定义

  • New/changed deps use caret ranges; no exact pins in any
    pubspec.yaml
    .
  • pubspec.lock
    delta is staged in the same commit as the
    pubspec.yaml
    change.
  • scripts/audit-deps.sh
    exits 0 (or every hit has a justified
    ALLOW
    entry).
  • On an SDK bump: the version record is updated and the pinned lint
    include:
    file is confirmed present in the resolved package (
    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:
    grep -r "package:<name>" lib/
    is empty.
  • 新增或变更的 dependency 使用 caret range;任何
    pubspec.yaml
    中都没有精确 pin。
  • pubspec.lock
    delta 与
    pubspec.yaml
    变更在同一次 commit 中 stage。
  • scripts/audit-deps.sh
    退出码为 0(或每处命中都有合理的
    ALLOW
    条目)。
  • 升级 SDK 时:版本记录已更新,并确认 pinned lint
    include:
    文件存在于 resolved package 中(
    references/sdk-pin-and-lint-include.md
    )。
  • 已为所有新 dependency 记录许可证;如果是 vendoring 候选,则必须为宽松型许可证。
  • 能力关键型原生 plugin 位于 interface 之后;升级后已在真实设备上执行验证。
  • 移除 dependency 后:
    grep -r "package:<name>" lib/
    的结果为空。

Related skills

相关 skills

  • codegen-and-toolchain
    — workspace linking, SDK pinning at the toolchain level, generated-code commit-vs-gitignore.
  • run-codegen
    — the deterministic
    build_runner
    pass to run after upgrading a codegen package.
  • lint-and-style-config
    — the strict
    analysis_options.yaml
    the version-pinned
    include:
    feeds.
  • service-boundary-and-native
    — the injectable-interface-per-side-effect pattern the vendoring seam relies on.
  • naming-conventions
    — owns the
    Service
    -vs-
    Gateway
    boundary-suffix rule; a plugin wrapper like
    SecureStorageGateway
    takes the
    Gateway
    suffix.
  • ci-pipeline-and-gates
    — wiring the audit and lock-freshness checks into CI.
  • codegen-and-toolchain
    —— workspace linking、toolchain 级别的 SDK pinning,以及 generated code 应 commit 还是 gitignore。
  • run-codegen
    —— 升级 codegen package 后运行的确定性
    build_runner
    pass。
  • lint-and-style-config
    —— version-pinned
    include:
    所提供的严格
    analysis_options.yaml
    。
  • service-boundary-and-native
    —— vendoring 隔离点所依赖的 injectable-interface-per-side-effect 模式。
  • naming-conventions
    —— 负责
    Service
    与
    Gateway
    的边界后缀规则;像
    SecureStorageGateway
    这样的 plugin wrapper 使用
    Gateway
    后缀。
  • ci-pipeline-and-gates
    —— 将 audit 和 lock freshness 检查接入 CI。

References

参考资料