Skip to content

feat(alpha.5): SPA mode + client router + desktop proof#121

Open
SisyphusZheng wants to merge 48 commits into
mainfrom
dev
Open

feat(alpha.5): SPA mode + client router + desktop proof#121
SisyphusZheng wants to merge 48 commits into
mainfrom
dev

Conversation

@SisyphusZheng

@SisyphusZheng SisyphusZheng commented Jun 25, 2026

Copy link
Copy Markdown
Member

Changes

Implements #114 alpha.5 scope: SPA mode, client-side router, desktop proof, and Desktop Reader dogfood app.

Part 1: SPA Core (#115-#119)

Issue Description Tests
#115 Client Router Core — createRouter() in @openelement/router 15 ✅
#116 Route Lazy Loading — manifest generation in adapter-vite 16 ✅
#117 SPA Bootstrap — defineApp({ mode: 'spa' }) 12 ✅
#118 Client Data Layer — loader/action SPA adapt 7 ✅
#119 Deno Desktop Proof — examples/deno-desktop-spa/ fmt/lint/check ✅

93/93 SPA tests pass (50 new + 43 existing).

Part 2: Desktop Reader (#122-#127)

Slice Description Tests
S1 Shell scaffold — 16 files, Deno.serve + /api/* fmt/lint/check ✅
S2 Fixtures — 3 public domain books + PDFs
S3 Route registry — 5 DOM-returning routes 21/21 SPA
S4 Persistence — repo.ts (GitHub), search.ts (pdf-parse), storage.ts (localStorage) 15/15
S5 UX — keyboard shortcuts, theme, notes, Markdown export
S6 Smoke tests — export + routes + storage 27/27

Reader architecture:

  • GitHub repo as content backend (READER_REPO env var)
  • Sha-based incremental sync → ~/.open-reader/
  • Server-side pdf-parse → /api/search endpoint
  • Browser-native PDF via
  • No Preact/React — pure DOM DocumentFragment routes

Verification

  • deno fmt
  • deno lint
  • deno check
  • deno test packages/router/__tests__/ packages/app/__tests__/ ✅ 93/93
  • deno task desktop-reader:smoke ✅ 27/27

Bot Review

Awaiting opencode-review.

#115 Client Router Core
- createRouter() in @openelement/router/client-router
- history/hash/auto modes, named/optional params, guards, query parsing
- 15 tests

#116 Route Lazy Loading
- route-manifest.ts in @openelement/adapter-vite
- scan routes/ → generate lazy import() manifest
- 16 tests

#117 SPA Bootstrap
- defineApp({ mode: 'spa' }) in @openelement/app/spa
- mount/dispose lifecycle, router integration
- 12 tests

#118 Client Data Layer
- loader/action run client-side in SPA mode
- useLoaderData()/useActionData() work without server
- 7 tests

#119 Deno Desktop Proof
- examples/deno-desktop-spa/ — SPA + Deno.serve() + deno desktop
- fmt/lint/check pass
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 25, 2026

Copy link
Copy Markdown

Deploying openelement with  Cloudflare Pages  Cloudflare Pages

Latest commit: 80af290
Status: ✅  Deploy successful!
Preview URL: https://3393e844.lessjs.pages.dev
Branch Preview URL: https://dev.lessjs.pages.dev

View logs

Comment thread packages/app/__tests__/spa-data.test.ts Fixed
Comment thread packages/router/src/client-router.ts Fixed
Comment thread packages/app/__tests__/spa-data.test.ts Fixed
@github-actions

Copy link
Copy Markdown
Contributor

Here's my review:


CRITICAL

1. SPA hardcodes mode: 'history', bypassing auto detection

packages/app/src/spa.ts:162-165

router = createRouter({
  mode: 'history',   // ← always history, even for file://
  routes: options.routes ?? [],
});

The resolveMode('auto') logic in client-router.ts:35-41 (file:// → hash, http:// → history) is never exercised. For the Deno Desktop proof this happens to work because it serves via HTTP, but any file:// SPA will break. Either pass mode: 'auto' or expose a routerMode option on SpaAppOptions.

2. Duplicate popstate listener — double render on every navigation

packages/app/src/spa.ts:168-171 + spa.ts:204-205
createRouter already registers a popstate handler internally (client-router.ts:204-205). mount() then registers a second popstate handler (spa.ts:168-171). On every back/forward navigation, both fire: the router updates currentRoute, then the SPA handler calls renderRoute(). The test at spa-bootstrap.test.ts:253 expects removal of 2 listeners on dispose — confirming this duplication.

Fix: have the router accept an onChange callback, or inline the render call inside the router's handler.


MEDIUM

3. Unused ManifestEntry interface

packages/adapter-vite/src/route-manifest.ts:116-119
The interface is defined but never referenced anywhere — not in tests, not in the generator. Generated manifest output uses routeManifest constants, not ManifestEntry[]. Remove the dead type.

4. Deno Desktop example has dead code + browser import that won't resolve

examples/deno-desktop-spa/main.ts:3

const _app = defineApp({ mode: 'spa' });  // ← server-side, no DOM, does nothing

The _app variable is unused. The actual SPA runs via the inline <script type="module"> tag, but that inline script imports @openelement/app as a bare specifier without an import map in the HTML. Browsers can't resolve bare specifiers natively — the script won't execute.

5. Build task doesn't cover new module

packages/router/deno.json:27

"build": "deno check src/data-context.ts"

The new src/client-router.ts is not checked by the build task. Should be deno check src/data-context.ts src/client-router.ts or deno check src/.


LOW

6. Re-exporting deps "for testing convenience" is an anti-pattern

packages/adapter-vite/src/route-manifest.ts:197-201

export { scanRoutes };
export type { RouteEntry };

Tests should import these directly from @openelement/ssg / @openelement/protocol. Re-exporting pollutes the public API surface. The test file (route-manifest.test.ts) imports from ../src/route-manifest.ts for generateRouteManifestContent and writeRouteManifest but doesn't use these re-exports — they appear to be unused entirely.

7. Wasteful clearDataStack loop

packages/app/src/spa.ts:47-53

for (let i = 0; i < 20; i++) {
  __internal_popData();
}

The stack depth is always 1 after any render cycle (one pushLoaderData per renderRoute/handleFormSubmit). The magic number 20 is arbitrary — one pop would suffice. If the comment is concerned about leaks, expose __internal_stackDepth or use a while loop, but the current pattern is just pop(undefined) 19 times unnecessarily.

8. Inconsistent @std/assert import version

packages/router/__tests__/client-router.test.ts:7

import { assertEquals, assertExists, assertFalse } from 'jsr:@std/assert@1';

Other test files use jsr:@std/assert@^1.0.0 with the ^ semver range. This will pin to @1 exactly.


Summary

Priority Count Key issues
Critical 2 Hardcoded history mode, duplicate popstate listener
Medium 3 Dead type, broken example, incomplete build task
Low 3 Anti-pattern re-exports, wasteful loop, version inconsistency

The router core (client-router.ts) and the data-context integration are solid. The main problems are in the SPA glue layer (spa.ts) — the mode: 'history' hardcode and the duplicate popstate listener should be fixed before merge. The rest are cleanup-level.

New%20session%20-%202026-06-25T11%3A33%3A47.542Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Here's my review:


CRITICAL — none found

The previously-flagged critical issues (hardcoded mode: 'history', duplicate popstate listener) are already resolved. The current code correctly uses options.routerMode ?? 'auto' (spa.ts:162) and the onChange: renderRoute callback pattern avoids duplicate listeners.


BUG

1. replace(sep, ...) only replaces first path separator on Windows

packages/adapter-vite/src/route-manifest.ts:97-98

const parts = absSourcePath.replace(sep, posix.sep).split(posix.sep);
const fromParts = fromDir.replace(sep, posix.sep).split(posix.sep);

String.prototype.replace with a string argument substitutes only the first match. On Windows (sep = \), a path like C:\project\routes\index.tsx becomes C:/project\routes\index.tsx (backslash after routes is not converted). Use replaceAll instead:

const parts = absSourcePath.replaceAll(sep, posix.sep).split(posix.sep);
const fromParts = fromDir.replaceAll(sep, posix.sep).split(posix.sep);

2. parseQuery does not decode + as space (form-urlencoded)

packages/router/src/client-router.ts:91

result[decodeURIComponent(key)] = decodeURIComponent(val);

decodeURIComponent does not convert + to space. In application/x-www-form-urlencoded, + represents a space. A query like ?q=hello+world would retain the literal +. Standard fix: .replace(/\+/g, ' ') before decodeURIComponent.


MEDIUM

3. Deno Desktop example: routes/index.tsx is never imported

examples/deno-desktop-spa/routes/index.tsx — The component defines a Preact counter but the inline <script type="module"> in main.ts:23-27 calls defineApp({ mode: 'spa' }) with no routes, so this file is dead code. Either wire it into the router via an import map + component registration, or add a comment explaining it's a placeholder for file-system routing.

4. @openelement/router missing from @openelement/app dependencies

packages/app/deno.jsonspa.ts has runtime imports (createRouter, __internal_popData, etc.) from @openelement/router. The peerDependencies list only includes preact and preact-render-to-string. Users installing @openelement/app and importing defineApp (main entry re-exports it) will get a module resolution error unless @openelement/router is also installed. Add @openelement/router as a peer dependency.


LOW

5. void options.onChange?.() swallows render errors

packages/router/src/client-router.ts:162

void options.onChange?.();

If renderRoute() (the onChange callback) throws synchronously (or returns a rejected promise), the error is silently discarded. Wrap in a try/catch with console.error so render bugs are visible during development.

6. clearDataStack() name is misleading — pops once, not "all"

packages/app/src/spa.ts:49-51

/** Pop all remaining data from the stack (idempotent). */
function clearDataStack(): void {
    __internal_popData();
}

The JSDoc says "all" but the implementation pops one frame. The implementation comment ("one render cycle leaves at most one frame") correctly explains why this is sufficient, but the JSDoc should match. Suggested: /** Pop the last render cycle's data frame from the stack. */

7. Vite alias ordering in pipeline test is fragile but correct

packages/create/__tests__/cli.test.ts:415-424 — The specific aliases (@openelement/router/data-context, @openelement/router/client-router) come before the generic @openelement/router, which is correct. If someone reorders these, client-router would break silently (resolved to data-context.ts). Add a comment noting the ordering requirement.


SUMMARY

Priority Count Key issues
Bug 2 Windows sep replace bug, + not decoded in query strings
Medium 2 Dead routes file in example, missing peer dependency
Low 3 Swallowed render errors, misleading JSDoc, fragile alias order

The router core (client-router.ts) and data-context integration are solid. The onChange callback pattern correctly avoids the duplicate-listener bug that was in earlier versions. The test coverage is thorough (84/84 passing) with well-designed mock helpers. The formatError cause-chain enhancement in errors.ts is a nice defensive improvement.

The two bugs (replaceAll and + decoding) should be fixed before merge. The rest are cleanup-level.

New%20session%20-%202026-06-25T12%3A01%3A28.277Z
opencode session  |  github run

Comment thread packages/router/src/client-router.ts Fixed
@github-actions

Copy link
Copy Markdown
Contributor

Review complete. The two prior bot reviews already caught and fixed 12+ issues. The remaining concerns are the data stack race condition (acknowledged but unfixed), the replace guard redirect semantics mismatch, and minor build-config/test-coverage gaps.

New%20session%20-%202026-06-25T12%3A19%3A14.178Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Here's my review:


Review: feat(alpha.5) — SPA mode + client router + desktop proof

I reviewed all 24 changed files against the 3 prior bot review threads. The follow-up commits (77442ca, 7d0346a, fc44aa7) addressed all 12+ previously-flagged issues: the hardcoded mode: 'history', duplicate popstate listener, replacereplaceAll Windows bug, + decoding, unused imports, dead types, missing peer dep, build task coverage, and onChange error swallowing are all resolved.


BUG

None found. The clearDataStack() single-pop is correct__internal_pushActionData (data-context.ts:42-45) mutates the top frame's .actionData field rather than pushing a new frame, so the stack depth is always 1 after any render cycle.

MEDIUM

1. replace() guard redirect path is untested
client-router.ts:239-245 — when replace() encounters a guard that returns a string, it delegates to navigate(result). The navigate()+guard redirect test at client-router.test.ts:396-420 covers the equivalent path, but there's no test invoking router.replace() that triggers a guard redirect. Add a symmetric test confirming replace('/protected') with a guard returning '/login' calls pushState('/login') (not replaceState).

2. Deno.mkdir with empty catch swallows all errors
route-manifest.ts:138-142 — the catch {} block is intended to handle EEXIST, but it swallows permission errors, disk-full errors, etc. Suggested pattern:

try {
  await Deno.mkdir(outDir, { recursive: true });
} catch (e) {
  if (!(e instanceof Deno.errors.AlreadyExists)) throw e;
}

LOW

3. formatError cause chain test is shallow
core-helpers.test.ts:179-183 — tests only a single cause level. The cycle-detection seen Set (errors.ts:31-36) and non-Error cause cutoff are untested. Add a 3-level chain test and a simple cycle test (a.cause = b; b.cause = a).

4. relativeToOutput silently produces wrong paths for disjoint directory trees
route-manifest.ts:95-123 — if fromDir and absSourcePath share no common prefix (different drive letters on Windows), i=0, upCount = fromParts.length, producing ../../... that walks out of the project. Defensive: return the absolute path with file:// prefix or throw an explicit error.

5. Dead routes/index.tsx in the example
examples/deno-desktop-spa/routes/index.tsx — the file has a comment acknowledging it's a placeholder (lines 3-5), and the inline <script> in main.ts:24-26 calls defineApp with no routes. Acceptable for an alpha proof, but should be wired in before stable.


Summary

Priority Count Key items
Bug 0
Medium 2 Untested replace() guard redirect, error-swallowing mkdir catch
Low 3 Shallow formatError test, relativeToOutput cross-drive, dead example route

The router core (client-router.ts) is well-designed — the proxy-based null-prototype params record, safe setParam with proto-poisoning protection, and onChange callback pattern are clean. The SPA glue layer (spa.ts:149-195) correctly delegates navigation event ownership to the router. The test suite (84/84) is thorough with good mock helpers.

Verdict: merge-ready. The 2 medium items can be addressed in a follow-up without blocking this PR.

New%20session%20-%202026-06-25T12%3A25%3A04.029Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: SPA mode + client router

The three prior bot reviews already caught and fixed 12+ issues (hardcoded mode: 'history', duplicate popstate, replaceAll, + decoding, dead types, missing peer dep, error swallowing, etc.). The follow-up commits addressed them. Below are remaining issues in the current code (a2270d2).


BUG

1. navigate() guard redirects can infinitely recurse
packages/router/src/client-router.ts:219-234

When route A's guard returns '/B' and B's guard returns '/A', navigate recurses without bound. There's no depth counter, no visited-set, no cycle detection.

// Line 226-228
if (typeof result === 'string') {
  return navigate(result); // ← unbounded recursion, no depth guard
}

Fix: Add a maxRedirects = 10 counter or a _depth internal parameter that throws after exceeding the limit.

2. renderRoute stale-render race (acknowledged, unfixed)
packages/app/src/spa.ts:89-100

The ponytail comment at lines 86-88 acknowledges this: if a second navigation fires before the first runLoader() resolves, popData() at line 93 pops the wrong frame, and the stale loader eventually pushes a second frame. This can cause data leaks between routes.

// Line 93: pops data pushed by the NEXT render, not this render
__internal_popData();
// ...
const loaderData = await runLoader(); // ← async gap: another renderRoute may have started

Fix: Add a render-id counter — skip stale renders when the counter has changed.


HIGH / MEDIUM

3. replace() guard redirect pushes history instead of replacing
packages/router/src/client-router.ts:242-243

When router.replace('/protected') hits a guard that returns '/login', the redirect calls navigate(result), which calls pushState. The caller expected replaceState.

// Line 243
return navigate(result); // ← pushes history, should replace

Semantics: replace() means "don't add a history entry" — the redirect should honor that. The test at client-router.test.ts:471-493 explicitly expects pushState on the redirect, confirming this is current behavior, but it's wrong. navigate() can accept an optional flag.


LOW

4. Fragile route count via regex
packages/adapter-vite/src/route-manifest.ts:155-156

const matches = content.match(/=> import\(/g);
return matches ? matches.length : 0;

Counting routes by matching => import( in the generated string is fragile — a future change to the manifest format would silently return 0. Consider counting pageRoutes.length directly from the already-computed entries array.


Summary

Priority Count Key issues
Bug 2 Guard redirect infinite recursion, stale-render race
High 1 replace() guard redirect uses pushState
Low 1 Fragile route count via regex

The router core (client-router.ts) is well-designed — proxy-based null-prototype params, proto-poisoning protection, and the onChange callback pattern are clean. The test suite (84/84) is thorough. The two bugs above should be fixed before merge; the high item is a one-line fix; the low item is cleanup.

New%20session%20-%202026-06-25T12%3A32%3A55.020Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review

The follow-up commits addressed all 12+ issues from prior reviews — hardcoded mode, duplicate popstate, replaceAll, + decoding, missing peer dep, error swallowing, fragile alias order, shallow tests, cross-drive handling. The remaining concerns are below.


BUG

1. Guard redirect infinite recursionpackages/router/src/client-router.ts:227

if (typeof result === 'string') {
  return navigate(result); // ← no depth guard
}

If route A's guard returns '/B' and B's guard returns '/A', navigate recurses indefinitely → stack overflow. Add a maxRedirects counter or _depth internal parameter that throws after exceeding ~10 hops.

2. Deno.mkdir with recursive: true never throws AlreadyExistspackages/adapter-vite/src/route-manifest.ts:146-150

Per Deno docs, mkdir(path, { recursive: true }) silently succeeds when the directory exists. The catch (err) { if (!(err instanceof Deno.errors.AlreadyExists)) throw err; } handler is dead codeAlreadyExists is never thrown with recursive: true. Remove the try/catch entirely (permissions errors should propagate), or drop recursive: true and keep the catch if the intent is to handle the non-recursive case.


MEDIUM

3. replace() guard redirect uses pushStatepackages/router/src/client-router.ts:243

async function replace(path: string): Promise<void> {
  // ... guard check ...
  if (typeof result === 'string') {
    return navigate(result); // ← pushState, not replaceState
  }
  history.replaceState(...);
}

router.replace('/protected') with a guard redirecting to '/login' adds a history entry via navigate()'s pushState. The caller expects replace semantics. Fix: add a flag like navigate(result, { replace: true }) that uses replaceState instead of pushState. The existing test at client-router.test.ts:471-493 explicitly asserts pushState on this path — it will need updating.

4. Stale-render racepackages/app/src/spa.ts:86-95

The ponytail comment at lines 86-88 acknowledges: if renderRoute's await runLoader() is in-flight and a second navigation triggers another renderRoute, the second call pops the stack while the first hasn't pushed yet. The first call's stale loader result then pushes onto the wrong frame and renders against the wrong currentRoute. Add a render-id counter and skip stale renders when the counter has changed.


LOW

5. Fragile route count in generated manifestpackages/adapter-vite/src/route-manifest.ts:155

const matches = content.match(/=> import\(/g);
return matches ? matches.length : 0;

Counting => import( in the generated string is fragile — a future format change silently returns 0. Count pageRoutes.length directly: you already compute it in generateRouteManifestContent (the entries array). Have that function return both the content and the count, or return entries.length from writeRouteManifest.


Summary

Priority Count Key issues
Bug 2 Guard redirect infinite recursion, dead AlreadyExists catch
Medium 2 replace() guard uses pushState, stale-render race
Low 1 Fragile route count via regex

The router core (client-router.ts) is well-designed — proxy-based null-prototype params, setParam proto-poisoning protection, and the onChange callback pattern are clean. Test coverage is thorough (84/84). The two bugs should be fixed; mediums can follow up.

New%20session%20-%202026-06-25T12%3A47%3A39.495Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

I reviewed the 24 changed files against all 4 prior bot review threads. The follow-up commits addressed 12+ previously-flagged issues: hardcoded mode: 'history', duplicate popstate listener, replaceAll Windows bug, + decoding, Deno.mkdir error swallowing, onChange error swallowing, unused assertExists import, missing peer dep, and build task coverage are all resolved.


BUG

1. Guard redirect infinite recursion — no depth counter
packages/router/src/client-router.ts:227

if (typeof result === 'string') {
  return navigate(result); // ← unbounded recursion, no depth guard
}

If guard A returns '/B' and guard B returns '/A', the call stack recurses until overflow. Add a _depth internal parameter or a maxRedirects=10 counter.

2. Stale-render race (acknowledged, unfixed)
packages/app/src/spa.ts:89-100

__internal_popData();                    // line 93: may pop the wrong frame
const loaderData = await runLoader();    // line 96: async gap — another renderRoute may have started
__internal_pushLoaderData(loaderData);   // line 97: pushes a second frame, data leak between routes

If a second navigation fires before the first runLoader() resolves, popData on line 93 pops the newer frame, and the stale push on line 97 leaves a leaked frame. The ponytail comment on lines 86-88 acknowledges this. Fix with a render-id counter that skips stale renders.

HIGH

3. replace() guard redirect uses pushState instead of replaceState
packages/router/src/client-router.ts:242-243

if (typeof result === 'string') {
  return navigate(result); // ← calls pushState, should be replaceState
}

router.replace('/protected') triggers a guard redirect to '/login'. navigate() calls pushState, adding a history entry the caller explicitly asked to replace. The test at client-router.test.ts:471-493 confirms this behavior (expecting pushState). Either fix the semantics or rename the test to acknowledge the deviation.

MEDIUM

4. Fragile route count via regex
packages/adapter-vite/src/route-manifest.ts:155-156

const matches = content.match(/=> import\(/g);
return matches ? matches.length : 0;

The entries array is already computed on line 67. A future format change to the generated manifest would silently return 0. Use entries.length directly:

return entries.length;

5. Deno Desktop example: dead routes/index.tsx
examples/deno-desktop-spa/routes/index.tsx — The file has a comment acknowledging it's a placeholder, and the inline <script> in main.ts:24-26 calls defineApp with no routes. Acceptable for alpha proof, should be wired in before stable.

LOW

6. clearDataStack() JSDoc misleading
packages/app/src/spa.ts:48-52 — The JSDoc says "Pop the last render cycle's data frame from the stack (idempotent)" — correct now, but idempotent is wrong since a second call on empty stack returns undefined (from pop() on empty array). The implementation is correct; just the word "idempotent" is inaccurate.


Summary

Priority Count Key issues
Bug 2 Guard redirect infinite recursion, stale-render race
High 1 replace() guard redirect uses pushState
Medium 2 Fragile regex route count, dead example route
Low 1 Misleading JSDoc

What's good: The router core (client-router.ts) is well-designed — proxy-based null-prototype params, proto-poisoning protection, and the onChange callback pattern cleanly avoid the earlier duplicate-listener bug. The test suite (84/84) is thorough with well-designed mock helpers. The formatError cause-chain enhancement with cycle detection in errors.ts is well-tested. The two bugs above should be fixed before merge; the high item is a one-line fix; the rest are cleanup.

New%20session%20-%202026-06-25T12%3A52%3A40.192Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

I reviewed all 26 changed files against the 7 prior bot review threads. The follow-up commits resolved all previously-flagged issues: hardcoded mode: 'history', duplicate popstate listener, replaceAll Windows bug, + decoding, Deno.mkdir error swallowing, missing peer dep, dead AlreadyExists catch, fragile route count regex, onChange error swallowing, build task coverage, formatError shallow tests, and relativeToOutput cross-drive handling. The CodeQL "Remote property injection" flag is a false positive — isSafeParamName() blocks __proto__/prototype/constructor, storage is in a Map, and exposure is via a Proxy on Object.create(null).


BUG

1. notifyChange() outer try/catch is dead code
packages/router/src/client-router.ts:215-216

void Promise.resolve(options.onChange?.()).catch((err) => {
  console.error('[router] onChange failed:', err);
});
} catch (err) {
  console.error('[router] onChange failed:', err); // ← never reached
}

Promise.resolve() wraps synchronous throws into a rejected promise, which the .catch() handler already processes. The outer try/catch will never catch anything — it's unreachable dead code. Remove the outer try/catch block.


MEDIUM

2. Desktop example renders an empty DOM — no visible SPA behavior
examples/deno-desktop-spa/main.ts:24-26

const app = defineApp({ mode: 'spa' });
app.mount('#root');

No routes are passed, so renderRoute() matches no route, renderComponent() finds router.currentRoute === null, and the <div id="root"> stays empty. The proof demonstrates infrastructure (mount/dispose cycle, createRouter, listener wiring) but doesn't visibly render anything. For an alpha.5 proof this is acceptable, but routes/index.tsx (a Preact counter component using useSignal) is dead code — it's never imported. Either wire it in or drop it.

3. route-manifest.ts is not type-checked by the build task
packages/adapter-vite/deno.json:27

"build": "deno check src/index.ts"

src/index.ts does not import route-manifest.ts (it's only reachable via the ./route-manifest export path). The file is covered by tests (which import it directly and thus type-check it), but a bare deno task build won't catch type errors in it. Consider adding src/route-manifest.ts or changing to deno check src/.


LOW

4. Stale comment: "ponytail: detect file:// protocol" now uses resolveMode('auto')
packages/router/src/client-router.ts:42

The comment says "ponytail: detect file:// protocol" — this is no longer a ponytail; resolveMode is the canonical resolution path and supports history, hash, and auto. Remove the // ponytail prefix.

5. Router build task uses positional args — fragile if files are reordered
packages/router/deno.json:27

"build": "deno check src/data-context.ts src/client-router.ts"

Positional arguments are order-sensitive for readability but not correctness. Consider deno check src/ for robustness.


What's good

  • Router core (client-router.ts) is clean: proxy-based null-prototype params record, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation calls.
  • SPA glue (spa.ts) correctly fixes the stale-render race with a renderId counter (spa.ts:89-96), delegates all navigation event ownership to the router, and properly disposes.
  • Test suite (84/84 in PR description) is thorough with well-designed mock helpers — the stale-render test (spa-data.test.ts:294-330), guard redirect cycle test (client-router.test.ts:422-439), and replace guard semantics test (client-router.test.ts:491-513) all pass with correct behavior.
  • formatError cause-chain in errors.ts:26-37 is well-tested with 4 tests covering depth, cycles, and non-Error causes.

Summary

Priority Count Key items
Bug 1 Dead outer try/catch in notifyChange()
Medium 2 Empty DOM in desktop example, route-manifest.ts not in build check
Low 2 Stale ponytail comment, fragile build args

All previously-flagged critical/high issues are resolved. This is merge-ready — the one bug is a dead-code cleanup that doesn't affect behavior. The router and SPA bootstrap code are production-quality.

New%20session%20-%202026-06-25T13%3A04%3A34.099Z
opencode session  |  github run

@SisyphusZheng

Copy link
Copy Markdown
Member Author

Bot review triage

Severity Issue Resolution
🔴 Bug Dead outer try/catch in notifyChange() ❌ Bot wrong — outer try/catch catches synchronous throws from onChange() during argument evaluation. Promise.resolve().catch() only handles async rejections; sync throws would crash the router. Restored with explanatory comment. Test confirms: onChange errors are reported and do not stop router state update.
🟡 Medium Desktop example renders empty DOM ✅ Fixed — added ponytail comment noting routes available but not yet wired
🟡 Medium route-manifest.ts not in build check ✅ Fixed — changed to deno check src/
🟢 Low Stale ponytail comment ✅ Fixed
🟢 Low Fragile build args ✅ Fixed — deno check src/

93/93 tests pass. Awaiting final review.

@github-actions

Copy link
Copy Markdown
Contributor

Now I have a thorough understanding of the code. Let me provide my final review.


Review: feat(alpha.5) — SPA mode + client router + desktop proof

I reviewed all 26 changed files against the 7 prior bot review threads. The follow-up commits (77442ca, 7d0346a, fc44aa7, a2270d2, 976bdb1, 39cbf7c, 9969c19) resolved all 14+ previously-flagged issues: hardcoded mode: 'history', duplicate popstate, replaceAll Windows bug, + query decoding, unused imports/dead types, missing peer dep, Deno.mkdir error swallowing, onChange error swallowing, build task coverage, formatError shallow tests, relativeToOutput cross-drive handling, fragile route count regex, guard redirect cycles (MAX_GUARD_REDIRECTS = 10), replace() guard redirect semantics, stale-render race (renderId counter), CodeQL assertExists, and stale ponytail comments.


CRITICAL — none found

All previously-flagged critical/high issues are resolved in the current code (9969c19).


BUG — none found

The notifyChange() outer try/catch (client-router.ts:211-220) is not dead code as a prior review claimed. Promise.resolve(expr) evaluates expr first — if options.onChange?.() throws synchronously (which the test at client-router.test.ts:351-353 confirms it can), that throw propagates before Promise.resolve() is reached. The outer catch handles sync throws; .catch() handles async rejections. Both are required. The test at line 335-364 validates this.


LOW

1. handleFormSubmit doesn't participate in renderId stale-guardspa.ts:118-148

async function handleFormSubmit(event: Event): Promise<void> {
    // ...
    const loaderData = await runLoader();   // line 141 — async gap
    __internal_pushLoaderData(loaderData); // line 144
    __internal_pushActionData(actionData); // line 145
    renderComponent();                      // line 147 — no renderId check
}

If a navigation fires mid-flight during the action's await, renderComponent() renders against stale router.currentRoute and pushes data onto the wrong frame. The main renderRoute() path correctly uses renderId (line 89-96) but handleFormSubmit bypasses it. Low-priority for alpha.5 since form submissions rarely coincide with navigation.

2. spa-data.test.ts mock helpers are duplicated from spa-bootstrap.test.ts — both files define nearly identical MockHistory, mockLocation, mockEvents, etc. A shared _test-utils.ts module would reduce ~100 LOC of duplication. Not blocking.

3. examples/deno-desktop-spa/routes/index.tsx is dead code — The file acknowledges this in a comment (lines 3-5), and main.ts:26 notes it's unwired proof infrastructure. Acceptable for alpha.5.

4. packages/app/deno.json:28 build task — Still deno check src/index.ts (unlike the router package which was updated to deno check src/). spa.ts IS checked via index.ts's import, but i18n-plugin.ts and preact.ts are not. Pre-existing; deno check src/ would be more robust.


What's good

  • Router core (client-router.ts): proxy-based null-prototype params record, isSafeParamName proto-poisoning protection, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation calls with options.replace flag. Clean separation of rematch()/notifyChange().
  • SPA glue (spa.ts): renderId counter correctly guards against stale-loader races (spa.ts:89-96), onChange: renderRoute callback avoids the earlier duplicate-listener bug, duck-type checks (isRenderableNode, isFormElement) work without global HTMLElement/Node in test environments. Clean mount/dispose lifecycle.
  • Route manifest (route-manifest.ts): cross-drive Windows detection (lines 114-120), replaceAll for proper path separator conversion, entries.length direct count instead of fragile regex.
  • formatError cause chain (errors.ts:26-37): cycle detection via seen Set, non-Error cause cutoff, 4 well-designed tests.
  • Test suite (84/84): thorough mock helpers, covers guard redirect cycles, replace semantics, stale-render guard, proto-poisoning, query + decoding, auto mode detection, dispose idempotency, and edge cases.

Summary

Priority Count Key items
Bug 0
Low 4 No renderId guard in handleFormSubmit, duplicated test mocks, dead example route, app build task coverage

Verdict: merge-ready. All 14+ previously-flagged issues across 7 review rounds are resolved. The router and SPA bootstrap code is production-quality for alpha.5. The 4 low items are cleanup-level that can be addressed in follow-up PRs.

New%20session%20-%202026-06-25T13%3A09%3A02.244Z
opencode session  |  github run

S1: Shell scaffold — deno.json, main.ts (Deno.serve HTTP + /api/*), CSS theming
S2: Fixtures — 3 public domain books + placeholder PDFs
S3: Route registry — 5 routes (bookshelf, reading, notes, search, settings)
S4: Persistence — repo.ts (GitHub pull + sha cache), search.ts (pdf-parse), storage.ts (localStorage CRUD)
S5: Reader UX — keyboard shortcuts, theme controls, note creation, Markdown export
S6: Smoke tests — 27/27 (6 export + 6 routes + 15 storage)

Architecture:
- GitHub repo as content backend (READER_REPO env var, default open-element/reader-fixtures)
- Sha-based incremental sync to ~/.open-reader/
- Server-side pdf-parse text extraction → /api/search endpoint
- Browser-native PDF rendering via <embed>
- No Preact/React — pure DOM DocumentFragment routes
@github-actions

Copy link
Copy Markdown
Contributor

Now I have a complete picture of all the code. Here is my review:


Review: feat(alpha.5) — SPA mode + client router + desktop proof

The 15+ prior bot review rounds have been thorough. All 14+ previously-flagged critical/high issues (hardcoded mode, duplicate popstate, replaceAll, + query decoding, guard redirect cycles, stale-render renderId guard, replace semantics, error swallowing, preventDefault ordering, component resolution, hardcoded title/lang, etc.) are resolved in the current code. The router core and SPA glue are production-quality.

Below are the remaining issues I found independently:


BUG

1. ensureSearchIndex() race — first search returns empty, subsequent searches skip indexing entirely

examples/deno-desktop-reader/main.ts:108-123, 138-146

// line 109-110: marked "ready" BEFORE any async work
if (searchIndexReady) return;
searchIndexReady = true;

// line 141: fire-and-forget
ensureSearchIndex();
return json(search(q, CACHE_DIR)); // reads on-disk index before indexing completes

searchIndexReady is set to true on line 110 before any indexing. On the first search request, search() reads the on-disk index immediately (empty or stale) while the background indexing hasn't completed. On subsequent requests, searchIndexReady is already true, so ensureSearchIndex returns immediately — indexing never successfully runs. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after indexing completes.


MEDIUM

2. saveSearchIndex uses sync I/O inside async function, blocking server event loop

examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  const data = new Uint8Array(await Deno.readFile(pdfPath));
  const parsed = await pdfParse(data);
  index[bookId] = parsed.text;
  // ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

For large PDFs, writeTextFileSync can block the server for several milliseconds. Use await Deno.writeTextFile instead.

3. syncBooks() exported but never called — dead code

examples/deno-desktop-reader/app/repo.ts:90-122

Fully implemented public function with SHA-based cache logic, but main.ts never imports or invokes it. Either wire it in main.ts or add a ponytail comment explaining it's for a future slice.

4. SPA shell references client-entry.js which is never generated

packages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this at main.ts:178-188. Acknowledged alpha.5 wiring gap.


LOW

5. Route manifest JSDoc path mismatch

packages/adapter-vite/src/route-manifest.ts:12

// In your client entry:
// import { routeManifest } from '../.openelement/route-manifest';

But writeRouteManifest writes to join(outDir, 'route-manifest.ts') — e.g., dist/route-manifest.ts. The JSDoc path doesn't match the actual output location.

6. ~100 LOC duplicated test mock infrastructure

spa-bootstrap.test.ts (lines 19-80) and spa-data.test.ts (lines 11-80) define nearly identical MockHistory, StubElement, mockLocation, mockEvents, mockAddEventListener, mockRemoveEventListener, etc. A shared _test-utils.ts file would eliminate this duplication.

7. ensureSearchIndex() silently swallows all errors

examples/deno-desktop-reader/main.ts:113-122

try {
  const books = JSON.parse(raw);
  for (const book of books) {
    try {
      await indexBook(path, book.id, CACHE_DIR);
    } catch { /* skip */ }        // per-book errors silently swallowed
  }
} catch { /* skip */ }           // top-level JSON parse errors silently swallowed

If books.json is malformed JSON, or pdf-parse fails on every book, the function silently returns with searchIndexReady = true and the search endpoint will always return empty results with no indication of failure.


What's good

  • Router core (client-router.ts): Proxy-based null-prototype params, isSafeParamName proto-poisoning protection, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId counter guards stale-loader races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, onChange: renderRoute avoids duplicate listeners, duck-type checks work without browser globals, clean mount/dispose lifecycle.
  • Route manifest (route-manifest.ts): Cross-drive Windows detection, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts): Cycle-safe via seen Set, non-Error cutoff, well-tested.
  • Test suite: 84/84 SPA + 27/27 reader smoke, thorough edge-case coverage.
Priority Count Key items
Bug 1 ensureSearchIndex race — first search empty, subsequent skip indexing
Medium 3 Sync I/O blocks event loop, dead syncBooks export, client-entry.js wiring gap
Low 3 JSDoc path mismatch, test mock duplication, empty catch blocks in search index

Verdict: Merge-ready. The 1 bug is in example/dogfood code, not the framework. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A04%3A01.970Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

The 15+ prior bot review rounds and follow-up commits resolved all critical/high issues. The router core, SPA glue, and route manifest generator are production-quality. Below are the remaining issues in the current code (9969c19 range).


BUG

1. ensureSearchIndex() race — first search returns emptyexamples/deno-desktop-reader/main.ts:109-110, 141

searchIndexReady = true;      // line 110 — set BEFORE any async indexing
ensureSearchIndex();           // line 141 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // reads on-disk index immediately

searchIndexReady is set to true on line 110 before await indexBook() on line 120 runs. On first search, search() reads the empty on-disk index before indexing finishes. On subsequent requests, searchIndexReady is already true, so indexing is permanently skipped. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after indexing completes.

2. Sync I/O blocks event loop in indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks server
}

indexBook is async but calls Deno.writeTextFileSync. Use await Deno.writeTextFile for non-blocking I/O.


MEDIUM

3. syncBooks() exported but never calledexamples/deno-desktop-reader/app/repo.ts:90

Fully implemented with sha-based cache + GitHub sync logic but main.ts never imports or invokes it. Dead code for the alpha.5 proof.

4. ${base}client-entry.js never generated by the adapter buildpackages/adapter-vite/src/build.ts:92

The adapter writes index.html directly in closeBundle, but no build step produces client-entry.js. The desktop reader works around this via runtime injection (reader/main.ts:178-188). Acknowledged alpha.5 wiring gap.


LOW

5. ~100 LOC duplicated test mocksspa-bootstrap.test.ts:19-60 and spa-data.test.ts:11-60

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, and firePopstate. Extract to a shared _test-utils.ts.

6. JSDoc path mismatchroute-manifest.ts:12

Shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g. dist/route-manifest.ts).


What's good

  • Router core (client-router.ts:302): proxy null-prototype params, isSafeParamName proto-poisoning, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts:228): renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles lazy { default } modules, preventDefault after action check, duck-type checks work without browser globals.
  • Route manifest (route-manifest.ts:167): cross-drive Windows detection, replaceAll paths, direct entries.length count.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set.
  • All three packages use deno check src/.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 2 ensureSearchIndex race, sync I/O in indexBook
Medium 2 Dead syncBooks export, client-entry.js not wired
Low 2 Test mock duplication, JSDoc path mismatch

Verdict: merge-ready. The 2 bugs are in example/dogfood code, not the framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A05%3A35.780Z
opencode session  |  github run

Root cause: @openelement/core JSX returns VNodes (virtual DOM),
but renderComponent checked isRenderableNode (needs nodeType).
All route output was silently discarded — pure white page.

Fix: import renderToDom from @openelement/core/csr
and convert VNode→DOM before appending to root element.
Comment thread packages/app/src/spa.ts
let renderId = 0;

/** Duck-type check since `Node` may not exist in test environments (e.g. Deno). */
function isRenderableNode(value: unknown): value is Node {
@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

Reviewed all 26 changed files independently. The 15+ prior bot review rounds resolved 14+ critical/high/bug issues — all previously-flagged items (hardcoded mode, duplicate popstate, replaceAll, + decoding, guard redirect cycles, stale-render renderId, replace semantics, error swallowing, unused/duplicate imports, build task scope, hardcoded shell title, preventDefault ordering, component resolution, etc.) are fixed. The router core and SPA glue are production-quality.


BUG

1. ensureSearchIndex() race — first search returns empty, subsequent skip indexingexamples/deno-desktop-reader/main.ts:108-123, 141

searchIndexReady = true;      // line 110 — set BEFORE async indexing
ensureSearchIndex();           // line 141 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // runs before indexing completes

searchIndexReady is set to true on line 110 before await indexBook() on line 119 runs. On the first search request, search() reads the empty on-disk index before background indexing finishes — returns empty results. On subsequent requests, searchIndexReady is already true, so indexing never actually runs. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after indexing completes.


MEDIUM

2. ${base}client-entry.js never generated by adapter buildpackages/adapter-vite/src/build.ts:92

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this via runtime injection (reader/main.ts:186-188). Acknowledged alpha.5 wiring gap — the router works standalone, but the adapter build output isn't wired end-to-end.

3. Sync I/O blocks event loop inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks server
}

saveSearchIndex (line 98) calls Deno.writeTextFileSync. For large PDFs this can block the server for multiple milliseconds. Use await Deno.writeTextFile instead.


LOW

4. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-122

Complete public function with sha-based cache + GitHub sync logic, but main.ts never imports or calls it. Either wire it in or add a ponytail comment for the future slice.

5. ~100 LOC duplicated test mocksspa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-80

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, and firePopstate. Extract to a shared _test-utils.ts.

6. Dead example routeexamples/deno-desktop-spa/routes/index.tsx — acknowledged placeholder (comment lines 3-5). Acceptable for alpha.5.


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning protection, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId counter guards stale-loader races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, duck-type checks work without browser globals.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set, non-Error cutoff.
  • All three packages use deno check src/.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 1 ensureSearchIndex race — first search empty
Medium 2 client-entry.js not wired, sync I/O in indexBook
Low 3 Dead syncBooks, test mock duplication, dead example route

Verdict: merge-ready. The 1 bug is in example/dogfood code, not the framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A15%3A51.240Z
opencode session  |  github run

Comment thread packages/app/src/spa.ts
let renderId = 0;

/** Duck-type check since `Node` may not exist in test environments (e.g. Deno). */
function isRenderableNode(value: unknown): value is Node {
…ird-party CE

Each route = OpenElement subclass with render():
- BookshelfPage, ReadingPage, NotesPage, SearchPage, SettingsPage, WcInteropPage
- customElements.define() in each module
- @openelement/ui: open-button, open-card, open-input throughout
- Third-party CE: sl-button, sl-card, mwc-checkbox, mwc-button
@github-actions

Copy link
Copy Markdown
Contributor

Now I have a thorough understanding of all the key changed files. Here's my independent review:


Review: feat(alpha.5) — SPA mode + client router + desktop proof

The prior bot review rounds and follow-up commits resolved 14+ issues. The core router (client-router.ts), SPA glue (spa.ts), and route manifest generator (route-manifest.ts) are production-quality for alpha.5. Below are remaining issues I found.


BUG

1. isRenderableNode is dead code — packages/app/src/spa.ts:48-50

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Defined but never called anywhere in the file (confirmed by grep). The render path at lines 92-96 now uses resolveComponentResult + renderToDom instead, making this duck-type check dead. CodeQL flagged it correctly. Remove lines 47-50 (the comment and function).

2. ensureSearchIndex() race — first search returns empty, subsequent searches skip indexing — examples/deno-desktop-reader/main.ts:108-123, 141

// line 109-110: marked "ready" BEFORE any async indexing
if (searchIndexReady) return;
searchIndexReady = true;
// ... lines 113-122: await indexBook() — actual indexing ...

// line 141: fire-and-forget, NOT awaited
ensureSearchIndex();
return json(search(q, CACHE_DIR)); // reads on-disk index immediately

searchIndexReady is set to true on line 110 before await indexBook() on line 119 runs. On the first search request, search() reads the empty on-disk index before background indexing finishes → empty results. On subsequent requests, searchIndexReady is already true, so indexing never actually completes. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after indexing completes.


MEDIUM

3. saveSearchIndex uses sync I/O inside async function — examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

For large PDFs, writeTextFileSync can block the server for milliseconds. Use await Deno.writeTextFile(dir + '/search-index.json', JSON.stringify(index, null, 2)) instead.

4. client-entry.js referenced but never generated — packages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

No build step produces this file (the adapter bypasses Vite's HTML pipeline). The desktop reader works around it via runtime injection at main.ts:186-188. Acknowledged alpha.5 wiring gap.


LOW

5. ~100 LOC duplicated test mocks — spa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-80

Both define near-identical MockHistory, mockLocation, mockEvents, StubElement, mockDocument, resetMocks, and firePopstate. Extract to shared _test-utils.ts.

6. ensureSearchIndex() empty catch blocks swallow all errors — main.ts:120, 122

try {
  await indexBook(path, book.id, CACHE_DIR);
} catch { /* skip */ }        // per-book errors silently lost
// ...
} catch { /* skip */ }         // top-level JSON parse errors silently lost

If books.json is malformed or pdf-parse fails on every book, the function silently returns with searchIndexReady = true and the search endpoint is permanently broken with no diagnostic output. At minimum, console.warn the failures.

7. Dead example route — examples/deno-desktop-spa/routes/index.tsx

Acknowledged placeholder (comments at lines 3-5 and main.ts:26-27). Acceptable for alpha.5 proof.


What's good

  • Router core: proxy-based null-prototype params, isSafeParamName proto-poisoning protection, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue: renderId counter guards stale-loader races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, duck-type checks (isFormElement) work without browser globals.
  • Route manifest: cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain: cycle-safe via seen Set, non-Error cutoff.
  • All three packages use deno check src/.
  • 93/93 SPA + 27/27 reader smoke tests pass.

Priority Count Key items
Bug 2 isRenderableNode dead code, ensureSearchIndex race
Medium 2 Sync I/O in indexBook, client-entry.js not wired
Low 3 Test mock duplication, silent error swallowing, dead example route

Verdict: merge-ready after removing the dead isRenderableNode function. The ensureSearchIndex race is in example/dogfood code, not framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A26%3A42.520Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

The prior bot rounds resolved 14+ critical/high issues. The router core, SPA bootstrap, and route manifest generator are production-quality. Below are remaining issues in the current code (1b4b201).


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:48-50

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Never called anywhere. The render path uses resolveComponentResult + renderToDom instead. CodeQL flagged this. Remove lines 47-50.

2. ensureSearchIndex() race — first search returns emptyexamples/deno-desktop-reader/main.ts:108-123, 141

searchIndexReady = true;      // line 110 — set BEFORE async indexing
// ... async indexBook() ...
ensureSearchIndex();           // line 141 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // reads on-disk index immediately

searchIndexReady = true before indexing completes → search() reads empty on-disk index. Subsequent requests skip indexing because the flag is already true. Fix: await ensureSearchIndex() before search(), move searchIndexReady = true to after indexing.


MEDIUM

3. saveSearchIndex uses sync I/O in async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

Use await Deno.writeTextFile for non-blocking I/O.

4. syncBooks() dead codeexamples/deno-desktop-reader/app/repo.ts:90-122
Fully implemented with sha-based cache + GitHub sync but never imported/called from main.ts. Either wire it up or add a ponytail comment.

5. ${base}client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

No build step produces this file. The desktop reader works around it via runtime injection (main.ts:186-188). Standalone SPA builds would 404. Acknowledged alpha.5 wiring gap.


LOW

6. ensureSearchIndex() empty catch blocksmain.ts:120, 122
try { await indexBook(...) } catch { /* skip */ } — all indexing errors silently swallowed. Malformed books.json or pdf-parse failures leave search permanently broken with zero diagnostics. At minimum console.warn the failures.

7. findAppScript() called on every SPA page loadreader/main.ts:182
Uses Deno.readDirSync (blocking) per request. Cache the result since the filename won't change between requests.

8. Route manifest JSDoc path mismatchroute-manifest.ts:12
JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g. dist/route-manifest.ts).

9. ~100 LOC duplicated test mocksspa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-80
Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, etc. Extract to shared _test-utils.ts.


What's good

  • Router core: proxy null-prototype params, isSafeParamName proto-poisoning, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, correct dual sync/async error handling in notifyChange().
  • SPA glue: renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } modules, preventDefault after action check, duck-type checks work without browser globals.
  • Route manifest: cross-drive Windows detection, replaceAll, direct entries.length.
  • formatError: cycle-safe via seen Set.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
