Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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 version, updates the `package.json`s and the `CHANGELOG.md`s, and
deletes the files.

`datocms-plugin-sdk` and `datocms-react-ui` are a `fixed` group: they always
share the same version and are released together, exactly as they were under
Lerna. So the package list inside a changeset matters far less than the bump
level you pick.

## 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 hook, a new component, a new `ctx` method.
- `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 `2.3.0`)
which is published under `next` instead of `latest`;
- **real prerelease versions** — run `npx changeset pre enter next` first and
the same command produces `2.3.0-next.0`, `2.3.0-next.1`, … That mode is
recorded in `.changeset/pre.json`, which you commit. Run
`npx changeset pre exit` when the line is done.

`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.
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"access": "public",
"baseBranch": "master",
"fixed": [["datocms-plugin-sdk", "datocms-react-ui"]],
"linked": [],
"updateInternalDependencies": "patch",
"ignore": []
}
5 changes: 5 additions & 0 deletions .changeset/pin-unbounded-dependency-ranges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"datocms-plugin-sdk": patch
---

Pin the `@datocms/cma-client` and `emoji-regex-xs` dependencies, which were both declared as `"*"`. An unbounded range means every install resolves whatever the latest major happens to be, so a breaking release of either package could break the SDK for new installs without a single change on our side. They are now `^5.0.0` and `^2.0.0`: the widest range that excludes the next major, so plugins already using either package keep a single copy in their tree instead of getting a duplicate.
14 changes: 7 additions & 7 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@ name: Node.js CI

