diff --git a/.gitignore b/.gitignore index cfaa4c56d..ba9ef0eba 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ node_modules/ # Deno .deno/ -.deno-cache/ -.deno_cache/ vendor/* # Keep vendored dependency attribution tracked for license compliance !vendor/jsr.io/ @@ -23,13 +21,10 @@ dist/ custom-dist/ dist-test-*/ www/dist/ -npm/ packages/adapter-vite/__fixtures__/nitro-proof/.output-*/ packages/adapter-vite/__fixtures__/nitro-proof/.wrangler/ packages/adapter-vite/__fixtures__/nitro-proof/.nitro/ packages/adapter-vite/__fixtures__/nitro-proof/node_modules/ -*.tsbuildinfo -.tsbuildinfo www/app/data/_generated-blog-data.ts .openElement/ www/.openElement/ @@ -73,7 +68,6 @@ test-results/ .gstack/ .qoder/ .codebuddy/settings.local.json -docs/autoflow/cells/*/state.json # Vite cache .vite/ @@ -82,27 +76,9 @@ docs/autoflow/cells/*/state.json # Generated www data modules and the UI manifest are intentionally tracked. # repo:hygiene fails if a tracked file also matches .gitignore. -# Test / build / lint / coverage output files -test-*.txt -cov-*.txt -ui-test*.txt -lint-*.txt -coverage-*.txt -coverage-output.txt -.git-commit-msg.txt -docs/vite-build-*.txt - # Root-level logo source files (design originals, keep local) /open-logo*.svg /open-favicon.svg -debug.log hub-index/ hub-submission.json -lighthouse-result*.json - -# Root-level audit artifact -/audit-ast.json - -# Vite virtual assets -assets/_virtual_* diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c93cca9..475a8ccf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,9 +28,9 @@ evidence. - Adds WebKit smoke to CI, gates examples checks, single-sources the guide with real Chinese rendering, and covers the SPA action chain in a real browser. -- Removes dead exports, fields and scripts (breaking; see the release - note for migration), removes `renderIntent.streaming`, converges - fmt/lint exclusions, and retires stale files and config entries. +- Removes dead exports, fields and scripts (breaking; see the GitHub + prerelease notes for migration), removes `renderIntent.streaming`, + converges fmt/lint exclusions, and retires stale files and config entries. ## 0.41.0-alpha.17 diff --git a/README.md b/README.md index c0055957e..b607fbc9c 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.41.0-alpha.18` (`v0.41.0-alpha.18`). This release +Published package line: `0.41.0-alpha.19` (`v0.41.0-alpha.19`). This release keeps the project in alpha while the five-package product and external adoption evidence continue to mature; the abandoned beta naming is not an active line. @@ -51,11 +51,11 @@ external adoption. It is not presented as an already-achieved market claim. ## Current release state -The five-package convergence is published as `0.41.0-alpha.18`. npm beta.1 +The five-package convergence is published as `0.41.0-alpha.19`. npm beta.1 through beta.3 remain withdrawn partial artifacts and are not compatibility baselines. External adopter pilot #390 remains the primary repository-external -condition before the project can make a stability commitment. Alpha.18 is the -active second audit sweep (see +condition before the project can make a stability commitment. Alpha.18 +completed the second audit sweep (see [ADR-0117](./docs/adr/ADR-0117-second-audit-round-alpha18-sweep.md)). Stable `0.41.0` is released only when alpha releases need no more architecture, diff --git a/README.zh.md b/README.zh.md index 009a664eb..60d1ee53e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -6,7 +6,7 @@ Elements 是可长期保存的应用组件模型;JSX 与 Basic Element 是作者层; Declarative Shadow DOM 是默认服务端表示;交互区域按需升级。 -已发布包线为 `0.41.0-alpha.18`(`v0.41.0-alpha.18`)。项目继续使用 alpha +已发布包线为 `0.41.0-alpha.19`(`v0.41.0-alpha.19`)。项目继续使用 alpha 命名,直到五包产品与外部采用证据充分成熟;已放弃的 beta 命名不再是当前版本线。 ## 当前产品 @@ -41,9 +41,9 @@ official build path = Vite + Nitro ## 当前发布状态 -五包收敛已作为 `0.41.0-alpha.18` 发布。npm beta.1 至 beta.3 仍是已撤回的 +五包收敛已作为 `0.41.0-alpha.19` 发布。npm beta.1 至 beta.3 仍是已撤回的 不完整历史产物,不构成兼容基线。外部 adopter pilot #390 仍是作出稳定性承诺前 -最重要的仓库外条件。Alpha.18 是当前第二轮审计清扫版本(ADR-0117)。 +最重要的仓库外条件。Alpha.18 已完成第二轮审计清扫(ADR-0117)。 只有后续 alpha 不再需要架构、公开接口或采用工作时,才发布 stable `0.41.0`。 request-time data、forms、sessions 与 cache 仍是后续产品工作;当前承诺是具有 diff --git a/SECURITY.md b/SECURITY.md index daa02ff93..e51628fea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,7 @@ update within ten business days. Please include affected package and version, reproduction steps, impact, and any proposed mitigation. We coordinate a fix, credit reporters when requested, and publish an advisory when users need to act. -Only the latest published `0.41.x` line is supported while alpha.7 is active. +Only the latest published release on the current alpha line is supported. Pre-release packages may change before stable release; security fixes are made on the active release line whenever practical. diff --git a/deno.json b/deno.json index cdf7fe289..06f0730d9 100644 --- a/deno.json +++ b/deno.json @@ -37,7 +37,6 @@ "dev": "cd www && deno run --allow-read --allow-write --allow-net --allow-env --allow-ffi --allow-sys npm:vite --config vite.config.ts", "build": "deno task generate:ui-manifest && (cd www && deno run --config ../deno.json --allow-read --allow-write --allow-net --allow-env --allow-ffi --allow-sys --allow-run ../packages/adapter-vite/src/cli/build.ts) && deno task www:check-artifact-truth", "preview": "cd www && deno run --allow-read --allow-write --allow-net --allow-env --allow-ffi npm:vite preview --config vite.config.ts", - "build:docs": "deno task build", "workflow:check": "deno run --allow-read --allow-run=git tools/check-project-workflow.ts", "workflow:check-slimming": "deno run --allow-read tools/check-workflow-slimming.ts", "actions:check-pins": "deno run --allow-read --allow-env tools/check-action-pins.ts", @@ -64,7 +63,6 @@ "consumer:local": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/consumer-local.ts", "consumer:packaged": "deno task pack:dry-run && deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/consumer-packaged-starter.ts && deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/consumer-local.ts --packaged-import-map-check", "third-party-wc:smoke": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net --allow-sys tools/third-party-wc-smoke.ts", - "desktop-reader:smoke": "cd examples/deno-desktop-reader && deno test -A app/__tests__/", "examples:check": "cd examples/deno-desktop-reader && deno task check && deno task smoke && cd ../deno-desktop-mastodon && deno task check && deno task smoke", "stress:dogfood": "deno run --allow-read --allow-run --allow-env tools/run-dogfood-stress.ts", "dogfood:evidence": "deno run --allow-read --allow-write --allow-run tools/run-dogfood-evidence.ts", @@ -84,8 +82,8 @@ "test:visual-baselines": "OPEN_VISUAL_REGRESSION=1 deno run -A npm:@playwright/test@1.59.1 test www/e2e/visual-baselines.spec.ts --config www/e2e/playwright.config.ts --project=chromium", "test:e2e:browsers": "deno run -A npm:@playwright/test@1.59.1 test --config www/e2e/playwright.config.ts", "test:e2e:install": "deno run -A npm:playwright@1.59.1 install chromium", - "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\"", - "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\"", + "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", "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", @@ -96,14 +94,13 @@ "lint": "deno lint", "fmt": "deno fmt", "fmt:check": "deno fmt --check", - "clean": "rm -rf packages/*/dist www/dist packages/*/*.tgz", + "clean": "rm -rf packages/*/dist www/dist packages/*/*.tgz dist custom-dist dist-test-* playwright-report test-results .openElement www/.openElement", "hooks:install": "git config core.hooksPath .githooks", "hooks:uninstall": "git config core.hooksPath", "autoflow:dev": "deno run --allow-read --allow-run --allow-env tools/autoflow/mod3.ts dev", "autoflow:push": "deno run --allow-read --allow-run --allow-env tools/autoflow/mod3.ts push", "autoflow:ci": "deno run --allow-read --allow-run --allow-env tools/autoflow/mod3.ts ci", "autoflow:patch-release": "deno run --allow-read --allow-write --allow-run --allow-env tools/autoflow/mod3.ts patch-release", - "autoflow:minor-plan": "deno run --allow-read --allow-run tools/autoflow/mod3.ts minor-plan", "autoflow:release": "deno run --allow-read --allow-write --allow-run --allow-env tools/autoflow/mod3.ts release", "autoflow:release-dispatch": "deno run --allow-read --allow-write --allow-run --allow-env tools/autoflow/mod3.ts release-dispatch", "autoflow:release-prepare": "deno run --allow-read --allow-write --allow-run --allow-env tools/autoflow/mod3.ts release-prepare", @@ -122,7 +119,6 @@ "exclude": [ "www/content/blog/", "www/app/data/", - "www/design/mockups/", "www/public/assets/", "custom-dist/", "dist-test-ssg-render/", @@ -157,7 +153,6 @@ "**/dist/**", "**/node_modules/**", ".deno", - ".deno_cache", "custom-dist", "dist-test-ssg-render", "vendor", diff --git a/docs/adr/ADR-0118-third-audit-round-alpha19-cleanup-sweep.md b/docs/adr/ADR-0118-third-audit-round-alpha19-cleanup-sweep.md new file mode 100644 index 000000000..6d317532b --- /dev/null +++ b/docs/adr/ADR-0118-third-audit-round-alpha19-cleanup-sweep.md @@ -0,0 +1,46 @@ +# ADR-0118: Third Audit Round and Alpha.19 Cleanup Sweep + +- Status: ACCEPTED +- Date: 2026-07-25 + +## Context + +A third full-repository audit on 2026-07-25 (after alpha.18 shipped) reviewed +architecture, redundancy and cleanliness across all five packages, the tooling +layer, the docs site and the governance documents. It produced 26 issues +(#481–#506), all accepted into milestone `v0.41.0-alpha.19`: three +high-severity, twenty medium/low-severity findings grouped around correctness +residuals, surface drift, gate blind spots and hygiene. + +The audit's meta-finding is an evolution of the alpha.17/18 sibling-path +pattern: several alpha.18 fixes are correct on every path they touched but +break on **combinations** of inputs — for example the reflect short-circuit +misbehaves only when `removeAttribute` meets a default value, and the `For` +drift token breaks only when content collides with the separator. Where the +second round taught us to enumerate sibling paths, the third round teaches +that acceptance for state-dependent fixes must enumerate input combinations, +not just code paths. + +## Decision + +- Cut `v0.41.0-alpha.19` as a pure cleanup sweep governed by issues + #481–#506. It adds no new product surface and no new packages. +- Every state-dependent fix ships with a "input combinations enumerated" + acceptance note, extending the sibling-path doctrine from ADR-0117. +- Gate blind spots found by the audit (www bare version mentions, governance + doc bodies, export-star seams, assertion style) are mechanized so the same + class of drift cannot return silently. +- Confirmed dead code, dead config entries and stale comments are deleted + with zero-consumer evidence recorded in each issue. + +## Consequences + +Positive: the audit's combination-path meta-finding becomes an enforceable +acceptance rule; governance and surface drift gains mechanical coverage; the +cleanup lowers maintenance surface before the `0.41.0` freeze decision. + +Negative: the alpha line lengthens once more; some deletions remain breaking +surface changes requiring release-note migration notes. + +Neutral: ADR-0116 and ADR-0117 remain the parent decisions; this ADR records +the third audit round as a continuation of the same doctrine. diff --git a/docs/current/HYDRATION_CONTRACT.md b/docs/current/HYDRATION_CONTRACT.md index aa58c633a..52a90c2ae 100644 --- a/docs/current/HYDRATION_CONTRACT.md +++ b/docs/current/HYDRATION_CONTRACT.md @@ -2,12 +2,10 @@ The v0.41 alpha line exposes one supported component runtime: `@openelement/element`. Hydration internals are not application-authoring -surface: the implementation modules remain importable as internal subpaths -(`@openelement/element/open-element-render` and -`@openelement/element/open-element-hydration`) for build tooling and Deno -type generation, but they carry no compatibility promise and application code -must not import them. Build-time helpers for adapters live behind -`@openelement/element/build-utils`. +surface: the `open-element-render` and `open-element-hydration` implementation +modules are internal-only — their package subpath exports were removed in +alpha.19 and application or tooling code must not import them. Build-time +helpers for adapters live behind `@openelement/element/build-utils`. ## Supported imports @@ -40,3 +38,12 @@ static, hydrate, or CSR implementation packages. `/@fs/` absolute-path specifier convention. That branch is covered by unit tests (`module-specifier.test.ts`) but has not been verified on a real Windows build; the regular root-relative path is exercised on every change. +- `For` list tokens carry no per-item identity: objects without an `id`/`key` + field are replaced in place at the same position rather than matched and + moved. This is an accepted limitation of the current binding. (The token + wire signature itself has switched to a length-prefixed encoding; parsers + must not assume the old fixed-width format.) +- `reflect` attribute mirroring normalizes values at the boundary: `NaN` + becomes `0`, `-0` becomes `0`, objects are mirrored as `String(value)`, a + failed `Number` parse falls back to `0`, and `removeAttribute` restores the + declared default, which is then re-mirrored to the attribute. diff --git a/docs/current/PACKAGE_SURFACE.md b/docs/current/PACKAGE_SURFACE.md index 29362e12f..6ec58c07b 100644 --- a/docs/current/PACKAGE_SURFACE.md +++ b/docs/current/PACKAGE_SURFACE.md @@ -23,6 +23,15 @@ authoring modes = Basic Element standalone + full application Application authors should normally learn `element`, `app`, `adapter-vite`, and `create`; `ui` is optional. +## Vocabulary + +- **Hydration** — element-level: how and when a component's client JavaScript + is loaded (`load`, `idle`, `visible`, `only`; see `HYDRATION_CONTRACT.md`). +- **Upgrade** — island-level: the moment a server-rendered custom element is + defined and its instance takes over the existing markup. +- **Activation** — framework takeover: marker activation, event binding and + state restoration performed by the runtime after upgrade. + The Element/App root surface exposes one functional element authoring helper: `defineElement`. The alpha-only `defineLayout` alias was removed in alpha.13; layouts use `defineElement` with the same definition object. @@ -39,7 +48,7 @@ promise and are not application-authoring surface. { "@openelement/element": { "supported": [".", "jsx-runtime", "jsx-dev-runtime", "build-utils"], - "internal": ["open-element-render", "open-element-hydration"] + "internal": [] }, "@openelement/app": { "supported": [".", "hono", "model", "spa", "preact"], @@ -66,10 +75,18 @@ promise and are not application-authoring surface. `createRuntimeAdapter` and the runtime handler types) for build adapters. They were removed from the element root export; application code must not import them. -- `@openelement/element/open-element-render` and - `@openelement/element/open-element-hydration` are hydration implementation - modules (see `HYDRATION_CONTRACT.md`), kept importable for build tooling - and Deno type generation. +- The `open-element-render` and `open-element-hydration` modules are + internal-only hydration implementation modules (see + `HYDRATION_CONTRACT.md`); their subpath exports were removed in alpha.19. + The module files remain inside the package for internal relative imports + only. +- The branded types `SafeHtml` and `UnsafeHtml` and the internal + `StyleSheetRule` type are no longer exported from the element root + (alpha.18 release notes already claimed their removal; alpha.19 makes it + true). Their declarations stay in the internal protocol files. +- The element root no longer carries `export type *` seams (alpha.19); the + public type surface is an explicit export list in + `packages/element/src/index.ts`. - `@openelement/app/i18n` is the optional locale-expansion integration point. - App's router implementation (`internal/router`) is not exported; the router types (`RouteConfig`, `RouterInstance`, `RouterMode`) were removed from the diff --git a/docs/current/VERSION_PLAN.md b/docs/current/VERSION_PLAN.md index 33f684a10..55070c641 100644 --- a/docs/current/VERSION_PLAN.md +++ b/docs/current/VERSION_PLAN.md @@ -1,21 +1,21 @@ -# v0.41.0-alpha.18 — second audit sweep plan +# v0.41.0-alpha.19 — third audit round cleanup sweep plan -> Current source package line: `v0.41.0-alpha.18`\ -> Current npm registry line: `v0.41.0-alpha.18`\ -> Active release target: `v0.41.0-alpha.18`\ +> Current source package line: `v0.41.0-alpha.19`\ +> Current npm registry line: `v0.41.0-alpha.19`\ +> Active release target: `v0.41.0-alpha.19`\ > Next stability candidate: `v0.41.0`\ > Current maturity stage: alpha ## Objective -Alpha.18 executes the second audit sweep governed by -[`ADR-0117`](../adr/ADR-0117-second-audit-round-alpha18-sweep.md). The -2026-07-24 re-audit verified the alpha.16/17 remediation and found -half-closed sibling paths, documentation claims without evidence, one broken -public feature (`reflect: true` props), and a redundancy inventory. Alpha.18 -closes every listed item or explicitly defers it with evidence. It adds no -new product area. Every package carries a "sibling paths enumerated" -acceptance note. +Alpha.19 executes the third audit round cleanup sweep governed by +[`ADR-0118`](../adr/ADR-0118-third-audit-round-alpha19-cleanup-sweep.md). The +2026-07-25 full-repository audit (architecture, redundancy, cleanliness) +produced 26 accepted issues (#481–#506) under milestone +`v0.41.0-alpha.19`. Alpha.19 closes every listed issue or explicitly defers +it with evidence. It adds no new product surface and no new packages. Every +state-dependent fix carries an "input combinations enumerated" acceptance +note, extending the sibling-path doctrine of ADR-0117. ```text OpenElement = Web Components-native fullstack application framework @@ -30,200 +30,126 @@ The authoritative five-package contract remains ## Entry truth -- `v0.41.0-alpha.17` is the current source and published npm package line; - its immutable tag and two-stage evidence must not be rewritten. +- At entry to this plan, `v0.41.0-alpha.18` is the current source and + published npm package line; its immutable tag and two-stage evidence must + not be rewritten. - The external adopter pilot #390 and stable gate #37 remain open. -- The root-level ``/`` CSR edge and the `/@fs/` Windows - verification are carried over from the alpha.17 plan, which shipped with - them unmet; this plan either completes them or defers them with explicit - recorded evidence. - -## Alpha.18 execution packages - -### A. P0 correctness - -- Fix the `reflect: true` static-prop write loop: equality short-circuit in - the reflect subscriber, no default-value attribute write before - `syncStaticPropsFromAttributes` runs, and a real-browser test proving - reflected SSR attributes survive connect. Sibling paths: attribute removal - restore, re-connect, multi-engine behavior. -- Pass `cemClassifications` into the SSG descriptor so dev/server and static - output share one admission plan; add a build-level test asserting a - CEM-admitted package island renders identically in both paths. Sibling - paths: `build-ssg.ts` plan overwrite, evidence decisions source. -- Wire the 500 contract to consumers: dynamic-route failures must not be - written as normal pages (fail the build or record the failure loudly), the - ISR manifest must not register failed pages, static-route non-200 outcomes - must surface in the build summary; redirect/notFound fields join - `SsgPageOutput`'s type contract. Sibling paths: `toSSG` filtering, handler - parity. -- Fix `www/public/theme-init.js` honoring `prefers-color-scheme` (currently - always dark) and tighten the e2e assertion so both values are no longer - tolerated. Sibling paths: FOUC contract, theme-toggle propagation. -- Run router guards on popstate/hashchange navigation, with a browser test - for guard-on-back. Sibling paths: redirect chains on popstate, hash mode. -- Resolve the carried-over root-level ``/`` CSR edge: attach - before commit or document the constraint, with a regression test, and - simplify the e2e probe that worked around it. Sibling paths: root `For`, - fallback re-render path. - -### B. Release tooling closure - -- Fix the local `patch-release` path so final evidence and the closure record - land on `main` (or the flow is re-sequenced so they cannot strand on - `dev`); a local release must leave main CI green. Sibling paths: CI - dispatch path, publish-existing path (both verified working in alpha.17). -- Make version-anchor gates reject stale version claims, parameterized on - `PREVIOUS_PACKAGE_VERSION` instead of hardcoded versions; extend anchor - maintenance to the `VERSION_PLAN.md` header so it cannot expire again. - Sibling paths: README/ROADMAP/STATUS body lines outside the header. -- Add failure recovery to the release executor: re-runs skip completed steps, - and a finalize failure after a successful publish must not flip the - evidence to `failed`. Sibling paths: tag-conflict resume, empty-commit - re-run. -- Classify critical-path infra probe failures: in CI, a probe failure is a - gate failure, not a skip. Sibling paths: nitro probe, spawn failure. - -### C. Evidence honesty - -- WebKit: either add a WebKit smoke project to CI or reword - `BROWSER_BASELINE.md` and the acceptance text to "Chromium and Firefox in - CI, WebKit manually verified per release" with the manual run recorded in - release evidence. Sibling paths: `test:e2e:browsers` wiring. -- Examples: fix the type errors in `deno-desktop-mastodon` and - `deno-desktop-reader`, add an examples check/test gate to CI, and correct - `VERIFICATION.md` to match what the gate actually proves. Sibling paths: - examples unit tests in automation. -- Guide content: pick one source of truth for guide pages (render the - markdown or delete it), make the zh guide actually render Chinese, and add - a consistency check so tsx and md cannot drift again. Sibling paths: visual - baselines encoding the wrong state, bare version mentions. -- STATUS/VERIFICATION honesty: completion states must enumerate unmet - sub-items; the `/@fs/` Windows branch is verified or its limitation is - recorded in the alpha.18 release note. Sibling paths: all gates-table rows. -- SPA action chain: add a browser-level test for submit → action → loader → - actionData through shadow boundaries. Sibling paths: action failure - normalization. - -### D. Convergence and redundancy cleanup - -- Rendering consistency: align camelCase prop serialization with static-prop - observation (one casing rule); make the `For` drift token content-sensitive - or document the same-length limitation; fix the `client-runtime` double - hydration; skip non-event function props in the CSR path. Sibling paths: - SSR string path, hydration walk, CSR DOM path — add one cross-path - consistency test. -- SSG/handler parity: give the SSG `renderRoute` the error-boundary layer or - document the divergence; remove or implement the dead `streaming` - contract; complete the routeInfo type declarations. -- Delete confirmed dead code: `honoEntryCode`/`clientEntryCode`/ - `ssgEntryCode` fields, `unwrap`, `registerStaticObservedAttributes` (if - still unused), `OpenElementRenderer`, the app-model speculative type - cluster, `normalizeBasePath`/`normalizeRoutePath`, the orphaned `csr.ts` - barrel, the `openMdx` alias, unused ui tagName re-exports, - `open-hero-ping.tsx`, `tools/check-dist-no-object-object.ts`, - `tools/smoke-deploy.ts`, `tools/deploy-pages.ts` (+test), and resolve - `tools/check-import-map.ts` (delete or re-gate). -- Export surface tightening: move the listed internal-only element exports - behind internals (breaking, with migration note); deduplicate - `open-dialog`'s hand-rolled inert against native `showModal` semantics. -- Config hygiene: converge `deno.json` fmt/lint excludes to the config - blocks, drop entries for non-existent paths, deduplicate `.gitignore`, and - retire `REVIEW-REPORT.md` per the archive policy (git history preserves - it). Remove `www/app/routes` from fmt/lint exclusions or record an ADR for - keeping them. Register or delete the orphan tasks `actions:check-pins` and - `verify:configs`. -- Clean local build artifacts (`dist/`, `custom-dist/`, `playwright-report/`, - `.openElement/`, `packages/*/*.tgz`) and keep the root-hygiene gate as the - standing guard. - -### E. Alpha.18 release closure - -- Run release-prepare only after A–D pass, synchronizing all five manifests, + +## Scope + +- All 26 issues #481–#506, grouped below by theme; each issue records its own + zero-consumer or reproduction evidence. +- Gate mechanization for every audit-discovered blind spot so the same drift + class cannot return silently. +- The alpha.19 release closure: bump, gates, publish, tag, prerelease and + two-stage evidence. + +## Tasks by issue group + +### A. High-severity truth and assertion gaps + +- #481: fix www pages claiming stale package lines; extend the www-truth gate + to bare version mentions. +- #482: align governance doc bodies (STATUS/ROADMAP/README/SECURITY) with + their headers; extend the version-anchor/currency gates to body text. +- #483: replace the 25 always-true `assertExists(boolean)` assertions with + behavior assertions; widen the assertion-style gate. + +### B. Surface seams and declarations + +- #486: correct package READMEs teaching internal or nonexistent subpaths; + add a package-README surface gate. +- #487, #488: close the `export type *` star seams exposing internal protocol + types (SafeHtml/UnsafeHtml/StyleSheetRule and ~50 others); make the + interface snapshot seam-aware. +- #490: single-source `routeInfo` declarations including the emitted + `filePath`/`module` fields. +- #505: decide the test-only-consumed public exports batch. + +### C. Correctness residuals and input combinations + +- #491, #492: reflect-prop fixes proven in Firefox/WebKit smoke, not only + Chromium; enumerate removeAttribute × default-value combinations. +- #493: keep URL and router state consistent on popstate redirect-then-block; + fix guard-reject fail-open asymmetry. +- #494: make the `For` drift token collision-proof against separator + smuggling. +- #495: strengthen the theme-init `prefers-color-scheme` e2e to the claimed + FOUC contract. +- #496: single-source the `HydrationStrategy` literal list. +- #500: converge island declaration construction and the build-plan hydrate + evidence. +- #489: give `graph:check` real teeth with a dependency-direction gate. + +### D. Release tooling and evidence durability + +- #497: untangle the release executor core from `mod3.ts`; test resume + orchestration and the rebase-conflict resume path. +- #484: declare the workflow inputs sent by `autoflow:release-dispatch`. +- #485: record alpha.18 migration notes durably; protect curated notes from + tooling overwrite. +- #499: extend the clean task to all eight artifact classes. + +### E. Hygiene and vocabulary + +- #498: fix the CONTRIBUTING structure map and the comment-honesty batch. +- #501: execute the safe-deletion batch (11 zero-consumer items). +- #502: config hygiene batch (workspace-orphan example, ui publish.exclude, + test-fixtures, orphan tasks, dead doc refs). +- #503: consolidate hydration/upgrade/activate/mount vocabulary and field + names. +- #504: gate hardening batch (deno-api AST blind spots, guide guard residue, + #37 text refresh, VERIFICATION wording). +- #506: deduplicate the JSON codegen boundary, formatJson bypasses and + diverged preact test stubs. + +### F. Alpha.19 release closure + +- Run release-prepare only after A–E pass, synchronizing all five manifests, Create CLI, starter mappings and current version anchors to - `0.41.0-alpha.18`. -- Pass AutoFlow, CodeQL, dependency review, Pages, all-browser E2E, Nitro, - packaged consumers, artifacts and npm publish dry-run on `dev` and `main`. + `0.41.0-alpha.19`. - Publish all five npm packages under the `alpha` and `latest` dist-tags, - then verify fresh Deno, Node ESM, starter, Nitro Node/Workers, third-party - Web Component and CDN consumers through the supported `nitro-mount` seam. -- Create an immutable `v0.41.0-alpha.18` tag, GitHub prerelease with curated - notes (including migration notes for removed exports), and a completed - two-stage evidence record; finish with `origin/main` and `origin/dev` at - the same SHA. - -## Pull request order - -1. ADR-0117, this plan, and the current contradictions cleanup (README, - ROADMAP, STATUS, VERSION_PLAN header). -2. P0 correctness fixes (package A). -3. Release tooling closure (package B). -4. Evidence honesty (package C). -5. Convergence and redundancy cleanup (package D). -6. `dev -> main` release PR and post-publish evidence finalization. - -Every implementation PR targets `dev`, identifies its plan package, lists the -sibling paths it enumerated, and passes AutoFlow before merge. + create the immutable `v0.41.0-alpha.19` tag and GitHub prerelease, verify + fresh Deno, Node ESM, starter and Nitro consumers through the supported + `nitro-mount` seam, and complete the two-stage evidence record. ## Acceptance -- Reflected static props survive connect without write loops in Chromium, - Firefox and WebKit, proven by browser tests. -- Dev, SSR and SSG outputs share one admission plan; CEM-admitted islands - render identically across paths. -- Failed renders cannot ship silently: dynamic 500s fail or flag the build, - static non-200s surface in the summary, and the ISR manifest is accurate. -- Local and CI release paths both leave main CI green with completed - evidence; anchor gates reject stale version claims in every governed - document. -- Every documentation claim about browsers, verification and completion is - backed by automation or reworded to the evidence. -- The redundancy inventory is deleted; `deno.json`/`.gitignore` exclusions - have one source of truth; no orphan tasks remain. -- Chromium, Firefox and WebKit pass the functional matrix, or the documented - WebKit policy plus a recorded manual run takes its place in release - evidence. -- The external adopter pilot #390 continues; new P0/P1 pilot findings become - issues. +- Every issue #481–#506 is closed with its recorded evidence, or explicitly + deferred with a recorded reason. +- Each state-dependent fix (C group) enumerates the input combinations it was + verified against. +- The new and hardened gates (A, B, D, E groups) fail on the stale inputs + they were built for and pass on the corrected repo state. - npm, dist-tags, exact-version starter, tag, GitHub prerelease, docs and - final evidence all agree on `0.41.0-alpha.18`. -- Alpha.17 evidence remains unchanged. + final evidence all agree on `0.41.0-alpha.19`. +- Alpha.18 evidence remains unchanged. ## Non-goals -- Do not add packages, publish JSR artifacts or claim broad fullstack parity. -- Do not introduce speculative auth, database, session or cache products. +- Do not add packages, product surface or new public APIs beyond what an + issue requires. - Do not restructure the `OpenElement` base class or introduce a diffing renderer. -- Do not promise stable `0.41.0` merely because alpha.18 publishes. +- Do not promise stable `0.41.0` merely because alpha.19 publishes. - Do not fabricate, simulate or replace external adopter evidence with internal CI runs. ## Test matrix -- Every package A fix lands with reproduction-first tests; reflect props and - popstate guards are proven in a real browser project. -- Package B changes land with executor tests (resume, finalize failure) and - anchor-gate fixtures (stale rejection). -- Package C adds the SPA action browser test and the examples CI gate. +- Every correctness fix lands reproduction-first; reflect and popstate fixes + are proven in the browser engines their acceptance claims name. +- Gate changes land with fixtures proving both the stale rejection and the + corrected-state pass. - `deno task test`, `arch:check`, `graph:check`, `package-surface:check`, `type-safety:check` and `deno-api:check` pass for every PR. -- `deno task test:e2e` (Chromium) and the Firefox smoke pass for every PR. +- `deno task test:e2e` (Chromium) and the Firefox/WebKit smoke pass for every + PR; the pre-release matrix covers Chromium, Firefox and WebKit. ## Release evidence requirements -- Two-stage evidence under `docs/release/` for `v0.41.0-alpha.18`, including +- Two-stage evidence under `docs/release/` for `v0.41.0-alpha.19`, including npm version and dist-tag verification and post-publish consumer smoke. -- The release note records the breaking export removals with migration - steps, the `/@fs/` verification outcome or limitation, and the WebKit - policy outcome. -- Any new #390 pilot intake published during alpha.18 is anonymized and +- The release note records any breaking deletions with migration steps. +- Any new #390 pilot intake published during alpha.19 is anonymized and linked from the release evidence. - -## Stable decision after alpha.18 - -`0.41.0` may be prepared only when the audit findings are closed without -half-fixed sibling paths, documentation claims match automation, the -adopter pilot finds no unresolved architecture-level break, and #37's -applicable `0.41.0` gates are evidenced. Otherwise the evidence selects a -narrowly scoped next alpha instead of weakening the stable contract. diff --git a/docs/governance/BRANCHING.md b/docs/governance/BRANCHING.md index 1b676819a..fb9dce2a7 100644 --- a/docs/governance/BRANCHING.md +++ b/docs/governance/BRANCHING.md @@ -1,7 +1,6 @@ # Branching Strategy -> Part of openElement governance. Referenced by `docs/governance/PROJECT_WORKFLOW.md` -> and `.workbuddy/skills/autoflow/SKILL.md`. +> Part of openElement governance. Referenced by `docs/governance/PROJECT_WORKFLOW.md`. ## Branch Model: Trunk-Based + AutoFlow Cells @@ -17,13 +16,12 @@ main ←── merge (CI 全绿) ── dev ## Branch Types -| Prefix | Purpose | From | Merge To | Created By | -| ----------------- | ------------------------ | ----- | ---------------- | ----------------- | -| `dev` | Development trunk | — | `main` | human/agent | -| `main` | Release trunk | — | — | CI merge only | -| `autoflow/cell-*` | AutoFlow autonomous cell | `dev` | `dev` (on green) | `autoflow:evolve` | -| `fix/*` | Bug fix | `dev` | `dev` (PR) | human/agent | -| `feat/*` | Feature | `dev` | `dev` (PR) | human/agent | +| Prefix | Purpose | From | Merge To | Created By | +| -------- | ----------------- | ----- | ---------- | ------------- | +| `dev` | Development trunk | — | `main` | human/agent | +| `main` | Release trunk | — | — | CI merge only | +| `fix/*` | Bug fix | `dev` | `dev` (PR) | human/agent | +| `feat/*` | Feature | `dev` | `dev` (PR) | human/agent | ## Rules @@ -33,14 +31,3 @@ main ←── merge (CI 全绿) ── dev 4. **Push `dev` before merging to `main`.** Wait for CI on `dev` first. 5. **Tag only on `main`** after CI passes. 6. **Never force-push `main` or `dev`.** - -## AutoFlow Cell Branch Lifecycle - -``` -autoflow:evolve detects drift - → creates autoflow/cell-v0.x-001 from dev - → executes testgen → implement → review - → runs harness gates - → ✅ green → merge to dev → delete branch - → ❌ red → delete branch → retry or escalate -``` diff --git a/docs/governance/PROJECT_WORKFLOW.md b/docs/governance/PROJECT_WORKFLOW.md index cf38c71dd..d76cbf0ef 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.41.0-alpha.18`, completed -implementation anchor `v0.41.0-alpha.18`, active sweep target +Current execution anchor: published package line `v0.41.0-alpha.19`, completed +implementation anchor `v0.41.0-alpha.19`, active sweep target `v0.41.0-alpha.18`, and alpha maturation under `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.41.0-alpha.18.md b/docs/release/v0.41.0-alpha.18.md index 537e27c28..41032f237 100644 --- a/docs/release/v0.41.0-alpha.18.md +++ b/docs/release/v0.41.0-alpha.18.md @@ -1,5 +1,42 @@ # v0.41.0-alpha.18 +## Breaking changes and migration + +**Build behavior** + +- Dynamic-route render failures now **fail the build** instead of silently + publishing a 500 page. Opt out with + `openElement({ ssg: { dynamicRouteFailure: 'warn' } })` (failed pages are + still never written nor registered in the ISR manifest). +- `@openelement/app`: the `renderIntent.streaming` option is removed; + `definePage` strict validation now rejects it. Remove it from page + descriptors. + +**Export removals** (all had zero in-repo consumers) + +- `@openelement/element` root: `unwrap`, `registerStaticObservedAttributes`, + `OpenElementRenderer`, the speculative app-model types + (`OpenElementRouteGraph` and friends), `normalizeBasePath`, + `normalizeRoutePath`, plus static-props helpers (`disposeStaticProps`, + `initializeStaticProps`, `syncStaticPropsFromAttributes`, + `handleStaticPropAttributeChange`), `normalizePropDecl`, `renderToDom`, + `isSignalLike`, `escapeAttrValue`, `SignalLike` and related types — build + tooling lives in `@openelement/element/build-utils`. +- `@openelement/adapter-vite` root: `openMdx`, `insertAfterHead`, + `assertNoScriptTags`, `validateSafeUrl`, four island-manifest types. +- `@openelement/ui`: seven unused `*TagName` aliases. + +**Behavior changes** + +- `reflect: true` static props no longer write loops or overwrite SSR + attributes on connect; defaults materialize on first state change. +- Multi-word camelCase props now use kebab-case attributes consistently + (`itemCount` → `item-count`) — the SSR/hydration link previously broken. +- `For` branch tokens are content-sensitive; same-length content drift now + falls back to client re-render. +- Router guards now run on browser back/forward navigation. +- `theme-init` honors `prefers-color-scheme` (previously always dark). + AutoFlow3 patch release evidence: `publish-existing-v0.41.0-alpha.18-2026-07-25T13-24-21-244Z`. - Previous package line: `0.41.0-alpha.17` diff --git a/docs/release/v0.41.0-interface-snapshot.json b/docs/release/v0.41.0-interface-snapshot.json index 37df233bf..5a24c6df6 100644 --- a/docs/release/v0.41.0-interface-snapshot.json +++ b/docs/release/v0.41.0-interface-snapshot.json @@ -7,29 +7,22 @@ ".": "./src/index.ts", "./build-utils": "./src/build-utils.ts", "./jsx-dev-runtime": "./src/jsx-dev-runtime.ts", - "./jsx-runtime": "./src/jsx-runtime.ts", - "./open-element-hydration": "./src/open-element-hydration.ts", - "./open-element-render": "./src/open-element-render.ts" + "./jsx-runtime": "./src/jsx-runtime.ts" }, "declarations": { ".": { - "sha256": "35290be84ac939a8c58a47ce1f55dab5150a59ff33ff4a8934f5ec15a61cfa16", + "sha256": "57ecd6d850bbb9ed7f139e52f56dfbda3a07613a3570d149ffd3697473ddd273", "publicDeclarations": [ "export type { ElementDefinition } from './types.ts';", "export type {", "export type { VNode } from './internal/protocol/vnode.ts';", - "export type * from './internal/protocol/framework.ts';", - "export type * from './internal/protocol/manifest.ts';", - "export type * from './internal/protocol/render.ts';", - "export type * from './internal/protocol/style-sheet.ts';", - "export type * from './internal/protocol/vnode.ts';", + "export type { RenderOutput, SsrAdmissionDecision } from './internal/protocol/render.ts';", "export type { Context } from './internal/core/index.ts';", "export type { RenderError } from './internal/core/index.ts';", "export type { Signal } from './internal/protocol/signal.ts';", "export type { IslandOptions } from './internal/protocol/island.ts';", "export type { StyleSheetLike } from './internal/protocol/style-sheet.ts';", "export type { Action, ActionContext, Loader, LoaderContext } from './internal/protocol/data.ts';", - "export type { HydrationStrategy, LocalePath } from './internal/protocol/framework.ts';", "export type {", "export type { OpenElementRouteKind, OpenElementRouteNode } from './internal/protocol/app-model.ts';", "export type {" @@ -48,21 +41,6 @@ "./jsx-runtime": { "sha256": "d63e409708a4a1e5203c5e68ec91b0daf8a06679824c6f6054c6f8e9ad44e2ef", "publicDeclarations": [] - }, - "./open-element-hydration": { - "sha256": "8280f617f3dcb6fc7d03ade7ce22a15a74e3164b67a27d3670cbd97f42cc8dce", - "publicDeclarations": [ - "export function hydrateExistingDom(" - ] - }, - "./open-element-render": { - "sha256": "ce4067ae7eb62fa958065b5d35f2c72409512228e7a5f412cfe7b5f5f3669d88", - "publicDeclarations": [ - "export interface OpenElementLike {", - "export function renderIntoLightDom(", - "export function renderIntoShadowRoot(", - "export function renderErrorFallback(" - ] } } }, @@ -184,11 +162,11 @@ "publicDeclarations": [] }, "./cli/build-client": { - "sha256": "9d79685a263ebd1014826a40d0377631b26e6370b78aa9509aeb3966a4b30659", + "sha256": "e4ec975531f3d39eef603901e5a034b89e86436ae042f6a40b8d3a344583d175", "publicDeclarations": [] }, "./cli/build-ssg": { - "sha256": "784788fc8e914583533c814a51d023fca062f9aa2ff5039a14748779065a98e6", + "sha256": "10fdc527b3a19974af767a71516d5957475be84f551a57c635488cdbe16b937f", "publicDeclarations": [ "export interface SsgEntryDescriptorInputs {", "export function buildSsgEntryDescriptor(" @@ -231,7 +209,7 @@ ] }, "./plugin": { - "sha256": "d4e9230cc30f41f3eeece30437abb4ff8e95057803c47f520c1cc36e2eda0f56", + "sha256": "ce53992a9feb7490df8cf62a22cb97d53ffcf2842176010f79a1a3b2092e907a", "publicDeclarations": [ "export function optionalPackageStubsPlugin(): Plugin {", "export function createOpenPlugin(" @@ -279,7 +257,7 @@ }, "declarations": { ".": { - "sha256": "600c64d12cb8272d2689aca138d46167d7441cae1a5e1445cf11a16e89e2638e", + "sha256": "c7eb3865d060b6463d1228dfc227b1734037b3df2576e2258fc5f369153ca8df", "publicDeclarations": [] }, "./open-badge": { @@ -290,7 +268,7 @@ ] }, "./open-button": { - "sha256": "9a4694535c3527b29a81d981218fc26d919ef819556eb13529fff4131fae07ad", + "sha256": "5e2ab4736e53b397c1336f0c069587d7edcc2ca6ea446b7bee518e3ed09761af", "publicDeclarations": [ "export const tagName = 'open-button';", "export class OpenButton extends OpenElement {" @@ -304,21 +282,21 @@ ] }, "./open-card": { - "sha256": "fdf1fd51c5526a65c828e6aeb77fe66b9b607d640806b82653d44d4a878a1619", + "sha256": "3a4a9fde6d98124eb9833bbd58d8495efd26cf873c72245e750ef35431a5268a", "publicDeclarations": [ "export const tagName = 'open-card';", "export class OpenCard extends OpenElement {" ] }, "./open-code-block": { - "sha256": "b997436c5c07fc83aa7d79e9b2908b42aa85af2a2f5b7287840cd4bc014360a2", + "sha256": "6f0bc31309727bcd5371beacdd4d312b246a9333cf1e8aa134920fdd27d5f0d6", "publicDeclarations": [ "export const tagName = 'open-code-block';", "export class OpenCodeBlock extends OpenElement {" ] }, "./open-dialog": { - "sha256": "e0a4bf78c47d08c0281c539bb8e2193bcfc100e07f0fc65bd795eedf520c5ab0", + "sha256": "6ac1ac4f6b3364c5da6d79c327cbc3868f99a6d3708b9293073873cc5c9aa372", "publicDeclarations": [ "export const tagName = 'open-dialog';", "export class OpenDialog extends OpenElement {" @@ -332,7 +310,7 @@ ] }, "./open-input": { - "sha256": "c85f90dc0687b52a9aa4e4926a6fd7f34f7197301d308c0acfa8d4bd5422286f", + "sha256": "a85e5ffb970b7159e7c8a940f9c22e5ff2810d5bedec9de8d7f4ed8b6c5c7870", "publicDeclarations": [ "export const tagName = 'open-input';", "export class OpenInput extends OpenElement {" @@ -362,7 +340,7 @@ ] }, "./open-theme-toggle": { - "sha256": "0f3618577345a670e8eefff9927bc6757a5bd591a0a0f2ab0b7d019a74831a22", + "sha256": "555523aa7fbc72097b71fde61b823ba2dc967bdb837a826f440f7fd307822708", "publicDeclarations": [ "export const tagName = 'open-theme-toggle';", "export class OpenThemeToggle extends OpenElement {" diff --git a/docs/release/v0.41.0.md b/docs/release/v0.41.0.md index c283abcf5..f1af3aac7 100644 --- a/docs/release/v0.41.0.md +++ b/docs/release/v0.41.0.md @@ -85,8 +85,8 @@ through four alphas and one beta before the stable tag. `secrets.NPM_TOKEN` for provenance publishing. - Keep JSR monitor and metadata waiters as historical observation tools only; npm is the only required registry closure path. -- Keep `jsr-consumer-monitor.yml` and `wait-jsr-release-metadata.ts` as - historical observation tools, not release gates. +- Keep `wait-jsr-release-metadata.ts` as a historical observation tool, not a + release gate (`jsr-consumer-monitor.yml` was removed in #352). ### Consumer Smoke Coverage diff --git a/docs/roadmap/ROADMAP.md b/docs/roadmap/ROADMAP.md index 1f2c00feb..73d5476bb 100644 --- a/docs/roadmap/ROADMAP.md +++ b/docs/roadmap/ROADMAP.md @@ -4,11 +4,11 @@ Execution and release state follow the [`Project Workflow`](../governance/PROJECT_WORKFLOW.md). > Source of truth for forward product planning.\ -> Published package line: `v0.41.0-alpha.18`.\ -> Active execution target: `v0.41.0-alpha.18`.\ +> Published package line: `v0.41.0-alpha.19`.\ +> Active execution target: `v0.41.0-alpha.19`.\ > Current implementation state: five-package convergence is published; -> alpha.17 closed the first audit remediation and alpha.18 runs the second -> audit sweep (ADR-0117).\ +> alpha.17 closed the first audit remediation and alpha.18 completed the +> second audit sweep (ADR-0117).\ > Maturity stage: alpha; the abandoned beta naming is retired. ## Product direction @@ -113,13 +113,13 @@ making the standard Custom Element contract span both layers. See the official ## Current release state -`0.41.0-alpha.18` is the published package line. npm beta.1 through beta.3 are +`0.41.0-alpha.19` 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. Alpha.17 completed the first audit remediation (test credibility, convergence, -release tooling). Alpha.18 runs the second audit sweep (ADR-0117): +release tooling). Alpha.18 completed the second audit sweep (ADR-0117): sibling-path closures, evidence honesty, reflect-prop correctness and redundancy cleanup. Stable `0.41.0` requires a separate approved plan after those efforts and the #390 pilot uncover no further architecture, diff --git a/docs/status/STATUS.md b/docs/status/STATUS.md index dd254e222..b8db35fda 100644 --- a/docs/status/STATUS.md +++ b/docs/status/STATUS.md @@ -1,9 +1,9 @@ # OpenElement Status > Updated: 2026-07-24\ -> Repository package line: `v0.41.0-alpha.18`\ -> npm registry line: `v0.41.0-alpha.18`\ -> Active release target: `v0.41.0-alpha.18`\ +> Repository package line: `v0.41.0-alpha.19`\ +> npm registry line: `v0.41.0-alpha.19`\ +> Active release target: `v0.41.0-alpha.19`\ > Next stability candidate: `v0.41.0`\ > Product graph: five packages\ > Current maturity stage: alpha @@ -11,10 +11,12 @@ ## Current position OpenElement is a Web Components-native, static-first application framework. -Alpha.17 is the current published and verified line; it closed the first audit -remediation (ADR-0116 packages B–D). Alpha.18 runs the second audit sweep -(ADR-0117): sibling-path closures, evidence honesty and redundancy cleanup, -before a separate decision about stable `0.41.0`. +Alpha.19 runs the third audit cleanup sweep (ADR-0118): third-audit issue +closure across documentation, tests, runtime edges and release tooling. +Alpha.18 completed the second audit sweep (ADR-0117): sibling-path closures, +evidence honesty and redundancy cleanup. Alpha.17 remains the previous +verified baseline; it closed the first audit remediation (ADR-0116 packages +B–D). A stable `0.41.0` remains a separate decision. Custom Elements are the application component contract; JSX and Basic Element are authoring modes; Declarative Shadow DOM is the default server output; @@ -56,9 +58,10 @@ and output path. | Version | Focus | | ----------------- | ------------------------------------------------ | -| `0.41.0-alpha.17` | Current verified published baseline | +| `0.41.0-alpha.17` | First audit remediation baseline (ADR-0116) | | `0.41.0-alpha.18` | Second audit sweep (ADR-0117) | -| `0.41.0` | Separate stable decision after alpha.18 evidence | +| `0.41.0-alpha.19` | Third audit cleanup sweep (ADR-0118) | +| `0.41.0` | Separate stable decision after alpha.19 evidence | | `0.42.0` | WC Application Loop | | `0.43.0` | Universal WC SSR compatibility and diagnostics | | `1.0.0` | Stable five-package product | @@ -68,6 +71,8 @@ and output path. - Mandatory workflow: [`PROJECT_WORKFLOW.md`](../governance/PROJECT_WORKFLOW.md) - Active version contract: [`VERSION_PLAN.md`](../current/VERSION_PLAN.md) - Second-sweep scope decision: [`ADR-0117`](../adr/ADR-0117-second-audit-round-alpha18-sweep.md) +- Third-sweep cleanup decision: [`ADR-0118`](../adr/ADR-0118-third-audit-round-alpha19-cleanup-sweep.md) - Current package surface: [`PACKAGE_SURFACE.md`](../current/PACKAGE_SURFACE.md) - Alpha.17 immutable release record: [`v0.41.0-alpha.17.md`](../release/v0.41.0-alpha.17.md) +- Alpha.18 immutable release record: [`v0.41.0-alpha.18.md`](../release/v0.41.0-alpha.18.md) - Complete forward roadmap: [`ROADMAP.md`](../roadmap/ROADMAP.md) diff --git a/examples/deno-desktop-mastodon/VERIFICATION.md b/examples/deno-desktop-mastodon/VERIFICATION.md index ec76c7a42..be9b2a159 100644 --- a/examples/deno-desktop-mastodon/VERIFICATION.md +++ b/examples/deno-desktop-mastodon/VERIFICATION.md @@ -2,8 +2,9 @@ ## Automated checks (CI-enforced) -The root `deno task examples:check` gate runs in CI (AutoFlow `ci` and -`release` tiers, triggered by `examples/` changes) and covers: +The root `deno task examples:check` gate runs unconditionally in CI (AutoFlow +`ci` and `release` tiers run every triggered gate regardless of changed +paths) and covers: - [x] `deno task check` — type check of `main.ts` and `mastodon.tsx`. - [x] `deno task smoke` — 23 unit tests (format, cache, api, server smoke). diff --git a/examples/deno-desktop-reader/VERIFICATION.md b/examples/deno-desktop-reader/VERIFICATION.md index 465c747b3..31c957e4e 100644 --- a/examples/deno-desktop-reader/VERIFICATION.md +++ b/examples/deno-desktop-reader/VERIFICATION.md @@ -6,33 +6,38 @@ router, UI, Open Props tokens, SPA mode, Deno Desktop host behavior, or Reader. ## Smoke matrix -| Area | Evidence | -| ---------------------------- | --------------------------------------------------------------------- | -| Browser app boot | Browser dev server smoke, `test:visual-smoke` | -| Deno Desktop target | Deno Desktop smoke, `desktop-reader:smoke` | -| Local fixture source | `desktop-reader:smoke` host-store tests | -| Local folder source | `desktop-reader:smoke` host-store tests | -| GitHub PDF source | `desktop-reader:smoke` host-store tests | -| PDF/text reading | Browser smoke, `test:visual-smoke` | -| Search | `desktop-reader:smoke` host-store tests | -| Annotation and note jump | Browser smoke, Reader route tests | -| Markdown export | `desktop-reader:smoke` export tests | -| Reading settings | Browser smoke, storage tests | -| OpenElement UI/Open Props | `test:visual-smoke`, UI package tests | -| Preact islands | `desktop-reader:smoke` (island registration) | -| Third-party WC compatibility | `desktop-reader:smoke` (`/wc-interop` VNode tag smoke), browser smoke | -| Desktop/narrow screenshots | `test:visual-smoke` | +The root `deno task examples:check` gate runs unconditionally in the AutoFlow +`ci` and `release` tiers and executes `deno task check` and `deno task smoke` +inside this example; `smoke` below refers to that example-local task. + +| Area | Evidence | +| ---------------------------- | ---------------------------------------------------------------- | +| Browser app boot | Browser dev server smoke, `test:visual-smoke` | +| Deno Desktop target | Deno Desktop smoke, example-local `deno task smoke` | +| Local fixture source | `deno task smoke` host-store tests | +| Local folder source | `deno task smoke` host-store tests | +| GitHub PDF source | `deno task smoke` host-store tests | +| PDF/text reading | Browser smoke, `test:visual-smoke` | +| Search | `deno task smoke` host-store tests | +| Annotation and note jump | Browser smoke, Reader route tests | +| Markdown export | `deno task smoke` export tests | +| Reading settings | Browser smoke, storage tests | +| OpenElement UI/Open Props | `test:visual-smoke`, UI package tests | +| Preact islands | `deno task smoke` (island registration) | +| Third-party WC compatibility | `deno task smoke` (`/wc-interop` VNode tag smoke), browser smoke | +| Desktop/narrow screenshots | `test:visual-smoke` | ## Automated release checks ```sh -deno task desktop-reader:smoke +deno task --cwd examples/deno-desktop-reader smoke deno task test:visual-smoke ``` -`desktop-reader:smoke` runs the Reader unit and host-store tests. It fails when -core Reader workflows regress. `test:visual-smoke` builds the docs site and -Reader, then captures screenshot evidence for the docs shell and Reader shell. +The example-local `deno task smoke` runs the Reader unit and host-store tests. +It fails when core Reader workflows regress. `test:visual-smoke` builds the +docs site and Reader, then captures screenshot evidence for the docs shell and +Reader shell. ## Browser dev server smoke diff --git a/examples/open-element-in-fresh/README.md b/examples/open-element-in-fresh/README.md index 3a3bec3d8..011bc5cea 100644 --- a/examples/open-element-in-fresh/README.md +++ b/examples/open-element-in-fresh/README.md @@ -1,24 +1,22 @@ # openElement in Fresh — alpha.4 Interop Proof -A minimal [Fresh 2.3+](https://fresh.deno.dev) project that demonstrates -openElement custom elements (``, ``) running inside a -Fresh app with Preact islands. +A minimal [Fresh 2.3+](https://fresh.deno.dev) project that demonstrates openElement custom elements +(``, ``) running inside a Fresh app with Preact islands. ## What It Proves -1. **openElement custom elements in Fresh** — `` and `` - are rendered as standard HTML custom element tags in the Fresh server-side - route. No special JSX, no wrapper components needed. +1. **openElement custom elements in Fresh** — `` and `` are rendered as + standard HTML custom element tags in the Fresh server-side route. No special JSX, no wrapper + components needed. -2. **Third-party framework boot** — The `OpenElements.tsx` island defines inline - custom element stubs (``, ``) using the native - `customElements.define` API. This proves openElement-compatible registrations - can be shipped from within Preact islands. +2. **Third-party framework boot** — The `OpenElements.tsx` island defines inline custom element + stubs (``, ``) using the native `customElements.define` API. This proves + openElement-compatible registrations can be shipped from within Preact islands. -3. **Bilateral interop** — The same page hosts both openElement custom elements - and a Preact counter island (`PreactCounter.tsx`). Each owns its lifecycle - independently. The Preact island uses `@preact/signals` for state; the - openElement elements use their own shadow DOM and event system. No conflict. +3. **Bilateral interop** — The same page hosts both openElement custom elements and a Preact counter + island (`PreactCounter.tsx`). Each owns its lifecycle independently. The Preact island uses + `@preact/signals` for state; the openElement elements use their own shadow DOM and event system. + No conflict. ## Quick Start @@ -48,22 +46,19 @@ examples/open-element-in-fresh/ ## How It Works -1. **SSR** — Fresh renders the route (`routes/index.tsx`) on the server. Custom - element tags like `` and `` are emitted as plain - HTML. The `` island is serialized as an interactive island - marker (Fresh handles this automatically). +1. **SSR** — Fresh renders the route (`routes/index.tsx`) on the server. Custom element tags like + `` and `` are emitted as plain HTML. The `` island is + serialized as an interactive island marker (Fresh handles this automatically). 2. **Client Hydration** — When the page loads in the browser: - - `OpenElements.tsx` island activates → defines inline custom element classes - via `customElements.define` → browser upgrades the `` and - `` tags already in the DOM. - - `PreactCounter.tsx` island activates → Preact mounts the counter component - independently. - -3. **Interop Guarantee** — openElement custom elements are standard Web - Components. They use shadow DOM, `customElements.define`, and native DOM - APIs. Fresh/Preact islands are standard Preact components hydrated via - Fresh's island hydration. The two systems share the DOM but not state or + - `OpenElements.tsx` island activates → defines inline custom element classes via + `customElements.define` → browser upgrades the `` and `` tags already + in the DOM. + - `PreactCounter.tsx` island activates → Preact mounts the counter component independently. + +3. **Interop Guarantee** — openElement custom elements are standard Web Components. They use shadow + DOM, `customElements.define`, and native DOM APIs. Fresh/Preact islands are standard Preact + components hydrated via Fresh's island hydration. The two systems share the DOM but not state or lifecycle. ## Migration Notes (Fresh 1.x → 2.x) @@ -77,13 +72,11 @@ examples/open-element-in-fresh/ ## Known Limitation -The `OpenElements` island uses inline custom element stubs instead of -`@openelement/ui`. Root cause: `deno pack` does not apply JSX transformation -when publishing `packages/ui` to npm — the output `.js` files retain raw JSX -which Vite cannot transpile. +The `OpenElements` island uses inline custom element stubs instead of `@openelement/ui`. Root cause: +`deno pack` does not apply JSX transformation when publishing `packages/ui` to npm — the output +`.js` files retain raw JSX which Vite cannot transpile. -**Fix (alpha.5):** The `compilerOptions.jsx` config is already in -`packages/ui/deno.json`. The remaining blocker is the `deno pack` transpilation -gap — when publishing to npm, JSX is not transformed to `jsx()` calls in the -output `.js` files. Once the pack pipeline is fixed, replace stubs with -`import "@openelement/ui"`. +**Fix (alpha.5):** The `compilerOptions.jsx` config is already in `packages/ui/deno.json`. The +remaining blocker is the `deno pack` transpilation gap — when publishing to npm, JSX is not +transformed to `jsx()` calls in the output `.js` files. Once the pack pipeline is fixed, replace +stubs with `import "@openelement/ui"`. diff --git a/examples/open-element-in-fresh/deno.json b/examples/open-element-in-fresh/deno.json index 62448c821..51959db22 100644 --- a/examples/open-element-in-fresh/deno.json +++ b/examples/open-element-in-fresh/deno.json @@ -15,6 +15,13 @@ ] } }, + "fmt": { + "useTabs": false, + "lineWidth": 100, + "indentWidth": 2, + "semiColons": true, + "singleQuote": true + }, "exclude": [ "**/_fresh/*" ], diff --git a/packages/adapter-vite/__tests__/app-vite.test.ts b/packages/adapter-vite/__tests__/app-vite.test.ts index bdf90f955..ef9cb27ec 100644 --- a/packages/adapter-vite/__tests__/app-vite.test.ts +++ b/packages/adapter-vite/__tests__/app-vite.test.ts @@ -4,7 +4,14 @@ * Tests that openElement() correctly combines openPipeline() + openContent() + openI18n() * with a shared OpenElementBuildContext. This is the primary user-facing API. */ -import { assertArrayIncludes, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0'; +import { + assertArrayIncludes, + assertEquals, + assertExists, + assertRejects, + assertStringIncludes, +} from 'jsr:@std/assert@^1.0.0'; +import { join } from 'node:path'; import { openElement } from '../src/app-vite.ts'; // ─── Plugin structure ────────────────────────────────────────── @@ -102,17 +109,83 @@ Deno.test('openElement() with content returns more plugins', () => { // ─── Options propagation ───────────────────────────────────── -Deno.test('openElement() accepts html config', () => { - const plugins = openElement({ html: { title: 'Test', lang: 'ja' } }); - assertExists(plugins); +/** + * Drive the umbrella plugins the way Vite does — config, configResolved, + * buildStart, then virtual-entry load — against a temp working directory and + * return the generated SSR entry code. + */ +async function renderUmbrellaEntry( + options: Parameters[0], + setup?: (tmp: string) => void, +): Promise { + const tmp = Deno.makeTempDirSync({ prefix: 'open-app-vite-' }); + const origCwd = Deno.cwd(); + try { + setup?.(tmp); + Deno.chdir(tmp); + const plugins = openElement(options); + const corePlugin = plugins.find((p) => p.name === 'open:core')!; + const virtualPlugin = plugins.find((p) => p.name === 'open:virtual-entry')!; + const config = (corePlugin as { config?: unknown }).config; + assertExists(config, 'config hook must exist'); + (config as (c: Record) => unknown)({}); + const configResolved = (corePlugin as { configResolved?: unknown }).configResolved; + assertExists(configResolved, 'configResolved hook must exist'); + (configResolved as (config: never) => void)({} as never); + const buildStart = (corePlugin as { buildStart?: unknown }).buildStart; + assertExists(buildStart, 'buildStart hook must exist'); + await (buildStart as () => Promise)(); + const load = (virtualPlugin as { load?: unknown }).load; + assertExists(load, 'load hook must exist'); + const code = (load as (id: string) => unknown)('\0virtual:open-hono-entry'); + assertExists(code, 'virtual entry load must return code'); + return String(code); + } finally { + Deno.chdir(origCwd); + try { + Deno.removeSync(tmp, { recursive: true }); + } catch { /* ignore */ } + } +} + +Deno.test('openElement() html config reaches the generated entry document', async () => { + // The document title/lang are only rendered once at least one route exists. + const code = await renderUmbrellaEntry({ html: { title: 'Test', lang: 'ja' } }, (tmp) => { + Deno.mkdirSync(join(tmp, 'app', 'routes'), { recursive: true }); + Deno.writeTextFileSync( + join(tmp, 'app', 'routes', 'index.ts'), + 'export default () => "

