Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ Current truth lives in:
Historical changelog details remain available through git history and release
evidence.

## 0.42.0-alpha.1

- First alpha of the 0.42 line (ADR-0120): request-time rendering gains
semantics — `renderIntent.mode` was inert metadata before this release.
- `rendering: 'dynamic'` routes skip prerendering, dynamic expansion and
i18n locale prerendering; they are served per request by the generated
`dist/server/index.js` (nitro-mount over the same SSR bundle), with
`dist/server/server-manifest.json` recording the partition.
- Hard rule (ADR-0120): pages with actions cannot be prerendered — a route
module exporting an action without `mode: 'dynamic'` fails the build.
- BuildPlan evidence records `requestTimeRoutes`; pure-static projects emit
no new artifacts (byte-identical public output proven against the 0.41.2
build: zero HTML/JS/CSS differences).
- Request-time e2e fixture under
`packages/adapter-vite/__fixtures__/request-time/` proves loader data
varies per request and islands hydrate identically to static pages
(Chromium, Firefox and WebKit, 12/12).
- Request-time HTML now receives the island client entry automatically —
the generated server entry injects the same script the static pipeline
injects post-build (found by the fixture; without it, islands on
request-time pages never hydrated). A latent `__headExtras is not
defined` codegen bug for projects without head extras is fixed too.
- Loader/action contract types (`Loader`/`LoaderContext`/`Action`/
`ActionContext`) are unchanged — they already shipped; this alpha wires
the rendering-mode semantics around them.

## 0.41.2

