From bd84aa9379b28b1ee734dd01928dc90eeebf12eb Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Tue, 25 Aug 2026 15:51:05 +0200 Subject: [PATCH 1/2] Replace Lerna with npm workspaces, Turborepo and Changesets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lerna 4 (2021) with `lerna bootstrap`, a command removed in Lerna 7, was still wired into the build script and CI. Two things it got wrong: - `lerna publish` commits, tags and pushes to GitHub *before* it publishes to npm, so a failure on the npm side left a tag pointing at a version nobody could install, and no way forward except by hand; - there was no changelog at all. `bin/publish.mjs` replaces it. Everything fallible — build, tests — runs before anything irreversible, and `changeset publish` publishes to npm first and then tags only the packages npm actually accepted. There is no rollback because every step is idempotent: the publish skips versions already on the registry, tagging skips existing tags, and each GitHub release skips itself. An interrupted release is resumed by re-running it. Versioning stays independent, which is what "Switch lerna to independent versioning" (5974555) asked for and what this repo has never actually done: the ten packages all sitting at 6.0.0 are the residue of the fixed mode that came before it. A changesets `fixed` group would have restored exactly the lockstep that commit set out to stop. Rehearsed: a patch on to-markdown moves to-markdown alone. Tags become per package (`datocms-structured-text-utils@6.1.0`), the changesets default and the scheme the other two monorepos now use. The 109 historical `vX.Y.Z` tags are left exactly where they are, but `git describe` will start answering differently. Each release now also gets GitHub release notes, one per tag, taken verbatim from the changelog section changesets just wrote. The per-package lockfiles are deleted; one root lockfile covers the workspace. They were already broken — `generic-html-renderer` and `to-plain-text` pinned `datocms-structured-text-utils@1.1.1` against a `^6.0.0` declaration, `dastdown` 5.1.11, `html-to-structured-text` 5.1.16, and `to-markdown` had no lockfile at all. Under workspaces the internal dependencies are symlinks, so the packages finally build against each other rather than against stale copies from the registry. Every declared dependency stayed inside its declared range, and all ten packages emit a `dist/` byte-identical to their published 6.0.0 tarballs, so nothing here needs a changeset. CI drops the `lerna bootstrap` step and moves to Node 22/24: both 18 and 20 are past end-of-life, and `@changesets/cli` declares `^22.11 || ^24 || >=26` — on Node 20 it dies with `enableCompileCache is not a function`, which says nothing about versions. `npm ci`, the build and the tests still work there; the release tooling does not. Published packages are unaffected. --- .changeset/README.md | 56 + .changeset/config.json | 11 + .github/workflows/node.js.yml | 15 +- .gitignore | 3 + .nvmrc | 1 + CLAUDE.md | 43 +- README.md | 37 +- bin/publish.mjs | 276 + lerna.json | 4 - package-lock.json | 13773 +++++----------- package.json | 18 +- .../package-lock.json | 31 - packages/dastdown/package-lock.json | 124 - .../generic-html-renderer/package-lock.json | 44 - .../html-to-structured-text/package-lock.json | 1678 -- packages/slate-utils/package-lock.json | 393 - packages/to-dom-nodes/package-lock.json | 108 - packages/to-html-string/package-lock.json | 43 - packages/to-plain-text/package-lock.json | 61 - packages/utils/package-lock.json | 125 - packages/utils/turbo.json | 10 + turbo.json | 9 + 22 files changed, 4301 insertions(+), 12562 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .nvmrc create mode 100755 bin/publish.mjs delete mode 100644 lerna.json delete mode 100644 packages/contentful-to-structured-text/package-lock.json delete mode 100644 packages/dastdown/package-lock.json delete mode 100644 packages/generic-html-renderer/package-lock.json delete mode 100644 packages/html-to-structured-text/package-lock.json delete mode 100644 packages/slate-utils/package-lock.json delete mode 100644 packages/to-dom-nodes/package-lock.json delete mode 100644 packages/to-html-string/package-lock.json delete mode 100644 packages/to-plain-text/package-lock.json delete mode 100644 packages/utils/package-lock.json create mode 100644 packages/utils/turbo.json create mode 100644 turbo.json diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..a6955ec --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,56 @@ +# Changesets + +This folder holds the pending release notes for the next version. + +Whenever you change something worth mentioning in a release, run `npx changeset` +and answer the two prompts (which packages, and whether it's a patch/minor/major). +That writes a small markdown file here, which you commit along with your changes. + +At release time `npm run publish` consumes every pending file: it computes the +resulting versions, updates the `package.json`s and the `CHANGELOG.md`s, and +deletes the files. + +## Each package versions on its own + +There is no group here: the ten packages are **independent**, so a changeset +touching `datocms-structured-text-to-markdown` bumps that package and nothing +else. This is what `lerna.json` asked for in "Switch lerna to independent +versioning", and it is what the repository now actually does — the ten packages +sitting at `6.0.0` are the residue of the fixed mode that came before it. + +So the package list inside a changeset carries real weight, unlike in a lockstep +repo. Pick every package whose _own_ behaviour changed. You do **not** need to +list packages that merely depend on one you changed: changesets bumps a +dependent by itself, whenever the new version falls outside the range that +dependent declares. + +`datocms-structured-text-utils` is the root of the graph — everything else +depends on it, directly or through `datocms-structured-text-generic-html-renderer` +— so a `major` there is a major for the whole repository in practice, even +though the version numbers will no longer move in lockstep to say so. + +## Which bump level? + +- `patch` — bug fixes only. It's the clearest signal in semver ("nothing new, + just a fix"), so we don't spend it on anything else. +- `minor` — new API surface. A new exported function, a new node type, a new + option on an existing one. +- `major` — something was removed or renamed. + +## Prereleases + +`npm run publish-next` publishes under the `next` dist-tag, leaving `latest` +untouched. It works in two modes: + +- **as-is** — the pending changesets produce a normal version (say `6.1.0`) + which is published under `next` instead of `latest`; +- **real prerelease versions** — run `npx changeset pre enter next` first and + the same command produces `6.1.0-next.0`, `6.1.0-next.1`, … That mode is + recorded in `.changeset/pre.json`, which you commit. Run + `npx changeset pre exit` when the line is done. + +Either way the GitHub release is marked as a prerelease, so it never becomes +the repository's "Latest release". + +`npm run publish` refuses to run while `.changeset/pre.json` exists, so a +forgotten pre mode can't quietly turn a real release into a prerelease. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..487f6ae --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "access": "public", + "baseBranch": "main", + "fixed": [], + "linked": [], + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 6b317cb..2c159dd 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,4 +1,6 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# Installs from the lockfile, builds every workspace through Turborepo and runs +# the test suite. +# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI @@ -15,8 +17,10 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + # 18 and 20 are both past end-of-life, and @changesets/cli needs + # ^22.11 || ^24 || >=26 — see .nvmrc. # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + node-version: [22.x, 24.x] steps: - uses: actions/checkout@v4 @@ -24,7 +28,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + # One install for the whole workspace; the internal packages are + # symlinked into the root node_modules, so there is no bootstrap step. - run: npm ci - - run: ./node_modules/.bin/lerna bootstrap - - run: npm run build --if-present + # Turborepo derives the build order from the manifests: `utils` before + # `generic-html-renderer` before the renderers that depend on it. + - run: npm run build - run: npm test diff --git a/.gitignore b/.gitignore index 984d6a8..755b4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ yarn.lock **/.rpt2_cache .vscode/settings.json + +# Turborepo +.turbo diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/CLAUDE.md b/CLAUDE.md index da16e2d..b2f6174 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,24 +4,30 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -This is a Lerna-managed monorepo for DatoCMS Structured Text (DAST) utilities. It provides TypeScript libraries for handling, converting, and rendering DatoCMS Structured Text documents across multiple formats. +This is an npm-workspaces monorepo for DatoCMS Structured Text (DAST) utilities, built with Turborepo and released with Changesets. It provides TypeScript libraries for handling, converting, and rendering DatoCMS Structured Text documents across multiple formats. + +Node 22 or later is required (see `.nvmrc`) — `@changesets/cli` declares `^22.11 || ^24 || >=26` and dies on Node 20 with an error that does not mention versions. The published packages have no such requirement. ## Commands ### Building ```bash -npm run build # Bootstrap all packages and build them -lerna bootstrap # Install dependencies for all packages -lerna run build # Build all packages +npm install # One install for the whole workspace; no bootstrap step +npm run build # turbo run build — every package, in dependency order +npx turbo run build --filter= # ...and its dependencies only ``` -Individual packages can be built by navigating to `packages/` and running: +Turborepo derives the order from the manifests (`dependsOn: ["^build"]`), so nobody maintains a list: `utils` builds before `generic-html-renderer` before the renderers that depend on it. `npm run