Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ jobs:
done
node packages/blocks/bin/deco.js --help > /dev/null

# tests/release.test.ts: lockstep versions, the peer ranges, and what
# npm would pack for each package (every export's files, including
# the clients' helper subpaths such as @decocms/apps-vtex/image, and
# no test or fixture), against the dist/ built above.
- name: Release checks
run: bun x vitest run tests/release.test.ts

# @decocms/tanstack's smoke app, built and run on workerd (`vite preview`
# runs the Worker in Miniflare): the worker's header matrix (edge cache,
# drafts, Studio framing, CMS redirects, __cseg) against a real TanStack
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ name: Release

on:
push:
# main releases the 8 lockstep v8 packages (@decocms/blocks + the 7
# @decocms/apps-* clients) under blocks-v*.*.* tags (.releaserc.json).
# v7 (@decocms/blocks 7.x, tanstack, nextjs, blocks-admin, blocks-cli,
# apps-commerce/-blog/-website) releases from the 7.x branch, with its
# main releases the 10 lockstep v8 packages (@decocms/blocks, the
# @decocms/tanstack and @decocms/nextjs bindings, the 7 @decocms/apps-*
# clients) under blocks-v*.*.* tags (.releaserc.json), on npm's `latest`.
# v7 (@decocms/blocks 7.x, tanstack 7.x, nextjs 7.x, blocks-admin,
# blocks-cli, apps-commerce/-blog/-website) releases from the 7.x branch, with its
# own copy of this workflow. Publishing goes through npm Trusted Publishing
# (OIDC) — each package has this workflow configured as its trusted
# publisher on npmjs.com; the "Release" step's id-token permission lets
Expand Down
2 changes: 1 addition & 1 deletion .releaserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@semantic-release/exec",
{
"prepareCmd": "VERSION=${nextRelease.version} node scripts/sync-versions.mjs && bun run build",
"publishCmd": "PACKAGES=\"blocks apps-algolia apps-magento apps-resend apps-sfmc-personalization apps-shopify apps-vtex apps-wake\"; if [ \"$DRY_RUN\" = \"true\" ]; then echo \"[dry-run] would publish version ${nextRelease.version} for: $PACKAGES\"; else failed=0; for name in $PACKAGES; do (cd \"packages/$name\" && npm publish --access public --tag ${nextRelease.channel ? nextRelease.channel : 'latest'}) || { echo \"::error::npm publish failed for packages/$name\"; failed=1; }; done; exit \"$failed\"; fi"
"publishCmd": "PACKAGES=\"blocks tanstack nextjs apps-algolia apps-magento apps-resend apps-sfmc-personalization apps-shopify apps-vtex apps-wake\"; if [ \"$DRY_RUN\" = \"true\" ]; then echo \"[dry-run] would publish version ${nextRelease.version} for: $PACKAGES\"; else failed=0; for name in $PACKAGES; do (cd \"packages/$name\" && npm publish --access public --tag ${nextRelease.channel ? nextRelease.channel : 'latest'}) || { echo \"::error::npm publish failed for packages/$name\"; failed=1; }; done; exit \"$failed\"; fi"
}
],
"@semantic-release/github"
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ docs/runbooks/ alert runbooks; the provisioned alerts' runbook_

v7-only design docs, plans and skills live on the `7.x` branch. The docs for users are the docs site (`/next/*`), not this repo.

The release allowlist in `.releaserc.json` is `blocks` plus the seven `apps-*`; the bindings join it, in lockstep, in the release PR.
The release allowlist in `.releaserc.json` is `blocks`, `tanstack`, `nextjs` and the seven `apps-*`; nothing else is published from this line. They release in lockstep: every release publishes all ten at one version, even a package with no change (`tests/release.test.ts` holds the allowlist equal to `packages/`).

### How packages build (per-file `tsc`, no bundler)

