From eb6013b7bd2bd6e220e993fb55a4be54f164fd60 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Mon, 21 Sep 2026 17:56:28 +0100 Subject: [PATCH 1/2] fix: use the core block editor registry --- scripts/package-boundary-check.mjs | 13 +++++++++++++ src/headless/env.ts | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/package-boundary-check.mjs b/scripts/package-boundary-check.mjs index 9c31ca3..14917ab 100644 --- a/scripts/package-boundary-check.mjs +++ b/scripts/package-boundary-check.mjs @@ -32,6 +32,9 @@ const basic = installPacked(tarball, 'basic'); const manifest = basic.require('block-runner/package.json'); await verifyBasicConsumer(basic); +const conflictingGutenberg = installPacked(tarball, 'conflicting-gutenberg', ['@wordpress/blocks@16.0.0']); +await verifyConflictingGutenbergConsumer(conflictingGutenberg); + const proof = installPacked(tarball, 'proof'); await verifyProofConsumer(proof); const installedDependencyInventory = compareInstalledDependencyInventories( @@ -103,6 +106,16 @@ async function verifyBasicConsumer({ consumer, packageRoot, cli, require }) { } } +async function verifyConflictingGutenbergConsumer({ packageRoot }) { + const library = await import(pathToFileURL(path.join(packageRoot, 'dist', 'index.js')).href); + const report = await library.author('

Pinned runtime.

Core blocks agree.

Save hooks retain their attributes.

', { + author: { name: 'acme/conflicting-gutenberg-runtime', title: 'Conflicting Gutenberg runtime' }, + }); + if (!report.ok || !report.package?.canonicalPlan) { + throw new Error(`Packed authoring failed with a conflicting top-level @wordpress/blocks version: ${JSON.stringify(report.items)}`); + } +} + async function verifyProofConsumer({ consumer, packageRoot, cli, require }) { const manifest = require('block-runner/package.json'); const pins = Object.entries(manifest.peerDependencies ?? {}).map(([name, version]) => `${name}@${version}`); diff --git a/src/headless/env.ts b/src/headless/env.ts index b06743a..5ec346d 100644 --- a/src/headless/env.ts +++ b/src/headless/env.ts @@ -17,11 +17,18 @@ export function bootHeadlessWordPressSync(): WpModules { installDomGlobals(); const require = createRequire(import.meta.url); + const blockLibraryEntry = require.resolve('@wordpress/block-library'); + // Core block save functions call block-editor helpers. Resolve the registry from that same + // block-editor package rather than from this package: npm may otherwise nest an identical + // @wordpress/blocks version here when the consumer has a newer top-level copy. + const blockLibraryRequire = createRequire(blockLibraryEntry); + const blockEditorEntry = blockLibraryRequire.resolve('@wordpress/block-editor'); + const blockEditorRequire = createRequire(blockEditorEntry); const { blockLibrary, blocks } = withMutedWordPressConsole(() => ({ blockLibrary: require('@wordpress/block-library') as { registerCoreBlocks: () => void; }, - blocks: require('@wordpress/blocks') as WpModules, + blocks: blockEditorRequire('@wordpress/blocks') as WpModules, })); withMutedWordPressConsole(() => { From b2b6ad52c8c09e93488bcf581a9289d96cc147a5 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Mon, 21 Sep 2026 18:59:25 +0100 Subject: [PATCH 2/2] fix: loosen exact-pinned optional peer dependencies to caret ranges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate defect from the runtime Gutenberg-registry fix earlier on this branch: this is an install-time problem, not a runtime one. All six proof-command peerDependencies (@playwright/test, @wordpress/e2e-test-utils-playwright, @wordpress/env, axe-core, pixelmatch, pngjs) were exact-pinned despite being marked optional via peerDependenciesMeta. npm 7+ refuses to resolve an exact-pinned peer against an incompatible version already present in a consumer's tree, even when the peer is optional — so a plain `npm install block-runner` fails with ERESOLVE in any project (e.g. a @wordpress/scripts theme) that already has its own newer @playwright/test or @wordpress/e2e-test-utils-playwright for unrelated e2e tests. The only workarounds were --legacy-peer-deps (which disables peer-conflict checking for the whole install, not just this package) or --force. Publish caret ranges to consumers while keeping the proof command's own internal tooling versions exact-pinned via devDependencies, which is what src/proof/runner.ts and scripts/package-boundary-check.mjs now read from directly instead of peerDependencies. Co-Authored-By: Claude Sonnet 5 --- dev/test/proof-packaging.test.ts | 2 +- docs/reference.md | 5 +++-- package-lock.json | 12 ++++++------ package.json | 12 ++++++------ scripts/package-boundary-check.mjs | 8 ++++---- src/proof/runner.ts | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/dev/test/proof-packaging.test.ts b/dev/test/proof-packaging.test.ts index 7f2b99b..b51a3e9 100644 --- a/dev/test/proof-packaging.test.ts +++ b/dev/test/proof-packaging.test.ts @@ -31,7 +31,7 @@ describe('proof packaging boundary', () => { for (const [name, version] of Object.entries(proofTooling)) { expect(packageJson.dependencies[name]).toBeUndefined(); expect(packageJson.devDependencies[name]).toBe(version); - expect(packageJson.peerDependencies[name]).toBe(version); + expect(packageJson.peerDependencies[name]).toBe(`^${version}`); expect(packageJson.peerDependenciesMeta[name]).toEqual({ optional: true }); } }); diff --git a/docs/reference.md b/docs/reference.md index 4fc4afb..3a78a8f 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -118,8 +118,9 @@ Library plus `convert`, `assemble`, `validate`, `fix`, `author`, `plugin`, `cont and `skill` need only Block Runner's production dependencies. WP-CLI remains an external requirement only when selected for context, token, or media resolution. -The browser-proof packages are exact optional peers, absent from a basic installation. -Install them only where real-WordPress proof will run. +The browser-proof packages are optional peers with compatible caret ranges, absent from a basic +installation. The proof setup below deliberately pins the known-good versions for reproducible +real-WordPress proof. Set up the runtime/editor proof boundary before requesting `runtime` or `editor`: diff --git a/package-lock.json b/package-lock.json index d862e63..ca056ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,12 +46,12 @@ "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { - "@playwright/test": "1.61.1", - "@wordpress/e2e-test-utils-playwright": "1.51.0", - "@wordpress/env": "11.15.0", - "axe-core": "4.11.0", - "pixelmatch": "7.1.0", - "pngjs": "7.0.0" + "@playwright/test": "^1.61.1", + "@wordpress/e2e-test-utils-playwright": "^1.51.0", + "@wordpress/env": "^11.15.0", + "axe-core": "^4.11.0", + "pixelmatch": "^7.1.0", + "pngjs": "^7.0.0" }, "peerDependenciesMeta": { "@playwright/test": { diff --git a/package.json b/package.json index 6cb7d59..e4113a8 100644 --- a/package.json +++ b/package.json @@ -120,12 +120,12 @@ "vitest": "4.1.11" }, "peerDependencies": { - "@playwright/test": "1.61.1", - "@wordpress/e2e-test-utils-playwright": "1.51.0", - "@wordpress/env": "11.15.0", - "axe-core": "4.11.0", - "pixelmatch": "7.1.0", - "pngjs": "7.0.0" + "@playwright/test": "^1.61.1", + "@wordpress/e2e-test-utils-playwright": "^1.51.0", + "@wordpress/env": "^11.15.0", + "axe-core": "^4.11.0", + "pixelmatch": "^7.1.0", + "pngjs": "^7.0.0" }, "peerDependenciesMeta": { "@playwright/test": { diff --git a/scripts/package-boundary-check.mjs b/scripts/package-boundary-check.mjs index 14917ab..21fef3d 100644 --- a/scripts/package-boundary-check.mjs +++ b/scripts/package-boundary-check.mjs @@ -61,7 +61,7 @@ async function verifyBasicConsumer({ consumer, packageRoot, cli, require }) { const manifest = require('block-runner/package.json'); for (const name of PROOF_TOOLING) { if (manifest.dependencies[name]) throw new Error(`Proof tooling remains a production dependency: ${name}`); - if (manifest.peerDependencies?.[name] !== manifest.devDependencies?.[name]) throw new Error(`Proof peer is not pinned to development tooling: ${name}`); + if (manifest.peerDependencies?.[name] !== `^${manifest.devDependencies?.[name]}`) throw new Error(`Proof peer is not a compatible range from development tooling: ${name}`); if (manifest.peerDependenciesMeta?.[name]?.optional !== true) throw new Error(`Proof peer is not optional: ${name}`); try { require.resolve(`${name}/package.json`); @@ -118,10 +118,10 @@ async function verifyConflictingGutenbergConsumer({ packageRoot }) { async function verifyProofConsumer({ consumer, packageRoot, cli, require }) { const manifest = require('block-runner/package.json'); - const pins = Object.entries(manifest.peerDependencies ?? {}).map(([name, version]) => `${name}@${version}`); + const pins = PROOF_TOOLING.map((name) => `${name}@${manifest.devDependencies[name]}`); run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--save-dev', '--save-exact', ...pins], { cwd: consumer }); for (const name of PROOF_TOOLING) { - const expected = manifest.peerDependencies[name]; + const expected = manifest.devDependencies[name]; const installed = require(`${name}/package.json`).version; if (installed !== expected) throw new Error(`Proof tooling pin mismatch for ${name}: expected ${expected}, got ${installed}`); } @@ -207,7 +207,7 @@ function compareInstalledDependencyInventories(proofEnabled, basic) { function proofToolingInstallCommand(manifest, profile) { const required = profile === 'full' ? PROOF_TOOLING : PROOF_TOOLING.slice(0, 4); - return `npm install --save-dev --save-exact ${required.map((name) => `${name}@${manifest.peerDependencies?.[name]}`).join(' ')}`; + return `npm install --save-dev --save-exact ${required.map((name) => `${name}@${manifest.devDependencies?.[name]}`).join(' ')}`; } function run(command, args, { cwd = ROOT, expectedStatus = 0, env = process.env } = {}) { diff --git a/src/proof/runner.ts b/src/proof/runner.ts index ca33b09..6fb78a1 100644 --- a/src/proof/runner.ts +++ b/src/proof/runner.ts @@ -1917,7 +1917,7 @@ function commandFailure(command: string, error: unknown): ProofCommandResult { } function proofToolingVersion(name: (typeof PROOF_TOOLING)[number]): string { - const version = packageJson.peerDependencies?.[name] ?? packageJson.devDependencies?.[name]; + const version = packageJson.devDependencies?.[name]; if (!version) throw new Error(`Missing pinned proof tooling declaration for ${name}.`); return version; }