Priority Count Key items
Bug 2 Dead isRenderableNode, ensureSearchIndex race
Medium 3 Sync I/O in indexBook, dead syncBooks, client-entry.js wiring gap
Low 4 Empty catches, uncached findAppScript, JSDoc mismatch, test mock duplication

Verdict: merge-ready. The 2 bugs are small fixes (remove dead function + fix race in example code). The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A29%3A04.839Z
opencode session  |  github run

DevBot added 2 commits June 26, 2026 08:36
spa.ts renderComponent: detect route.tagName → createElement + set loader data
reader.tsx: restore defineApp({ mode: 'spa' }) (was bypassed by hand-rolled bootstrap)
All 6 routes = OpenElement subclasses with proper shadow DOM
@openelement/ui: open-button/open-card/open-input throughout
Third-party CE: Shoelace + MWC via CDN
@github-actions

Copy link
Copy Markdown
Contributor

Independent Review: feat(alpha.5) — SPA mode + client router + desktop proof

I read all 6 core changed files independently. The 15+ prior bot reviews and follow-up commits resolved all previously-flagged critical/high issues — hardcoded mode, duplicate popstate, replaceAll, + decoding, guard redirect cycles, stale-render renderId guard, replace semantics, error swallowing, unused imports, duplicate imports, build task scope, hardcoded shell title/lang, preventDefault ordering, component resolution, and options shadowing are all fixed. Below are the remaining issues in the current code.


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:48-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Defined but never called anywhere in the file (or codebase). The render path uses resolveComponentResult + renderToDom (line 107-110) and the OpenElement path uses document.createElement + appendChild (line 96-103). CodeQL flagged this correctly. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns empty, subsequent skip indexingexamples/deno-desktop-reader/main.ts:109-110, 141

