Skip to content

Commit bb2bafe

Browse files
authored
blocks-engine 0.2.1: bundle the WordPress runtime — ~491MB → ~53MB consumer install (#377)
* docs(adr): 0004 bundle the WordPress runtime into dist Records the Tier 2 decision to ship the WP runtime as a self-contained CJS chunk in dist and move @wordpress/* to devDependencies, cutting the consumer install footprint from ~491 MB (post-Tier-1) to ~53 MB with no public API change. Captures the rejected alternatives (pnpm overrides are root-only and cannot help consumers; peer/optional deps still install the full tree; ESM bundling fails on dynamic require) and the consequences (React 18 isolated in the bundle per ADR 0002/0003; stub set guarded by the golden suite; DLA must declare its phantom @wordpress/interactivity import). * build(blocks-engine): bundle the WP runtime into dist/wp-runtime.cjs * refactor(blocks-engine): load WP runtime through requireWp resolver * build(blocks-engine): move @wordpress/* to devDependencies, inline parser * test(blocks-engine): add bundle-mode full-suite gate (test:bundle) ## Summary Adds BLOCKS_ENGINE_WP_RUNTIME_PATH env override to bundlePath() so vitest (which runs from src/) can load the pre-built dist/wp-runtime.cjs without MODULE_NOT_FOUND. Adds test:bundle script that builds the bundle then runs the full suite in forced bundle mode. ## Why vitest runs from src/, so the relative bundlePath() resolved to src/wp-runtime.cjs (non-existent) when BLOCKS_ENGINE_WP_RUNTIME=bundle was forced. The path override lets the test:bundle script pass the absolute dist path so forked worker children also inherit it. ## Testing - pnpm test: 439 passed (94 test files) — dev-mode baseline - pnpm test:bundle: 439 passed (94 test files) — bundle-mode, identical to dev - Bundle size: 21 MB (ceiling 30 MB — size-ok) * fix(blocks-engine): resolve wp-runtime.cjs from tsup shared ESM chunks ## Summary `bundlePath()` in `require-wp.ts` assumed the compiled code would always live at `dist/wp/`, so it used `../wp-runtime.cjs` to reach `dist/wp-runtime.cjs`. When tsup places the shared chunk at `dist/chunk-*.js` instead of `dist/wp/index.js`, `import.meta.url` points to `dist/`, making `../wp-runtime.cjs` resolve one level too high (to the package root rather than `dist/`). `existsSync` returns false, mode falls through to `deps`, and a real install throws `MODULE_NOT_FOUND` for `@wordpress/block-library`. ## Why Discovered during Gate 2 consumer smoke test: a fresh `npm install` of the packed tarball failed with `Cannot find module '@wordpress/block-library'` because `dist/wp-runtime.cjs` was not being detected in bundle mode. ## How `bundlePath()` now tries the parent-relative path (`../wp-runtime.cjs`) first; if it doesn't exist on disk, falls back to the same-dir path (`./wp-runtime.cjs`). Works from both `dist/wp/` (non-chunk-split) and `dist/` (chunk-split). Same logic applied symmetrically for the CJS `__dirname` branch. ## Testing - `pnpm test` 439/439 (dev mode) - `pnpm test:bundle` 439/439 (bundle mode) - `scripts/verify-consumer-footprint.sh` smoke: PASS, wpHtmlResidue=0 * build(blocks-engine): omit wp-runtime.cjs sourcemap from published package The esbuild sourcemap for the bundled WP runtime was 51.9 MB — 2.5× larger than the bundle itself (20.4 MB) — and was being included in the published tarball because `files: ["dist"]` picks up everything in dist/. npm's `files` field takes absolute precedence over `.npmignore`, so the only reliable fix is to stop generating the map. The sourcemap has no value for package consumers: it maps minified WP internals that consumers never read or debug. Removing it drops the consumer footprint from ~109 MB to ~57 MB, bringing it into the target range of 45–60 MB. The map can be regenerated locally by temporarily restoring `sourcemap: true` in this script. * test(blocks-engine): add consumer-footprint verification script (Gate 2) `scripts/verify-consumer-footprint.sh` is the Gate 2 end-to-end proof that the published package works for real consumers: 1. Runs `pnpm build` to produce a clean dist including `dist/wp-runtime.cjs` 2. `pnpm pack --pack-destination $WORK` — tarball lands in a temp dir, never in the source tree 3. Installs the tarball into a fresh throwaway consumer (`npm init -y && npm install`) 4. Reports `du -sh node_modules` — expected 45–60 MB (measured: 57 MB) 5. Asserts no `@wordpress/*` directories are installed 6. Runs a Node CJS smoke: imports `@automattic/blocks-engine/wp`, calls `bootstrap()` then `rawConvert()` on bare block-level HTML, asserts `wpHtmlResidue === 0` with no env var needed (bundle auto-detected via `existsSync`) * ci(blocks-engine): enforce bundle-mode gate and bundle size ceiling The dev-mode `pnpm test` job resolves the real @wordpress/* packages, so it never exercises the bundled runtime or its edit-only stub aliases. Add a `pnpm test:bundle` step (full suite forced against dist/wp-runtime.cjs) and a 30 MB size-ceiling assertion, so the stub-set fidelity guard and footprint budget promised in docs/adr/0004 are actually enforced on every PR. * chore(blocks-engine): release 0.2.1 — bundled WP runtime Bump to 0.2.1 and document the package-size reduction: the WordPress runtime is bundled into dist as a self-contained CJS chunk with @wordpress/* moved to devDependencies, cutting a clean consumer install from ~491 MB to ~53 MB with no public API change and identical reconstruct fidelity. Also records the preceding version-alignment fix (~2.0 GB -> ~491 MB) in the changelog. * fix(blocks-engine): satisfy tsc for jsdom and the bundle entry CI typecheck (`tsc --noEmit`, strict) failed on untyped imports the suite never flags: the two new tests statically imported `jsdom` (no @types/jsdom) and the esbuild-only `wp-runtime-entry.ts` re-exports untyped `@wordpress/*`. Add a shared `setupDomGlobals()` to dom-globals.ts that loads jsdom via createRequire (matching bootstrap.ts — no @types/jsdom needed) and have both tests use it instead of importing JSDOM directly. Exclude wp-runtime-entry.ts from tsconfig since it is a bundler input, not part of the typed surface. No dependency or lockfile change. Verified: typecheck clean, dev and bundle-mode suites 439/439, bundle 21 MB.
1 parent 7c8711f commit bb2bafe

19 files changed

Lines changed: 779 additions & 138 deletions

.github/workflows/js.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ jobs:
4646

4747
- name: Run tests
4848
run: pnpm test
49+
50+
- name: Run tests (bundle mode — fidelity against dist/wp-runtime.cjs)
51+
run: pnpm test:bundle
52+
53+
- name: Assert WP runtime bundle size ceiling (<=30MB)
54+
run: |
55+
size=$(du -m dist/wp-runtime.cjs | cut -f1)
56+
echo "wp-runtime.cjs = ${size}MB"
57+
test "$size" -le 30
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Bundle the WordPress runtime into `dist` (not a normal `@wordpress/*` dependency)
2+
3+
Ship the WordPress runtime the engine calls — `@wordpress/block-library` (`registerCoreBlocks`), `@wordpress/blocks` (`rawHandler`, `serialize`, `parse`, `createBlock`, `getBlockAttributes`), and `@wordpress/block-serialization-default-parser` — as a single self-contained **CJS chunk** in `dist`, loaded via `createRequire`. Move `@wordpress/*` to `devDependencies`; keep `jsdom`, `cheerio`, `domhandler` as runtime dependencies.
4+
5+
This cuts a consumer's install footprint from ~491 MB (post-Tier-1) to **~53 MB** (~20 MB bundle + ~32 MB jsdom/cheerio/domhandler), with no public API change. The bundle aliases nine edit-only leaf packages (`icons`, `ui`, `dataviews`, `image-cropper`, `server-side-render`, `commands`, `preferences`, `notices`, `keyboard-shortcuts`) to an empty stub at build time; `block-editor`, `components`, `core-data`, `patterns` are bundled real because they run the `@wordpress/private-apis` lock/unlock handshake at module load.
6+
7+
## Considered Options
8+
9+
- **Tier 1 only — version alignment (merged, PR #356).** Collapsed nested-duplicate `node_modules` (~2.0 GB → ~491 MB) but left the editor stack that `registerCoreBlocks()` pulls transitively (~296 MB of `@wordpress/*`). Necessary, not sufficient.
10+
11+
- **`pnpm.overrides` to empty-stub the unused packages.** Rejected. `overrides` are honored only in the install **root** (the consumer app), never from inside a dependency, so overrides in this package help only this repo's dev install — not consumers. Measured ~258 MB locally, ~0 benefit for DLA.
12+
13+
- **`peerDependencies` / `optionalDependencies` for the editor stack.** Rejected. `block-library` hard-depends on them; the consumer still installs the full tree.
14+
15+
- **Minimal custom block registration (register only emitted blocks).** Deferred. Could push below 20 MB but is a larger, riskier change; bundling already reaches the ~50 MB target. Revisit only if 50 MB proves insufficient.
16+
17+
- **ESM bundle.** Rejected. Fails at runtime with "Dynamic require not supported" (WP/React use dynamic `require`). CJS bundles work, and the engine already loads the runtime via `createRequire`.
18+
19+
## Consequences
20+
21+
- Consumers install no `@wordpress/*` at all; the runtime travels inside `dist`.
22+
- React 18 lives entirely inside the bundle — more isolated from a host's React 19 than today, consistent with the worker-`fork()` isolation of ADR 0002 and the entry points of ADR 0003 (default `.` never loads the bundle in-process; `/wp` and `/theme` and the worker child do).
23+
- The build gains a bundling step (esbuild via tsup): `format: cjs`, `minify`, `external: jsdom/cheerio/domhandler`, alias stub set → empty.
24+
- The stub set is correctness-sensitive: a future block reaching a stubbed package would regress output. Guarded by the full golden/reconstruct suite (incl. DLA goldens) run against the bundle, plus a CI assertion that production `dependencies` contain no `@wordpress/*` and the bundle stays under a size ceiling.
25+
- DLA (consumes only `@automattic/blocks-engine/theme`) is unaffected by the internal change, but must declare its direct `@wordpress/interactivity` import itself (currently an unresolved phantom dependency) — an independent cleanup.
26+
- The runtime chunk's sourcemap is intentionally **not** shipped: the map weighs ~52 MB (minified third-party WP code) and would more than double the consumer footprint. If the bundled runtime ever needs to be debugged, rebuild locally with `sourcemap: true` in `scripts/build-wp-runtime.mjs`.
27+
- Full design and validation gates: `docs/superpowers/specs/2026-06-29-blocks-engine-package-size-tier2-bundling-design.md` (local).

packages/blocks-engine/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
This package uses [Semantic Versioning](https://semver.org/). Deprecations are warned one minor version ahead of removal.
1010

11+
## [0.2.1] - 2026-06-29
12+
13+
### Changed
14+
15+
- The WordPress runtime (`@wordpress/block-library`, `@wordpress/blocks`, `@wordpress/block-serialization-default-parser`) is now compiled into a single self-contained CJS chunk shipped in `dist`, and `@wordpress/*` moved to `devDependencies`. A clean consumer install drops from ~491 MB to ~53 MB with no public API change. The runtime is loaded lazily through an internal resolver that prefers the bundle and falls back to real packages in development.
16+
- Nine edit-only `@wordpress` leaf packages (icons, ui, dataviews, image-cropper, server-side-render, commands, preferences, notices, keyboard-shortcuts) are aliased to an empty stub at build time, since the engine never executes block `edit` components — only `save`, `transforms`, and attribute sourcing. Fidelity is unchanged: the full reconstruct/golden suite passes identically against the bundle.
17+
18+
### Fixed
19+
20+
- Aligned all `@wordpress/*` dependencies to a single coherent release, eliminating nested duplicate `node_modules` (previously a mismatched version set installed ~12 copies of some packages). This alone cut a clean install from ~2.0 GB to ~491 MB.
21+
1122
## [0.2.0] - 2026-06-29
1223

1324
### Added
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Build-time stand-in for @wordpress packages reached only through block `edit`
2+
// components, which the engine never executes. Any access returns a no-op.
3+
const noop = function () { return null; };
4+
module.exports = new Proxy(noop, {
5+
get(_t, p) {
6+
if (p === '__esModule') return true;
7+
if (p === 'default') return noop;
8+
return noop;
9+
},
10+
});

packages/blocks-engine/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@automattic/blocks-engine",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "Convert HTML and static sites into WordPress block markup and block themes.",
55
"keywords": ["wordpress", "blocks", "gutenberg", "block-theme", "html", "static-site"],
66
"license": "GPL-3.0-or-later",
@@ -52,9 +52,10 @@
5252
}
5353
},
5454
"scripts": {
55-
"build": "tsup",
55+
"build": "tsup && node scripts/build-wp-runtime.mjs",
5656
"typecheck": "tsc --noEmit",
5757
"test": "vitest run",
58+
"test:bundle": "pnpm build && BLOCKS_ENGINE_WP_RUNTIME=bundle BLOCKS_ENGINE_WP_RUNTIME_PATH=\"$PWD/dist/wp-runtime.cjs\" vitest run",
5859
"bench": "node --import tsx scripts/tuner/bench.ts",
5960
"bench:record": "node --import tsx scripts/tuner/bench.ts --record",
6061
"bench:derive": "node --import tsx scripts/tuner/derive.ts",
@@ -63,13 +64,14 @@
6364
"dependencies": {
6465
"cheerio": "1.2.0",
6566
"domhandler": "5.0.3",
66-
"@wordpress/block-serialization-default-parser": "5.49.0",
67-
"@wordpress/blocks": "15.22.0",
68-
"@wordpress/block-library": "10.0.0",
6967
"jsdom": "29.0.1"
7068
},
7169
"devDependencies": {
7270
"@types/node": "^26.0.0",
71+
"@wordpress/block-library": "10.0.0",
72+
"@wordpress/block-serialization-default-parser": "5.49.0",
73+
"@wordpress/blocks": "15.22.0",
74+
"esbuild": "^0.24.0",
7375
"tsup": "8.0.0",
7476
"vitest": "2.0.0",
7577
"typescript": "5.7.3",

0 commit comments

Comments
 (0)