From fb8bb69a9af6874e108aa3ab237fbcecbd5a9997 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 28 Jun 2026 07:39:28 -0400 Subject: [PATCH 1/2] Map preserved island JS into companion-plugin payload (#488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Populate CompanionPluginPayload.preserved_js[] from the runtime-island feed so runtime-island JS rides the SSI companion plugin, scoped to the generated block that owns it. The island->generated-block association did not previously exist: runtime islands and generated custom blocks are produced from disjoint elements, and a generated block consumes its source subtree wholesale while stripping ' + . '' + . '' + . '' + . '', + ), + ) +)->toArray(); +$companionJsPayload = $companionJs['source_reports']['companion_plugin_payload'] ?? array(); +$preservedJs = $companionJsPayload['preserved_js'] ?? array(); +$generatedName = (string) ($companionJsPayload['blocks'][0]['name'] ?? ''); +$assert('' !== $generatedName, 'companion JS fixture generates a custom block to scope island JS against'); +$assert(1 === count($preservedJs), 'preserved_js carries exactly the one scoped island entry'); +$scopedIsland = $preservedJs[0] ?? array(); +$assert('ssi-acme/' . $generatedName === ($scopedIsland['block'] ?? ''), 'preserved island JS is scoped to its owning generated block'); +$assert(str_contains((string) ($scopedIsland['content'] ?? ''), 'window.__pricing'), 'preserved island entry carries the verbatim inline JS body'); +$assert(str_starts_with((string) ($scopedIsland['handle'] ?? ''), 'runtime-island-'), 'preserved island entry derives a stable generic handle'); +$assert('islands/' . ($scopedIsland['handle'] ?? '') . '.js' === ($scopedIsland['src'] ?? ''), 'preserved island entry points at a per-handle island file'); +$deferredJs = $companionJsPayload['preserved_js_deferred'] ?? array(); +$assert(1 === count($deferredJs), 'a free-standing script island with no generated-block owner is deferred, not dropped'); +$assert('no_generated_block_owner' === ($deferredJs[0]['reason'] ?? ''), 'deferred island records why it could not be scoped'); +$assert(str_contains((string) ($deferredJs[0]['content'] ?? ''), '__standalone'), 'deferred island still carries its verbatim JS for the scoping follow-up'); +$companionJsBlob = json_encode($companionJsPayload, JSON_UNESCAPED_SLASHES); +$assert(is_string($companionJsBlob) && ! str_contains($companionJsBlob, 'gtag'), 'telemetry island JS is dropped, never carried into the payload'); + $companionNoSite = $compiler->compile( array( 'files' => array( From 040b38ac55cff97fda9e38da5dbce769588667a7 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 28 Jun 2026 07:58:50 -0400 Subject: [PATCH 2/2] Promote free-standing preserved island JS to site-wide companion entries Free-standing behavior islands (no generated-block owner) were parked in preserved_js_deferred with reason=no_generated_block_owner, deferring their verbatim JS to nowhere. Promote them to real site-wide preserved_js entries (scope='site', no `block` key, deterministic order from the package index) so the companion-plugin consumer enqueues them for the whole site. The block-scoped path (#488) is unchanged: owned islands keep their `block` key and carry no `scope`. The owner_block_not_packaged anomaly (a named owner block that did not ship) is still deferred rather than guessed into a site-wide scope. Telemetry/droppable and external-unmaterialized scripts still contribute nothing. RuntimeIslandPackageBuilder stays product-neutral; this projection lives in the product-named producer. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CompanionPluginPayload.php | 71 ++++++++++----- php-transformer/tests/contract/run.php | 89 +++++++++++++++++-- 2 files changed, 127 insertions(+), 33 deletions(-) diff --git a/php-transformer/src/ArtifactCompiler/CompanionPluginPayload.php b/php-transformer/src/ArtifactCompiler/CompanionPluginPayload.php index afd6afa9..8861e1ce 100644 --- a/php-transformer/src/ArtifactCompiler/CompanionPluginPayload.php +++ b/php-transformer/src/ArtifactCompiler/CompanionPluginPayload.php @@ -20,14 +20,19 @@ * - site_name (string) optional human-readable name; defaults to slug. * - mu_plugin (bool) optional; emit as a must-use loader. * - blocks[] (array) each: name, block_json, render, view_js, assets{}. - * - preserved_js[] (array) each: content, handle, src, block. The JS->plugin - * wire-up (SSI #488): verbatim island JS projected from - * the generic runtime-island package, scoped to the - * generated block that owns it. - * - preserved_js_deferred[] (array) each: content, handle, src, reason. - * Preserve-worthy island JS with no sound generated-block - * scope; surfaced (not silently dropped) because the - * consumer drops unscoped entries. + * - preserved_js[] (array) verbatim island JS projected from the generic + * runtime-island package (SSI #488). Two shapes: + * block-scoped entries carry content, handle, src, block + * and are enqueued via render_block when that block renders; + * site-wide entries carry content, handle, src, scope='site', + * order (no `block` key) and are enqueued for the whole site. + * Free-standing behavior islands with no generated-block + * owner take the site-wide shape so their JS lands instead + * of being parked. + * - preserved_js_deferred[] (array) each: content, handle, src, reason. Island JS + * whose owning block exists but was not packaged into this + * payload (owner_block_not_packaged) — a genuine anomaly, + * surfaced rather than silently dropped or guessed. * * When the artifact carries no custom blocks (mapping still prefers * core/Automattic blocks with a core/html fallback, and no subtree qualified for @@ -100,8 +105,9 @@ public function fromBlockTypes(array $blockTypes, array $files, array $artifact, $payload = array( 'schema' => self::SCHEMA, 'blocks' => $blocks, - // Preserved island JS projected from the generic runtime-island package - // and scoped to its owning generated block (SSI #488). + // Preserved island JS projected from the generic runtime-island package: + // block-scoped to its owning generated block, or site-wide for free-standing + // behavior islands with no generated-block owner (SSI #488). 'preserved_js' => $preserved['preserved_js'], ); if ( array() !== $preserved['deferred'] ) { @@ -128,20 +134,22 @@ public function fromBlockTypes(array $blockTypes, array $files, array $artifact, * scaffold's preserved_js contract (issue #488), gated by the * preserve-vs-rebuild signal (issue #224). * - * Only islands the engine could soundly associate with a generated block are - * scoped and emitted: a script captured at custom-block generation carries the - * owning block's fully-qualified name (`owner_block`), and the consumer enqueues - * its JS via render_block only when that block renders. Free-standing islands - * (canvas, form, and standalone scripts elsewhere in the DOM) have no sound - * generated-block owner — the consumer drops unscoped entries — so they are - * surfaced as declared deferrals rather than emitted with a guessed scope or - * dropped silently. Telemetry/droppable and external-unmaterialized scripts - * carry no verbatim JS body, so they contribute neither an entry nor a deferral. + * An island captured at custom-block generation carries the owning block's + * fully-qualified name (`owner_block`). When that block ships in this payload the + * island is emitted block-scoped, and the consumer enqueues its JS via render_block + * only when that block renders. Free-standing islands (canvas, form, and standalone + * scripts elsewhere in the DOM) have no generated-block owner; they are promoted to + * site-wide entries (`scope='site'`, no `block`, deterministic `order`) so the + * consumer enqueues them for the whole site instead of parking the JS. The narrow + * anomaly where an owner_block is named but was not packaged is still deferred + * (owner_block_not_packaged) rather than guessed into a site-wide scope. + * Telemetry/droppable and external-unmaterialized scripts carry no verbatim JS body, + * so they contribute neither an entry nor a deferral. * * @param array $runtimeIslandPackage Generic runtime-island package. * @param array> $blocks Packaged companion blocks. * @param string $blockNamespace Per-site block namespace (`ssi-`). - * @return array{preserved_js: array>, deferred: array>} + * @return array{preserved_js: array>, deferred: array>} */ private function preservedJs(array $runtimeIslandPackage, array $blocks, string $blockNamespace): array { @@ -164,7 +172,7 @@ private function preservedJs(array $runtimeIslandPackage, array $blocks, string $preserved = array(); $deferred = array(); - foreach ( $islands as $island ) { + foreach ( $islands as $index => $island ) { if ( ! is_array($island) ) { continue; } @@ -189,14 +197,29 @@ private function preservedJs(array $runtimeIslandPackage, array $blocks, string $owner = is_scalar($island['owner_block'] ?? null) ? (string) $island['owner_block'] : ''; if ( '' !== $owner && isset($ownedBlocks[$owner]) ) { + // Scoped to the owning generated block: the consumer enqueues it via + // render_block only when that block renders (UNCHANGED, #488). $entry['block'] = $owner; $preserved[] = $entry; continue; } - // No sound generated-block scope: surface as a declared deferral so the - // JS loss is visible rather than silent. - $entry['reason'] = '' !== $owner ? 'owner_block_not_packaged' : 'no_generated_block_owner'; + if ( '' === $owner ) { + // Free-standing behavior island with no generated-block owner: promote + // to a site-wide preserved_js entry. The consumer enqueues these for + // the whole site rather than gating on a block, so the JS lands instead + // of being parked. Islands arrive ordered, so the package index gives a + // deterministic enqueue order. No `block` key by contract. + $entry['scope'] = 'site'; + $entry['order'] = (int) $index; + $preserved[] = $entry; + continue; + } + + // The owning block exists but was not packaged into this payload — a + // genuine anomaly. Keep deferring (rather than guessing a site-wide scope) + // so the loss is visible for follow-up. + $entry['reason'] = 'owner_block_not_packaged'; $deferred[] = $entry; } diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 848c97b1..f206149e 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -1830,9 +1830,10 @@ public function match(DOMElement $element, PatternContext $context): ?array // Preserved island JS producer (issue #488): verbatim behavior JS captured at // custom-block generation is projected into preserved_js, scoped to the block // that owns it (the consumer enqueues it via render_block only when that block -// renders). A free-standing script island with no sound generated-block owner is -// surfaced as a declared deferral — never silently dropped or scoped by guess — -// and telemetry JS is dropped entirely (preserve-vs-rebuild, issue #224). +// renders). A free-standing script island with no generated-block owner is +// promoted to a site-wide preserved_js entry (scope='site', no `block`) so its JS +// lands instead of being parked, and telemetry JS is dropped entirely +// (preserve-vs-rebuild, issue #224). $companionJs = $compiler->compile( array( 'site' => array( 'name' => 'Acme Co', 'slug' => 'acme' ), @@ -1855,19 +1856,89 @@ public function match(DOMElement $element, PatternContext $context): ?array $preservedJs = $companionJsPayload['preserved_js'] ?? array(); $generatedName = (string) ($companionJsPayload['blocks'][0]['name'] ?? ''); $assert('' !== $generatedName, 'companion JS fixture generates a custom block to scope island JS against'); -$assert(1 === count($preservedJs), 'preserved_js carries exactly the one scoped island entry'); -$scopedIsland = $preservedJs[0] ?? array(); +$assert(2 === count($preservedJs), 'preserved_js carries the scoped owner island and the promoted site-wide island'); + +// The owned island stays block-scoped (UNCHANGED, #488): carries a `block` key and +// no `scope` key. +$scopedIsland = null; +$siteWideIsland = null; +foreach ( $preservedJs as $islandEntry ) { + if ( isset($islandEntry['block']) ) { + $scopedIsland = $islandEntry; + } elseif ( 'site' === ($islandEntry['scope'] ?? '') ) { + $siteWideIsland = $islandEntry; + } +} +$assert(is_array($scopedIsland), 'preserved_js still carries a block-scoped island entry'); +$assert(! array_key_exists('scope', $scopedIsland), 'block-scoped island carries no scope key (scoped path unchanged)'); $assert('ssi-acme/' . $generatedName === ($scopedIsland['block'] ?? ''), 'preserved island JS is scoped to its owning generated block'); $assert(str_contains((string) ($scopedIsland['content'] ?? ''), 'window.__pricing'), 'preserved island entry carries the verbatim inline JS body'); $assert(str_starts_with((string) ($scopedIsland['handle'] ?? ''), 'runtime-island-'), 'preserved island entry derives a stable generic handle'); $assert('islands/' . ($scopedIsland['handle'] ?? '') . '.js' === ($scopedIsland['src'] ?? ''), 'preserved island entry points at a per-handle island file'); -$deferredJs = $companionJsPayload['preserved_js_deferred'] ?? array(); -$assert(1 === count($deferredJs), 'a free-standing script island with no generated-block owner is deferred, not dropped'); -$assert('no_generated_block_owner' === ($deferredJs[0]['reason'] ?? ''), 'deferred island records why it could not be scoped'); -$assert(str_contains((string) ($deferredJs[0]['content'] ?? ''), '__standalone'), 'deferred island still carries its verbatim JS for the scoping follow-up'); + +// The free-standing standalone island is now promoted to a site-wide preserved_js +// entry (the producer half of site-wide companion JS): scope='site', no `block` +// key, carries the verbatim JS, a stable handle/src, and a deterministic order. +$assert(is_array($siteWideIsland), 'free-standing standalone island is promoted to a site-wide preserved_js entry'); +$assert('site' === ($siteWideIsland['scope'] ?? ''), 'promoted free-standing island declares site scope'); +$assert(! array_key_exists('block', $siteWideIsland), 'site-wide island carries no block key by contract'); +$assert(str_contains((string) ($siteWideIsland['content'] ?? ''), '__standalone'), 'site-wide island carries its verbatim standalone JS body'); +$assert(str_starts_with((string) ($siteWideIsland['handle'] ?? ''), 'runtime-island-'), 'site-wide island derives a stable generic handle'); +$assert('islands/' . ($siteWideIsland['handle'] ?? '') . '.js' === ($siteWideIsland['src'] ?? ''), 'site-wide island points at a per-handle island file'); +$assert(is_int($siteWideIsland['order'] ?? null), 'site-wide island carries a deterministic integer order'); + +// With the standalone island promoted and no owner_block_not_packaged anomaly, the +// payload defers nothing. +$assert(! array_key_exists('preserved_js_deferred', $companionJsPayload), 'no deferral list when every preserve-worthy island is emitted'); $companionJsBlob = json_encode($companionJsPayload, JSON_UNESCAPED_SLASHES); $assert(is_string($companionJsBlob) && ! str_contains($companionJsBlob, 'gtag'), 'telemetry island JS is dropped, never carried into the payload'); +// owner_block_not_packaged anomaly (a named owner block that did not ship in this +// payload) is still deferred — never promoted to a site-wide scope by guess. Built +// directly against the producer since the compiler path does not naturally orphan a +// named owner. A sibling no-owner island in the same package proves the site-wide +// promotion and the deferral coexist. +$companionDirectPayload = ( new \Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler\CompanionPluginPayload() )->fromBlockTypes( + array(), + array(), + array( 'site' => array( 'slug' => 'acme', 'name' => 'Acme Co' ) ), + array( + array( + 'name' => 'pricing', + 'block_json' => array( 'apiVersion' => 3, 'name' => 'ssi-acme/pricing', 'render' => 'file:./render.php' ), + 'render' => ' array( + array( + 'disposition' => 'preserve', + 'js_handling' => 'preserve_verbatim', + 'handle_hint' => 'runtime-island-orphan', + 'owner_block' => 'ssi-acme/missing', + 'scripts' => array( array( 'content' => 'window.__orphan=1;' ) ), + ), + array( + 'disposition' => 'preserve', + 'js_handling' => 'preserve_verbatim', + 'handle_hint' => 'runtime-island-floating', + 'owner_block' => '', + 'scripts' => array( array( 'content' => 'window.__floating=1;' ) ), + ), + ), + ) +); +$directPreservedJs = $companionDirectPayload['preserved_js'] ?? array(); +$directDeferred = $companionDirectPayload['preserved_js_deferred'] ?? array(); +$assert(1 === count($directPreservedJs), 'no-owner island promotes to a single site-wide entry alongside the deferred anomaly'); +$assert('site' === ($directPreservedJs[0]['scope'] ?? ''), 'no-owner island promotes to a site-wide entry'); +$assert(! array_key_exists('block', $directPreservedJs[0]), 'promoted site-wide entry carries no block key'); +$assert(str_contains((string) ($directPreservedJs[0]['content'] ?? ''), '__floating'), 'promoted site-wide entry carries the no-owner JS'); +$assert(1 === count($directDeferred), 'owner_block_not_packaged island is still deferred, not promoted'); +$assert('owner_block_not_packaged' === ($directDeferred[0]['reason'] ?? ''), 'orphaned owner island defers with the owner_block_not_packaged reason'); +$assert(! array_key_exists('scope', $directDeferred[0]), 'owner_block_not_packaged deferral is not given a site-wide scope'); +$assert(str_contains((string) ($directDeferred[0]['content'] ?? ''), '__orphan'), 'owner_block_not_packaged deferral still carries its verbatim JS'); + $companionNoSite = $compiler->compile( array( 'files' => array(