searchIndexReady = true;      // line 110 — set BEFORE any async indexing
// ... await indexBook(...) ... // lines 115-120 — actual indexing

// line 141: fire-and-forget, NOT awaited
ensureSearchIndex();
return json(search(q, CACHE_DIR)); // reads on-disk index before indexing completes

searchIndexReady is set to true on line 110 before await indexBook() on line 119 runs. On the first search request, search() reads the empty on-disk index before background indexing finishes → empty results. On subsequent requests, searchIndexReady is already true, so indexing never completes. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after indexing completes.


MEDIUM

3. saveSearchIndex uses sync I/O inside async function, blocking server event loopexamples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  const data = new Uint8Array(await Deno.readFile(pdfPath));
  const parsed = await pdfParse(data);
  index[bookId] = parsed.text;
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

For large PDFs, writeTextFileSync blocks the server for tens to hundreds of milliseconds. Use await Deno.writeTextFile(indexFile, JSON.stringify(index, null, 2)) instead.

4. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-123

Complete public function with sha-based cache + GitHub sync logic, but main.ts never imports or calls it. Either wire it into main.ts on startup or add a comment noting it's for a future slice.

5. ${base}client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle (bypassing Vite's HTML pipeline). No build step produces client-entry.js. The desktop reader works around this via runtime HTML injection at reader/main.ts:186-193. Acknowledged alpha.5 wiring gap — the router works standalone, but the adapter build output isn't wired end-to-end yet.


LOW

6. ~100 LOC duplicated test mock infrastructurespa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-60

Both define near-identical MockHistory, mockLocation, mockHistory, mockEvents, mockAddEventListener, mockRemoveEventListener, StubElement, mockDocument, resetMocks, and firePopstate. Extract to a shared packages/app/__tests__/_test-utils.ts.

7. ensureSearchIndex() empty catch blocks silently swallow all errorsreader/main.ts:120, 122

try { await indexBook(...) } catch { /* skip */ }  // per-book errors lost
} catch { /* skip */ }                              // JSON parse errors lost