Expand All @@ -70,7 +70,8 @@ Every package publishes compiled JavaScript so plain Node (scripts, Playwright,
- **The `source` condition** lets in-repo tools use `.ts` without a build: `tsconfig.base.json` (`customConditions`) for type-checks and `vitest.config.ts` (`resolve.conditions`) for tests, so a test importing a file by path and one importing `@decocms/blocks` share one module. A consumer that opts into `source` must do so for every environment it builds; mixing `source` and dist in one process loads both copies.
- **The bin runs dist**: `bin/deco.js` imports `dist/v8/cli/main.js` (no tsx). `tests/globalSetup.ts` rebuilds blocks' dist before the suite because the CLI conformance tests spawn the bin.
- **Two TypeScripts**: builds and type-checks use TypeScript 7 (`typescript7`, npm alias of `typescript@7`); the plain `typescript` name stays on 5.x because TypeScript 7 has no JavaScript compiler API, and `deco schema` loads the app's `typescript` peer through that API (as do Next's type-check and a few conformance tests).
- **Release**: `.releaserc.json`'s `prepareCmd` runs `bun run build` after the version sync, and each package's `prepack` rebuilds, so `npm publish` never ships a stale or missing dist. CI (`.github/workflows/ci.yml`) type-checks, builds and imports every built export with plain Node.
- **Release**: `.releaserc.json`'s `prepareCmd` runs `bun run build` after the version sync, and each package's `prepack` rebuilds, so `npm publish` never ships a stale or missing dist. CI (`.github/workflows/ci.yml`) type-checks, builds, imports every built export of every package with plain Node (the `apps-*` helper subpaths such as `/image` included), and runs `tests/release.test.ts`, which also checks each export's shape and that `npm pack` ships every export's files and no test or fixture.
- **Ranges on `@decocms/blocks`**: the bindings and the `apps-*` take it as a **peer** with a caret range (`"^8.3.0"`), plus `"workspace:*"` in `devDependencies` for the workspace; never `workspace:*` in `dependencies`, which publishes as an exact pin and puts a second blocks copy next to the app on the next patch. `scripts/sync-versions.mjs` writes the release version, rewrites only `devDependencies`' `workspace:` ranges, and refuses an in-repo `dependencies` entry or a peer floor above the release. Raise the peer floor (all packages together) when a package starts using a newer blocks API.

### `@decocms/blocks` exports

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<img alt="Any framework" src="https://img.shields.io/badge/framework-any-FF4154?style=flat-square" />
</p>

`blocks` is a Bun workspace monorepo for Deco's CMS. This branch is the **next major (v8)**: one core package, `@decocms/blocks`, with the runtime, the `deco` CLI and the content protocol the site editor speaks; `@decocms/tanstack`, for TanStack Start sites on Cloudflare Workers; `@decocms/nextjs`, for any Next.js App Router app; and thin upstream clients, `@decocms/apps-*`. Packages export TypeScript source directly: no bundled dist layer, no duplicated runtime state.
`blocks` is a Bun workspace monorepo for Deco's CMS. This branch is the **next major (v8)**: one core package, `@decocms/blocks`, with the runtime, the `deco` CLI and the content protocol the site editor speaks; `@decocms/tanstack`, for TanStack Start sites on Cloudflare Workers; `@decocms/nextjs`, for any Next.js App Router app; and thin upstream clients, `@decocms/apps-*`. Each package publishes JavaScript compiled file for file by `tsc` (`dist/`, with declarations), plus its TypeScript source under a `source` export condition for the workspace: nothing is bundled, so no runtime state is duplicated.