on:
push:
branches: [main]
branches: [master]
pull_request:
branches: [main]
branches: [master]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
node-version: [22.x, 24.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: ./node_modules/.bin/lerna bootstrap
- run: npm run build --if-present
- run: npm run build
- run: npm test
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
yarn.lock

packages/sdk/types.json
packages/react-ui/types.json

Expand Down Expand Up @@ -50,3 +48,5 @@ packages/react-ui/styles.css

**/.rpt2_cache
.vscode/settings.json

**/.turbo
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
16 changes: 9 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# DatoCMS Plugin SDK monorepo

Lerna monorepo with **fixed/lockstep versioning** (both packages always share one version number) containing two npm packages:
npm-workspaces monorepo, built with Turborepo and released with Changesets, using **fixed/lockstep versioning** (both packages always share one version number). It contains two npm packages:

- `packages/sdk` → `datocms-plugin-sdk` — core TypeScript SDK for building DatoCMS plugins
- `packages/react-ui` → `datocms-react-ui` — React components mimicking the DatoCMS UI; depends on `datocms-plugin-sdk`

## Setup and commands (run from repo root)

```bash
npm install && npx lerna bootstrap # required on fresh checkout; package-local commands fail without it
npx lerna run build # builds in dependency order (sdk before react-ui)
npm test # Jest unit tests
npm run format # biome check + format
npm install # workspaces: one install at the root wires up both packages
npm run build # turbo; builds in dependency order (sdk before react-ui), and caches
npm test # Jest unit tests
npm run format # biome check + format
npx changeset # describe a change for the next release (commit the file it writes)
```

## Testing changes inside a real plugin
Expand All @@ -26,8 +27,9 @@ Use it instead of `npm link` (a symlinked React library breaks with duplicate-Re

## Gotchas

- CI (`.github/workflows/node.js.yml`) triggers on `main`, but the default branch is `master`, so PRs get no automated checks — run `npm test` and a full `npx lerna run build` locally before opening one.
- Releasing (maintainers only): `npm run publish` from the root → `lerna publish` bumps both packages, commits, tags `vX.Y.Z`, and publishes to npm.
- Every user-visible change needs a changeset (`npx changeset`) in the same PR, or it ships with no release note. `patch` is for bug fixes only; new API surface is `minor`.
- Releasing (maintainers only): `npm run publish` from the root, on a clean `master`. It builds and tests, applies the pending changesets, publishes to npm, then tags and pushes. An interrupted release is resumed by re-running it, never undone. See `bin/publish.sh`.
- Git tags are per-package now (`datocms-plugin-sdk@2.2.7`), not the single `vX.Y.Z` Lerna used to create.

## More detail

Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,29 @@ From there, the [Plugin SDK documentation](https://www.datocms.com/docs/plugin-s

## Developing

To work on the packages themselves (e.g. to prepare a PR or debug an issue in the framework itself), see the "Developing" section of each package's README: [`datocms-plugin-sdk`](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/README.md#developing), [`datocms-react-ui`](https://github.com/datocms/plugins-sdk/blob/master/packages/react-ui/README.md#developing). Both are developed in this Lerna monorepo and released in lockstep.
To work on the packages themselves (e.g. to prepare a PR or debug an issue in the framework itself), see the "Developing" section of each package's README: [`datocms-plugin-sdk`](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/README.md#developing), [`datocms-react-ui`](https://github.com/datocms/plugins-sdk/blob/master/packages/react-ui/README.md#developing). Both are developed in this npm-workspaces monorepo and released in lockstep.

## Releasing

Maintainers only. The two packages share one version number and are always
released together.

1. **Describe your change.** Run `npx changeset` in the same PR that makes the
change: it asks which packages are affected and whether the bump is a
patch/minor/major, then writes a small markdown file under `.changeset/`
which you commit. `patch` is for bug fixes only; new API surface is
`minor`. See [`.changeset/README.md`](.changeset/README.md).
2. **Release.** From an up-to-date, clean `master`, run `npm run publish`.
It builds and tests first, then applies the pending changesets (bumping the
versions and writing the `CHANGELOG.md`s), publishes to npm, and only then
tags and pushes to GitHub.

If a release is interrupted, **do not undo anything**: run `npm run publish`
again. It detects that some package is still missing from the registry and
resumes the publish instead of starting a new release.

`npm run publish-next` does the same under the `next` dist-tag, leaving
`latest` untouched.

## License

Expand Down
140 changes: 140 additions & 0 deletions bin/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
#
# Releases datocms-plugin-sdk and datocms-react-ui.
#
# The order of the steps below is the whole point: everything that can fail
# (network, tests, credentials) runs BEFORE anything irreversible happens, and
# the irreversible steps are ordered so that npm goes first and git follows.
#
# There is deliberately no rollback. `changeset publish` skips packages whose
# version is already on the registry, so if this script dies halfway through you
# recover by running it again: it notices that some package is still missing
# from npm and resumes the publish instead of starting a new release.

set -euo pipefail

cd "$(dirname "$0")/.."

DIST_TAG=""
while [ $# -gt 0 ]; do
case "$1" in
--tag|--dist-tag) DIST_TAG="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done

step() { printf '\n\033[1m==> %s\033[0m\n' "$1"; }
fail() { printf '\n\033[31mAborted: %s\033[0m\n' "$1" >&2; exit 1; }

BRANCH="$(git rev-parse --abbrev-ref HEAD)"

# Every workspace package, as "name version" pairs.
packages() {
npm query .workspace --no-workspaces-update 2>/dev/null \
| node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{for(const p of JSON.parse(s))console.log(p.name,p.version)})'
}
version() { node -p "require('./packages/sdk/package.json').version"; }
pending_changesets() { find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l | tr -d ' '; }

# The resume condition: at least one package whose local version is not yet on
# the registry. Checking a single package would be wrong — a release can die
# after publishing the first one.
unpublished() {
local name ver missing=""
while read -r name ver; do
npm view "$name@$ver" version >/dev/null 2>&1 || missing="$missing $name@$ver"
done < <(packages)
echo "${missing# }"
}

# ---------------------------------------------------------------------------
# Preflight: no mutations, just refuse to start from a state we can't finish.
# ---------------------------------------------------------------------------
step "Preflight"

# Normal releases happen on master. Prereleases are routinely cut from a feature
# branch, so --tag only asks that the branch be clean and pushed.
if [ -z "$DIST_TAG" ]; then
[ "$BRANCH" = "master" ] || fail "you are not on master. Use --tag to publish a prerelease from a branch."
[ ! -f .changeset/pre.json ] || fail "the repo is in changesets pre mode (.changeset/pre.json).
Run 'npx changeset pre exit' before cutting a real release."
fi

[ -z "$(git status --porcelain)" ] || fail "working tree is dirty. Commit or stash first."

git fetch --quiet origin "$BRANCH"
[ "$(git rev-parse HEAD)" = "$(git rev-parse "origin/$BRANCH")" ] || \
fail "$BRANCH and origin/$BRANCH have diverged. Pull (or push) first."

npm whoami >/dev/null 2>&1 || fail "you are not logged in to npm. Run 'npm login'."

echo "on $BRANCH, in sync with origin, npm user: $(npm whoami)"

# ---------------------------------------------------------------------------
# Decide between a fresh release and resuming an interrupted one.
# ---------------------------------------------------------------------------
if [ "$(pending_changesets)" -eq 0 ]; then
MISSING="$(unpublished)"
[ -n "$MISSING" ] || fail "no pending changesets: there is nothing to release.
Describe your changes with 'npx changeset' first."
step "Resuming the interrupted release of v$(version)"
echo "still missing from npm:$(printf ' %s' $MISSING)"
RESUMING=1
else
RESUMING=0
fi

if [ "$RESUMING" -eq 0 ]; then
CURRENT="$(version)"

# -------------------------------------------------------------------------
# Everything that can fail. Nothing has been mutated yet, so a network
# timeout here costs you nothing but the rerun.
# -------------------------------------------------------------------------
step "Building"
npm run build

step "Testing"
npm test

# -------------------------------------------------------------------------
# Mutations, local only: bump, relock, commit.
# Still nothing pushed, still nothing published.
# -------------------------------------------------------------------------
step "Applying pending changesets"
npx changeset version

NEXT="$(version)"
[ "$NEXT" != "$CURRENT" ] || fail "changeset version did not bump anything."
echo "$CURRENT -> $NEXT"

[ -n "$(unpublished)" ] || fail "version $NEXT is already on npm. Aborting before overwriting anything."

step "Refreshing the lockfile"
npm install --package-lock-only

step "Committing v$NEXT"
git add -A
git commit -m "v$NEXT"
fi

VERSION="$(version)"

# ---------------------------------------------------------------------------
# The irreversible step. npm first; changeset creates the git tags only for the
# packages it actually managed to publish.
# ---------------------------------------------------------------------------
step "Publishing v$VERSION to npm"
if [ -n "$DIST_TAG" ]; then
npx changeset publish --tag "$DIST_TAG"
else
npx changeset publish
fi

# ---------------------------------------------------------------------------
# git follows npm.
# ---------------------------------------------------------------------------
step "Pushing to GitHub"
git push --follow-tags origin "$BRANCH"

printf '\n\033[32mReleased v%s\033[0m\n' "$VERSION"
6 changes: 0 additions & 6 deletions lerna.json

This file was deleted.

Loading