Sparround

pub, pubspec and the analyzer

pubspec.yaml is the passport of a package — it describes both its dependencies and the package itself.

The main fields:

  • name — the package name in lower_snake_case; import 'package:<name>/...' is derived from it.
  • version — a semantic version (major.minor.patch); it matters for published packages.
  • environment: sdk:which Dart SDK versions it works with. The most frequently forgotten field: without sdk: ^3.6.0, code using records or pattern matching fails on an older SDK with a confusing error.
  • dependencies — libraries the package needs to run. Everyone using your package downloads these too.
  • dev_dependencies — only for development and testing: test, lints, build_runner, mocktail. They are not propagated to consumers — which is why putting test into dependencies by mistake is a classic error.
  • dependency_overrides — a temporary way past a version conflict. Left in place it is technical debt, and it must never appear in a published package.
yaml
name: billing_core
description: Domain logic for invoices and payments.
version: 1.4.2

environment:
  sdk: ^3.6.0        # which SDKs this package supports

dependencies:
  http: ^1.2.0       # needed at runtime by every consumer
  collection: ^1.19.0
  meta: ^1.16.0

dev_dependencies:
  test: ^1.25.0      # only for developing THIS package
  lints: ^5.0.0
  coverage: ^1.11.0

# Temporary escape hatch only - never in a published package.
# dependency_overrides:
#   http: 1.2.1

A typical pubspec.yaml — dependencies versus dev_dependencies

ConstraintWhat it allowsNote
`^1.4.2``>=1.4.2 <2.0.0`The default choice: new features and fixes arrive, breaking changes do not
`^0.3.1``>=0.3.1 <0.4.0`Before 1.0.0 the **minor** number counts as breaking — a favourite interview subtlety
`>=1.2.0 <1.5.0`A manually written rangeWhen there is a concrete reason (a known regression); document it in a comment
`1.4.2`That exact version onlyEffectively forbidden in a library: it forces version conflicts on consumers
`any`Any versionDangerous in practice — a future major release can break your package
`path: ../billing_core`From a local folderFor monorepos and local development; cannot be published
`git: {url: ..., ref: v1.4.2}`From a repository at a specific refFine for internal packages; pin `ref` to a tag or commit, never a branch

`pubspec.lock` records the exact resolved versions.

  • Applications (apps, CLIs, Flutter apps) — commit the lock file so the team and CI build the same versions and "works on my machine" disappears.
  • Libraries (published packages) — do not commit it: consumers resolve their own versions, your lock file has no effect on them and only adds noise.

Everyday commands:

  • dart pub get — installs what the lock file pins (this is what CI runs).
  • dart pub upgrade — moves to the newest versions the constraints allow and rewrites the lock file.
  • dart pub upgrade --major-versions — also raises the constraints themselves (may be breaking).
  • dart pub outdated — changes nothing and reports what is behind and whether a constraint must move.
  • dart pub deps — the dependency tree; invaluable when chasing a conflict.

Static analysis is one of the strongest parts of the Dart ecosystem:

  • dart analyze — type errors plus lint rules; in CI run it with --fatal-infos so informational messages block too.
  • dart format — one non-negotiable style; in CI, dart format --output=none --set-exit-if-changed .
  • analysis_options.yamlinclude: package:lints/recommended.yaml (for packages) or package:flutter_lints/flutter.yaml, plus your team's rules under linter: rules:.
  • dart fix --dry-run / dart fix --apply — automatically fixes a large share of lint warnings; it is the first command to run after an SDK upgrade.

Interview tip: for "what does your CI run for a Dart package?", list the commands in order and with the reason — it sounds prepared: dart pub getdart format --output=none --set-exit-if-changed . (takes formatting debates out of code review) → dart analyze --fatal-infos (stops warnings from accumulating) → dart test --coverage=coverage → for a published package, dart pub publish --dry-run and dart pub outdated. Then add one sentence: "dart fix --apply runs locally, not in CI — CI should report the mismatch, not rewrite the code."

📚 Sources and documentation