Skip to content

chore: replace Lerna with npm workspaces, Turborepo and Changesets - #48

Merged
stefanoverna merged 2 commits into
masterfrom
chore/replace-lerna-with-npm-workspaces-turborepo-changesets
Aug 25, 2026
Merged

chore: replace Lerna with npm workspaces, Turborepo and Changesets#48
stefanoverna merged 2 commits into
masterfrom
chore/replace-lerna-with-npm-workspaces-turborepo-changesets

Conversation

@stefanoverna

Copy link
Copy Markdown
Member

Replaces Lerna with npm workspaces + Turborepo + Changesets, mirroring
datocms/js-rest-api-clients@126b710.

Why

lerna publish commits, tags and pushes to GitHub before it publishes to
npm. If the npm side then failed — expired auth, a network blip, a partial
publish across the two packages — we were left with a tag pointing at a version
nobody could install, and no way forward except by hand. There was also no
changelog, and Lerna 6 still drove the build through lerna bootstrap, which
Lerna 7 removed.

Rather than making the rollback smarter, the release no longer needs one:

  1. Preflight mutates nothing — on master, clean tree, in sync with origin,
    logged in to npm, at least one pending changeset.
  2. Everything fallible runs first — build and tests before any mutation, so
    a timeout costs a rerun and nothing else.
  3. npm is published before git is taggedchangeset publish tags only what
    it actually published, then git push --follow-tags.
  4. No rollback, ever — an interrupted release is resumed by re-running
    npm run publish. The resume condition asks whether any package is still
    missing from the registry; a check on a single package gets the
    partial-publish case wrong (verified against the real registry).

Decisions

npm workspaces. The repo was already inconsistent: packageManager said
yarn@1.22.19, CI ran npm ci, and yarn.lock was gitignored (so deleting
it is a no-op in this diff — the visible change is the packageManager field).
The tracked lockfiles were three, not two: a root one covering only the root
devDependencies, plus one per package.

baseBranch: "master", matching the actual default branch. Verified that
changeset status diffs against it correctly.

fixed: [["datocms-plugin-sdk", "datocms-react-ui"]], preserving the
lockstep versioning Lerna was configured for. ⚠️ Git tags become per-package
(datocms-plugin-sdk@2.2.7); the single vX.Y.Z tag stops being created. Old
tags are untouched.

publish-next = changeset publish --tag next, not changeset pre. It also
works inside pre mode, so a genuine prerelease line is one
npx changeset pre enter next away — and npm run publish refuses to run while
.changeset/pre.json exists, so a forgotten pre mode can't silently turn a real
release into a prerelease. Preflight is relaxed for --tag releases: normal
releases require master, prereleases don't, because this repo's alphas are cut
from feature branches (v3.0.0-alpha.0, v3.0.1-alpha.0).

Fixed along the way

  • react-ui was being built against a published SDK, not its sibling source.
    packages/react-ui/package-lock.json pinned datocms-plugin-sdk to a
    registry copy of 2.0.17 — six versions behind. Same story inside the sdk:
    datocms-structured-text-utils was locked at 2.0.0 while the manifest
    declares ^5.1.16. Both now resolve through the workspace symlinks.
  • datocms-react-ui declared no react at all — no dependency, no peer
    dependency. The per-package lockfiles hid it; a unified install can't resolve
    the peers (react-intersection-observer wants ≤18, react-select pulls
    react-dom 19). React 17 is now an explicit devDependency, which is the
    minimum that unblocks the install and changes nothing for consumers. The
    missing peerDependencies is a real bug in a component library, but it
    changes the published surface, so it's left for its own decision.
  • @datocms/cma-client and emoji-regex-xs were declared as "*". Every
    install resolved whatever the latest major happened to be. Now ^5.0.0 and
    ^2.0.0: the widest range that still excludes the next major, so a plugin
    already depending on either keeps one copy instead of two. Both floors are
    tested, not assumed — build and suite are green with @datocms/cma-client@5.0.0
    and emoji-regex-xs@2.0.0 installed.
  • CI had never run. It triggered on main while the default branch is
    master. Turning it on is therefore part of this PR, and with the old
    10.x–15.x matrix it would have gone red instantly: it's now 22.x/24.x, with
    checkout@v4/setup-node@v4 and npm caching.

Heads-up: the Node floor moves from 20 to 22