Hello

"', + ); + }); + assertStringIncludes(code, '"Test"'); + assertStringIncludes(code, '"ja"'); }); -Deno.test('openElement() accepts packageIslands config', () => { - const plugins = openElement({ packageIslands: ['@openelement/ui'] }); - assertExists(plugins); +Deno.test('openElement() middleware.corsOrigin reaches the generated entry', async () => { + const code = await renderUmbrellaEntry({ middleware: { corsOrigin: ['https://example.com'] } }); + assertStringIncludes(code, 'https://example.com'); }); -Deno.test('openElement() accepts middleware config', () => { - const plugins = openElement({ middleware: { corsOrigin: '*' } }); - assertExists(plugins); +Deno.test('openElement() packageIslands are scanned during buildStart', async () => { + const tmp = Deno.makeTempDirSync({ prefix: 'open-app-vite-' }); + const origCwd = Deno.cwd(); + try { + Deno.chdir(tmp); + const plugins = openElement({ packageIslands: ['@nonexistent/package'] }); + const corePlugin = plugins.find((p) => p.name === 'open:core')!; + const buildStart = (corePlugin as { buildStart?: unknown }).buildStart; + assertExists(buildStart, 'buildStart hook must exist'); + // A configured packageIsland that cannot be imported must surface as a + // route-scan failure, proving the option is wired into buildStart. + await assertRejects( + () => (buildStart as () => Promise)(), + Error, + '@nonexistent/package', + ); + } finally { + Deno.chdir(origCwd); + try { + Deno.removeSync(tmp, { recursive: true }); + } catch { /* ignore */ } + } }); diff --git a/packages/adapter-vite/__tests__/assertion-style.test.ts b/packages/adapter-vite/__tests__/assertion-style.test.ts index 4d6f8cbf0..f4fb74228 100644 --- a/packages/adapter-vite/__tests__/assertion-style.test.ts +++ b/packages/adapter-vite/__tests__/assertion-style.test.ts @@ -2,24 +2,94 @@ import { assertEquals } from 'jsr:@std/assert@^1.0.0'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -const FILES = [ - 'www/__tests__/build-output.test.ts', - 'packages/create/__tests__/cli.test.ts', - 'packages/adapter-vite/__tests__/entry-generators.test.ts', - 'packages/adapter-vite/__tests__/build-manifest.test.ts', -]; +/** + * Audit gate: boolean expressions must not be passed to assertExists. + * + * assertExists only rejects null/undefined, so a predicate wrapped in it + * (e.g. a `.includes(...)` result) can never fail the test. Use assert, + * assertStringIncludes, or assertEquals for predicates instead. + * + * Every packages/*\/__tests__\/*.test.ts file is scanned. An assertExists + * call is flagged when its direct argument (text at paren depth 1, before the + * first top-level comma) contains a boolean construct: .includes(/ + * .startsWith(/.endsWith(/.some(/.every(/instanceof, or a comparison/logical + * operator. Nested expressions — e.g. a `.find((w) => ...)` narrowing guard + * whose callback uses predicates — are legitimate and not flagged. + */ -Deno.test('audit gate: no boolean expressions passed to assertExists in hardened tests', () => { +const BOOLEAN_PATTERN = + /\.(?:includes|startsWith|endsWith|some|every)\(|\binstanceof\b|===|!==|>=|<=|\|\||&&/; + +function listTestFiles(): string[] { + const files: string[] = []; + const packagesDir = join(Deno.cwd(), 'packages'); + for (const pkg of Deno.readDirSync(packagesDir)) { + if (!pkg.isDirectory) continue; + const testsDir = join(packagesDir, pkg.name, '__tests__'); + let entries: Deno.DirEntry[]; + try { + entries = [...Deno.readDirSync(testsDir)]; + } catch { + continue; // package without __tests__ + } + for (const entry of entries) { + if (entry.isFile && entry.name.endsWith('.test.ts')) { + files.push(join(testsDir, entry.name)); + } + } + } + return files.sort(); +} + +/** Extract the direct (depth-1) argument text of every assertExists call. */ +function directArguments(source: string): Array<{ text: string; line: number }> { + const results: Array<{ text: string; line: number }> = []; + const marker = 'assertExists' + '('; + let searchFrom = 0; + for (;;) { + const start = source.indexOf(marker, searchFrom); + if (start === -1) return results; + const line = source.slice(0, start).split('\n').length; + let depth = 1; + let text = ''; + let quote: string | null = null; + let i = start + marker.length; + for (; i < source.length; i++) { + const ch = source[i]; + if (quote) { + if (ch === '\\') i++; + else if (ch === quote) quote = null; + continue; + } + if (ch === "'" || ch === '"' || ch === '`') { + quote = ch; + continue; + } + if (ch === '(' || ch === '[' || ch === '{') { + if (depth === 1) text += ch; + depth++; + } else if (ch === ')' || ch === ']' || ch === '}') { + depth--; + if (depth === 0) break; + if (depth === 1) text += ch; + } else if (depth === 1) { + if (ch === ',') break; // stop before the optional message argument + text += ch; + } + } + results.push({ text, line }); + searchFrom = i; + } +} + +Deno.test('audit gate: no boolean expressions passed to assertExists', () => { const offenders: string[] = []; - for (const file of FILES) { - const content = readFileSync(join(Deno.cwd(), file), 'utf-8'); - const lines = content.split(/\r?\n/); - for (const [index, line] of lines.entries()) { - if ( - /assertExists\([^,\)]*(?:\.includes\(|===|!==|>=|<=|>|<|\|\||&&)/.test(line) - ) { - offenders.push(`${file}:${index + 1}: ${line.trim()}`); + for (const file of listTestFiles()) { + const content = readFileSync(file, 'utf-8'); + for (const { text, line } of directArguments(content)) { + if (BOOLEAN_PATTERN.test(text)) { + offenders.push(`${file}:${line}: ${text.trim().replaceAll(/\s+/g, ' ').slice(0, 80)}`); } } } diff --git a/packages/adapter-vite/__tests__/entry-descriptor.test.ts b/packages/adapter-vite/__tests__/entry-descriptor.test.ts index d97bea5aa..be4e79346 100644 --- a/packages/adapter-vite/__tests__/entry-descriptor.test.ts +++ b/packages/adapter-vite/__tests__/entry-descriptor.test.ts @@ -4,16 +4,10 @@ * Tests the two-step entry pipeline: * 1. buildEntryDescriptor - produces structured data * 2. renderEntry - renders data to code string - * - * Also integration-tests that generateHonoEntryCode still works. */ import { assertEquals, assertStringIncludes } from 'jsr:@std/assert@^1.0.0'; -import { - buildEntryDescriptor, - generateHonoEntryCode, - renderEntry, -} from '../src/internal/ssg/index.ts'; +import { buildEntryDescriptor, renderEntry } from '../src/internal/ssg/index.ts'; import type { RouteEntry } from '../src/internal/protocol/framework.ts'; // Test fixtures @@ -233,13 +227,13 @@ Deno.test('renderEntry: document config renders correctly', () => { assertStringIncludes(code, 'cdn.example.com'); }); -// Integration test: generateHonoEntryCode +// Integration test: buildEntryDescriptor + renderEntry end-to-end -Deno.test('generateHonoEntryCode: end-to-end produces runnable code', () => { - const code = generateHonoEntryCode(sampleRoutes, { +Deno.test('buildEntryDescriptor + renderEntry: end-to-end produces runnable code', () => { + const code = renderEntry(buildEntryDescriptor(sampleRoutes, { routesDir: 'app/routes', islandsDir: 'app/islands', - }); + })); assertStringIncludes(code, "import { Hono } from 'hono'"); assertStringIncludes(code, 'export default app'); diff --git a/packages/adapter-vite/__tests__/entry-renderer.test.ts b/packages/adapter-vite/__tests__/entry-renderer.test.ts index 70c7909d6..b82d5997d 100644 --- a/packages/adapter-vite/__tests__/entry-renderer.test.ts +++ b/packages/adapter-vite/__tests__/entry-renderer.test.ts @@ -15,11 +15,7 @@ import { assertFalse, assertStringIncludes, } from 'jsr:@std/assert@^1.0.0'; -import { - buildEntryDescriptor, - generateHonoEntryCode, - renderEntry, -} from '../src/internal/ssg/index.ts'; +import { buildEntryDescriptor, renderEntry } from '../src/internal/ssg/index.ts'; import type { RouteEntry } from '../src/internal/protocol/framework.ts'; // Fixtures @@ -482,23 +478,23 @@ Deno.test('renderEntry: SSG mode includes no DOM shim (DSD renderer)', () => { // Section -Deno.test('generateHonoEntryCode: CSP flows through full pipeline', () => { - const code = generateHonoEntryCode(basicRoutes, { +Deno.test('renderEntry: CSP flows through full pipeline', () => { + const code = renderEntry(buildEntryDescriptor(basicRoutes, { middleware: { csp: { policy: "default-src 'self'; script-src 'self' 'unsafe-inline'", nonce: false, }, }, - }); + })); assertStringIncludes(code, 'Content-Security-Policy'); assertStringIncludes(code, "default-src 'self'"); assertStringIncludes(code, 'export default app'); }); -Deno.test('generateHonoEntryCode: complex scenario with all features', () => { - const code = generateHonoEntryCode(withSpecialRoutes, { +Deno.test('renderEntry: complex scenario with all features', () => { + const code = renderEntry(buildEntryDescriptor(withSpecialRoutes, { routesDir: 'app/routes', islandsDir: 'app/islands', middleware: { @@ -524,7 +520,7 @@ Deno.test('generateHonoEntryCode: complex scenario with all features', () => { html: { lang: 'zh-CN', title: 'openElement' }, headExtras: '', upgradeStrategy: 'idle' as const, - }); + })); // All features present assertStringIncludes(code, 'Content-Security-Policy'); diff --git a/packages/adapter-vite/__tests__/index-plugin.test.ts b/packages/adapter-vite/__tests__/index-plugin.test.ts index 1909dd276..863fdefd6 100644 --- a/packages/adapter-vite/__tests__/index-plugin.test.ts +++ b/packages/adapter-vite/__tests__/index-plugin.test.ts @@ -96,18 +96,6 @@ Deno.test('createOpenPlugin() accepts options without error', () => { assertOpenPluginArray(plugins); }); -Deno.test('createOpenPlugin() core plugin has config hook defined', () => { - const plugins = createOpenPlugin(); - const corePlugin = plugins.find((p) => p.name === 'open:core')!; - assertExists(corePlugin.config, 'core plugin must define config hook'); -}); - -Deno.test('createOpenPlugin() core plugin has buildStart hook defined', () => { - const plugins = createOpenPlugin(); - const corePlugin = plugins.find((p) => p.name === 'open:core')!; - assertExists(corePlugin.buildStart, 'core plugin must define buildStart hook'); -}); - // ─── createOpenPlugin() inject / headExtras branches ───────────────────── Deno.test('createOpenPlugin() inject.stylesheets -> headExtras', () => { @@ -188,17 +176,17 @@ Deno.test('createOpenPlugin() corePlugin.config captures resolve.alias', () => { assertExists(build.rollupOptions, 'should include rollupOptions'); const rollupOptions = build.rollupOptions as Record; const input = rollupOptions.input as string[]; - assertExists(input.includes('virtual:open-hono-entry'), 'should include virtual entry in input'); + assertArrayIncludes(input, ['virtual:open-build-trigger']); }); -Deno.test('createOpenPlugin() corePlugin.config returns rollupOptions with virtual entry', () => { +Deno.test('createOpenPlugin() corePlugin.config returns rollupOptions with build trigger input', () => { const plugins = createOpenPlugin(); const corePlugin = plugins.find((p) => p.name === 'open:core')!; const result = (corePlugin.config as Function)({} as never) as Record; const build = result.build as Record; const rollupOptions = build.rollupOptions as Record; const input = rollupOptions.input as string[]; - assertExists(input.includes('virtual:open-hono-entry')); + assertArrayIncludes(input, ['virtual:open-build-trigger']); }); // ─── createOpenPlugin() configResolved + generateEntry ─────────────────── @@ -227,7 +215,7 @@ Deno.test('createOpenPlugin() virtualEntryPlugin.resolveId matches VIRTUAL_ENTRY undefined as never, {} as never, ); - assertExists(result); + assertEquals(result, '\0virtual:open-hono-entry'); }); Deno.test('createOpenPlugin() virtualEntryPlugin.load returns code for resolved ID', () => { diff --git a/packages/adapter-vite/__tests__/plugin.test.ts b/packages/adapter-vite/__tests__/plugin.test.ts index d662e5781..367105994 100644 --- a/packages/adapter-vite/__tests__/plugin.test.ts +++ b/packages/adapter-vite/__tests__/plugin.test.ts @@ -14,8 +14,11 @@ import { assertStringIncludes, assertThrows, } from 'jsr:@std/assert@^1.0.0'; +import { join } from 'node:path'; import { createOpenPlugin } from '../src/plugin.ts'; +type PluginOptions = Parameters[0]; + type HookRecord = { config?: unknown; load?: unknown; @@ -75,60 +78,102 @@ Deno.test('optional i18n fallback emits an explicit configuration warning', () = // ─── Option Defaults ────────────────────────────────────────── -Deno.test('openPlugin: defaults routesDir to app/routes', () => { - const plugins = createOpenPlugin({}); - // Default is applied internally - verify plugin creation succeeds - assertExists(plugins); - assertEquals(plugins.length, 8); -}); +/** + * Drive config -> configResolved -> buildStart -> virtual-entry load against a + * temp working directory and return the generated SSR entry code. This mirrors + * how Vite drives the plugin pipeline, so the emitted code reflects the + * resolved options (routesDir, islandsDir, upgradeStrategy, ...). + */ +async function renderVirtualEntry( + options: PluginOptions, + setup?: (tmp: string) => void, +): Promise { + const tmp = Deno.makeTempDirSync({ prefix: 'open-plugin-opts-' }); + const origCwd = Deno.cwd(); + try { + setup?.(tmp); + Deno.chdir(tmp); + const plugins = createOpenPlugin(options); + const corePlugin = plugins.find((p) => p.name === 'open:core')!; + const virtualPlugin = plugins.find((p) => p.name === 'open:virtual-entry')!; + callConfig(corePlugin); + const configResolved = (corePlugin as { configResolved?: unknown }).configResolved; + assertExists(configResolved, 'configResolved hook must exist'); + (configResolved as (config: never) => void)({} as never); + const buildStart = (corePlugin as { buildStart?: unknown }).buildStart; + assertExists(buildStart, 'buildStart hook must exist'); + await (buildStart as () => Promise)(); + const code = callLoad(virtualPlugin, '\0virtual:open-hono-entry'); + assertExists(code, 'virtual entry load must return code'); + return String(code); + } finally { + Deno.chdir(origCwd); + try { + Deno.removeSync(tmp, { recursive: true }); + } catch { /* ignore */ } + } +} -Deno.test('openPlugin: defaults islandsDir to app/islands', () => { - const plugins = createOpenPlugin({}); - assertExists(plugins); - assertEquals(plugins.length, 8); +function writeRouteIndex(dir: string): void { + Deno.mkdirSync(dir, { recursive: true }); + Deno.writeTextFileSync(join(dir, 'index.ts'), 'export default () => "