If books.json is malformed or pdf-parse fails on every book, search is permanently broken with zero diagnostics. At minimum, console.warn the failures.

8. findAppScript() uses blocking Deno.readDirSync per requestreader/main.ts:83-94

Called on every SPA page load. Cache the result since the filename won't change between requests.

9. Dead example routeexamples/deno-desktop-spa/routes/index.tsx

Acknowledged placeholder (comments at lines 3-5). Acceptable for alpha.5 proof.


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning protection, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling.
  • SPA glue (spa.ts): renderId counter guards stale-loader races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, OpenElement tagName support.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts): cycle-safe via seen Set, non-Error cutoff.
  • All three packages use deno check src/.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 2 Dead isRenderableNode, ensureSearchIndex race
Medium 3 Sync I/O in indexBook, dead syncBooks export, client-entry.js wiring gap
Low 4 Test mock duplication, silent error swallowing, uncached findAppScript, dead example route

Verdict: merge-ready. The 2 bugs are in dead code (remove 4 lines) and example/dogfood code (race condition in reader), not the framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A39%3A29.283Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

I've independently reviewed all key changed files. Here is my consolidated review:


Review: feat(alpha.5) — SPA mode + client router + desktop proof

The 15+ prior bot review rounds and follow-up commits resolved all previously-flagged critical/high issues (hardcoded mode, duplicate popstate, replaceAll, + decoding, guard redirect cycles, stale-render renderId, replace semantics, error swallowing, unused/duplicate imports, build task scope, hardcoded shell title, preventDefault ordering, component resolution). The router core and SPA glue are production-quality. Below are remaining issues.