@changesets/cli@3 declares engines: ^22.11 || ^24 || >=26 and dies on Node 20
(enableCompileCache is not a function). Node 20 has been EOL since April.
.nvmrc is now 22 and packageManager is npm@10.9.8 — the same as
js-rest-api-clients, and exactly the npm that ships with Node 22.22.3.
The published packages are unaffected; this only changes what you need to
develop and release. The alternative was @changesets/cli@^2, which would have
left our repos split across two changesets majors.

Verification

check result
npm ci from the regenerated lockfile
Cold build, every dist/ deleted first ✅ 2/2; both packages emit dist/{cjs,esm,types} plus styles.css, types.json, manifest.json
Warm build 2 cached, 2 total — 11ms >>> FULL TURBO
Build order ✅ with dist/ wiped, --filter=datocms-react-ui --force pulls in datocms-plugin-sdk:build first
Release rehearsal on a throwaway copy ✅ a changeset touching only the SDK bumps both to 2.2.7, rewrites ^2.2.6^2.2.7, writes both CHANGELOG.mds, consumes the file
Test suite ✅ 2 suites, 7 tests — pure unit tests, no credentials, no network
bin/publish.sh preflight + resume detection ✅ aborts off master; resume logic checked against the real registry, including the partial-publish case

Nothing was published, and no remote tags were touched.

Pre-migration diagnosis (reported, not fixed)

No drift of the three kinds worth worrying about: every published version has its
tag, no orphan tags. The gaps in the npm version lists (the SDK jumping
2.1.12.1.5) are just Lerna publishing only the changed package. Two things
to know, neither of them damage:

  • v3.0.0-alpha.0 and v3.0.1-alpha.0 are tagged and published but are not on
    master
    — they come from a feature branch (April 2026).
  • The next dist-tag is still on 2.2.0-alpha.3, and those two 3.0.x alphas
    carry no dist-tag at all, so they're installable only by exact version. Which
    means they didn't come out of npm run publish-next.

`lerna publish` commits, tags and pushes to GitHub *before* it publishes to npm,
so an expired token or a network blip on the npm side left a tag pointing at a
version nobody could install, recoverable only by hand. On top of that there was
no changelog, and Lerna 6 still drove the build through `lerna bootstrap`, which
Lerna 7 removed.

Rather than making the release smarter, remove the need for a rollback:

- everything that can fail (preflight, build, tests) runs before anything is
  mutated, so a timeout costs nothing but the rerun;
- npm is published before git is tagged, so a tag can no longer point at a
  version that was never released;
- `changeset publish` skips packages already on the registry, so an interrupted
  release is resumed by re-running `npm run publish`, not undone. The resume
  condition asks whether *any* package is still missing from npm, which is the
  case a single-package check gets wrong.

Along the way:

- npm workspaces replace `lerna bootstrap` and the two per-package lockfiles.
  This also fixes what those lockfiles were pinning: react-ui was being built
  against a *published* `datocms-plugin-sdk@2.0.17` instead of its sibling
  source, and the sdk against `datocms-structured-text-utils@2.0.0` despite
  declaring `^5.1.16`. Both now resolve through the workspace;
- Turborepo derives the build order from the manifests (sdk before react-ui);
- changesets carry the release notes, with `patch` reserved for bug fixes.
  `datocms-plugin-sdk` and `datocms-react-ui` are a `fixed` group, so they keep
  moving in lockstep exactly as they did under Lerna;
- `@datocms/cma-client` and `emoji-regex-xs` were declared as `"*"`, so every
  install resolved whatever the latest major happened to be. They are now
  `^5.0.0` and `^2.0.0` — the widest range that still excludes the next major,
  so a plugin already depending on either keeps one copy instead of two. Both
  floors are tested, not assumed: the build and the suite are green with
  `@datocms/cma-client@5.0.0` and `emoji-regex-xs@2.0.0` installed;
- `datocms-react-ui` declared no `react` at all, so a fresh install could not
  resolve its peers; React 17 is now an explicit devDependency;
- the CI workflow triggered on `main` while the default branch is `master`, so
  it had never run. It now runs, on Node 22/24 rather than 10-15;
- the development Node floor moves from 20 (EOL) to 22, which `@changesets/cli`
  requires. Published packages are unaffected;
- `packageManager` said `yarn@1.22.19` while CI ran `npm ci` and `yarn.lock` was
  gitignored. It now says what we actually use.
v4 targets Node 20, which the runners now force onto Node 24 and warn about on
every run.
@stefanoverna
stefanoverna merged commit 5b90e51 into master Aug 25, 2026
2 checks passed
@stefanoverna
stefanoverna deleted the chore/replace-lerna-with-npm-workspaces-turborepo-changesets branch August 25, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant