Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9f0c08b
feat(alpha.5): SPA mode + client router + desktop proof
Jun 25, 2026
77442ca
fix alpha5 spa review and ci issues
Jun 25, 2026
7d0346a
fix alpha5 codeql review followups
Jun 25, 2026
fc44aa7
fix router params codeql sink
Jun 25, 2026
a2270d2
test alpha5 review edge cases
Jun 25, 2026
f5ee690
docs: add alpha5 desktop reader practice plan
Jun 25, 2026
5c86b50
docs: detail alpha5 desktop reader plan
Jun 25, 2026
976bdb1
fix alpha5 router race and reader differentiation
Jun 25, 2026
39cbf7c
fix: bot review final round — dead catch, build tasks, comments
Jun 25, 2026
9969c19
fix: restore outer try/catch — needed for sync throws in onChange
Jun 25, 2026
2fc334b
feat(alpha.5): Desktop Reader — full product dogfood app
Jun 25, 2026
04f3012
fix(reader): fixtures fallback for PDFs, workspace-relative imports
Jun 25, 2026
58383ac
chore: commit workspace changes (desktop-reader smoke task + spa rout…
Jun 25, 2026
0d831fd
fix(reader): serve app/* static files with correct MIME types
Jun 25, 2026
fe1c950
feat(reader): self-contained SPA runtime + esbuild bundle
Jun 25, 2026
65aa121
fix(reader): remove invalid desktop config fields (title, width, height)
Jun 25, 2026
21a7519
fix(reader): add --allow-env --allow-read --allow-net to desktop build
Jun 25, 2026
f14be41
fix(reader): lazy search index — don't block Deno.serve() startup
Jun 25, 2026
94bbd59
fix(reader): catch all handler errors, proper Uint8Array wrappers
Jun 25, 2026
c104b3b
fix(reader): /dist/ path resolution — double dist/ bug
Jun 25, 2026
4872ae5
fix(reader): --include flags embed all assets in desktop build
Jun 25, 2026
ae10fbd
fix(reader): auto-call bootReader() — module loaded but never executed
Jun 25, 2026
75580ff
fix(reader): call setRouter(app) so book cards navigate
Jun 25, 2026
f94a1bd
feat(alpha.5): SPA mode in adapter-vite + Reader full rebuild
Jun 25, 2026
e3ac086
fix(reader): restore fixtures + README
Jun 25, 2026
ad2f9e9
fix(reader): add --include dist to desktop build
Jun 25, 2026
fb2c3aa
fix alpha5 reader ci and review comments
Jun 25, 2026
cdcf3bc
fix reader route test crypto mock
Jun 25, 2026
567be77
fix spa shell html metadata
Jun 25, 2026
02f4c75
fix ci deno formatting
Jun 25, 2026
71c271d
tighten reader dogfood and spa form handling
Jun 25, 2026
5375f81
harden consumer local nitro cleanup
Jun 25, 2026
3a77e64
fix(reader): add --include fixtures to desktop build (PDF serving)
Jun 25, 2026
db9bfd5
fix(reader): auto-call Reader() in bundle — was exported but never ex…
Jun 25, 2026
f0ee56a
fix(reader): replace client-entry.js with reader bundle in SPA shell
Jun 26, 2026
a231eb5
fix(reader): client-entry.js → reader bundle + gitignore .app
Jun 26, 2026
b150330
fix(spa): renderToDom — VNode→DOM in renderComponent
Jun 26, 2026
3b50c07
refactor(reader): OpenElement-native routes — shadow DOM + UI CE + th…
Jun 26, 2026
1b4b201
fix(reader): CDN third-party CE + full OpenElement-native build
Jun 26, 2026
8f4bda4
fix(spa): OpenElement tagName support + restore defineApp in reader
Jun 26, 2026
dd5b660
fix(reader): inject Preact island client entry into SPA shell
Jun 26, 2026
a702478
fix(reader): inject CSS stylesheet into SPA HTML
Jun 26, 2026
773eea8
fix(reader): inline styles in bookshelf shadow DOM
Jun 26, 2026
ba516a0
fix(reader): use <open-card> <open-button> with inline styles — no ha…
Jun 26, 2026
bccb294
debug(reader): add loading... fallback text to root div
Jun 26, 2026
f76d452
fix(reader): import default exports to prevent tree-shaking customEle…
Jun 26, 2026
4d39c48
fix(reader): pick largest reader-*.js (not 0KB chunk) + tree-shake fix
Jun 26, 2026
80af290
fix(reader): remove CDN Shoelace+MWC (bare specifier error), clean wc…
Jun 26, 2026
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
47 changes: 33 additions & 14 deletions docs/release/v0.41.0-alpha.5-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,54 @@ primitives.

### v0.41.0-alpha.5 - SPA Mode Core

- [ ] Add `appMode: 'spa'` to the openElement app config.
- [ ] Implement a client-side router in `@openelement/router` or
- [x] Add `defineApp({ mode: 'spa' })` to `@openelement/app`.
- [x] Implement a client-side router in `@openelement/router` and wire it into
`@openelement/app`:
- History-based navigation (`pushState`/`popstate`).
- Optional hash-based navigation for `file://` and legacy embedded contexts.
- `auto` mode that uses history on HTTP(S) and hash navigation for `file://`
and legacy embedded contexts.
- Route params, query strings, and guards without a server route manifest.
- [ ] Runtime bootstrap:
- [x] Runtime bootstrap:
- Mount the app shell into a plain DOM node (no DSD template required).
- Support full client-side render on first load.
- Support hydrating an existing SSR shell if the same app is later used in
hybrid mode.
- Dispose and remount on hot reload during development.
- [ ] SPA data layer:
- [x] SPA data layer:
- In-memory loader/action data context.
- Optional async route guards.
- Lazy route loading.

Implementation notes:

- `SpaAppOptions.routerMode` can explicitly select `history`, `hash`, or `auto`;
the default is `auto`.
- The SPA layer does not register its own `popstate` listener. The router owns
browser navigation events and calls an `onChange` callback after it rematches,
avoiding duplicate renders on back/forward navigation.
- Query and route params are accumulated in null-prototype records so URL query
keys cannot write through object prototypes.

Deferred from alpha.5:

- Hydrating an existing SSR shell in hybrid mode.
- Lazy route loading as a first-class app API.

### v0.41.0-alpha.5 - Desktop Shell (Deno Desktop)

When Deno canary ships with native `deno desktop` support, validate SPA mode
targeting a Deno-powered desktop window instead of Tauri 2 / Electron.

- [ ] Create `examples/deno-desktop-spa/`.
- [ ] Deno canary ships with desktop support; no deferral needed.
- [x] Create `examples/deno-desktop-spa/`.
- [x] Keep browser imports resolvable in the served HTML via an import map
instead of relying on bare npm specifiers in native browsers.
- [ ] Deno canary desktop compile remains evidence-gated by Deno canary
availability on the release runner.

Tauri 2 / Electron / stable Deno desktop are deferred to v0.42 or later; Deno Desktop is the preferred
lightweight shell for the Deno-native ecosystem.

### v0.41.0-alpha.5 - Documentation

- [ ] Write `docs/integrations/spa-mode.md`.
- [x] Document the alpha.5 SPA/router contract in this release plan.
- [ ] Document differences between SSG/SSR mode and SPA mode.
- [ ] Document recommended disposal patterns for desktop shells.

Expand All @@ -71,13 +88,15 @@ Run on `dev` after all workstreams completed:
- `deno task consumer:packaged` — must pass
- `deno task test:e2e` — must pass with zero failures
- `deno task autoflow:ci` — must pass 21/21 gates
- Tauri 2 SPA example smoke test — must pass
- Deno Desktop SPA example smoke test — must pass when Deno canary desktop
support is available in CI

## Acceptance

- [ ] `appMode: 'spa'` boots an openElement app without SSR.
- [ ] Client-side router supports history and optional hash navigation.
- [ ] Deno Desktop example validates SPA mode in desktop context (or deferred to v0.42).
- [x] `defineApp({ mode: 'spa' })` boots an openElement app without SSR.
- [x] Client-side router supports history, hash, and auto navigation.
- [ ] Deno Desktop example validates SPA mode in desktop context, with CI
evidence pending Deno canary desktop support.
- [ ] SSG/ISR features remain cleanly out of scope for SPA mode.
- [ ] All verification gates pass.

Expand Down
24 changes: 14 additions & 10 deletions docs/roadmap/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ v0.41-v1.0 blocker.
| v0.41.0-alpha.1 | npm Distribution + Audit Cleanup | Replace JSR release closure with npm via `deno pack`; audit-driven cleanup and protocol restoration; ship first npm/JSR dual-published alpha. | Released |
| v0.41.0-alpha.2 | Signal-DOM Deepening | Extract `HydrationScope` to `@openelement/core/hydrate`; renderer/activation split; `BindingDescriptor` registry; static subpath validation. | Released |
| v0.41.0-alpha.5 | Cross-Framework WC Integration | Consume Lit/Shoelace/Material Web Components inside openElement; document interop contract; pure-ESM/pure-ECMAScript npm quality gates. | Release candidate |
| v0.41.0-alpha.5 | SPA Mode + Deno Desktop Proof | First-class single-page-application mode with client-side router; Deno Desktop shell validation via Deno canary (Tauri 2/Electron deferred to v0.42+). | Planned |
| v0.41.0-alpha.5 | SPA Mode + Deno Desktop Proof | First-class single-page-application mode with client-side router; Deno Desktop shell validation via Deno canary (Tauri 2/Electron deferred to v0.42+). | PR hardening |
| v0.41.0-beta.1 | v0.41.0 Stabilization | Close alpha feedback, update docs/starters/examples, freeze public surface for v0.41.0. | Planned |
| v0.41.0 | Deno-native npm distribution + WC Interop | Stable npm-first distribution, hardened signal-DOM architecture, validated third-party WC integration, lightweight external-framework runtime, and SPA/desktop shell proof. | Planned |
| v0.42.0 | Server Primitives | Add server request/action primitives and prove Node + Workers runtime paths through Nitro | Planned |
Expand Down Expand Up @@ -841,27 +841,30 @@ and publish decisions.
## v0.41.0-alpha.5 - SPA Mode + Desktop Shell Proof

Add a first-class single-page-application mode for desktop-style shells
(Tauri 2, Electron, Capacitor-style embedded WebViews). openElement's default
remains SSG/SSR-first, but alpha.5 proves the same component model works when
there is no server and no pre-rendered HTML.
(Deno Desktop first; Tauri 2, Electron, and Capacitor-style embedded WebViews
remain follow-up targets). openElement's default remains SSG/SSR-first, but
alpha.5 proves the same component model works when there is no server and no
pre-rendered HTML.

Core work:

- Add `appMode: 'spa'` to the openElement app config.
- Add `defineApp({ mode: 'spa' })` to `@openelement/app`.
- Client-side router:
- History-based navigation (`pushState`/`popstate`).
- Optional hash-based navigation for file:// and legacy embedded contexts.
- `auto` mode that selects hash navigation for `file://` and history for
HTTP(S).
- Route params, query strings, and guards without a server route manifest.
- Runtime bootstrap:
- Mount the app shell into a plain DOM node (no DSD template required).
- Hydrate or fully client-render on first load.
- Fully client-render on first load.
- Dispose and remount on hot reload during development.
- Data layer for SPA:
- In-memory loader/action data context.
- Optional async route guards and lazy route loading.
- Optional async route guards.
- Validation:
- Tauri 2 example project under `examples/tauri-spa/`.
- Electron example project under `examples/electron-spa/` (optional).
- Deno Desktop example project under `examples/deno-desktop-spa/`.
- Native browser import map in the served HTML so the example does not rely
on bare npm specifier resolution.
- E2E smoke for navigation, route params, and signal-driven updates inside the
desktop shell.

Expand All @@ -870,6 +873,7 @@ Non-goals:
- No attempt to make SSG/ISR features work inside SPA mode.
- No server primitives (deferred to v0.42.0+).
- No official mobile shell in alpha.5.
- No Tauri 2 or Electron proof in alpha.5.

## Cross-Project Decision: Mastodon Desktop Client

Expand Down
1 change: 1 addition & 0 deletions examples/deno-desktop-spa/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
30 changes: 30 additions & 0 deletions examples/deno-desktop-spa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# openElement Desktop SPA Proof

Runs openElement SPA mode inside a Deno Desktop window (deno canary).

## Setup

Install deno canary:

```sh
deno upgrade canary
```

## Run

```sh
deno task dev # Run as HTTP server (development)
deno task build # Compile to desktop binary
./deno-desktop-spa # Open desktop window (macOS)
```

## Architecture

- `main.ts` — `Deno.serve()` HTTP server that serves a browser module with an
import map for `@openelement/app`
- `routes/index.tsx` — SPA page with interactive counter
- `deno.json` — desktop config: webview backend, 1024×768 window
- Deno Desktop compiles the project to a self-contained binary

The import map is intentional: browsers do not resolve npm bare specifiers
natively, even when the surrounding process is Deno-powered.
25 changes: 25 additions & 0 deletions examples/deno-desktop-spa/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@openelement/example-desktop-spa",
"version": "0.41.0-alpha.5",
"tasks": {
"dev": "deno run -A main.ts",
"build": "deno desktop main.ts"
},
"imports": {
"@/": "./",
"@openelement/app": "npm:@openelement/app@^0.41.0-alpha.5",
"preact": "npm:preact@^10.29.1",
"@preact/signals": "npm:@preact/signals@^2.9.0"
},
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "preact",
"lib": ["dom", "dom.asynciterable", "dom.iterable", "deno.ns"]
},
"desktop": {
"backend": "webview",
"title": "openElement Desktop",
"width": 1024,
"height": 768
}
}
33 changes: 33 additions & 0 deletions examples/deno-desktop-spa/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Deno.serve((_req) => {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>openElement Desktop</title>
<script type="importmap">
{
"imports": {
"@openelement/app": "https://esm.sh/@openelement/app@0.41.0-alpha.5"
}
}
</script>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
button { padding: 0.5rem 1rem; font-size: 1rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f0f0f0; }
button:hover { background: #e0e0e0; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import { defineApp } from '@openelement/app';
const app = defineApp({ mode: 'spa' });
app.mount('#root');
</script>
</body>
</html>`;
return new Response(html, {
headers: { 'content-type': 'text/html' },
});
});
20 changes: 20 additions & 0 deletions examples/deno-desktop-spa/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useSignal } from '@preact/signals';

// Placeholder for the eventual Deno Desktop file-system route. The current
// alpha.5 smoke serves an inline browser route from main.ts because native
// browsers do not execute TSX modules without a bundling/transpile step.
export default function Home() {
const count = useSignal(0);
return (
<div>
<h1>openElement Desktop — Deno Desktop Proof</h1>
<p>
This page is rendered via <code>defineApp({'{'} mode: 'spa' {'}'})</code>{' '}
inside a Deno Desktop window.
</p>
<button type='button' onClick={() => count.value += 1}>
Count: {count}
</button>
</div>
);
}
8 changes: 4 additions & 4 deletions examples/open-element-in-fresh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ 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"`.
`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"`.
Loading
Loading