v7 (`@decocms/tanstack` 7.x, `@decocms/nextjs` 7.x, `@decocms/blocks-admin`, `@decocms/blocks-cli`, `@decocms/apps-commerce` and the rest) is maintained on the [`7.x` branch](https://github.com/decocms/blocks/tree/7.x).

Expand All @@ -50,7 +50,7 @@ v7 (`@decocms/tanstack` 7.x, `@decocms/nextjs` 7.x, `@decocms/blocks-admin`, `@d

## Packages

All packages are versioned and released in lockstep.
All packages are versioned and released in lockstep, starting at 8.3.0 for the bindings. The bindings and the clients take `@decocms/blocks` as a peer dependency (`^8.3.0`), so a site installs one copy of it.

| Package | Purpose |
| --- | --- |
Expand Down
35 changes: 28 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/apps-algolia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"test": "vitest run --root ../.. packages/apps-algolia/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-magento/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"test": "vitest run --root ../.. packages/apps-magento/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-resend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"test": "vitest run --root ../.. packages/apps-resend/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-sfmc-personalization/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"test": "vitest run --root ../.. packages/apps-sfmc-personalization/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-shopify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
"test": "vitest run --root ../.. packages/apps-shopify/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-vtex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
"test": "vitest run --root ../.. packages/apps-vtex/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion packages/apps-wake/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
"test": "vitest run --root ../.. packages/apps-wake/",
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
},
"dependencies": {
"peerDependencies": {
"@decocms/blocks": "^8.3.0"
},
"devDependencies": {
"@decocms/blocks": "workspace:*"
},
"publishConfig": {
Expand Down
77 changes: 56 additions & 21 deletions scripts/sync-versions.mjs
Original file line number Diff line number Diff line change
@@ -1,37 +1,72 @@
#!/usr/bin/env node
// Release-time helper: writes ${VERSION} into every packages/*/package.json,
// rewriting "workspace:*" dependencies to the concrete version so `npm publish`
// (run per-package, outside the workspace context) resolves correctly.
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
// Release-time helper (.releaserc.json prepareCmd): writes ${VERSION} into
// every packages/*/package.json, so all the packages publish in lockstep.
//
// Ranges between the packages:
// - A package that builds on @decocms/blocks takes it as a peer dependency
// with a caret range ("^8.3.0"), so a site has one blocks copy and a blocks
// patch release never installs a second one next to an app or a binding.
// The range is written by hand and left as is here: raise its floor when a
// package starts using a newer blocks API.
// - "workspace:" ranges are for the workspace only. They are allowed in
// devDependencies (rewritten to the release version, since npm cannot read
// them) and refused in dependencies and peerDependencies, where an exact
// pin would reach the sites.
//
// PACKAGES_DIR overrides the packages folder (the release test runs this on
// a copy of the manifests).
import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";

const version = process.env.VERSION;
if (!version) {
console.error("VERSION env var required");
if (!version || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version)) {
console.error(`VERSION env var required (a semver version), got ${JSON.stringify(version)}`);
process.exit(1);
}

const packagesDir = join(import.meta.dirname, "..", "packages");
const packageNames = new Set(
readdirSync(packagesDir).map((dir) => {
const pkg = JSON.parse(readFileSync(join(packagesDir, dir, "package.json"), "utf8"));
return pkg.name;
}),
const packagesDir = process.env.PACKAGES_DIR ?? join(import.meta.dirname, "..", "packages");
const dirs = readdirSync(packagesDir).filter((dir) =>
existsSync(join(packagesDir, dir, "package.json")),
);
const manifests = dirs.map((dir) => {
const path = join(packagesDir, dir, "package.json");
return { dir, path, pkg: JSON.parse(readFileSync(path, "utf8")) };
});
const packageNames = new Set(manifests.map(({ pkg }) => pkg.name));

for (const dir of readdirSync(packagesDir)) {
const pkgPath = join(packagesDir, dir, "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const [major, minor, patch] = version.split(/[.-]/).map(Number);
const floorAbove = (range) => {
const m = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range);
if (!m) return true; // not a plain caret range: refused below
const [a, b, c] = m.slice(1).map(Number);
if (a !== major) return true;
return b > minor || (b === minor && c > patch);
};

const errors = [];
for (const { dir, pkg } of manifests) {
pkg.version = version;
for (const depField of ["dependencies", "peerDependencies", "devDependencies"]) {
if (!pkg[depField]) continue;
for (const dep of Object.keys(pkg[depField])) {
if (packageNames.has(dep) && pkg[depField][dep] === "workspace:*") {
pkg[depField][dep] = version;
for (const field of ["dependencies", "peerDependencies", "devDependencies"]) {
for (const [dep, range] of Object.entries(pkg[field] ?? {})) {
if (!packageNames.has(dep)) continue;
if (field === "devDependencies") {
if (range.startsWith("workspace:")) pkg[field][dep] = version;
continue;
}
if (field === "dependencies") {
errors.push(`${dir}: ${dep} must be a peer dependency, not a dependency (${range})`);
} else if (floorAbove(range)) {
errors.push(
`${dir}: peer ${dep} "${range}" must be a caret range in major ${major} whose floor is at most ${version}`,
);
}
}
}
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
}
if (errors.length) {
console.error(`sync-versions: refusing to release ${version}:\n ${errors.join("\n ")}`);
process.exit(1);
}

for (const { path, pkg } of manifests) writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`);
console.log(`Synced version ${version} across ${packageNames.size} packages`);
Loading
Loading