The cleanStaleChunks plugin lives in the shared vite config, so its
closeBundle hook also fired under `vp test` — where generateBundle never
runs, produced stayed empty, and the sweep deleted every hashed chunk
in web/static (28 files → 3). The entries still referenced their old
imports, so every page 404'd and Alpine never started: the whole reader
and media system appeared dead after merely running the test suite.
Guard: skip the sweep when produced.size === 0 — not a build run, no
build output to clean. Verified: build → 28 files, test → 28 files;
back-to-back builds stay byte-identical.
Test infrastructure uses the vite-plus stack: vp test (bundled Vitest 5),
jsdom environment configured in vite.config.ts, colocated web/src/*.test.ts
files that tsc type-checks, vp fmt formats, and the bundler never imports.
- markdown.test.ts pins the notes XSS defenses: parser-level html escaping,
DOMPurify FORBID_TAGS, scheme checks, footnote rendering, html->markdown
round-trips incl. the headingless-table rule.
- filter_builder.test.ts pins the deep-link wire contract (ruleToFlatParam),
smart-serialization rules (isFlatable), and pruneIncomplete semantics.
- password_validation.test.ts covers the rules fetch + fallback and the
full submit-gating matrix through the real DOM flow (debounce included).
- api.test.ts pins response/error contracts incl. the HTTP-status and
Unknown-error fallbacks and the 404-as-null progress contract.
- storage.test.ts covers token/theme/library round-trips, the __all__
sentinel, and the SSR cookie.
- toast.test.ts pins config completeness for every ToastType (the missing
'warning' entry regression) and render behavior.
Found + fixed while writing these: the no-useless-escape fix in
d7d14957 had de-escaped the ']' in hasSpecialChar's character class,
closing it early — special-character validation silently stopped
matching most specials. The new password suite caught it immediately.
jsdom chosen over happy-dom after empirical comparison: DOMPurify's
sanitizer output is mangled under happy-dom (forbidden tags survive),
which would have made the security suite test nothing.
Note: AGENTS.md gains a 'Web (TypeScript) tests' section documenting the
workflow and the happy-dom gotcha.
emptyOutDir is false because web/static also holds tracked assets,
so *-<hash>.js chunks from every previous build accumulated
indefinitely and leaked into Docker images via the build context
(the reader serves whichever chunk the import chain names, so the
orphans are pure confusion + bloat). A closeBundle plugin now
deletes any hashed chunk this build did not produce.
PDFs failed to open with error:
Invalid factory url: "http://localhost:8765/static/undefined"
Root cause: foliate-js's pdfjsPath() uses new URL(dynamicPath, import.meta.url)
to resolve runtime asset paths (standard_fonts/, cmaps/). Vite transforms this
pattern into a static asset map lookup at build time, but can only resolve
known static file paths — not dynamically-constructed directory paths. The
lookup returns undefined, producing a broken URL.
Fix (two parts):
1. foliate-js fork (commit d164d6f): Export an overridable config.pdfjsPath
function. Module-level code (worker, CSS) continues using import.meta.url
directly (works fine with Vite for static filenames). The makePDF function
uses config.pdfjsPath for runtime paths, allowing consumers to override it.
2. Bookhoard changes:
- Update foliate-js dependency to d164d6f
- Override config.pdfjsPath in reader.ts to resolve to /static/vendor/pdfjs/
- Add Vite plugin (pdfjsAssets) that copies standard_fonts/ and cmaps/ from
node_modules to the build output during vite build (the standard approach
used by react-pdf and other pdfjs-dist consumers)
- Remove manual cp commands from build:ts scripts
Migrate from git submodule to npm package management for better
developer experience and simplified deployment.
Changes:
- Add @bookhoard/foliate-js from GitHub fork
(john-okeefe/foliate-js#bookhoard-panel-detection)
- Update vite alias to point to node_modules instead of vendor
- Delete .gitmodules (no submodules tracked)
- Remove scripts/setup-git-hooks.sh (no longer needed)
- Delete web/vendor/foliate-js/ submodule directory
- Remove sc-commit git alias (submodule-specific)
Benefits:
- Standard npm workflow (npm install / npm update)
- No authentication issues for end users (public GitHub)
- Simpler deployment (npm ci in containers)
- foliate-js protected in node_modules (AI won't rewrite)
- Independent project management
- Cleaner git history
Technical details:
- Import remains unchanged: import "foliate-js/view.js"
- Vite alias maps "foliate-js" to "/node_modules/@bookhoard/foliate-js"
- Build verified working (reader.js includes foliate-js)
- Package installed from git branch: bookhoard-panel-detection
Update Vite configuration to support foliate-js library integration:
1. Add import alias for foliate-js:
- Maps 'foliate-js' imports to web/vendor/foliate-js submodule
- Allows clean imports: import { View } from 'foliate-js/view.js'
2. Update build target to ESNext:
- Change from 'es2020' to 'esnext' to support top-level await
- Required by foliate-js pdf.js which uses top-level await
- ES2022+ support is excellent in all modern browsers (Chrome 112+, Firefox 115+, Safari 16.4+)
These changes enable Vite to bundle foliate-js into reader.js without
requiring a separate build step for the library.
- Add reader.ts as second entry point alongside main.ts in vite.config.ts
- Set assetsDir to empty string to output chunks to /static/ root
- Remove reader-shell import from main.ts (loaded separately via reader.js)
- Update .gitignore to exclude web/static/assets/*.js chunks
- Update Bruno environment with new test IDs
Configure Vite to bundle all code into a single chunk using manualChunks,
preventing Alpine.js from being split into multiple modules that caused
"redeclaration of let Xo" errors during initialization.
This resolves the critical bug where Alpine.js would load twice on pages
using @Header, breaking all @click handlers and causing form buttons to
fall back to default browser behavior (unwanted navigation/form submission).
Technical details:
- The default Vite code-splitting was creating multiple ESM chunks
- Alpine's reactive system uses let Xo internally
- Multiple chunks caused Xo to be declared multiple times
- manualChunks() forces everything into a single bundle
Fixes #XXX (Alpine.js redeclaration error)
Created vite.config.ts with optimized settings for the Bookhoard project:
- Output directory: web/static/ (matches existing esbuild setup)
- Single entry point: web/src/main.ts
- Output file: main.js (preserves existing template references)
- ES2020 target for modern browser compatibility
- Source maps enabled for debugging
- esbuild minification for optimal bundle size
- emptyOutDir: false to preserve other static assets (htmx.min.js, CSS, images)
Configuration maintains the same build output structure as esbuild,
ensuring no changes needed to templates or deployment process.