BUG

1. isRenderableNode is dead code — packages/app/src/spa.ts:48-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Defined but has zero call sites anywhere in the codebase (confirmed by grep). The render path at lines 88-112 uses resolveComponentResult + renderToDom instead. CodeQL correctly flagged this. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns empty, subsequent skip indexing — examples/deno-desktop-reader/main.ts:108-123, 141

if (searchIndexReady) return;
searchIndexReady = true;      // line 110 — set BEFORE async indexing
// ... await indexBook() ... // lines 113-122 — actual indexing happens async

Then called fire-and-forget at line 141: ensureSearchIndex(); — not awaited. search() at line 143 reads the on-disk index immediately. searchIndexReady is true before indexing completes, so first search gets empty results, and subsequent requests skip indexing permanently. Fix: await ensureSearchIndex() before search(), move searchIndexReady = true to after the indexing loop.

3. Orphaned section header — packages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation following. Either a placeholder that was never written or a leftover from removed code. Remove or add the implementation.


MEDIUM

4. saveSearchIndex sync I/O in async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

Use await Deno.writeTextFile instead.

5. ensureSearchIndex() empty catch blocks swallow all errors — main.ts:120, 122

try { await indexBook(...) } catch { /* skip */ }  // per-book
} catch { /* skip */ }                              // top-level JSON parse