Hello

"'); +} + +function writeIsland(dir: string): void { + Deno.mkdirSync(dir, { recursive: true }); + Deno.writeTextFileSync(join(dir, 'my-counter.ts'), 'export const tagName = "my-counter"'); +} + +Deno.test('openPlugin: defaults routesDir to app/routes', async () => { + const code = await renderVirtualEntry({}, (tmp) => writeRouteIndex(join(tmp, 'app', 'routes'))); + assertStringIncludes(code, '/app/routes/index.ts'); }); -Deno.test('openPlugin: defaults componentsDir to app/components', () => { - const plugins = createOpenPlugin({}); - assertExists(plugins); - assertEquals(plugins.length, 8); +Deno.test('openPlugin: defaults islandsDir to app/islands', async () => { + const code = await renderVirtualEntry({}, (tmp) => writeIsland(join(tmp, 'app', 'islands'))); + assertStringIncludes(code, '/app/islands/my-counter.ts'); }); -Deno.test('openPlugin: respects custom routesDir', () => { - const plugins = createOpenPlugin({ routesDir: 'src/pages' }); - assertExists(plugins); - assertEquals(plugins.length, 8); +Deno.test('openPlugin: respects custom routesDir', async () => { + const code = await renderVirtualEntry( + { routesDir: 'src/pages' }, + (tmp) => writeRouteIndex(join(tmp, 'src', 'pages')), + ); + assertStringIncludes(code, '/src/pages/index.ts'); }); -Deno.test('openPlugin: respects custom islandsDir', () => { - const plugins = createOpenPlugin({ islandsDir: 'src/widgets' }); - assertExists(plugins); - assertEquals(plugins.length, 8); +Deno.test('openPlugin: respects custom islandsDir', async () => { + const code = await renderVirtualEntry( + { islandsDir: 'src/widgets' }, + (tmp) => writeIsland(join(tmp, 'src', 'widgets')), + ); + assertStringIncludes(code, '/src/widgets/my-counter.ts'); }); -Deno.test('openPlugin: respects custom componentsDir', () => { - const plugins = createOpenPlugin({ componentsDir: 'src/ui' }); - assertExists(plugins); - assertEquals(plugins.length, 8); +Deno.test('openPlugin: accepts default and custom componentsDir', () => { + // componentsDir is only consumed by the build closeBundle phase; here we can + // only assert both forms construct a valid pipeline. + assertEquals(createOpenPlugin({}).length, 8); + assertEquals(createOpenPlugin({ componentsDir: 'src/ui' }).length, 8); }); -// ─── Upgrade Strategy Default ───────────────────────────────── +// ─── Upgrade Strategy ───────────────────────────────────────── -Deno.test('openPlugin: default upgradeStrategy is idle', () => { - const plugins = createOpenPlugin({}); - // The default 'idle' is applied in generateEntry -> generateHonoEntryCode - // Verification: plugin construction succeeds with default - assertExists(plugins); -}); +Deno.test('openPlugin: island.upgradeStrategy flows into the SSR admission plan', async () => { + const setup = (tmp: string) => writeIsland(join(tmp, 'app', 'islands')); -Deno.test('openPlugin: accepts upgradeStrategy=load', () => { - const plugins = createOpenPlugin({ island: { upgradeStrategy: 'load' } }); - assertExists(plugins); -}); + // Default ('idle'): local islands are SSR-admitted and imported by the entry. + const defaultCode = await renderVirtualEntry({}, setup); + assertStringIncludes(defaultCode, 'import * as __island_my_counter'); -Deno.test('openPlugin: accepts upgradeStrategy=visible', () => { - const plugins = createOpenPlugin({ island: { upgradeStrategy: 'visible' } }); - assertExists(plugins); + // 'only': islands are excluded from SSR and marked client-only in the plan. + const onlyCode = await renderVirtualEntry({ island: { upgradeStrategy: 'only' } }, setup); + assertEquals(onlyCode.includes('import * as __island_my_counter'), false); + assertStringIncludes(onlyCode, 'client-only'); + + // 'load' / 'visible' remain valid construction options. + assertEquals(createOpenPlugin({ island: { upgradeStrategy: 'load' } }).length, 8); + assertEquals(createOpenPlugin({ island: { upgradeStrategy: 'visible' } }).length, 8); }); // ─── Invalid Options ────────────────────────────────────────── diff --git a/packages/adapter-vite/__tests__/ssg-postprocess.test.ts b/packages/adapter-vite/__tests__/ssg-postprocess.test.ts index 24fe7e599..46b504671 100644 --- a/packages/adapter-vite/__tests__/ssg-postprocess.test.ts +++ b/packages/adapter-vite/__tests__/ssg-postprocess.test.ts @@ -3,7 +3,13 @@ * * Tests the SSG post-processing functions using temp directories. */ -import { assertEquals, assertExists, assertFalse } from 'jsr:@std/assert@^1.0.0'; +import { + assert, + assertEquals, + assertExists, + assertFalse, + assertStringIncludes, +} from 'jsr:@std/assert@^1.0.0'; import { buildIslandChunkMap, buildSpeculationRulesJson, @@ -69,8 +75,8 @@ Deno.test('buildIslandChunkMap scans manifest.json for island chunks', async () mkdirSync(viteDir, { recursive: true }); const manifest = { - 'src/islands/counter.ts': { file: 'islands/island-counter-abc123.js' }, - 'src/islands/theme.ts': { file: 'islands/island-theme-def456.js' }, + 'src/islands/counter-island.ts': { file: 'islands/island-counter-island-abc123.js' }, + 'src/islands/open-theme-toggle.ts': { file: 'islands/island-open-theme-toggle-def456.js' }, '.openElement-client-entry.ts': { file: 'islands/client.js' }, }; writeFileSync(join(viteDir, 'manifest.json'), JSON.stringify(manifest), 'utf-8'); @@ -83,8 +89,8 @@ Deno.test('buildIslandChunkMap scans manifest.json for island chunks', async () assertExists(result['counter-island']); assertExists(result['open-theme-toggle']); - assertExists(result['counter-island'].includes('counter')); - assertExists(result['open-theme-toggle'].includes('theme')); + assertStringIncludes(result['counter-island'], 'counter'); + assertStringIncludes(result['open-theme-toggle'], 'theme'); } finally { cleanup(tmp); } @@ -101,7 +107,7 @@ Deno.test('buildIslandChunkMap respects basePath option', async () => { writeFileSync(join(viteDir, 'manifest.json'), JSON.stringify(manifest), 'utf-8'); const result = await buildIslandChunkMap(tmp, 'dist', ['counter-island'], '/my-app/'); - assertExists(result['counter-island'].startsWith('/my-app/')); + assert(result['counter-island'].startsWith('/my-app/')); } finally { cleanup(tmp); } @@ -160,8 +166,9 @@ Deno.test( result['my-counter'].includes('islands/islands/'), 'Path must NOT have double islands/ prefix, got: ' + result['my-counter'], ); - assertExists( - result['my-counter'].includes('client/islands/island-my-counter-abc123.js'), + assertStringIncludes( + result['my-counter'], + 'client/islands/island-my-counter-abc123.js', 'Path should be client/islands/island-my-counter-abc123.js, got: ' + result['my-counter'], ); } finally { @@ -312,8 +319,8 @@ Deno.test('injectClientScript adds script tag to HTML files', () => { injectClientScript(tmp, '/client/islands/client.js'); const content = readFileSync(htmlPath, 'utf-8'); - assertExists(content.includes('/client/islands/client.js')); - assertExists(content.includes('