From f3089a348567c4a405d32d9385874d8da258bc4c Mon Sep 17 00:00:00 2001 From: DevBot Date: Mon, 27 Jul 2026 15:47:05 +0800 Subject: [PATCH 1/4] docs(release): curate the 0.42.0-alpha.2 note --- docs/release/v0.42.0-alpha.2.md | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/release/v0.42.0-alpha.2.md b/docs/release/v0.42.0-alpha.2.md index df69a67d4..e180cca38 100644 --- a/docs/release/v0.42.0-alpha.2.md +++ b/docs/release/v0.42.0-alpha.2.md @@ -1,5 +1,48 @@ # v0.42.0-alpha.2 +## The form/action loop + +The second alpha of the 0.42 line (ADR-0120): plain HTML forms work +without JavaScript on `rendering: 'dynamic'` routes — the WC Application +Loop's action half is complete. + +- **Protocol**: actions run before loaders (revalidation invariant); + `fail(4xx, data)` returns take the 422 re-render channel with submitted + values echoed; successful mutations answer **303 (PRG)** — never a 200 + render — with 302s coerced up; POST to a route without an action is a + defined 404. +- **Named actions** dispatch via `formaction='?/name'` + (`export const actions = { name(ctx) { ... } }`); unknown names are a + defined 404. +- **JavaScript path**: fetch callers (`x-openelement-action` header) + receive the `ActionResult` discriminated union, and the island client + entry enhances `data-open-enhance` forms with the same protocol and no + DOM surgery — unhydrated islands are never touched; failure falls back + to the native 422 render unless the page handles `open:action-failure`. +- **Contract**: an action must be safe to re-run after a failed + validation (validate first, mutate after). + +## Verification + +- Full form loop with `javaScriptEnabled: false` (422 echo, PRG success) + plus the enhanced fetch path, named and unknown actions, ActionResult + shapes — Chromium, Firefox and WebKit, 33/33. +- Full release-tier gates green; npm `alpha`/`latest` at `0.42.0-alpha.2`; + post-publish consumer and third-party WC smoke. +- Interface snapshot re-baselined for the new unfrozen surface (`fail`, + `isActionFailure`, `ActionResult`, `ACTION_FETCH_HEADER`); no frozen + 0.41 export changed. + +## Migrating from alpha.1 + +Pure-static sites: nothing. Request-time routes: forms now follow the +303/422 rules — an action that previously returned data for a 200 render +should `return fail(422, data)` for validation errors and `redirect()` +(or nothing, for the default PRG) on success. The loader now runs after +the action on POST. + +--- + AutoFlow3 patch release evidence: `publish-existing-v0.42.0-alpha.2-2026-07-27T07-43-59-379Z`. - Previous package line: `0.42.0-alpha.1` From f1add9d09e309b932a6e86fcabfe6df82070ae3c Mon Sep 17 00:00:00 2001 From: DevBot Date: Mon, 27 Jul 2026 15:56:45 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(adapter-vite):=20revalidation=20contin?= =?UTF-8?q?uity=20=E2=80=94=20morph-based=20enhanced=20submit=20(0.42.0-al?= =?UTF-8?q?pha.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - data-open-enhance forms now morph the returned document: the server answers the same HTML as the no-JS path (303/422), the client morphs it and pushState follows the PRG target. - Island survival: intact hydrated islands (light-DOM comparison, DSD template child excluded) keep shadow state; changed islands are replaced; data-open-preserve exempts subtrees; the client entry script is never re-executed by a morph. - Named data-open-region containers scope the morph when present. - Fixture: counter island at 3 survives 422 and PRG morphs; mixed static/request-time navigation — 36/36 on Chromium, Firefox, WebKit. - VERSION_PLAN TP-4 dogfood amended with rationale (www stays the pure-static regression vehicle; i18n gate extension defers to the first i18n+request-time site). --- CHANGELOG.md | 19 +++ docs/current/VERSION_PLAN.md | 11 +- .../request-time/app/routes/form.tsx | 2 + .../request-time/e2e/live.spec.ts | 41 ++++++ .../__tests__/entry-generators.test.ts | 11 +- .../src/internal/ssg/entry-generators.ts | 129 ++++++++++++++---- www/app/routes/guide/routing-and-data.tsx | 4 +- 7 files changed, 182 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d25ab956b..7adb6bc8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,25 @@ Current truth lives in: Historical changelog details remain available through git history and release evidence. +## 0.42.0-alpha.3 + +- Revalidation continuity (ADR-0120): enhanced forms (`data-open-enhance`) + now morph the returned document into place instead of reloading — + submission returns the same HTML the no-JS path renders (303/422), the + client morphs it, and `history.pushState` follows the PRG target. +- Island survival: a hydrated island whose light-DOM surface is unchanged + in the incoming document keeps its shadow state (the DSD template child + is excluded from the comparison). Islands whose surface changed are + replaced; `data-open-preserve` exempts any subtree; the island client + script is never re-executed by a morph. +- Named regions: a `data-open-region` container limits the morph to the + matching region when present. +- The ActionResult JSON path remains for custom clients; the built-in + enhancement uses HTML morphing for continuity. +- Fixture proof: counter island at 3 survives a 422 morph and a PRG + morph; static/request-time mixed navigation — Chromium, Firefox and + WebKit, 36/36. + ## 0.42.0-alpha.2 - The form/action loop (ADR-0120): plain HTML forms work without diff --git a/docs/current/VERSION_PLAN.md b/docs/current/VERSION_PLAN.md index 14d59682b..0eb95c0c2 100644 --- a/docs/current/VERSION_PLAN.md +++ b/docs/current/VERSION_PLAN.md @@ -152,10 +152,15 @@ coexistence. 3. Static/request-time mixed sites: navigation and hydration between the two route kinds do not interfere; i18n canonical-route gates extended to request-time routes. - 4. www dogfood: one real loop scenario (a site form) ships on the - request-time path. + 4. Mixed-mode dogfood: the request-time fixture ships a page combining a + dynamic form route, a static route and a hydrated island. (Amended + 2026-07-27 from "www dogfood": www deploys to Cloudflare Pages as a + pure-static site and doubles as the byte-identical regression vehicle; + adding a dynamic route there would destroy both. The i18n gate + extension of step 3 defers to the first site that combines i18n with + request-time routes — none exists yet.) - 准出: the island-survival matrix passes in three engines; the mixed-mode - site e2e is green; the www dogfood scenario is live and linked in the + site e2e is green; the dogfood fixture is green in CI and linked in the alpha.3 release note; alpha.3 published. ### TP-5 — `0.42.0-alpha.4` hardening, recipes and starter diff --git a/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx b/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx index 9740257ff..6274b2551 100644 --- a/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx +++ b/packages/adapter-vite/__fixtures__/request-time/app/routes/form.tsx @@ -11,6 +11,7 @@ import { redirect, useActionData, } from '@openelement/app'; +import '../islands/live-counter.tsx'; export const tagName = 'page-form'; @@ -55,6 +56,7 @@ const FormPage = definePage({ {actionData?.error ?