If books.json is malformed or pdf-parse fails on every book, the function silently returns with searchIndexReady = true and search is permanently broken. At minimum, console.warn the failures.

6. ${base}client-entry.js referenced but never generated — packages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this at main.ts:186-188. Acknowledged alpha.5 wiring gap.


LOW

7. Route manifest JSDoc path mismatch — packages/adapter-vite/src/route-manifest.ts:12
JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g., dist/route-manifest.ts).

8. findAppScript() uncached sync I/O per request — main.ts:85
Deno.readDirSync runs on every SPA page load. Cache the result since the filename won't change between requests.


What's good

  • Router core (client-router.ts:302): proxy null-prototype params, isSafeParamName proto-poisoning (CodeQL flag false positive — uses Map.set() not property assignment), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated recursively, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts:243): renderId counter guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, navOptions avoids shadowing.
  • Route manifest (route-manifest.ts:167): cross-drive Windows detection, replaceAll, direct entries.length count.
  • formatError (errors.ts): cycle-safe via seen Set.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 3 Dead isRenderableNode, ensureSearchIndex race, orphaned errors header
Medium 3 Sync I/O in indexBook, silent error swallowing, client-entry.js wiring gap
Low 2 JSDoc path mismatch, uncached findAppScript

Verdict: merge-ready. The 3 bugs are small fixes (delete dead function, reorder 2 lines + await in search endpoint, delete orphaned comment). The router and SPA bootstrap code are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A37%3A20.387Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

Reviewed all key changed files independently. The 16+ prior bot review rounds and follow-up commits resolved 14+ critical/high issues — every previously-flagged item (hardcoded mode, duplicate popstate, replaceAll, + decoding, guard redirect cycles, stale-render renderId, replace semantics, error swallowing, unused/duplicate imports, build task scope, hardcoded shell title/lang, preventDefault ordering, component resolution, options shadowing) is fixed. Below are the remaining issues in the current code.


BUG

1. isRenderableNode is dead code — packages/app/src/spa.ts:49-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Zero call sites anywhere in the codebase (grep confirms). The render path uses resolveComponentResult + renderToDom instead. CodeQL flagged this correctly. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns empty, subsequent searches skip indexing — examples/deno-desktop-reader/main.ts:122, 131-134, 153-155

searchIndexReady = true;          // line 122 — set BEFORE async indexing
// ... await indexBook() ...     // lines 129-133 — actual indexing
ensureSearchIndex();              // line 153 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 155 — reads on-disk index immediately

searchIndexReady is set true on line 122 before await indexBook() on line 131. On first request, search() reads the empty on-disk index while background indexing still runs → empty results. On subsequent requests, searchIndexReady is already true so indexing never completes. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after the indexing loop.

3. Orphaned section header — packages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation following. Either a placeholder never written or a leftover from removed code. Remove or add the implementation.


MEDIUM

4. saveSearchIndex uses sync I/O inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

saveSearchIndex calls Deno.writeTextFileSync (search.ts:98). For large PDFs this blocks the server. Use await Deno.writeTextFile instead.

5. ${base}client-entry.js referenced but never generated — packages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this via runtime HTML injection (main.ts:198-207). Acknowledged alpha.5 wiring gap.


LOW

6. ensureSearchIndex empty catch blocks swallow all errors — main.ts:132, 134

try { await indexBook(...) } catch { /* skip */ }
} catch { /* skip */ }

Malformed books.json or universal pdf-parse failures leave search permanently broken with zero diagnostics. At minimum, console.warn the failures.

7. findAppScript() uncached Deno.readDirSync per request — main.ts:83-94

Called on every SPA page load. Cache the result — the filename won't change between requests.

8. ~100 LOC duplicated test mocks — spa-bootstrap.test.ts + spa-data.test.ts

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, and firePopstate. Extract to shared _test-utils.ts.


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning protection (CodeQL flag is false positive — uses Map.set(), not property assignment), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId counter guards stale-loader races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, OpenElement tagName support.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set, non-Error cutoff.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 3 Dead isRenderableNode, ensureSearchIndex race, orphaned errors header
Medium 2 Sync I/O in indexBook, client-entry.js wiring gap (acknowledged)
Low 3 Silent error swallowing, uncached findAppScript, test mock duplication

Verdict: merge-ready after removing the dead isRenderableNode function (4 lines). The ensureSearchIndex race and sync I/O are in example/dogfood code, not the framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A44%3A56.219Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

The 15+ prior bot review rounds and follow-up commits resolved 14+ previously-flagged critical/high/bug issues. The router core, SPA glue, and route manifest generator are production-quality for alpha.5. Below are remaining unfixed findings.


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:49-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Zero call sites across the entire codebase (grep confirms). The render path at lines 88-112 uses resolveComponentResult + renderToDom instead. CodeQL flagged this correctly. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns empty, subsequent searches skip indexingexamples/deno-desktop-reader/main.ts:120-122, 153-155

if (searchIndexReady) return;
searchIndexReady = true;          // line 122 — set BEFORE any async indexing work
// ... await indexBook() ...     // line 131 — actual indexing

ensureSearchIndex();              // line 153 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 155 — reads on-disk index immediately

searchIndexReady is set true on line 122 before await indexBook() on line 131 runs. On the first search request, search() reads the empty on-disk index while background indexing hasn't completed → empty results. On subsequent requests, searchIndexReady is already true, so indexing never completes. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after the indexing loop.


