From c946b6d71c1992f201a41d69212d36c99b0d8474 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 11 Aug 2026 22:04:50 -0400 Subject: [PATCH] chore: wire vendored foliate-js into the build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $foliate is a Vite-only alias, deliberately absent from kit.alias and tsconfig paths: TypeScript cannot resolve it, so the ambient declaration in src/lib/reader/foliate.d.ts is the only candidate and svelte-check never walks the ~11k lines of untyped vendored JS. The generated tsconfig includes src/**/*.js and checkJs is on, so src/lib/vendor is excluded as well — `exclude` replaces rather than merges, hence the repeated service-worker entries. optimizeDeps.include for construct-style-sheets-polyfill: it sits behind a dynamic import in fixed-layout.js, so Vite would otherwise discover it mid-session and force a page reload on the first fixed-layout EPUB. Verified with a temporary import that the graph resolves: view.js and paginator.js are emitted as chunks, and the bundled pdf.js is our stub rather than upstream's bare `import '@pdfjs/pdf.min.mjs'`. --- frontend/src/lib/reader/foliate.d.ts | 90 ++++++++++++++++++++++++++++ frontend/src/lib/reader/foliate.ts | 32 ++++++++++ frontend/tsconfig.json | 16 ++++- frontend/vite.config.ts | 18 +++++- 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/reader/foliate.d.ts create mode 100644 frontend/src/lib/reader/foliate.ts diff --git a/frontend/src/lib/reader/foliate.d.ts b/frontend/src/lib/reader/foliate.d.ts new file mode 100644 index 0000000..cb52e99 --- /dev/null +++ b/frontend/src/lib/reader/foliate.d.ts @@ -0,0 +1,90 @@ +/** + * Hand-written types for the vendored foliate-js (src/lib/vendor/foliate-js). + * + * `$foliate` is a Vite-only alias, so TypeScript cannot resolve it and this + * ambient declaration is the only candidate — which is the point: svelte-check + * never walks the untyped vendored JS. Only the surface Chitai calls is typed. + * When you start using a new method, add it here rather than casting to `any`. + */ +declare module '$foliate/view.js' { + /** A TOC entry. `id` is assigned by foliate's `assignIDs`, not by the book. */ + export interface FoliateTocItem { + id: number; + label: string; + href: string; + subitems?: FoliateTocItem[]; + } + + /** `event.detail` of the `relocate` event. Shape from SectionProgress.getProgress. */ + export interface FoliateRelocateDetail { + /** Overall progress through the book, 0–1. Chitai stores this as `percentage`. */ + fraction: number; + section: { current: number; total: number }; + location: { current: number; next: number; total: number }; + /** Estimated remaining reading time, in minutes. */ + time: { section: number; total: number }; + /** Null when the book has no TOC entry covering this position. */ + tocItem?: FoliateTocItem | null; + pageItem?: { label: string; href: string } | null; + cfi: string; + range?: Range; + } + + /** `event.detail` of the `load` event. `doc` is the section's iframe document. */ + export interface FoliateLoadDetail { + doc: Document; + index: number; + } + + export interface FoliateRenderer extends HTMLElement { + /** + * A single string sets the stylesheet appended to , which wins over the + * book's own CSS on equal specificity. A [before, after] tuple also sets one + * prepended to , which the book's CSS overrides. Re-applied by the + * paginator on every section load. Absent on the fixed-layout renderer. + */ + setStyles?(styles: string | [before: string, after: string]): void; + getContents(): { doc: Document; index: number }[]; + destroy(): void; + } + + export interface FoliateBook { + toc?: FoliateTocItem[]; + /** 'rtl' for right-to-left books; drives goLeft/goRight. */ + dir?: string; + metadata?: Record; + rendition?: { layout?: string }; + } + + /** Accepted by goTo/select/init: a CFI or href, a spine index, or a fraction. */ + export type FoliateTarget = string | number | { fraction: number }; + + export class View extends HTMLElement { + book: FoliateBook; + renderer: FoliateRenderer; + /** True when the book is pre-paginated; the renderer is then foliate-fxl. */ + isFixedLayout: boolean; + lastLocation: FoliateRelocateDetail | null; + + open(book: File | string | FoliateBook): Promise; + close(): void; + init(opts: { lastLocation?: FoliateTarget | null; showTextStart?: boolean }): Promise; + + /** Returns undefined on failure — it logs and swallows. Check before relying on it. */ + resolveNavigation(target: FoliateTarget): { index: number; anchor?: unknown } | undefined; + goTo(target: FoliateTarget): Promise<{ index: number } | undefined>; + goToFraction(fraction: number): Promise; + + prev(distance?: number): Promise; + next(distance?: number): Promise; + /** Direction-aware: inverts against prev/next for rtl books. */ + goLeft(): Promise; + goRight(): Promise; + } + + export function makeBook(file: File | string): Promise; + + export class ResponseError extends Error {} + export class NotFoundError extends Error {} + export class UnsupportedTypeError extends Error {} +} diff --git a/frontend/src/lib/reader/foliate.ts b/frontend/src/lib/reader/foliate.ts new file mode 100644 index 0000000..735c71f --- /dev/null +++ b/frontend/src/lib/reader/foliate.ts @@ -0,0 +1,32 @@ +import type { View } from '$foliate/view.js'; + +let pending: Promise | undefined; + +/** + * Loads foliate-js and registers its custom elements. + * + * view.js calls customElements.define() at module scope and subclasses + * HTMLElement, and its class fields construct DOM helpers eagerly — so importing + * it on the server throws. This module is safe to import anywhere because it + * touches nothing at module scope; the vendored code is only pulled in when + * loadFoliate() is called, which must be from onMount or a browser guard. + * + * The promise is cached so concurrent callers share one load. ESM already dedupes + * module evaluation; this mainly makes the "define runs once" contract explicit. + * Editing a file under src/lib/vendor invalidates the module and re-runs + * customElements.define, which throws — hard-refresh after re-vendoring. + */ +export function loadFoliate(): Promise { + return (pending ??= import('$foliate/view.js').then(() => undefined)); +} + +/** + * Creates a . Call only after loadFoliate() has resolved. + * + * Built imperatively rather than written in markup so SSR never emits an unknown + * element for Svelte to hydrate, and so svelte-check has no unknown attributes to + * complain about. + */ +export function createFoliateView(): View { + return document.createElement('foliate-view') as View; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a5567ee..114ad9f 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -10,7 +10,21 @@ "sourceMap": true, "strict": true, "moduleResolution": "bundler" - } + }, + // `exclude` replaces the generated list rather than merging with it, so the + // service-worker entries from .svelte-kit/tsconfig.json are repeated here. + // src/lib/vendor is added: it is vendored third-party JS and checkJs is on, + // so without this svelte-check reports thousands of errors from foliate-js. + "exclude": [ + "node_modules/**", + "src/service-worker.js", + "src/service-worker/**/*.js", + "src/service-worker.ts", + "src/service-worker/**/*.ts", + "src/service-worker.d.ts", + "src/service-worker/**/*.d.ts", + "src/lib/vendor/**" + ] // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2d35c4f..ea0da9c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,23 @@ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; +import { fileURLToPath } from 'node:url'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()] + plugins: [tailwindcss(), sveltekit()], + resolve: { + alias: { + // Deliberately a Vite-only alias, and deliberately not in kit.alias or + // tsconfig paths. TypeScript cannot resolve `$foliate/*`, so the ambient + // declaration in src/lib/reader/foliate.d.ts is the only candidate and + // svelte-check never walks the ~11k lines of untyped vendored JS. + $foliate: fileURLToPath(new URL('./src/lib/vendor/foliate-js', import.meta.url)) + } + }, + optimizeDeps: { + // Behind a dynamic import in foliate's fixed-layout.js, so Vite would + // otherwise discover it mid-session and force a full page reload the first + // time someone opens a fixed-layout EPUB. + include: ['construct-style-sheets-polyfill'] + } });