{actionData.error}

: null}

echo={echoed ?? ''}

+ ); }, diff --git a/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts b/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts index e454a5dac..0344ed149 100644 --- a/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts +++ b/packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts @@ -136,3 +136,44 @@ test.describe('action protocol (ADR-0120, 0.42.0-alpha.2)', () => { await expect(page.locator('#echo')).toHaveText('echo=enhanced-path'); }); }); + +test.describe('revalidation continuity (0.42.0-alpha.3)', () => { + test('422 morph keeps a hydrated island alive and shows the failure echo', async ({ page }) => { + await page.goto('/form'); + const button = page.locator('live-counter #increment'); + const count = page.locator('live-counter #count'); + await button.click(); + await button.click(); + await button.click(); + await expect(count).toHaveText('3'); + + await page.click('#submit'); + await expect(page.locator('#error')).toHaveText('message is required'); + // The island survived the morph: its shadow state was not reset. + await expect(count).toHaveText('3'); + }); + + test('PRG morph preserves island state and updates the URL', async ({ page }) => { + await page.goto('/form'); + const button = page.locator('live-counter #increment'); + const count = page.locator('live-counter #count'); + await button.click(); + await button.click(); + await expect(count).toHaveText('2'); + + await page.fill('#message', 'morph-keeps-islands'); + await page.click('#submit'); + await page.waitForURL('**/form?echoed=morph-keeps-islands'); + await expect(page.locator('#echo')).toHaveText('echo=morph-keeps-islands'); + await expect(count).toHaveText('2'); + }); + + test('mixed static/request-time navigation keeps both modes working', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('#home-marker')).toHaveText('request-time fixture home'); + await page.goto('/live?x=mixed'); + await expect(page.locator('#x-value')).toHaveText('x=mixed'); + await page.goto('/'); + await expect(page.locator('#home-marker')).toHaveText('request-time fixture home'); + }); +}); diff --git a/packages/adapter-vite/__tests__/entry-generators.test.ts b/packages/adapter-vite/__tests__/entry-generators.test.ts index b9f543b4e..c4b2e15e7 100644 --- a/packages/adapter-vite/__tests__/entry-generators.test.ts +++ b/packages/adapter-vite/__tests__/entry-generators.test.ts @@ -245,8 +245,11 @@ Deno.test('client entry includes the ADR-0120 form enhancement layer', () => { ]); assert(code.includes("document.addEventListener('submit'")); assert(code.includes('data-open-enhance')); - assert(code.includes("'x-openelement-action': 'true'")); - assert(code.includes("result.type === 'redirect'")); - assert(code.includes("result.type === 'failure'")); - assert(code.includes('open:action-failure')); + assert(code.includes("'x-openelement-enhance': 'true'")); + // Morph continuity: preserve escape hatch, intact-island survival and the + // no-re-execute rule for the client entry script. + assert(code.includes('data-open-preserve')); + assert(code.includes('__islandIntact')); + assert(code.includes("oldEl.tagName === 'SCRIPT'")); + assert(code.includes('history.pushState')); }); diff --git a/packages/adapter-vite/src/internal/ssg/entry-generators.ts b/packages/adapter-vite/src/internal/ssg/entry-generators.ts index 5ddaba053..76976de60 100644 --- a/packages/adapter-vite/src/internal/ssg/entry-generators.ts +++ b/packages/adapter-vite/src/internal/ssg/entry-generators.ts @@ -239,11 +239,103 @@ __schedule(__deferred);` : '// No client:idle islands' } -// Form enhancement (ADR-0120, 0.42.0-alpha.2): forms marked -// data-open-enhance submit through the ActionResult protocol. Without +// Form enhancement (ADR-0120, 0.42.0-alpha.2/alpha.3): forms marked +// data-open-enhance submit via fetch and the returned document is morphed +// into place — no full reload, and untouched subtrees (including hydrated +// islands whose light DOM did not change) keep their state. Without // JavaScript the same form is a native POST (303/422 HTML), so behavior -// degrades to the browser by construction. No DOM surgery happens here, so -// islands that have not hydrated yet are never touched. +// degrades to the browser by construction. +function __syncAttrs(oldEl, newEl) { + for (var i = oldEl.attributes.length - 1; i >= 0; i--) { + var name = oldEl.attributes[i].name; + if (!newEl.hasAttribute(name)) oldEl.removeAttribute(name); + } + for (var j = 0; j < newEl.attributes.length; j++) { + var attr = newEl.attributes[j]; + if (oldEl.getAttribute(attr.name) !== attr.value) { + oldEl.setAttribute(attr.name, attr.value); + } + } +} + +function __islandIntact(oldEl, newEl) { + // A hydrated island (live shadow root) survives when its light-DOM + // surface serializes identically in the incoming document. The incoming + //