skip to content

In a Dart pub workspace, when `api_client` declares `core: ^2.0.0`, which copy of `core` does it use inside the workspace and after publishing?

level: middleimportance: should knowfreq 28%

answer

  1. the local member wins
  2. whatever source is declared
  3. the constraint must still match
  4. outside: the hosted source again
  5. no path dependency needed

basics

~20 s

Inside the workspace api_client resolves to the local packages/core, whatever source the dependency names, as long as that copy's version satisfies ^2.0.0. Once api_client is published, its users get core from pub.dev as an ordinary hosted dependency.

solid answer

~40 s

Members that depend on each other always resolve to the copy in the workspace, regardless of the dependency's declared source. So `api_client` can declare a plain hosted `core: ^2.0.0`: during development pub uses the local `packages/core`, edits show up without publishing, and no `path:` dependency is needed. The local copy must still satisfy the constraint - if `core`'s pubspec says `version: 3.0.0`, resolution fails. Outside the workspace the declared source applies again, so someone depending on a published `api_client` gets the hosted `core` as a transitive dependency. That keeps every pubspec publishable, since pub.dev accepts only hosted and SDK dependencies. `dart pub publish` acts on the package in the current directory; `dart pub -C packages/core publish` targets one member, and `core` should go out before the `api_client` version that needs it.

code

yaml · 14 lines
yaml
# packages/api_client/pubspec.yaml
name: api_client
version: 1.3.0
resolution: workspace

environment:
  sdk: ^3.13.0

dependencies:
  # A normal hosted constraint: inside the workspace pub uses
  # packages/core (its version must satisfy ^2.0.0); consumers of
  # the published api_client get core from pub.dev.
  core: ^2.0.0
  http: ^1.2.0

go deeper

for a junior

Recall that members depending on each other use the local copy inside the workspace, so edits appear without publishing.

for a middle

Explain that the local copy replaces the source but not the constraint, and that the declared hosted source applies again once a package is consumed outside the workspace.

for a senior

Plan releases: keep pubspecs publishable, publish dependencies before dependents, and remember that in-workspace tests don't prove what consumers resolve from pub.dev.

for a principal

Decide which members are published and which stay internal, since every published member turns local version bumps into a public compatibility promise.

## The rule In a pub workspace, several packages share one resolution. When one **member** depends on another member, pub resolves that dependency to **the local copy in the workspace, regardless of the source the dependency declares**. dart.dev's example is a `client_package` that depends on `shared: ^2.3.0`: inside the workspace the local `shared` is used. Take a monorepo with an app, `apps/field_app`, and four local packages: `core`, `api_client`, `ui_kit` and `storage`. If `api_client` declares ```yaml dependencies: core: ^2.0.0 ``` that line reads like an ordinary **hosted** dependency on pub.dev. Inside the workspace, pub nevertheless uses `packages/core` from disk. Edit a function in `core` and `api_client` sees it at once - no publish, no version bump, no path dependency. ## The constraint must still match The local copy replaces the *source*, not the *constraint*. dart.dev is explicit that the local version still has to satisfy the declared range: - `core` at `version: 2.4.0` and a constraint of `^2.0.0` - resolves to the local copy. - `core` bumped to `version: 3.0.0` while `api_client` still says `^2.0.0` - `dart pub get` fails for the whole workspace, because the one resolution has no solution. So a breaking bump in `core` forces every dependent member to raise its constraint in the same change, which is the point: the monorepo never builds against a combination it has not agreed on. ## Why no path dependency | | `path: ../core` dependency | workspace member with a hosted constraint | |---|---|---| | Local edits visible immediately | yes | yes | | Can the dependent package be published to pub.dev? | no - pub.dev accepts only hosted and SDK dependencies | yes - the pubspec already names a hosted source | | Lockfiles | one per package, so third-party versions can drift | one shared `pubspec.lock` for everyone | | What a consumer of the published package gets | not applicable | the hosted `core` from pub.dev | The workspace lets development use local code while the pubspec describes what consumers will get. ## After publishing Outside the workspace the declared source applies again. If `api_client` is published and someone else depends on it, their resolution fetches `core` from pub.dev as a **transitive hosted dependency** - the local copy in your repository is invisible to them. Two consequences: 1. Every `core` version that a published `api_client` requires must itself be on pub.dev. Publish **`core` first**, then `api_client`. 2. Tests passing inside the workspace prove only the local combination. What consumers get depends on the hosted versions their constraints allow. ## Publishing one member - `dart pub publish` publishes **the package in the current directory**; in a workspace it does not publish all members. - `dart pub -C packages/core publish` is the same as changing into `packages/core` and publishing. - A freshly published version can take a few minutes to become available on pub.dev. When the second package depends on it, dart.dev suggests waiting, or using `--skip-validation` for the second publish. - Members never meant for pub.dev, such as `field_app`, should carry `publish_to: none`. ## Adding a fifth package When the team adds `packages/sync` to the monorepo, the member-to-member rule decides how it is wired: 1. Create `packages/sync/pubspec.yaml` with a `name`, a `version`, `resolution: workspace` and an SDK constraint of `^3.6.0` or higher. 2. If the root uses the `packages/*` glob (Dart 3.11+), nothing else is needed at the root; otherwise add the path to the root `workspace` list. 3. In each member that uses it, declare `sync` with an ordinary constraint that its local `version` satisfies, such as `sync: ^0.1.0`. 4. If `sync` is internal, give it `publish_to: none`; if it will be published, keep its own dependencies hosted. 5. Run `dart pub get` anywhere in the repository; the shared `pubspec.lock` at the root now includes `sync` and its dependencies. ## Summary - Member-to-member dependencies resolve to the local copy, whatever source they declare. - The local version must still satisfy the constraint. - Outside the workspace, the hosted source is used, so publish dependencies before their dependents.

  • Why not connect the packages with `path: ../core` dependencies instead of a workspace?
    Path dependencies make local edits visible too, but a package that has one can't be published to pub.dev, and each package still resolves separately with its own lockfile, so third-party versions drift. A workspace keeps hosted constraints in every pubspec and one shared resolution.
  • You bump `core` to 3.0.0 in the repository. What breaks, and how do you fix it?
    Every member still declaring `core: ^2.x` no longer matches the local version, so `dart pub get` fails for the whole workspace. Raise those constraints to `^3.0.0` and adapt their code in the same change; the failure is the workspace forcing the repository to agree on one `core`.
  • Does running `dart pub publish` at the repository root publish every member?
    No. It acts on the package in the current directory - at the root, the root package, which normally has `publish_to: none`. Publish members one by one, from their directories or with `dart pub -C packages/<name> publish`, dependencies first.

saying these in an interview costs you the question

  • Workspace members must reference each other with path dependencies
  • Inside a workspace the local copy is used even if its version breaks the constraint
  • Users of a published api_client also get the local core from your repository
  • dart pub publish at the workspace root publishes every member at once
  • dart pub add at the root adds the dependency to every member