Skip to content

Populate companion preserved_js from runtime islands (+ fix silent JS loss in generated blocks)#273

Closed
chubes4 wants to merge 2 commits into
trunkfrom
cook/companion-preserved-js-mapping
Closed

Populate companion preserved_js from runtime islands (+ fix silent JS loss in generated blocks)#273
chubes4 wants to merge 2 commits into
trunkfrom
cook/companion-preserved-js-mapping

Conversation

@chubes4

@chubes4 chubes4 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

What

Populates CompanionPluginPayload.preserved_js[] from runtime-island data so behavior JS embedded in generated custom blocks is carried into the SSI companion plugin (scoped per block), instead of being lost. The consumer already merged (SSI #496); this builds the producer half (#488).

Notable: fixes a silent JS-loss bug

While building the island→block association (which did not exist — generated blocks and runtime islands came from disjoint elements, and sanitizeHtmlString stripped <script> bodies from generated content), I found inline behavior scripts inside generated custom blocks were being silently dropped. The fix mints the association at generation time.

Change (php-transformer)

  • FallbackEmitter::maybeGenerateCustomBlock() now scans the generated subtree for runtime <script>s and records each as a script island tagged with the owning block's FQN via a generic owner_block field (shared scriptIslandMetadata() helper).
  • RuntimeIslandPackageBuilder carries owner_block through (stays product-neutral).
  • CompanionPluginPayload::preservedJs() projects islands → preserved_js, gated by Feature parity: explicit preserve-vs-rebuild decision per interactive region (verbatim JS only on verbatim markup) #224: only disposition=preserve + js_handling=preserve_verbatim, skipping telemetry/droppable. Per entry: content (verbatim JS), handle, src. Islands with a matching owner_block are scoped (block=ssi-<slug>/<name>); preserve-worthy islands with no sound owner go to preserved_js_deferred[] with a reason — never scoped by guess, never silently dropped.

Scope (honest)

  • Built end-to-end: scripts living inside a generated custom block → scoped, enqueued only when that block renders.
  • Deferred (declared): free-standing islands (standalone scripts, canvas/form) — no sound generated-block owner exists yet (reason: no_generated_block_owner), the #488 follow-up.
  • External unmaterialized scripts: skipped (consumer requires content). Materialized external: carried.

Verification

  • composer test + composer parity144 fixtures green; new contract coverage asserts the populated scoped entry, the deferral path, and that telemetry JS never reaches the payload. Parity unaffected (only adds source-report data; serialized output unchanged).

AI assistance

  • AI assistance: Yes
  • Tool(s): Claude Code (Claude Opus 4.8, 1M context)
  • Used for: Verification, the association design, implementation, and tests under human review.

chubes4 and others added 2 commits June 28, 2026 07:39
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 <script> bodies, so any behavior script inside a generated
subtree was silently lost. Capture those scripts at the one sound
association point — custom-block generation — recording each as a script
runtime island tagged with the owning block's fully-qualified name via a
generic `owner_block` field. RuntimeIslandPackageBuilder carries the
field through product-neutrally; CompanionPluginPayload owns the SSI-named
projection.

The mapping is gated by preserve-vs-rebuild (#224): only
disposition=preserve / js_handling=preserve_verbatim islands carry JS,
and telemetry/droppable and external-unmaterialized scripts contribute
nothing. Each entry emits content (verbatim JS), handle (handle_hint),
and src (islands/<handle>.js). An island with a sound owner_block that
matches a packaged block is scoped (block=ssi-<slug>/<name>); a
free-standing island with no sound owner is surfaced in
preserved_js_deferred[] with a reason rather than scoped by guess or
dropped silently (the consumer drops unscoped entries).

Replaces the "preserved_js stays empty" contract assertion with coverage
that asserts a populated, correctly-scoped entry, the deferral path, and
that telemetry JS is never carried.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@chubes4

chubes4 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Parking as superseded. End-to-end verification showed the companion-plugin / preserved-JS path is the wrong mechanism for interactivity: custom-block generation is structurally inert across all 72 website fixtures (0 generated — the generation gate at the html_unsupported_element tail is unreachable), and generated blocks strip <script>/on* via wp_kses_post anyway. The real interactive behavior is delivered deterministically by native conversion (core/navigation responsive overlay, core/details + core/accordion). Closing; branch retained for reference.

@chubes4 chubes4 closed this Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant