GAP·MAP

Package management & monorepos

[ middle depth ]

What a workspace is

A workspace is one repo holding many packages that install with a single command. The install symlinks local packages to each other instead of downloading them from the registry, so packages/ui can depend on packages/utils and get the working-copy source.

npm and yarn configure this with a workspaces array in the root package.json ("workspaces": ["packages/*"]). pnpm uses a separate pnpm-workspace.yaml and a workspace: protocol on the dependency ("utils": "workspace:*") that forces the local link and is rewritten to a normal semver range on publish, so consumers never see workspace:. Run one package's script with npm run test -w ui, or run it everywhere with --workspaces.

Why the lockfile gets committed

package.json holds loose ranges like ^1.2.3. The lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml) records the exact resolved tree, with the concrete version and an integrity hash for every package, so a later install rebuilds an identical tree. It exists to make installs reproducible across a teammate, CI, and production.

Wrong "The lockfile is generated, so it belongs in .gitignore." Without it, two installs run days apart resolve different transitive versions through those caret ranges, and you get "works on my machine." Right commit it, and in CI run npm ci. npm ci needs a committed lockfile, wipes node_modules first, installs the locked tree exactly, and errors when the lockfile and package.json disagree instead of quietly rewriting the lockfile the way npm install does.

Caret and tilde, exactly

^1.2.3 means >=1.2.3 <2.0.0: minor and patch updates, never the next major. ~1.2.3 means >=1.2.3 <1.3.0: patch updates only.

Wrong "^1.2.3 might pull 2.0.0 once it ships." Caret fixes the left-most non-zero part, here the major, so 2.0.0 is out of range. On a 0.x version the rule bites harder: ^0.2.3 allows only >=0.2.3 <0.3.0, because the minor is now the left-most non-zero part, so caret on 0.x is as strict as a tilde.

Phantom dependencies

A phantom (undeclared) dependency is a package your code imports without listing it in package.json. It resolves only because some other dependency pulled it in and npm hoisted it to the flat root node_modules, where any module can reach it.

// package.json declares: express
// code runs:             require("ms")   // never declared
// works only because express -> debug -> ms got hoisted to the root

It breaks the day that transitive package is removed, bumped, or deduped so ms no longer sits at the root, and it breaks at once under pnpm, which exposes only declared dependencies. Declare every package you import.

[ senior depth ]

How pnpm's store and symlinks beat flat hoisting

npm and yarn build a flat node_modules: shared dependencies hoist to the root, which is exactly why phantom dependencies resolve. pnpm works from a content-addressable global store instead. Every version of every package is stored once, and a project's node_modules holds hard links into that store, so the same package shared across ten projects costs the disk of one copy. On update, if a new version changes one file out of a hundred, only that one file is added to the store.

The layout is not flat. Only a package's direct dependencies are symlinked into the top of node_modules; everything else lives under node_modules/.pnpm/ in versioned folders. Node resolves each symlink to its real path, so imports work, and a package can reach only the dependencies it declared. That removes phantom dependencies by construction. The escape hatch is nodeLinker: hoisted, which rebuilds a flat npm-style tree for tools that assume hoisting and trades the phantom-dep protection back for compatibility.

The duplicate-React singleton

A component library that uses React must not put react in its own dependencies. If it does, the installer can place a second copy of React in the tree, and hooks break with "Invalid hook call," because the react in app code and the react inside react-dom resolve to two different modules.

Wrong "List react in dependencies so it is guaranteed present." Right declare it as a peerDependency so the host app supplies the one shared copy. Confirm a duplicate with npm ls react; two entries means two copies. npm v7 and later auto-install peer deps, and when two packages demand incompatible peer ranges the install fails with ERESOLVE. --legacy-peer-deps unblocks it but can reintroduce the duplicate, and the same flag has to be passed to npm ci.

Task graphs and cache correctness

Turborepo and Nx model tasks as a DAG and cache each run by a hash of its inputs. In turbo.json, dependsOn: ["^build"] is topological (build this package's dependencies first); dependsOn: ["build"] without the caret means the same package's build runs first. A cache hit restores prior outputs and logs and skips the work.

The failure mode is a false cache hit. If an environment variable changes behavior but is not listed in the task's env, the hash does not move and a stale artifact is restored. If outputs is not declared, the produced files are never cached at all. Remote caching shares those artifacts across machines and CI, which makes cache poisoning a real risk, so Turborepo signs artifacts with HMAC-SHA256 and treats a failed check as a miss.

Fixed versus independent publishing

Changesets records each intended release as a markdown file, then changeset version applies the bumps and changeset publish ships them. The config picks a model. fixed releases a group of packages together on every bump whether or not each one changed, which keeps versions aligned but pads changelogs with no-op bumps. The default is independent: each package versions on its own changesets, which keeps versions meaningful but makes coordinated cross-package releases harder. linked sits between them, sharing a version only when packages change together.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more System Design

was this useful?