MEDIUM

3. saveSearchIndex uses sync I/O inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks event loop
}

For large PDFs, writeTextFileSync blocks the server for tens to hundreds of milliseconds. Use await Deno.writeTextFile instead.

4. ${base}client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this via runtime injection at main.ts:198-207. Acknowledged alpha.5 wiring gap — standalone SPA builds would 404.

5. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-123

Complete public function with sha-based cache + GitHub sync logic, but main.ts never imports or calls it. Either wire in main.ts on startup or add a ponytail comment.


LOW

6. Orphaned section headerpackages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line with no implementation following. Either missing code or dead comment. Remove or add implementation.

7. findAppScript() blocking I/O per requestexamples/deno-desktop-reader/main.ts:83-94

Deno.readDirSync runs on every SPA page load. Cache the result since the filename won't change between requests.

8. ensureSearchIndex() empty catch blocksmain.ts:132, 134

} catch { /* skip */ }

Malformed books.json or universal pdf-parse failures leave search permanently broken with zero diagnostics. At minimum, console.warn the failures.

9. ~100 LOC duplicated test mocksspa-bootstrap.test.ts + spa-data.test.ts

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, and firePopstate. Extract to shared _test-utils.ts.

10. Route manifest JSDoc path mismatchpackages/adapter-vite/src/route-manifest.ts:12

JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g., dist/route-manifest.ts).


What's good

  • Router core (client-router.ts): proxy null-prototype params, isSafeParamName proto-poisoning (CodeQL false positive — uses Map.set()), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated recursively, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, navOptions avoids shadowing.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length.
  • formatError cause chain: cycle-safe via seen Set, 4 tests.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
  • 93/93 SPA + 27/27 reader smoke tests pass.

Priority Count Key items
Bug 2 Dead isRenderableNode, ensureSearchIndex race
Medium 3 Sync I/O in indexBook, client-entry.js wiring gap, dead syncBooks
Low 5 Orphaned errors header, uncached findAppScript, silent error swallowing, test mock duplication, JSDoc path mismatch

Verdict: merge-ready after removing dead isRenderableNode (4 lines in spa.ts:48-51). The ensureSearchIndex race is in example/dogfood code, not framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T00%3A47%3A10.993Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

I've read all the key changed files. Here's my independent review:


Review: feat(alpha.5) — SPA mode + client router + desktop proof

The 20+ prior bot review rounds and follow-up commits resolved 14+ previously-flagged issues. The router core, SPA glue, and route manifest generator are production-quality. Below are remaining issues in the current code.


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:48-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Zero call sites anywhere in the codebase. The render path uses resolveComponentResult + renderToDom (lines 107-110) and OpenElement tagName path (lines 96-103). CodeQL correctly flagged this. Remove lines 48-51.

2. Orphaned section headerpackages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation following. Either a placeholder never written or leftover from removed code. Remove or add the implementation.

3. ensureSearchIndex() race — first search returns empty, subsequent searches skip indexingexamples/deno-desktop-reader/main.ts:120-135, 153-155

searchIndexReady = true;          // line 122 — set BEFORE any async indexing
// ... await indexBook() ...     // lines 127-133 — actual indexing happens async
ensureSearchIndex();              // line 153 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 155 — reads on-disk index immediately

searchIndexReady is set true on line 122 before await indexBook() on line 131 runs. On first request, search() reads the empty on-disk index while background indexing still runs → empty results. On subsequent requests, searchIndexReady is already true, so indexing never completes. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after the indexing loop.

4. Sync I/O blocks event loop in async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync — blocks server event loop
}

For large PDFs this blocks the server. Use a non-blocking write: replace the body of saveSearchIndex with await Deno.writeTextFile.


MEDIUM

5. client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this via runtime injection at main.ts:198-207. Acknowledged alpha.5 wiring gap.

6. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-123

Complete public function with sha-based cache + GitHub sync, but main.ts never imports or calls it. Either wire it in on startup or add a ponytail comment.

7. ensureSearchIndex() empty catch blocks swallow all errorsmain.ts:132, 134

try { await indexBook(...) } catch { /* skip */ }
} catch { /* skip */ }

If books.json is malformed or pdf-parse fails on every book, the function silently returns with searchIndexReady = true and search is permanently broken with zero diagnostics. At minimum, console.warn the failures.

8. findAppScript() blocking Deno.readDirSync per requestmain.ts:85

Called on every SPA page load. Cache the result since the filename won't change between requests.


LOW

9. ~100 LOC duplicated test mocksspa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-60

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, and firePopstate. Extract to shared _test-utils.ts.

10. Route manifest JSDoc path mismatchpackages/adapter-vite/src/route-manifest.ts:12

JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g., dist/route-manifest.ts).


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning, cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts): cycle-safe via seen Set, 4 tests covering depth, cycles, and boundary cases.
  • All three packages use deno check src/.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 4 Dead isRenderableNode, orphaned errors header, ensureSearchIndex race, sync I/O in indexBook
Medium 4 client-entry.js wiring gap, dead syncBooks, silent error swallowing, uncached findAppScript
Low 2 Test mock duplication, JSDoc path mismatch

Verdict: Fix the 4 bugs (delete dead function + orphaned comment, fix the search race + sync I/O). The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T01%3A00%3A32.614Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Based on my independent review of all core changed files against the 20+ prior bot review rounds, here is my consolidated review:


Review: feat(alpha.5) — SPA mode + client router + desktop proof

The follow-up commits across 20+ review rounds resolved all 14+ previously-flagged critical/high issues: hardcoded modeauto, duplicate popstate → onChange callback, replaceAll Windows fix, + query decoding, MAX_GUARD_REDIRECTS, renderId stale guards in both renderRoute() and handleFormSubmit(), resolveComponentResult for { default } modules, preventDefault after action check, navOptions shadowing fix, hardcoded SPA shell title/lang → ctx.phase3.html, notifyChange() dual sync/async error handling, and deno check src/ in all packages. The router core and SPA bootstrap are production-quality.


BUG

1. isRenderableNode is dead code — packages/app/src/spa.ts:48-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Zero call sites across the entire codebase. The render paths at lines 96-103 (OpenElement tagName) and lines 107-110 (resolveComponentResult + renderToDom) don't reference it. Confirmed by grep and CodeQL.

2. Orphaned section header — packages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation following. Either a placeholder never written or leftover from removed code.

3. ensureSearchIndex() race — examples/deno-desktop-reader/main.ts:122, 153

searchIndexReady = true;          // line 122 — set BEFORE async indexing
// ... await indexBook() ...     // lines 129-131 — actual async indexing

ensureSearchIndex();              // line 153 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 155 — reads empty on-disk index

searchIndexReady = true on line 122 before await indexBook() runs. First search reads empty disk index → empty results. Subsequent requests skip indexing because flag is already true. Fix: await ensureSearchIndex() before search(), move searchIndexReady = true to after indexing completes.

4. Sync I/O blocks event loop inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync (line 98)
}

saveSearchIndex at line 98 calls Deno.writeTextFileSync — synchronous I/O inside an async function blocks the server event loop. Use await Deno.writeTextFile.


MEDIUM

5. ${base}client-entry.js referenced but never generated — packages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this via runtime HTML injection at main.ts:198-207. Acknowledged alpha.5 wiring gap — standalone SPA builds would 404.

6. syncBooks() fully implemented but dead code — examples/deno-desktop-reader/app/repo.ts:90-123

Complete public function with sha-based cache + GitHub sync logic, but main.ts never imports or calls it. Either wire in main.ts startup or add a ponytail comment.


LOW

7. ensureSearchIndex() empty catch blocks — main.ts:132, 134

try { await indexBook(...) } catch { /* skip */ }
} catch { /* skip */ }

Malformed books.json or universal pdf-parse failures leave search permanently broken with zero diagnostics. At minimum, console.warn the failures.

8. findAppScript() blocking sync I/O per request — main.ts:85

Deno.readDirSync runs on every SPA page load. Cache the result — the filename won't change between requests.

9. ~100 LOC duplicated test mocks — spa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-60

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, etc. Extract to shared _test-utils.ts.

10. Route manifest JSDoc path mismatch — route-manifest.ts:12

JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest at line 161 writes to join(outDir, 'route-manifest.ts') (e.g. dist/route-manifest.ts).


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning protection (CodeQL false positive — uses Map.set()), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated recursively, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId counter guards stale-loader races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, OpenElement tagName support.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set, non-Error cutoff.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 4 Dead isRenderableNode, orphaned errors header, ensureSearchIndex race, sync I/O in indexBook
Medium 2 client-entry.js wiring gap, dead syncBooks
Low 4 Silent catch blocks, uncached findAppScript, test mock duplication, JSDoc mismatch

Verdict: merge-ready after removing dead isRenderableNode (4 lines) and orphaned header (1 line). The two example-code bugs (ensureSearchIndex race + sync I/O) are in dogfood code, not framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T01%3A03%3A01.031Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Review: feat(alpha.5) — SPA mode + client router + desktop proof

The 20+ prior bot reviews and follow-up commits resolved 14+ critical/high issues. The router core and SPA bootstrap are production-quality. Below are the remaining issues in the current code.


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:49-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Zero call sites in the entire codebase (grep confirms). The render path uses resolveComponentResult + renderToDom instead. CodeQL flagged this. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns empty, subsequent skip indexingexamples/deno-desktop-reader/main.ts:120-135, 153

if (searchIndexReady) return;
searchIndexReady = true;           // line 122 — set BEFORE any async indexing
// ... await indexBook() ...      // lines 127-133 — actual async indexing

ensureSearchIndex();               // line 153 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 155 — reads on-disk index immediately

searchIndexReady is set true on line 122 before await indexBook() on line 131. First request: search() reads empty on-disk index → empty results. Subsequent requests: searchIndexReady is already true, so indexing never completes. Fix: await ensureSearchIndex() before search(), and move searchIndexReady = true to after the indexing loop.

3. Orphaned section headerpackages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation following. Either missing code or dead comment. Remove or add implementation.


MEDIUM