- Patch release: release-tooling self-repair (TP-0 of the 0.42.0 plan) — no
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.41.2` (`v0.41.2`) — the stable five-package
Published package line: `0.42.0-alpha.1` (`v0.42.0-alpha.1`) — the stable five-package
release under ADR-0119's scoped interface freeze; the abandoned beta naming
is not an active line.

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Elements 是可长期保存的应用组件模型;JSX 与 Basic Element 是作者层;
Declarative Shadow DOM 是默认服务端表示;交互区域按需升级。

已发布包线为 `0.41.2`(`v0.41.2`)——ADR-0119 范围化接口冻结下的
已发布包线为 `0.42.0-alpha.1`(`v0.42.0-alpha.1`)——ADR-0119 范围化接口冻结下的
stable 五包版本;已放弃的 beta 命名不再是当前版本线。

## 当前产品
Expand Down
3 changes: 3 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
"test:e2e:firefox-smoke": "deno run -A npm:@playwright/test@1.59.1 test --config www/e2e/playwright.config.ts --project=firefox --grep \"DSD Layers|Layout Island Shell|Island Script Loading|Theme Toggle|Theme initialization|data-signal bindings|SSR/hydration mismatch degradation|router guards on browser history traversal|reflect: true static props\"",
"test:e2e:webkit-smoke": "deno run -A npm:@playwright/test@1.59.1 test --config www/e2e/playwright.config.ts --project=webkit --grep \"DSD Layers|Layout Island Shell|Island Script Loading|Theme Toggle|Theme initialization|data-signal bindings|SSR/hydration mismatch degradation|router guards on browser history traversal|reflect: true static props\"",
"test:e2e:browsers:install": "deno run -A npm:playwright@1.59.1 install chromium firefox webkit",
"fixture:request-time:build": "cd packages/adapter-vite/__fixtures__/request-time && deno run --config ../../../../deno.json --allow-read --allow-write --allow-net --allow-env --allow-ffi --allow-sys --allow-run ../../src/cli/build.ts",
"fixture:request-time:e2e": "deno run -A npm:@playwright/test@1.59.1 test --config packages/adapter-vite/__fixtures__/request-time/e2e/playwright.config.ts --project=chromium",
"fixture:request-time:e2e:browsers": "deno run -A npm:@playwright/test@1.59.1 test --config packages/adapter-vite/__fixtures__/request-time/e2e/playwright.config.ts",
"nitro:proof:node": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/nitro-proof.ts node",
"nitro:proof:workers": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/nitro-proof.ts workers",
"test:coverage:check": "deno run --allow-read --allow-run --allow-write --allow-env --allow-net --allow-ffi --allow-sys tools/check-coverage.ts --threshold 73 --branch-threshold 82 --function-threshold 77",
Expand Down
4 changes: 2 additions & 2 deletions docs/current/VERSION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# v0.42.0 — WC Application Loop release plan

> Current source package line: `v0.41.2`\
> Current npm registry line: `v0.41.2`\
> Current source package line: `v0.42.0-alpha.1`\
> Current npm registry line: `v0.42.0-alpha.1`\
> Active release target: `v0.41.1`\
> Planning release target: `v0.42.0` (WC Application Loop)\
> Next release line: `v0.43.0` (Universal WC SSR)\
Expand Down
4 changes: 2 additions & 2 deletions docs/governance/PROJECT_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.41.2`, completed
implementation anchor `v0.41.2`, and `0.42.0` WC Application Loop planning
Current execution anchor: published package line `v0.42.0-alpha.1`, completed
implementation anchor `v0.42.0-alpha.1`, 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
Expand Down
6 changes: 3 additions & 3 deletions docs/roadmap/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.41.2`.\
> Active execution target: `v0.41.2`.\
> Published package line: `v0.42.0-alpha.1`.\
> Active execution target: `v0.42.0-alpha.1`.\
> 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).\
Expand Down Expand Up @@ -116,7 +116,7 @@ making the standard Custom Element contract span both layers. See the official

## Current release state

`0.41.2` is the published package line. npm beta.1 through beta.3 are
`0.42.0-alpha.1` 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.
Expand Down
6 changes: 3 additions & 3 deletions docs/status/STATUS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# OpenElement Status

> Updated: 2026-07-27\
> Repository package line: `v0.41.2`\
> npm registry line: `v0.41.2`\
> Active release target: `v0.41.2`\
> Repository package line: `v0.42.0-alpha.1`\
> npm registry line: `v0.42.0-alpha.1`\
> Active release target: `v0.42.0-alpha.1`\
> Next release line: `v0.42.0`\
> Product graph: five packages\
> Current maturity stage: stable (0.41.x)
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* live-counter — minimal signal counter island.
*
* Hydrates on load; clicking the button increments the count. Used on the
* request-time /live page to prove hydration is identical to static pages.
*/
import { defineCustomElement, OpenElement, signal } from '@openelement/element';
import { defineIslandConfig } from '@openelement/app';

export const tagName = 'live-counter';
export const openElement = defineIslandConfig({ hydrate: 'load', ssr: true, dsd: true });

export default class LiveCounter extends OpenElement {
#count = signal(0);

constructor() {
super();
this.registerSignal('count', this.#count);
}

override render() {
return (
<div class='counter-row'>
<button id='increment' type='button' onClick={() => this.#count.value++}>+</button>
<span id='count' data-signal='count'></span>
</div>
);
}
}

defineCustomElement(tagName, LiveCounter);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* /form — request-time page with an action export.
*
* Exercises the ADR-0120 hard rule from its valid side: a route with an
* action must declare renderIntent mode 'dynamic', and then builds fine.
*/
import { definePage, useActionData } from '@openelement/app';

export const tagName = 'page-form';

interface FormActionData {
echoed: string;
}

export function action(ctx: { formData: Record<string, unknown> }): FormActionData {
return { echoed: String(ctx.formData?.message ?? '') };
}

const FormPage = definePage({
renderIntent: { mode: 'dynamic' },
head: { title: 'request-time fixture — form' },
render() {
const actionData = useActionData() as FormActionData | undefined;
return (
<main>
<h1>request-time form</h1>
<form method='post'>
<input id='message' name='message' type='text' />
<button id='submit' type='submit'>Send</button>
</form>
<p id='echo'>echo={actionData?.echoed ?? ''}</p>
</main>
);
},
});

customElements.define(tagName, FormPage);
export default FormPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** Static home page (default renderIntent mode 'auto') — prerendered at build time. */
import { definePage } from '@openelement/app';

export const tagName = 'page-home';

const HomePage = definePage({
head: { title: 'request-time fixture — home' },
render() {
return (
<main>
<h1 id='home-marker'>request-time fixture home</h1>
<p>This page is prerendered; /live is rendered at request time.</p>
</main>
);
},
});

customElements.define(tagName, HomePage);
export default HomePage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* /live — request-time rendered page (renderIntent mode 'dynamic').
*
* The loader derives data from the incoming request (query param `x`) and a
* per-process counter, so two requests must never return identical HTML.
* A counter island verifies hydration behaves the same as on static pages.
*/
import { definePage } from '@openelement/app';
import '../islands/live-counter.tsx';

export const tagName = 'page-live';

interface LiveData {
x: string;
nonce: number;
}

let requestCounter = 0;

export function loader(ctx: { request: Request }): LiveData {
const url = new URL(ctx.request.url);
requestCounter += 1;
return {
x: url.searchParams.get('x') ?? '',
nonce: requestCounter,
};
}

const LivePage = definePage<LiveData>({
renderIntent: { mode: 'dynamic' },
head: { title: 'request-time fixture — live' },
render({ data }) {
return (
<main>
<h1>request-time live</h1>
<p id='x-value'>x={data?.x ?? ''}</p>
<p id='nonce'>nonce={data?.nonce ?? 0}</p>
<live-counter></live-counter>
</main>
);
},
});

customElements.define(tagName, LivePage);
export default LivePage;
61 changes: 61 additions & 0 deletions packages/adapter-vite/__fixtures__/request-time/e2e/live.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Request-time rendering E2E (0.42.0-alpha.1).
*
* Proves that renderIntent: { mode: 'dynamic' } routes are rendered per
* request (not prerendered), that islands hydrate on request-time pages
* exactly like on static pages, and that static routes are untouched.
*/
import { expect, test } from '@playwright/test';

test.describe('request-time rendering', () => {
test('GET /live renders loader data per request', async ({ request }) => {
const first = await request.get('/live?x=42');
expect(first.ok()).toBe(true);
const firstHtml = await first.text();
expect(firstHtml).toContain('x=42');

const second = await request.get('/live?x=99');
expect(second.ok()).toBe(true);
const secondHtml = await second.text();
expect(secondHtml).toContain('x=99');
expect(secondHtml).not.toContain('x=42');

// Per-request proof: the nonce increments between requests, so the two
// responses cannot both come from a prerendered file.
const firstNonce = /nonce=(\d+)/.exec(firstHtml)?.[1];
const secondNonce = /nonce=(\d+)/.exec(secondHtml)?.[1];
expect(firstNonce).toBeTruthy();
expect(secondNonce).toBeTruthy();
expect(Number(secondNonce)).toBeGreaterThan(Number(firstNonce));
});

test('counter island hydrates on the request-time page', async ({ page }) => {
await page.goto('/live?x=42');
const button = page.locator('live-counter #increment');
const count = page.locator('live-counter #count');
await expect(button).toBeVisible();
await button.click();
await expect(count).toHaveText('1');
await button.click();
await expect(count).toHaveText('2');
});

test('GET / serves the prerendered static page', async ({ page, request }) => {
const response = await request.get('/');
expect(response.ok()).toBe(true);
const html = await response.text();
expect(html).toContain('request-time fixture home');

await page.goto('/');
await expect(page.locator('#home-marker')).toHaveText('request-time fixture home');
});

test('action route builds and responds to POST', async ({ request }) => {
const response = await request.post('/form', {
form: { message: 'hello-action' },
});
expect(response.ok()).toBe(true);
const html = await response.text();
expect(html).toContain('echo=hello-action');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Playwright configuration for the request-time rendering fixture.
*
* Tests run against the built fixture app (packages/adapter-vite/__fixtures__/
* request-time/dist), served by e2e/server.ts: static files from disk,
* renderIntent 'dynamic' routes delegated to dist/server/index.js.
*
* Prerequisites:
* deno task fixture:request-time:build
*
* Run: deno task fixture:request-time:e2e
*/
import { defineConfig } from '@playwright/test';
import process from 'node:process';

const PORT = Number(process.env.REQUEST_TIME_E2E_PORT ?? 4180);
const baseURL = `http://127.0.0.1:${PORT}`;

export default defineConfig({
testDir: '.',
testMatch: '*.spec.ts',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 1,
workers: 1,
reporter: 'list',
timeout: 60_000,

use: {
baseURL,
trace: 'on-first-retry',
},

webServer: {
// `exec` prevents the shell Playwright launches from orphaning Deno when
// the suite finishes or is interrupted.
command:
`exec deno run --config ../../../../../deno.json -A server.ts --port ${PORT} --dir ../dist`,
url: baseURL,
reuseExistingServer: false,
timeout: 60_000,
},

projects: [
{
name: 'chromium',
use: { browserName: 'chromium' },
},
{
name: 'firefox',
use: { browserName: 'firefox' },
},
{
name: 'webkit',
use: { browserName: 'webkit' },
},
],
});
Loading
Loading