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/README.md b/README.md index 1286a1e25..323c371f5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Elements are the durable application contract; JSX and Basic Element are the authoring layer; Declarative Shadow DOM is the default server representation; interactive regions upgrade selectively. -Published package line: `0.42.0-alpha.2` (`v0.42.0-alpha.2`) — the stable five-package +Published package line: `0.42.0-alpha.3` (`v0.42.0-alpha.3`) — the stable five-package release under ADR-0119's scoped interface freeze; the abandoned beta naming is not an active line. diff --git a/README.zh.md b/README.zh.md index 4f5c955f7..38cc3267a 100644 --- a/README.zh.md +++ b/README.zh.md @@ -6,7 +6,7 @@ Elements 是可长期保存的应用组件模型;JSX 与 Basic Element 是作者层; Declarative Shadow DOM 是默认服务端表示;交互区域按需升级。 -已发布包线为 `0.42.0-alpha.2`(`v0.42.0-alpha.2`)——ADR-0119 范围化接口冻结下的 +已发布包线为 `0.42.0-alpha.3`(`v0.42.0-alpha.3`)——ADR-0119 范围化接口冻结下的 stable 五包版本;已放弃的 beta 命名不再是当前版本线。 ## 当前产品 diff --git a/docs/current/VERSION_PLAN.md b/docs/current/VERSION_PLAN.md index 14d59682b..eaeab4e6a 100644 --- a/docs/current/VERSION_PLAN.md +++ b/docs/current/VERSION_PLAN.md @@ -1,7 +1,7 @@ # v0.42.0 — WC Application Loop release plan -> Current source package line: `v0.42.0-alpha.2`\ -> Current npm registry line: `v0.42.0-alpha.2`\ +> Current source package line: `v0.42.0-alpha.3`\ +> Current npm registry line: `v0.42.0-alpha.3`\ > Active release target: `v0.41.1`\ > Planning release target: `v0.42.0` (WC Application Loop)\ > Next release line: `v0.43.0` (Universal WC SSR)\ @@ -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/docs/governance/PROJECT_WORKFLOW.md b/docs/governance/PROJECT_WORKFLOW.md index f6c29a9ac..b12f635c3 100644 --- a/docs/governance/PROJECT_WORKFLOW.md +++ b/docs/governance/PROJECT_WORKFLOW.md @@ -11,8 +11,8 @@ complete because an issue, chat message, or SOP says it is complete. It is complete only when the repository contains the decision, the execution package, the implementation, and the gates that prove the claim. -Current execution anchor: published package line `v0.42.0-alpha.2`, completed -implementation anchor `v0.42.0-alpha.2`, and `0.42.0` WC Application Loop planning +Current execution anchor: published package line `v0.42.0-alpha.3`, completed +implementation anchor `v0.42.0-alpha.3`, and `0.42.0` WC Application Loop planning under ADR-0120 and `docs/current/VERSION_PLAN.md`. OpenElement is one Web Components-native, static-first application framework: Basic Element is an authoring mode, not a 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` diff --git a/docs/roadmap/ROADMAP.md b/docs/roadmap/ROADMAP.md index ca6fc52cf..3d6841dfe 100644 --- a/docs/roadmap/ROADMAP.md +++ b/docs/roadmap/ROADMAP.md @@ -4,8 +4,8 @@ Execution and release state follow the [`Project Workflow`](../governance/PROJECT_WORKFLOW.md). > Source of truth for forward product planning.\ -> Published package line: `v0.42.0-alpha.2`.\ -> Active execution target: `v0.42.0-alpha.2`.\ +> Published package line: `v0.42.0-alpha.3`.\ +> Active execution target: `v0.42.0-alpha.3`.\ > Current implementation state: five-package convergence is published; > alpha.17 closed the first audit remediation, alpha.18 completed the > second audit sweep (ADR-0117), and alpha.19 completed the third (ADR-0118).\ @@ -116,7 +116,7 @@ making the standard Custom Element contract span both layers. See the official ## Current release state -`0.42.0-alpha.2` is the published package line. npm beta.1 through beta.3 are +`0.42.0-alpha.3` is the published package line. npm beta.1 through beta.3 are immutable partial artifacts and remain withdrawn from the active release story. The planned beta name was cancelled so the version label honestly reflects that breaking architecture and interface changes are still allowed. diff --git a/docs/status/STATUS.md b/docs/status/STATUS.md index 20b24c447..073774557 100644 --- a/docs/status/STATUS.md +++ b/docs/status/STATUS.md @@ -1,9 +1,9 @@ # OpenElement Status > Updated: 2026-07-27\ -> Repository package line: `v0.42.0-alpha.2`\ -> npm registry line: `v0.42.0-alpha.2`\ -> Active release target: `v0.42.0-alpha.2`\ +> Repository package line: `v0.42.0-alpha.3`\ +> npm registry line: `v0.42.0-alpha.3`\ +> Active release target: `v0.42.0-alpha.3`\ > Next release line: `v0.42.0`\ > Product graph: five packages\ > Current maturity stage: stable (0.41.x) 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/deno.json b/packages/adapter-vite/deno.json index 4385e51d1..c81b41829 100644 --- a/packages/adapter-vite/deno.json +++ b/packages/adapter-vite/deno.json @@ -1,6 +1,6 @@ { "name": "@openelement/adapter-vite", - "version": "0.42.0-alpha.2", + "version": "0.42.0-alpha.3", "exports": { ".": "./src/index.ts", "./nitro-mount": "./src/nitro-mount.ts", 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 + //