4. Sync I/O blocks event loop inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

export async function indexBook(...): Promise<void> {
  // ... async pdfParse ...
  saveSearchIndex(dir, index); // → Deno.writeTextFileSync (line 98) — blocks server
}

saveSearchIndex at line 98 calls Deno.writeTextFileSync. Use await Deno.writeTextFile instead.

5. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-123

Complete public function with SHA-based cache + GitHub sync logic, never imported or called. Either wire in main.ts on startup or add comment noting it's for a future slice.

6. client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this at main.ts:198-211. Acknowledged alpha.5 wiring gap.


LOW

7. ensureSearchIndex() empty catch blocksmain.ts:132, 134

try { await indexBook(...) } catch { /* skip */ }
} catch { /* skip */ }

Malformed books.json or universal pdf-parse failures leave search permanently broken with zero diagnostics. console.warn the failures.

8. findAppScript() uncached sync I/O per requestmain.ts:85

Deno.readDirSync runs on every SPA page load. Cache the result.

9. ~80 LOC duplicated test mocksspa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-80

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, and firePopstate. Extract to shared _test-utils.ts.

10. Route manifest JSDoc path mismatchroute-manifest.ts:12

JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g. dist/route-manifest.ts).


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning (CodeQL flag false positive — uses Map.set()), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling in notifyChange().
  • SPA glue (spa.ts): renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, navOptions avoids shadowing.
  • Route manifest (route-manifest.ts): cross-drive Windows detection, replaceAll, direct entries.length.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set.
  • All three packages use deno check src/.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 3 Dead isRenderableNode, ensureSearchIndex race, orphaned errors header
Medium 3 Sync I/O in indexBook, dead syncBooks, client-entry.js wiring gap (acknowledged)
Low 4 Silent catch blocks, uncached findAppScript, test mock duplication, JSDoc mismatch

Verdict: merge-ready after removing dead isRenderableNode (4 lines) and orphaned header (1 line). The two example-code bugs (ensureSearchIndex race + sync I/O) are in dogfood code, not framework core. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T01%3A06%3A00.246Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Independent Review

Reviewed all core changed files. The 20+ prior bot rounds resolved 14+ previously-flagged issues. Below are the remaining findings in the current code (ad2f9e9).


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:49-51

Zero call sites across the entire codebase (grep confirms). The render path uses resolveComponentResult + renderToDom instead. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns emptyexamples/deno-desktop-reader/main.ts:126-139, 157

searchIndexReady = true;          // line 126 — set BEFORE async indexing
// ... await indexBook() ...     // line 135 — actual indexing (async)
ensureSearchIndex();              // line 157 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 159 — reads on-disk index immediately

searchIndexReady is set true before await indexBook() runs. First request reads empty on-disk index before indexing finishes. Subsequent requests see the flag already true and skip indexing entirely. Fix: await ensureSearchIndex() before search(), move searchIndexReady = true to after the indexing loop.

3. Orphaned section headerpackages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation. Either missing code or dead leftover.

4. Sync I/O blocks event loop inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

saveSearchIndex(dir, index); // → Deno.writeTextFileSync (line 98)

saveSearchIndex calls Deno.writeTextFileSync inside an async function. Blocks the server event loop. Use await Deno.writeTextFile instead.


MEDIUM

5. client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this at main.ts:198-211. Acknowledged alpha.5 wiring gap.

6. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-123

Complete public function with SHA-based cache + GitHub sync, never called from main.ts. Either wire in or add a ponytail comment.

7. ensureSearchIndex() empty catch blocksmain.ts:134, 138

Malformed books.json or universal pdf-parse failures leave search permanently broken with zero diagnostics. At minimum, console.warn the failures.

8. findAppScript() blocking Deno.readDirSync per requestmain.ts:85

Uncached sync I/O runs on every SPA page load. Cache the result.


LOW

9. Route manifest JSDoc path mismatchroute-manifest.ts:12

JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts').

10. ~100 LOC duplicated test mocksspa-bootstrap.test.ts:19-80 + spa-data.test.ts:11-80

Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, etc. Extract to shared _test-utils.ts.


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning (CodeQL flag is false positive — uses Map.set()), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection, correct dual sync/async error handling.
  • SPA glue (spa.ts): renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners, navOptions avoids shadowing.
  • Route manifest (route-manifest.ts): cross-drive Windows detection, replaceAll, direct entries.length count.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set, 4 tests.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
  • 93/93 SPA + 27/27 reader smoke tests pass.
Priority Count Key items
Bug 4 Dead isRenderableNode, ensureSearchIndex race, orphaned errors header, sync I/O in indexBook
Medium 4 client-entry.js wiring gap (acknowledged), dead syncBooks, silent catch blocks, uncached findAppScript
Low 2 JSDoc path mismatch, test mock duplication

Verdict: Fix the 4 bugs (delete dead function + orphaned header, fix search race + sync I/O). All are targeted local fixes. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T01%3A08%3A45.101Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Consolidated Review

The 20+ prior bot rounds resolved ~14 critical/high issues (hardcoded modeauto, duplicate popstate → onChange callback, replaceAll, + decoding, MAX_GUARD_REDIRECTS, renderId stale guards in both renderRoute() and handleFormSubmit(), resolveComponentResult for { default } modules, preventDefault ordering, navOptions shadowing fix, ctx.phase3.html title/lang, dual sync/async error handling, deno check src/ in all packages, merged duplicate import, generic serverError()). The router core and SPA bootstrap are production-quality. Below are remaining issues in the current code (80af290).


BUG

1. isRenderableNode is dead codepackages/app/src/spa.ts:48-51

function isRenderableNode(value: unknown): value is Node {
  return value !== null && typeof value === 'object' && 'nodeType' in value;
}

Zero call sites. The render paths at lines 96-103 (OpenElement tagName) and lines 106-111 (resolveComponentResult + renderToDom) don't reference it. Flagged by CodeQL and 5+ prior bots. Remove lines 48-51.

2. ensureSearchIndex() race — first search returns empty, subsequent skip indexingexamples/deno-desktop-reader/main.ts:124-139, 157

searchIndexReady = true;          // line 126 — set BEFORE await indexBook()
// ... await indexBook() ...     // line 135 — actual async indexing

ensureSearchIndex();              // line 157 — fire-and-forget, NOT awaited
return json(search(q, CACHE_DIR)); // line 159 — reads on-disk index immediately

searchIndexReady is set true before await indexBook() runs. First request: search() reads empty on-disk index before indexing finishes. Subsequent: flag already true, indexing never completes. Fix: await ensureSearchIndex() before search(), move searchIndexReady = true to after the indexing loop.

3. Sync I/O blocks event loop inside async indexBook()examples/deno-desktop-reader/app/search.ts:35

saveSearchIndex(dir, index); // → Deno.writeTextFileSync (line 98)

saveSearchIndex calls Deno.writeTextFileSync inside an async function. Blocks the server event loop. Use await Deno.writeTextFile(indexFile, JSON.stringify(index, null, 2)) instead.

4. Orphaned section headerpackages/core/src/errors.ts:172

// ─── SSR Error Context ──────────────────────────────────────────────

Last line of the file with no implementation. Either missing code or dead leftover.


MEDIUM

5. client-entry.js referenced but never generatedpackages/adapter-vite/src/build.ts:92

<script type="module" src="${base}client-entry.js"></script>

The adapter writes index.html directly in closeBundle, bypassing Vite's HTML pipeline. No build step produces client-entry.js. The desktop reader works around this via runtime HTML injection at main.ts:198-214. Standalone SPA builds would 404. Acknowledged alpha.5 wiring gap.

6. syncBooks() fully implemented but dead codeexamples/deno-desktop-reader/app/repo.ts:90-123
Complete public function with SHA-based cache + GitHub sync logic, never imported or called from main.ts. Either wire in on startup or add a ponytail comment.


LOW

7. ensureSearchIndex() empty catch blocks swallow all errorsmain.ts:136, 138
Malformed books.json or universal pdf-parse failures leave search permanently broken with zero diagnostics. console.warn the failures.

8. findAppScript() uncached Deno.readDirSync per requestmain.ts:83-94
Runs blocking I/O on every SPA page load. Cache the result in a module-level variable.

9. Route manifest JSDoc path mismatchroute-manifest.ts:12
JSDoc shows import { routeManifest } from '../.openelement/route-manifest' but writeRouteManifest writes to join(outDir, 'route-manifest.ts') (e.g. dist/route-manifest.ts).

10. ~100 LOC duplicated test mocksspa-bootstrap.test.ts + spa-data.test.ts
Both define near-identical MockHistory, StubElement, mockLocation, mockEvents, mockDocument, resetMocks, firePopstate. Extract to shared _test-utils.ts.


What's good

  • Router core (client-router.ts): proxy-based null-prototype params, isSafeParamName proto-poisoning (CodeQL flag false positive — uses Map.set()), cycle-safe guard redirects (MAX_GUARD_REDIRECTS = 10), proper replace semantics propagated through recursive commitNavigation, resolveMode('auto') file:// detection.
  • SPA glue (spa.ts): renderId guards stale races in both renderRoute() and handleFormSubmit(), resolveComponentResult handles { default } lazy modules, preventDefault after action check, onChange: renderRoute avoids duplicate listeners.
  • Route manifest (route-manifest.ts): cross-drive Windows detection with explicit error, replaceAll path conversion, direct entries.length count.
  • formatError cause chain (errors.ts:26-37): cycle-safe via seen Set.
  • All three packages use deno check src/. Peer dep for @openelement/router present.
Priority Count Key items
Bug 4 Dead isRenderableNode, ensureSearchIndex race, sync I/O in indexBook, orphaned header
Medium 2 client-entry.js wiring gap (acknowledged), dead syncBooks
Low 4 Silent catch blocks, uncached findAppScript, JSDoc mismatch, test mock duplication

Verdict: Fix the 4 bugs. The ensureSearchIndex race and sync I/O are in example/dogfood code, not framework core; isRenderableNode is a dead-code cleanup. The router and SPA bootstrap are production-quality for alpha.5.

New%20session%20-%202026-06-26T01%3A12%3A49.332Z
opencode session  |  github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants