Skip to content

Conversation

@FyreByrd
Copy link
Collaborator

@FyreByrd FyreByrd commented Nov 12, 2025

Fixes #865

This pull request implement hash-based access for asset files that are designed to be updated.
This includes most images, language resources, and other assets that do not need direct access.

In most cases vite was able to handle this, with the files included via glob import.
There were a few that we manually had to hash instead, notably icons and images included in static files (i.e. manifest.json, HTML books).

Summary by CodeRabbit

  • Build & Performance

    • Centralizes generated assets, adds content hashing for cache-busted assets, and pre-bundles images, audio, video, styles and data to reduce runtime network lookups.
  • CI / Workflows

    • Removes older lint/test workflows and adds a consolidated lint & test workflow with multi-job orchestration and caching.
  • Bug Fixes

    • Safer handling of optional assets, fonts and missing files to prevent runtime errors.
  • Chores

    • Updates cleanup and check scripts and expands ignore lists for generated assets.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Walkthrough

Migrates generated assets from static/ into src/gen-assets/, adds MD5-based hashed-asset helpers, threads a ConvertBookContext through converters, updates many converters/components/routes to use hashed/generated assets and Vite import.meta.glob maps, and consolidates CI workflows into a single main.yml.

Changes

Cohort / File(s) Summary
CI workflows
\.github/workflows/lint.yml, \.github/workflows/test.yml, \.github/workflows/main.yml
Removed lint.yml and test.yml; added consolidated main.yml with setup, lint, and test jobs including Node/Java toolchain setup, caching, multi-project conversion, and per-project build/test orchestration.
Asset hashing utilities
convert/fileUtils.ts
New MD5-based helpers: getHashedName, getHashedNameFromContents, createHashedFile, createHashedFileFromContents for computing hashed filenames and writing hashed assets.
Conversion pipeline — context & filters
convert/convertBooks.ts, convert/convertConfig.ts, convert/index.ts
Introduced ConvertBookContext; extended FilterFunction and many filter helpers to accept context; propagated dataDir/hashedFileName through book parsing; reordered ConvertManifest in pipeline.
Converters — path & hashing updates
convert/convertAbout.ts, convert/convertBadges.ts, convert/convertContents.ts, convert/convertManifest.ts, convert/convertMedia.ts, convert/convertPlans.ts, convert/convertReverseIndex.ts, convert/convertSQLite.ts, convert/convertStyles.ts, convert/convertMarkdown.ts
Repointed outputs from static/...src/gen-assets/...; contents and manifest processing now create/consume hashed assets; added guarded directory creation and hashed-asset wiring; minor signature change in convertMarkdownsToMilestones.
Conversion — task output change
convert/convertContents.ts
ContentsTaskOutput now includes a data property (ContentsData) in the task result.
Runtime asset loading — glob maps
src/lib/data/*.ts, src/lib/data/*.js, src/lib/scripts/*.ts, src/lib/video/*, src/routes/**
Replaced base-relative fetches with eager import.meta.glob maps pointing at /src/gen-assets/... for catalogs, plans, audio, timings, pkf, wasm/sqlite, clips, thumbnails, videos, quizzes, reversals, badges, illustrations, styles, etc.; added strict existence checks and adjusted fetch/use sites.
Components — use generated assets / API tweaks
src/lib/components/BookTabs.svelte, BottomNavigationBar.svelte, CollectionList.svelte, FontList.svelte, HtmlBookView.svelte, ScriptureViewSofria.svelte, Sidebar.svelte, TextAppearanceSelector.svelte
Switched from base/template URLs to eager glob lookups under /src/gen-assets/...; HtmlBookView prefers book.hashedFileName ?? book.file; optional chaining added for safe font lookups; createIllustrationBlock now returns { block, source }; imageSrcSet signature updated.
Routes & layout — head and asset imports
src/app.html, src/routes/+layout.svelte, src/routes/about/+page.js, src/routes/lexicon/*, src/routes/quiz/*, src/routes/share/*, src/routes/text/+page.svelte
Removed hard-coded head tags from app.html; layout now imports favicon/apple/manifest and styles from generated assets; routes replaced fetches with module imports/import.meta.glob lookups and updated quiz collection lookup to use page.params.collection in one page.
Data loaders & examples
data-sandbox/index.ts, example/index.ts, src/lib/data/catalogData.ts, src/lib/data/plansData.ts, src/lib/data/stores/lexicon.ts
Data loaders now reference src/gen-assets via glob maps; example/index.ts ensures TEMP_DIR removal/creation; minor loader path adjustments.
Config & scripts
config/index.d.ts, package.json, svelte.config.js
Added optional hashedFileName?: string to BookConfig; updated npm scripts (check, check:watch, simplified clean); added svelte aliases $static -> ./static and $assets -> ./src/gen-assets/.
Ignores
.gitignore, .prettierignore
.gitignore adjusted (broadened manifest/static ignores, added src/gen-assets related rules); .prettierignore added /project_data.

Sequence Diagram(s)

sequenceDiagram
    participant Converter as Converter (build)
    participant FS as File System
    participant Bundler as Vite Bundler
    participant Runtime as Browser

    rect rgb(248,248,240)
    Note over Converter,FS: Conversion phase creates hashed assets
    Converter->>FS: read original asset -> compute MD5 -> createHashedFile(...)
    FS-->>Converter: hashed asset written under src/gen-assets/
    end

    rect rgb(240,248,255)
    Note over Bundler,Runtime: Bundling exposes asset URLs via import.meta.glob
    Bundler->>FS: embed mapping (path -> URL) from src/gen-assets
    Runtime->>Bundler: page request
    Bundler-->>Runtime: page with resolved hashed asset URLs (via glob maps)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas needing extra attention:

  • convert/convertBooks.ts (context threading, many signature changes).
  • convert/convertManifest.ts and convert/convertContents.ts (manifest/icon hashing, manifestUrl.json, asset hashing correctness).
  • convert/fileUtils.ts (hash computation, path resolution, write-safety).
  • All import.meta.glob usages across components/routes (ensure base/query patterns and missing-asset handling).
  • src/lib/data/audio.ts and src/lib/data/stores/lexicon.ts (runtime asset lookups and error paths).
  • src/routes/quiz/[collection]/[id]/+page.svelte (change from refs → page.params).

Possibly related PRs

Suggested reviewers

  • chrisvire

Poem

🐇
I hop and hash each file I find,
Carrots renamed so caches unwind.
Globs gather treats from gen-assets rows,
Build hums softly while the river flows.
A rabbit's wink — the new site grows.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.81% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement hashed file paths' clearly summarizes the main change in the PR, which involves implementing hash-based asset paths to solve cache-busting issues.
Linked Issues check ✅ Passed The PR implements all primary objectives from issue #865 [#865]: hash-based filenames for static assets using MD5, runtime asset resolution via glob imports and new tracking mechanisms, and asset path refactoring across workflows and components.
Out of Scope Changes check ✅ Passed All changes directly support the hashing implementation objective. Workflow consolidations, .gitignore modifications, icon/audio asset updates, and generated assets infrastructure are all necessary components of implementing hash-based cache busting.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/hash-buster

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FyreByrd FyreByrd force-pushed the feature/hash-buster branch 2 times, most recently from de5e608 to ddfe3f2 Compare November 13, 2025 17:19
@FyreByrd
Copy link
Collaborator Author

Rebased on main

FyreByrd added a commit that referenced this pull request Nov 14, 2025
This is necessary to use the `base` option in glob imports for #903

I did have to rebuild the package-lock.json in order to resolve some peer dependency issues.
@FyreByrd FyreByrd mentioned this pull request Nov 14, 2025
chrisvire pushed a commit that referenced this pull request Nov 14, 2025
This is necessary to use the `base` option in glob imports for #903

I did have to rebuild the package-lock.json in order to resolve some peer dependency issues.
judah-sotomayor and others added 5 commits November 14, 2025 14:25
Change convertMedia.ts to place icons in `src/generatedAssets`.
Replace uses of deprecated `base` with calls to `resolve()` in sidebar.
Add aliases for static, images, and generatedAssets.
Add gitignore line for new icons location.
@FyreByrd FyreByrd force-pushed the feature/hash-buster branch from ddfe3f2 to 73143b2 Compare November 14, 2025 20:28
@FyreByrd FyreByrd force-pushed the feature/hash-buster branch from 7440f73 to d9a8dc7 Compare November 17, 2025 16:27
@FyreByrd FyreByrd requested a review from chrisvire November 19, 2025 17:56
@FyreByrd FyreByrd marked this pull request as ready for review November 19, 2025 17:57
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/components/ScriptureViewSofria.svelte (1)

1186-1205: Add error handling for missing illustrations.

When an illustration file is not found in the glob imports (line 1187), imageSource silently becomes an empty string. This causes several issues:

  1. Setting img.src to an empty string (line 1193) triggers a network request to the current page URL
  2. The fullscreen zoom feature (line 1196) will malfunction
  3. No warning is logged, making debugging difficult

Apply this diff to add error handling:

 function createIllustrationBlock(source: string, caption: string | null) {
-    const imageSource = illustrations['./' + source]?.default ?? '';
+    const imageSource = illustrations['./' + source]?.default;
+    
+    if (!imageSource) {
+        console.warn(`Illustration not found: ${source}`);
+        // Return early or use a placeholder
+        const divFigure = document.createElement('div');
+        divFigure.classList.add('image-block');
+        divFigure.style.display = 'none';
+        return { block: divFigure, source: '' };
+    }
+    
     const divFigure = document.createElement('div');
     divFigure.classList.add('image-block');

Alternatively, if missing illustrations should still display (relying on checkImageExists to hide them), at least add logging:

-    const imageSource = illustrations['./' + source]?.default ?? '';
+    const imageSource = illustrations['./' + source]?.default ?? '';
+    if (!imageSource) {
+        console.warn(`Illustration file not found in glob imports: ${source}. Available keys:`, Object.keys(illustrations).slice(0, 5));
+    }
src/routes/share/+page.svelte (1)

105-110: Remove debug console.log.

The console.log statement appears to be a debugging artifact that should be removed before merging to production.

Apply this diff:

-                    {(console.log(
-                        badgeLanguages.includes($language)
-                            ? `Using ${appStoreBadge}`
-                            : `Fallback to ${fallbackAppStoreBadge}`
-                    ),
-                    '')}
🧹 Nitpick comments (17)
src/routes/lexicon/+page.svelte (1)

2-2: Remove unused import.

The base import is no longer used after switching to glob imports for asset loading.

Apply this diff:

-    import { base } from '$app/paths';
convert/convertSQLite.ts (1)

9-11: Update the comment to reflect the new destination path.

The comment references "static folder" but the files are now written to src/generatedAssets/.

Apply this diff:

 /**
- * Copies sqlite-wasm.wasm and data.sqlite to the static folder.
+ * Copies sqlite-wasm.wasm and data.sqlite to the generated assets folder.
  */
convert/convertAbout.ts (1)

8-11: Update the comment to reflect the new destination path.

The comment references "static folder" but the file is now written to src/generatedAssets/.

Apply this diff:

 /**
- * Copies about.partial.html to static folder
+ * Copies about.partial.html to generated assets folder
  */
src/lib/components/CollectionList.svelte (2)

9-12: Consider adding error handling for missing assets.

The glob pattern eagerly loads all illustrations, but there's no runtime validation that the required assets exist in the map.

If an illustration file is missing or the key doesn't match, line 52 will throw a runtime error. Consider adding a fallback or validation:

const illustrations = import.meta.glob('./*', {
    eager: true,
    base: '/src/generatedAssets/illustrations'
}) as Record<string, { default: Object }>;

// Helper function
function getIllustration(imageName: string): string | undefined {
    const key = './' + imageName;
    return illustrations[key]?.default as string | undefined;
}

Then use it with optional chaining:

{#if d.image}
    <img src={getIllustration(d.image) ?? '/fallback.png'} />
{/if}

50-53: Add defensive checks for missing illustration keys.

Direct property access without validation could cause runtime errors if the expected illustration doesn't exist in the preloaded map.

Apply this pattern:

-<img
-    class="layout-image"
-    src={illustrations['./' + d.image].default as string}
-/>
+<img
+    class="layout-image"
+    src={illustrations['./' + d.image]?.default as string ?? ''}
+    alt={d.name}
+/>

The ?. optional chaining prevents errors if the key doesn't exist.

src/routes/lexicon/+page.ts (1)

49-57: Add defensive check for missing reversal index URLs.

Direct property access on line 51 could throw a runtime error if the expected reversal index doesn't exist in the preloaded map.

Apply this pattern:

 for (const [key, ws] of Object.entries(dictionaryConfig.writingSystems)) {
     if (!ws.type.includes('main')) {
-        const response = await fetch(reversalIndexUrls[`./${key}/index.json`].default);
+        const url = reversalIndexUrls[`./${key}/index.json`]?.default;
+        if (!url) {
+            console.warn(`Reversal index URL not found for language: ${key}`);
+            continue;
+        }
+        const response = await fetch(url);
         if (response.ok) {
             reversalIndexes[key] = (await response.json()) as ReversalIndex;
         } else {
             console.warn(`Failed to load reversal index for language: ${key}`);
         }
     }
 }
src/routes/text/+page.svelte (1)

494-494: Consider adding a fallback for missing border assets.

While optional chaining is present (?.default), if the border asset doesn't exist, the CSS variable will be set to url(undefined), which is invalid CSS.

Consider adding a fallback:

-style="--borderImageSource=url({borders['./border.png']?.default});"
+style="--borderImageSource=url({borders['./border.png']?.default ?? 'none'});"

Or check if the border exists before rendering:

{#if borders['./border.png']?.default}
    style="--borderImageSource=url({borders['./border.png'].default});"
{/if}
convert/convertMedia.ts (1)

110-115: Ensure src/generatedAssets base directory exists before copying

If ConvertMedia ever runs before any other task that creates src/generatedAssets, cpSync to path.join('src/generatedAssets', p) can fail because the parent directory is missing. Since mkdirSync is already imported, you can defensively create the base dir once at the start of convertMedia.

     async convertMedia(
         dataDir: string,
         configData: ConfigTaskOutput,
         verbose: number,
         modifiedDirectories: string[]
     ) {
+        // Ensure base output directory exists before rimraf/copy operations
+        mkdirSync(path.join('src', 'generatedAssets'), { recursive: true });
@@
         await Promise.all(
             modifiedDirectories.map((p) =>
-                rimraf(path.join('src/generatedAssets', p)).then(() => {
-                    if (verbose) console.log(`removed ${path.join('src/generatedAssets', p)}`);
+                rimraf(path.join('src', 'generatedAssets', p)).then(() => {
+                    if (verbose) {
+                        console.log(`removed ${path.join('src', 'generatedAssets', p)}`);
+                    }
                     return p;
                 })
             )
         );
@@
         for (const p of modifiedDirectories) {
             cloneDirectory(
                 path.join(dataDir, p),
-                path.join('src/generatedAssets', p),
+                path.join('src', 'generatedAssets', p),
                 verbose,
                 !required.includes(p)
             );
@@
-                    const tabsPath = path.join('src', 'generatedAssets', 'icons', 'tabs');
+                    const tabsPath = path.join('src', 'generatedAssets', 'icons', 'tabs');

(The last hunk just keeps joins consistent.)

Also applies to: 118-125, 141-153

.github/workflows/main.yml (1)

44-49: Make project_data cache key sensitive to changes under test_data/projects

hashFiles('test_data/projects', 'convert/*.ts') hashes the directory entry, not its contents, so cache invalidation won’t occur when individual project zip files change. Using a recursive glob will better reflect the actual inputs:

-                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects', 'convert/*.ts') }}
+                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects/**', 'convert/*.ts') }}
@@
-                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects', 'convert/*.ts') }}
+                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects/**', 'convert/*.ts') }}
@@
-                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects', 'convert/*.ts') }}
+                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects/**', 'convert/*.ts') }}
@@
-                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects', 'convert/*.ts') }}
+                  key: ${{ runner.os }}-projects-${{ hashFiles('test_data/projects/**', 'convert/*.ts') }}

Also applies to: 116-122, 139-144, 179-184

convert/convertManifest.ts (1)

17-17: Minor optimization: Create icons directory only if icons exist.

The static/icons directory is created whenever a manifest exists, even if no icons are present. While harmless, you could optimize by creating it only when the first icon is encountered.

Consider moving this line inside the icon processing block (after line 38) with a flag to ensure it's created only once:

-        mkdirSync(path.join('static', 'icons'), { recursive: true });
         const fileContents = readFileSync(srcFile).toString();
         const lines = fileContents.split('\n');
+        let iconsCreated = false;
         contents = lines
             .map((line) => {
                 ...
                 if (line.includes('"src":') && line.includes('./icons')) {
+                    if (!iconsCreated) {
+                        mkdirSync(path.join('static', 'icons'), { recursive: true });
+                        iconsCreated = true;
+                    }
                     ...
                 }
             })
convert/fileUtils.ts (1)

5-8: Consider binary file handling and add error handling.

Two concerns with getHashedName:

  1. Binary files: Converting the file contents to a string using String(readFileSync(fullPath)) could corrupt binary data. If this function is used for images or other binary assets, the hash might be computed on corrupted data.

  2. Error handling: readFileSync can throw if the file doesn't exist or isn't readable.

If binary files are expected, read as Buffer directly:

 export function getHashedName(dataDir: string, src: string) {
     const fullPath = join(dataDir, src);
-    return getHashedNameFromContents(String(readFileSync(fullPath)), src);
+    try {
+        const contents = readFileSync(fullPath);
+        return getHashedNameFromContents(contents.toString('utf8'), src);
+    } catch (error) {
+        throw new Error(`Failed to read ${fullPath}: ${error.message}`);
+    }
 }

Alternatively, if binary files are in scope, update getHashedNameFromContents to accept Buffer | string and handle both cases.

src/lib/components/BookTabs.svelte (1)

51-51: Add fallback for missing icons.

The code directly accesses tabIcons[\./${getImageName(...)}`].default` without checking if the icon exists in the imported map. If an icon is missing, this will cause a runtime error.

Add a helper function with error handling:

+    function getIconSrc(tabType) {
+        const imageName = getImageName(tabType);
+        const icon = tabIcons[`./${imageName}`];
+        if (!icon) {
+            console.error(`Icon not found: ${imageName}`);
+            return ''; // or a fallback icon path
+        }
+        return icon.default;
+    }

     <picture class:invert={$theme === 'Dark'}>
         <img
-            src={tabIcons[`./${getImageName(bookTabs.mainType)}`].default}
+            src={getIconSrc(bookTabs.mainType)}

Apply the same pattern on line 67.

Also applies to: 67-67

src/lib/components/BottomNavigationBar.svelte (1)

136-136: Add fallback for missing icons.

Similar to BookTabs.svelte, accessing menuIcons[...] without validation could cause runtime errors if an icon is missing from the generated assets.

Add error handling:

+    function getIconSrc(item) {
+        const iconKey = `./${item.images[0].file}`;
+        const icon = menuIcons[iconKey];
+        if (!icon) {
+            console.error(`Menu icon not found: ${item.images[0].file}`);
+            return ''; // or a fallback icon
+        }
+        return icon.default;
+    }

     <img
-        src={menuIcons[`./${item.images[0].file}`].default}
+        src={getIconSrc(item)}
         alt=""
convert/convertStyles.ts (1)

79-102: Consider failing build on missing fonts.

Currently, if a font is not found, a warning is logged but the build continues with a potentially invalid path reference. Consider whether this should be a hard error to catch missing font files during build.

If you want to fail on missing fonts, apply this pattern:

         } else {
             console.warn(
                 `${srcFile}: Could not locate ${path.join(dataDir, fontPath)}`
             );
+            throw new Error(`Missing font file: ${fontPath}`);
         }
src/routes/quiz/[collection]/[id]/+page.js (1)

37-37: Add optional chaining for missing quiz files.

While the try-catch handles fetch errors, it won't provide a clear error message if the quiz file is not in the glob map. Consider adding optional chaining.

Apply this diff:

-            const response = await fetch(quizzes[`./${collection}/quizzes/${id}.json`].default);
+            const quizUrl = quizzes[`./${collection}/quizzes/${id}.json`]?.default;
+            if (!quizUrl) {
+                throw new Error(`Quiz not found: ${collection}/quizzes/${id}.json`);
+            }
+            const response = await fetch(quizUrl);
convert/convertContents.ts (2)

156-169: Consider using explicit string prefix removal.

The regex /contents\// without the g flag only replaces the first occurrence. If createHashedFile consistently returns paths starting with contents/, consider using more explicit string methods:

-                            ).replace(/contents\//, '');
+                            ).replace(/^contents\//, '');

The ^ anchor makes the intent clearer and ensures only the leading prefix is removed.


51-53: Consider consolidating the interface declarations.

The ContentsTaskOutput interface is declared twice (lines 51-53 and 286-288). While TypeScript's declaration merging makes this work, consolidating into a single declaration would be clearer:

-export interface ContentsTaskOutput extends TaskOutput {
-    taskName: 'ConvertContents';
-}
-
 const data: ContentsData = {};

 export interface ContentsTaskOutput extends TaskOutput {
     taskName: 'ConvertContents';
+    data: ContentsData;
 }

Then remove the duplicate declaration at lines 286-288.

Also applies to: 286-288

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ccb0ac and 55a8234.

📒 Files selected for processing (48)
  • .github/workflows/lint.yml (0 hunks)
  • .github/workflows/main.yml (1 hunks)
  • .github/workflows/test.yml (0 hunks)
  • .gitignore (1 hunks)
  • .prettierignore (1 hunks)
  • config/index.d.ts (1 hunks)
  • convert/convertAbout.ts (1 hunks)
  • convert/convertBadges.ts (1 hunks)
  • convert/convertBooks.ts (12 hunks)
  • convert/convertConfig.ts (5 hunks)
  • convert/convertContents.ts (3 hunks)
  • convert/convertManifest.ts (4 hunks)
  • convert/convertMedia.ts (4 hunks)
  • convert/convertPlans.ts (1 hunks)
  • convert/convertReverseIndex.ts (1 hunks)
  • convert/convertSQLite.ts (2 hunks)
  • convert/convertStyles.ts (3 hunks)
  • convert/fileUtils.ts (1 hunks)
  • convert/index.ts (1 hunks)
  • data-sandbox/index.ts (1 hunks)
  • example/index.ts (2 hunks)
  • package.json (1 hunks)
  • src/app.html (0 hunks)
  • src/lib/components/BookTabs.svelte (3 hunks)
  • src/lib/components/BottomNavigationBar.svelte (2 hunks)
  • src/lib/components/CollectionList.svelte (2 hunks)
  • src/lib/components/FontList.svelte (1 hunks)
  • src/lib/components/HtmlBookView.svelte (1 hunks)
  • src/lib/components/ScriptureViewSofria.svelte (5 hunks)
  • src/lib/components/Sidebar.svelte (6 hunks)
  • src/lib/components/TextAppearanceSelector.svelte (1 hunks)
  • src/lib/data/audio.ts (3 hunks)
  • src/lib/data/catalogData.ts (2 hunks)
  • src/lib/data/plansData.ts (2 hunks)
  • src/lib/data/scripture.js (2 hunks)
  • src/lib/data/stores/lexicon.ts (1 hunks)
  • src/lib/scripts/milestoneLinks.ts (2 hunks)
  • src/lib/video/index.ts (3 hunks)
  • src/routes/+layout.svelte (3 hunks)
  • src/routes/about/+page.js (1 hunks)
  • src/routes/lexicon/+page.svelte (2 hunks)
  • src/routes/lexicon/+page.ts (2 hunks)
  • src/routes/quiz/[collection]/[id]/+page.js (2 hunks)
  • src/routes/quiz/[collection]/[id]/+page.svelte (10 hunks)
  • src/routes/share/+page.js (1 hunks)
  • src/routes/share/+page.svelte (2 hunks)
  • src/routes/text/+page.svelte (3 hunks)
  • svelte.config.js (1 hunks)
💤 Files with no reviewable changes (3)
  • .github/workflows/lint.yml
  • .github/workflows/test.yml
  • src/app.html
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-06-16T18:47:15.122Z
Learnt from: judah-sotomayor
Repo: sillsdev/appbuilder-pwa PR: 840
File: src/lib/components/TextAppearanceSelector.svelte:165-197
Timestamp: 2025-06-16T18:47:15.122Z
Learning: In src/lib/components/TextAppearanceSelector.svelte, the user judah-sotomayor considers theme selection buttons not relevant for screen-reader users and prefers not to add accessibility labels to theme buttons.

Applied to files:

  • src/lib/components/TextAppearanceSelector.svelte
📚 Learning: 2025-07-04T18:51:27.821Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 845
File: src/routes/text/+page.svelte:3-3
Timestamp: 2025-07-04T18:51:27.821Z
Learning: In Svelte 5, the `page` store is imported from `$app/state` instead of `$app/stores`. This is part of the migration from Svelte 4 to Svelte 5 where `$app/stores` is being deprecated in favor of `$app/state`.

Applied to files:

  • src/routes/quiz/[collection]/[id]/+page.svelte
  • src/routes/share/+page.svelte
📚 Learning: 2025-07-04T18:51:27.821Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 845
File: src/routes/text/+page.svelte:3-3
Timestamp: 2025-07-04T18:51:27.821Z
Learning: In SvelteKit 2.12 and Svelte 5, `$app/stores` was deprecated in favor of `$app/state`. When migrating from Svelte 4 to Svelte 5, imports should be changed from `import { page } from '$app/stores';` to `import { page } from '$app/state';`. This change aligns with Svelte 5's reactivity model and provides more granular control over state updates.

Applied to files:

  • src/routes/quiz/[collection]/[id]/+page.svelte
  • src/routes/share/+page.svelte
📚 Learning: 2025-05-07T13:16:04.481Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: All USFM attribute encoding (href, title, etc.) should happen in the convertBooks process via the encodeJmpLinks function, not during the markdown to USFM conversion in convertMarkdown.ts.

Applied to files:

  • convert/convertBooks.ts
📚 Learning: 2025-05-07T13:16:04.481Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: The USFM attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) Markdown to USFM conversion in convertMarkdown.ts should NOT encode attributes like title or href, (2) The encoding of these attributes should happen only in the convertBooks.ts process via the encodeJmpLinks function to prevent double-encoding.

Applied to files:

  • convert/convertBooks.ts
🧬 Code graph analysis (11)
src/lib/data/scripture.js (1)
src/lib/data/stores/scripture.js (2)
  • docSet (110-110)
  • docSet (110-110)
src/routes/lexicon/+page.ts (1)
src/routes/quiz/[collection]/[id]/+page.js (1)
  • response (37-37)
src/lib/data/stores/lexicon.ts (1)
src/routes/quiz/[collection]/[id]/+page.js (1)
  • response (37-37)
convert/convertManifest.ts (1)
convert/fileUtils.ts (2)
  • createHashedFile (21-35)
  • createHashedFileFromContents (37-54)
convert/convertStyles.ts (1)
convert/fileUtils.ts (1)
  • createHashedFile (21-35)
convert/convertConfig.ts (1)
convert/fileUtils.ts (1)
  • getHashedName (5-8)
src/lib/data/plansData.ts (3)
src/routes/plans/+page.js (1)
  • plans (11-11)
src/routes/plans/[id]/+page.js (1)
  • planConfig (10-10)
src/routes/plans/[id]/settings/+page.js (1)
  • planConfig (8-8)
convert/index.ts (1)
convert/convertManifest.ts (1)
  • ConvertManifest (69-82)
src/lib/data/audio.ts (1)
src/lib/scripts/stringUtils.ts (1)
  • pathJoin (200-214)
convert/convertBooks.ts (2)
convert/fileUtils.ts (2)
  • createHashedFile (21-35)
  • getHashedNameFromContents (10-19)
src/routes/quiz/[collection]/[id]/+page.js (3)
  • collection (13-13)
  • quizzes (4-8)
  • id (12-12)
convert/convertContents.ts (1)
convert/fileUtils.ts (1)
  • createHashedFile (21-35)
🪛 actionlint (1.7.8)
.github/workflows/main.yml

14-14: the runner of "actions/checkout@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


129-129: the runner of "actions/checkout@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


169-169: the runner of "actions/checkout@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🔇 Additional comments (51)
.prettierignore (1)

16-16: Add /project_data to Prettier ignore list — good change.

The addition makes sense given the PR's focus on generated assets and hash-busting. The entry logically groups with other generated/data directories (like /data, /build, example_data, test_data).

Minor observation: There's a slight inconsistency in path notation—some entries use a leading slash (e.g., /build, /data), while others don't (e.g., static, example_data, test_data). If standardizing to all root-level paths with leading slashes is a codebase convention, consider applying it uniformly here as well (optional cleanup).

src/lib/components/TextAppearanceSelector.svelte (1)

161-161: Based on the script output, I can now verify the store initialization:

currentFont Store Analysis:

  • It's a derived store (line 170 in scripture.js) that returns config.fonts[0].family when not initialized
  • Otherwise returns $currentFonts[$refs.collection], which is initialized with fallback logic (lines 151-160)
  • Each collection gets either a valid matching font or defaults to config.fonts[0].family

The optional chaining change in TextAppearanceSelector.svelte:161 is valid defensive coding. While the store initialization appears solid, an edge case exists: if $currentFont doesn't match any font in config.fonts (due to data corruption or config changes), find() returns undefined, causing a runtime error without the ?. operator.

The fix correctly prevents this by safely returning undefined if no matching font is found, allowing the button to render without crashing.


convert/convertReverseIndex.ts (1)

34-34: Path migration verified—all reversal asset references already updated to new location.

The change from static/reversal to src/generatedAssets/reversal is correct. Verification confirms:

  • No hardcoded references to the old static/reversal path remain in the codebase
  • The consumer in src/routes/lexicon/+page.ts (line 13) already configured with base: '/src/generatedAssets/reversal'
  • Asset loading mechanism via import.meta.glob() correctly targets the new path
src/lib/components/FontList.svelte (1)

38-38: Need to search for where the configuration is loaded to understand if bookFonts/colFonts can actually contain fonts not in config.fonts.

Based on the script output from the first execution and the web search results, I have enough information to make a determination. Let me finalize the analysis:

From the earlier script output:

  • fontChoices is a derived store that returns either bookFonts, colFonts, or allFonts
  • allFonts is safely derived from config.fonts.map(), so it cannot have mismatches
  • bookFonts and colFonts come from configuration metadata and could potentially contain font names not in config.fonts
  • The optional chaining ?. on line 38 prevents a crash if find() returns undefined
  • If no font is found, the UI renders as empty string (undefined in template)

The change is a valid defensive improvement. The optional chaining is correct ES2020+ syntax supported in Svelte. The potential issue (font in fontChoices but not in config.fonts) could occur if bookCollections/books metadata has stale or incorrect references, but this is handled safely.

Good defensive improvement; confirm if fallback is needed.

The optional chaining prevents a potential crash if a font from $fontChoices is not found in config.fonts. This could occur if book/collection metadata contains font family names that don't exist in config.fonts.

If this scenario can happen (e.g., due to stale metadata), consider adding a fallback to display the font family name:

{config.fonts.find((x) => x.family === font)?.name ?? font}

This provides better UX than rendering an empty string.

convert/index.ts (1)

41-50: Reordering is safe—no inter-task dependencies exist.

Verification confirms that ConvertManifest and ConvertMedia are independent:

  • ConvertManifest reads only from the source manifest.json file in dataDir, not from generated media outputs
  • Neither task reads outputs from the other (checked via outputs.get())
  • Both write to separate files in src/generatedAssets with no blocking dependencies
  • The reordering has no functional impact
src/lib/data/audio.ts (2)

409-409: LGTM: Good defensive programming.

Adding optional chaining here prevents potential runtime errors if bookCollections is undefined or null.


19-27: Now let me examine the actual code and the repository context:

Incorrect review comment. The directories src/generatedAssets/audio and src/generatedAssets/timings don't exist in the repository. Glob patterns are treated like import specifiers and must be either relative (start with ./) or absolute (start with /, resolved relative to project root). Vite processes glob imports at build time through its macro expansion.

Since these directories cannot be verified to exist in the source code, the shell script verification approach is fundamentally flawed. However, examining the actual code and understanding how it's used is necessary before drawing conclusions about whether this represents a real issue or expected behavior.

Likely an incorrect or invalid review comment.

src/routes/lexicon/+page.svelte (1)

24-28: Consider whether eager loading of all reversal JSONs aligns with your bundle size targets.

The glob configuration is syntactically correct and follows Vite's documented behavior where base accepts an absolute path anchored at the project root, prepending this string to generated import specifiers.

However, I cannot verify the actual bundle size impact because src/generatedAssets/ is build-time generated and not available in the repository. The pattern of eager-loading (eager: true) is used consistently across 20+ locations in your codebase for similar assets (styles, badges, videos, images, collections, etc.), suggesting this is an intentional architectural choice to trade upfront bundle size for guaranteed runtime availability without network requests.

Confirm this eager-loading approach is appropriate given your expected number of reversal JSON files and acceptable initial load time for your users.

src/lib/components/ScriptureViewSofria.svelte (3)

1171-1174: LGTM!

The destructuring correctly adapts to the new return signature of createIllustrationBlock. Ignoring the source value is appropriate here since the image source is already applied within the function, and addIllustrations only needs the block element for placement.


1287-1291: LGTM!

The destructuring correctly extracts both block and source, and properly uses imageSource for the existence check. This demonstrates the intended usage of the refactored createIllustrationBlock return value.


62-65: ****

The original concern about the base option not being supported is incorrect. Vite's import.meta.glob accepts a base option as documented in the Vite Features guide, so the code at lines 62-65 is valid and will work as intended.

The src/generatedAssets/illustrations/ directory not appearing in the repository is expected—it's in .gitignore and is generated at build time.

The code correctly uses the base option to resolve the glob pattern to the intended directory, and the fallback to an empty string on line 1187 is handled by checkImageExists, which hides the element if the image cannot be fetched.

Likely an incorrect or invalid review comment.

convert/convertPlans.ts (1)

25-25: LGTM - Path migration is consistent.

The destination path change from static/plans to src/generatedAssets/plans aligns with the PR's cache-busting strategy.

convert/convertSQLite.ts (1)

14-23: LGTM - Consistent path migration.

The destination paths have been correctly updated to use src/generatedAssets/.

convert/convertAbout.ts (1)

13-13: LGTM - Consistent path migration.

The destination path has been correctly updated to use src/generatedAssets/.

example/index.ts (1)

73-77: LGTM - Good defensive coding.

Ensuring a clean temp directory by deleting and recreating it prevents potential issues with stale files from previous runs.

convert/convertBadges.ts (1)

11-11: LGTM - Consistent path migration.

The destination path change to src/generatedAssets/badges aligns with the PR's asset migration strategy.

src/routes/lexicon/+page.ts (1)

11-15: LGTM - Consistent asset preloading pattern.

The use of import.meta.glob with the ?url query parameter for reversal indexes aligns with the PR's asset-loading strategy.

src/routes/text/+page.svelte (1)

64-64: LGTM - Consistent asset preloading.

The border assets are eagerly preloaded using the same import.meta.glob pattern used throughout the PR.

src/lib/data/scripture.js (1)

8-12: PKF asset glob wiring looks consistent with generatedAssets usage

Using import.meta.glob('./*.pkf', { eager: true, base: '/src/generatedAssets/collections', query: '?url' }) keeps docset resolution aligned with the new generatedAssets layout and avoids hard‑coded static paths.

.gitignore (1)

20-27: Ignoring generated manifests and src/generatedAssets is appropriate

Treating static/manifest*.json and the entire src/generatedAssets tree as generated artifacts keeps the repo clean and matches the new hashed asset pipeline.

.github/workflows/main.yml (1)

9-122: Overall workflow structure and caching strategy look solid

Splitting setup, lint, and test into separate jobs with shared caches for node_modules and pre‑converted project_data should give good CI performance while keeping the expensive Java/AppBuilders step isolated.

src/lib/components/Sidebar.svelte (1)

6-8: Base URL handling and header assets integration look good

Switching to resolve('/') for the home link and importing the drawer header images via $images keeps routing/base handling and static assets consistent with the rest of the app.

Also applies to: 128-132

convert/convertManifest.ts (2)

1-3: LGTM!

The imports are appropriate for the hashing functionality being added.


61-67: LGTM!

The hashed manifest generation and URL tracking approach is clean and aligns well with the cache-busting objectives.

convert/fileUtils.ts (1)

10-19: LGTM! MD5 is appropriate for cache busting.

The use of MD5 with base64url encoding is suitable for generating cache-busting hashes. This is not a security concern since the hashes are used only to detect content changes, not for cryptographic purposes.

config/index.d.ts (1)

63-63: LGTM!

The addition of the optional hashedFileName field is well-documented and appropriately typed. The comment clarifies its current scope.

src/lib/components/HtmlBookView.svelte (1)

37-39: LGTM!

The conditional URL construction correctly uses hashedFileName when available and falls back to file for non-hashed books.

src/lib/components/BookTabs.svelte (1)

18-21: LGTM! Using import.meta.glob for eager preloading.

The use of import.meta.glob with eager: true is appropriate for preloading all tab icons. This ensures icons are bundled and available synchronously.

src/lib/components/BottomNavigationBar.svelte (1)

11-14: LGTM! Consistent pattern with other components.

The use of import.meta.glob for preloading menu icons follows the same pattern as BookTabs.svelte.

src/routes/about/+page.js (1)

5-6: ✓ Verified: $assets alias is correctly configured

The verification confirms that the $assets alias is properly set up in svelte.config.js at line 25, pointing to './src/generatedAssets/'. This ensures the dynamic import import('$assets/about.partial.html?raw') will resolve correctly. The code change from HTTP fetch to dynamic import is sound.

data-sandbox/index.ts (1)

22-22: Collection assets are correctly generated to the new path—no action required.

Verification confirms that convert/convertBooks.ts properly generates all collection assets to src/generatedAssets/collections:

  • Quiz files written to src/generatedAssets/collections/{collectionId}/quizzes/ (lines 435–445)
  • Index file written to src/generatedAssets/collections/index.json (line 535)
  • Catalog entries written to src/generatedAssets/collections/catalog/ (lines 514–517)

The path change in data-sandbox/index.ts line 22 aligns with where files are actually generated.

Note: Lines 419–421 in convert/convertBooks.ts create an unused static/collections directory (stale code from the previous path), but this does not affect functionality since all actual file writes target the correct new location.

src/routes/share/+page.js (1)

4-21: LGTM! Good defensive coding.

The dynamic asset resolution via import.meta.glob with optional chaining and conditional fetch provides robust error handling. The pattern correctly targets the generated badges directory.

src/routes/share/+page.svelte (1)

10-13: LGTM! Type-safe asset loading.

The eager glob import with explicit type assertion ensures compile-time safety. The pattern correctly targets SVG badges from the generated assets directory.

src/lib/video/index.ts (1)

4-12: LGTM! Consistent glob pattern for video assets.

The dual glob imports correctly separate thumbnails and videos into distinct maps with appropriate type assertions.

src/routes/+layout.svelte (2)

3-5: LGTM! Clean direct imports for static assets.

The direct imports leverage the $assets alias correctly and provide type safety for build-time known assets.


35-38: LGTM! Styles glob configured correctly.

The eager glob import for styles aligns with the asset generation strategy across the codebase.

svelte.config.js (1)

21-26: LGTM! Path aliases support new asset structure.

The aliases correctly map to the generated assets directories and enable cleaner imports throughout the codebase.

convert/convertConfig.ts (1)

506-506: LGTM! Signature change enables hash-based filenames.

The addition of dataDir parameter is necessary for computing hashed filenames and aligns with the PR objectives.

convert/convertStyles.ts (1)

21-23: LGTM! Styles migrated to generated assets.

The destination path correctly targets the new generatedAssets structure. Fonts remaining in static/fonts appears intentional for direct URL access in CSS.

src/routes/quiz/[collection]/[id]/+page.js (1)

4-8: LGTM! Recursive glob pattern appropriate for nested structure.

The ** pattern correctly matches the nested collection/quizzes directory structure. The ?url query is consistent with other JSON asset loading patterns.

convert/convertContents.ts (1)

81-88: LGTM!

The introduction of hasContentsDir flag and conditional directory creation is a clean approach to ensure the destination directory is ready before asset processing.

package.json (1)

12-13: LGTM!

Adding svelte-kit sync before svelte-check ensures generated types are up-to-date, preventing type check failures due to stale type definitions. This aligns with SvelteKit best practices.

src/routes/quiz/[collection]/[id]/+page.svelte (4)

4-4: LGTM!

The import of page from $app/state is correct for Svelte 5, replacing the deprecated $app/stores pattern. Based on learnings.


84-88: LGTM!

The backward compatibility approach using staticAssets flag based on version comparison is well-designed, ensuring smooth migration between asset resolution strategies.


146-147: LGTM!

Good use of optional chaining and nullish coalescing to handle missing images gracefully. This defensive pattern should be applied consistently to all glob map accesses (see comment on lines 222-227).


21-34: Bundle size impact of eager loading cannot be verified in this environment.

The directories containing assets (src/generatedAssets/*) are generated at build time and excluded from version control (in .gitignore). Unable to count actual assets or measure bundle impact in the sandbox.

However, code inspection reveals important context:

The eager loading appears necessary because:

  • Assets are accessed synchronously during quiz execution (audio playback, image display)
  • Functions like playSound(), getImageSource(), and getAnswerAudio() expect immediate access to asset objects
  • Lazy loading would require deferred imports, incompatible with these synchronous call patterns

The broad glob patterns (./*) do load all files in each directory, unlike the related +page.js which uses query: '?url' for URLs instead of modules. This design distinction suggests intentional module pre-loading for asset access.

To validate this approach: measure bundle size with/without eager loading, confirm whether all eagerly-loaded assets are actually used in typical quiz scenarios, and review if runtime performance is acceptable.

convert/convertBooks.ts (5)

204-227: LGTM!

The addition of context parameter to updateImgTags enables proper hashed file generation while maintaining clean separation of concerns. The function correctly handles missing images by filtering them out.


414-423: LGTM!

The conditional directory creation for HTML books ensures the necessary directory structure exists before file operations. The use of static/ for illustrations and HTML collection files is appropriate since these need to be statically served.


596-608: Verify hash computation timing for HTML books.

The hash is computed from the original content before filtration (before = getHashedNameFromContents(content, book.file)), but the filtered content is written to the file. This means:

  1. The filename hash is stable based on the source file
  2. If only the filters change (not the source), the filename remains the same
  3. Cached versions won't be busted when filters change

Is this the intended behavior? If filter changes should also bust the cache, consider computing the hash after filtration:

 function convertHtmlBook(context: ConvertBookContext, book: BookConfig, files: any[]) {
     const srcFile = path.join(context.dataDir, 'books', context.bcId, book.file);
 
     let content = fs.readFileSync(srcFile, 'utf-8');
-    const before = getHashedNameFromContents(content, book.file);
     content = applyFilters(content, htmlFilterFunctions, context.bcId, book.id, context);
+    const hashedName = getHashedNameFromContents(content, book.file);
-    // file name already in config from before filtration
-    // don't want to modify config to account for filtration
     files.push({
-        path: path.join('static', 'collections', context.bcId, before),
+        path: path.join('static', 'collections', context.bcId, hashedName),
         content
     });
 }

Alternatively, if the current approach is intentional, clarify in the comments why hashing should be pre-filtration.


367-367: LGTM!

The migration of asset paths from static/ to src/generatedAssets/ is consistent throughout the conversion pipeline, aligning with the PR's objective to implement hash-based cache busting for generated assets.

Also applies to: 438-438, 493-493, 500-500, 509-509, 528-528, 535-535


336-343: LGTM!

The introduction of ConvertBookContext is a clean design that bundles related contextual data, making the filter pipeline more maintainable and extensible.

Throw error from convertManifest if icon missing

Throw error if audio/timing missing

Throw error if catalog not found

Throw error if missing plan data

Throw error for missing docset

Throw error for missing wasm/sqlite

Log error for missing reversals
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
convert/convertBooks.ts (1)

191-277: Use ConvertBookContext.dataDir in isImageMissing to avoid hard-coded paths.

removeMissingFigures and updateImgTags now both hinge on isImageMissing(imageSource), but isImageMissing still uses a hard-coded 'data/illustrations' root:

return !fs.existsSync(path.join('data', 'illustrations', imageSource));

If convertBooks is ever run with a dataDir other than 'data', this will incorrectly treat existing images as missing (and strip <fig> / <img>), or vice versa. It also defeats the point of threading ConvertBookContext through the filters.

Consider changing isImageMissing to accept the context and use its dataDir, and pass that through from the filters. For example:

-function isImageMissing(imageSource: string): boolean {
-    return !fs.existsSync(path.join('data', 'illustrations', imageSource));
-}
+function isImageMissing(imageSource: string, context: ConvertBookContext): boolean {
+    return !fs.existsSync(path.join(context.dataDir, 'illustrations', imageSource));
+}

And update the call sites:

-        if (isImageMissing(imageSource)) {
+        if (isImageMissing(imageSource, _ctx)) {
...
-            if (isImageMissing(fileName)) {
+            if (isImageMissing(fileName, context)) {

This keeps the logic correct for non-default data directories and aligns with the new context plumbing.

Also applies to: 234-257

src/lib/data/catalogData.ts (1)

37-61: Clarify behavior when catalog fetch/parse fails (result may be undefined)

If the fetch fails, response.ok is false, or response.json() throws, the .catch will log the error but result will remain undefined, even though the function is typed to return Promise<CatalogData>.

Consider making the failure mode explicit, for example:

 export async function loadCatalog(docSet: string): Promise<CatalogData> {
     let result: CatalogData;
@@
-    await fetchFn(entry)
+    await fetchFn(entry)
         .then((response) => {
             if (!response.ok) {
                 throw new Error(
                     `Could not get catalog for ${docSet} (Response ${response.status}: ${response.statusText})`
                 );
             }
             return response.json();
         })
         .then((data) => {
             result = data;
         })
-        .catch((error) => {
-            console.error(`Could not get catalog for ${docSet}:`, error);
-        });
-    return result;
+        .catch((error) => {
+            console.error(`Could not get catalog for ${docSet}:`, error);
+            throw error;
+        });
+    return result!;

or alternatively adjust the return type to allow undefined and handle that at call sites. Either way, callers shouldn’t unexpectedly receive undefined where CatalogData is promised.

♻️ Duplicate comments (4)
src/lib/video/index.ts (2)

213-216: Handle missing thumbnail entries more defensively (optional)

If video.thumbnail ever doesn’t correspond to a key in thumbnails, the CSS will end up with url('undefined'). That won’t throw, but it does produce a broken background image. A small guard keeps this resilient:

-    videoContainerDiv.style.setProperty(
-        'background-image',
-        `url('${thumbnails['./' + video.thumbnail]}')`
-    );
+    const thumbnailUrl = thumbnails[`./${video.thumbnail}`] ?? '';
+    if (thumbnailUrl) {
+        videoContainerDiv.style.setProperty(
+            'background-image',
+            `url('${thumbnailUrl}')`
+        );
+    }

This still assumes your asset pipeline is normally correct, but degrades more gracefully if it’s not.


223-227: Guard against missing video asset map entries when wiring onclick (optional)

If videos[\./${video.filename}`]is missing, the current code will generateplayVideoFile('ID', 'undefined')`, which won’t crash but will silently fail to play anything. You can cheaply guard and fall back to the online URL:

-    if (sourceType === 'assets') {
-        videoLink.setAttribute(
-            'onclick',
-            `playVideoFile('${id}', '${videos[`./${video.filename}`]}'); return false;`
-        );
-    } else {
-        videoLink.setAttribute(
-            'onclick',
-            "playOnlineVideo('" + id + "', '" + videoUrl + "'); return false;"
-        );
-    }
+    if (sourceType === 'assets') {
+        const videoSrc = videos[`./${video.filename}`];
+        if (videoSrc) {
+            videoLink.setAttribute(
+                'onclick',
+                `playVideoFile('${id}', '${videoSrc}'); return false;`
+            );
+        } else {
+            // Fallback to online URL if local asset is missing
+            videoLink.setAttribute(
+                'onclick',
+                `playOnlineVideo('${id}', '${videoUrl}'); return false;`
+            );
+        }
+    } else {
+        videoLink.setAttribute(
+            'onclick',
+            `playOnlineVideo('${id}', '${videoUrl}'); return false;`
+        );
+    }

That way, an occasional mismatch between config and generated assets doesn’t break the UI interaction.

src/routes/quiz/[collection]/[id]/+page.svelte (1)

21-37: Defensively handle missing audio assets in glob maps (and avoid .replace on undefined)

The new glob-based audio loading is good, but a missing entry in quizAssets or clips can cause failures, especially here:

function getQuizAssetAudio(file) {
    return staticAssets ? 'assets/' + file : quizAssets[`./${file}`].replace(/^\//, '');
}

If quizAssets[\./${file}`]is undefined (bad filename, asset not generated, etc.), this will throw a runtime error due to.replaceonundefined`. Past reviews on this file have already flagged similar issues for glob maps.

You can harden this and also improve fallback behavior for clips:

    const staticAssets = compareVersions(config.programVersion, '12.0') < 0;

    function getQuizAssetAudio(file) {
-        return staticAssets ? 'assets/' + file : quizAssets[`./${file}`].replace(/^\//, '');
+        if (staticAssets) {
+            return `assets/${file}`;
+        }
+
+        const key = `./${file}`;
+        const url = quizAssets[key];
+        if (!url) {
+            console.warn(`Quiz audio asset not found for key ${key}, falling back to /assets/${file}`);
+            return `assets/${file}`;
+        }
+
+        return url.replace(/^\//, '');
    }

    function getAnswerAudio(quiz, correct) {
        let sound;
        if (correct) {
-            sound = quiz.rightAnswerAudio
-                ? clips[`./${getRandomAudio(quiz.rightAnswerAudio)}`]
-                : getQuizAssetAudio('quiz-right-answer.mp3');
+            sound = quiz.rightAnswerAudio
+                ? clips[`./${getRandomAudio(quiz.rightAnswerAudio)}`] ??
+                  getQuizAssetAudio('quiz-right-answer.mp3')
+                : getQuizAssetAudio('quiz-right-answer.mp3');
        } else {
-            sound = quiz.wrongAnswerAudio
-                ? clips[`./${getRandomAudio(quiz.wrongAnswerAudio)}`]
-                : getQuizAssetAudio('quiz-wrong-answer.mp3');
+            sound = quiz.wrongAnswerAudio
+                ? clips[`./${getRandomAudio(quiz.wrongAnswerAudio)}`] ??
+                  getQuizAssetAudio('quiz-wrong-answer.mp3')
+                : getQuizAssetAudio('quiz-wrong-answer.mp3');
        }
        return sound;
    }

You might also want similar null-safe access in the explanation/question/answer audio paths (lines 245, 255, 288, 306) to avoid silent failures when a clip entry is missing.

Also applies to: 87-91, 221-231, 235-246, 255-256, 288-289, 306-307

convert/fileUtils.ts (1)

5-18: Add error handling around synchronous file writes to avoid brittle builds

The hashing helpers look good, but createHashedFile and createHashedFileFromContents still call copyFileSync / writeFileSync without guarding against IO errors (permissions, disk full, missing parent directories, etc.). A failure here will hard-crash the conversion step with a less-informative stack trace.

You can wrap the writes similarly to earlier review guidance:

export function createHashedFile(dataDir: string, src: string, verbose: number, destPrefix = '') {
    const fullPath = join(dataDir, src);

    const hashedPath = join(destPrefix, getHashedName(dataDir, src));
    const dest = join('static', hashedPath);

-    if (hashedPath && !existsSync(dest)) {
-        copyFileSync(fullPath, dest);
-        if (verbose) console.log(`converted ${src} to ${dest}`);
-    } else if (verbose) {
+    if (hashedPath && !existsSync(dest)) {
+        try {
+            copyFileSync(fullPath, dest);
+            if (verbose) console.log(`converted ${src} to ${dest}`);
+        } catch (error: any) {
+            console.error(`Failed to copy ${fullPath} to ${dest}:`, error);
+            throw error;
+        }
+    } else if (verbose) {
         console.log(`skipping ${dest}`);
     }

    return hashedPath;
}

export function createHashedFileFromContents(
    contents: string,
    src: string,
    verbose: number,
    destPrefix = ''
) {
    const hashedPath = join(destPrefix, getHashedNameFromContents(contents, src));
    const dest = join('static', hashedPath);

-    if (!existsSync(dest)) {
-        writeFileSync(dest, contents);
-        if (verbose) console.log(`converted ${src} to ${dest}`);
-    } else if (verbose) {
+    if (!existsSync(dest)) {
+        try {
+            writeFileSync(dest, contents);
+            if (verbose) console.log(`converted ${src} to ${dest}`);
+        } catch (error: any) {
+            console.error(`Failed to write hashed contents for ${src} to ${dest}:`, error);
+            throw error;
+        }
+    } else if (verbose) {
         console.log(`skipping ${dest}`);
     }

    return hashedPath;
}

This keeps the build deterministic and surfaces IO problems clearly.

Also applies to: 31-45, 47-64

🧹 Nitpick comments (5)
src/lib/video/index.ts (1)

4-15: Glob-based asset maps look consistent with the hashed-assets approach

Using import.meta.glob('./*', { import: 'default', eager: true, base: '/src/gen-assets/…' }) to build thumbnails / videos maps is a solid way to resolve hashed assets at build time. The Record<string, string> typing matches the expected URL strings from default asset imports, so this looks good as-is. If the number of generated assets grows large, you might later consider narrowing the glob pattern or dropping eager: true to keep bundles smaller, but that’s an optional perf tweak, not a blocker.

convert/convertBadges.ts (1)

11-21: Make badge output directory creation recursive for robustness

mkdirSync(dstBadgeDir) will throw if src/gen-assets doesn’t exist yet and ConvertBadges happens to run before other generators create the parent. Using recursive: true matches the pattern used elsewhere (e.g., tab icon setup) and makes this safer.

-    if (!existsSync(dstBadgeDir)) {
-        mkdirSync(dstBadgeDir);
-    }
+    if (!existsSync(dstBadgeDir)) {
+        mkdirSync(dstBadgeDir, { recursive: true });
+    }
convert/convertManifest.ts (1)

65-71: Confirm how manifestUrl.json is consumed and consider old-manifest cleanup.

createHashedFileFromContents correctly writes a content-hashed static/manifest.<hash>.json and manifestUrl.json records only the hashed filename, not the /static/ prefix. Please confirm the runtime code that reads manifestUrl.json is prepending the correct static base (and BUILD_BASE_PATH if applicable), and consider whether you want a lightweight cleanup step to prune old manifest.*.json files to keep static/ from accumulating stale manifests over time.

convert/convertBooks.ts (1)

641-652: Consider hashing HTML after filters so filenames truly track final content.

In convertHtmlBook you currently:

let content = fs.readFileSync(srcFile, 'utf-8');
const before = getHashedNameFromContents(content, book.file);
content = applyFilters(content, htmlFilterFunctions, context.bcId, book.id, context);
files.push({ path: path.join('static', 'collections', context.bcId, before), content });

This means the filename hash is based on the pre-filter content, while the bytes written may change as filters evolve (e.g., updated <img> rewriting). That weakens cache-busting, since a code change that alters the HTML could leave the filename unchanged.

If feasible, consider hashing the final content instead:

-    const before = getHashedNameFromContents(content, book.file);
-    content = applyFilters(content, htmlFilterFunctions, context.bcId, book.id, context);
+    content = applyFilters(content, htmlFilterFunctions, context.bcId, book.id, context);
+    const before = getHashedNameFromContents(content, book.file);

If you intentionally want the hash to depend only on the original source file (to keep paths stable across filter tweaks), it’d be worth adding a brief comment to document that tradeoff.

convert/convertStyles.ts (1)

21-24: Avoid using path.join for URL segments in CSS font paths

The new font handling correctly hashes and relocates fonts, but using path.join to build fontPath (and then writing it back into CSS) can introduce backslashes on Windows (fonts\MyFont.woff2), which won’t match /fonts/MyFont.woff2 in CSS and can break the replacement.

You can keep filesystem joins for disk paths but use POSIX-style joining (or manual concatenation) for URL segments:

-                    const fontName = line.split('/fonts/')[1].replace(/("|')\).*$/, '');
-                    const fontPath = path.join('fonts', fontName);
+                    const fontName = line.split('/fonts/')[1].replace(/("|')\).*$/, '');
+                    const fontPathFs = path.join('fonts', fontName);
+                    const fontPathUrl = `fonts/${fontName}`;
                     let finalPath = fontPath;
-                    if (existsSync(path.join(dataDir, fontPath))) {
-                        finalPath = createHashedFile(dataDir, fontPath, verbose);
+                    if (existsSync(path.join(dataDir, fontPathFs))) {
+                        finalPath = createHashedFile(dataDir, fontPathFs, verbose);
                     } else if (existsSync(path.join(dataDir, 'cloud', fontName))) {
                         finalPath = createHashedFile(
                             path.join(dataDir, 'cloud'),
                             fontName,
                             verbose,
                             'fonts'
                         );
                     } else {
                         console.warn(
-                            `${srcFile}: Could not locate ${path.join(dataDir, fontPath)}`
+                            `${srcFile}: Could not locate ${path.join(dataDir, fontPathFs)}`
                         );
                     }
-                    line = line.replace('/' + fontPath, '/' + finalPath);
+                    line = line.replace('/' + fontPathUrl, '/' + finalPath);

This keeps CSS URLs consistent across platforms while still using path.join for filesystem operations.

Also applies to: 79-101

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55a8234 and b702d70.

📒 Files selected for processing (37)
  • .github/workflows/main.yml (1 hunks)
  • .gitignore (1 hunks)
  • convert/convertAbout.ts (1 hunks)
  • convert/convertBadges.ts (1 hunks)
  • convert/convertBooks.ts (18 hunks)
  • convert/convertManifest.ts (4 hunks)
  • convert/convertMarkdown.ts (1 hunks)
  • convert/convertMedia.ts (4 hunks)
  • convert/convertPlans.ts (1 hunks)
  • convert/convertReverseIndex.ts (1 hunks)
  • convert/convertSQLite.ts (2 hunks)
  • convert/convertStyles.ts (3 hunks)
  • convert/fileUtils.ts (1 hunks)
  • data-sandbox/index.ts (1 hunks)
  • package.json (1 hunks)
  • src/lib/components/BookTabs.svelte (3 hunks)
  • src/lib/components/BottomNavigationBar.svelte (2 hunks)
  • src/lib/components/CollectionList.svelte (2 hunks)
  • src/lib/components/HtmlBookView.svelte (1 hunks)
  • src/lib/components/ScriptureViewSofria.svelte (5 hunks)
  • src/lib/components/Sidebar.svelte (6 hunks)
  • src/lib/data/audio.ts (3 hunks)
  • src/lib/data/catalogData.ts (2 hunks)
  • src/lib/data/plansData.ts (2 hunks)
  • src/lib/data/scripture.js (2 hunks)
  • src/lib/data/stores/lexicon.ts (1 hunks)
  • src/lib/scripts/milestoneLinks.ts (2 hunks)
  • src/lib/video/index.ts (3 hunks)
  • src/routes/+layout.svelte (3 hunks)
  • src/routes/lexicon/+page.svelte (2 hunks)
  • src/routes/lexicon/+page.ts (2 hunks)
  • src/routes/quiz/[collection]/[id]/+page.js (2 hunks)
  • src/routes/quiz/[collection]/[id]/+page.svelte (10 hunks)
  • src/routes/share/+page.js (1 hunks)
  • src/routes/share/+page.svelte (2 hunks)
  • src/routes/text/+page.svelte (3 hunks)
  • svelte.config.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/routes/lexicon/+page.svelte
  • convert/convertSQLite.ts
  • src/routes/+layout.svelte
  • .gitignore
  • convert/convertPlans.ts
  • src/routes/share/+page.svelte
  • src/lib/components/ScriptureViewSofria.svelte
  • package.json
  • src/lib/components/HtmlBookView.svelte
  • src/lib/scripts/milestoneLinks.ts
  • convert/convertAbout.ts
  • src/lib/data/plansData.ts
  • src/routes/lexicon/+page.ts
  • src/lib/components/BottomNavigationBar.svelte
  • src/routes/share/+page.js
  • src/lib/components/CollectionList.svelte
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-05-07T13:16:04.481Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: All USFM attribute encoding (href, title, etc.) should happen in the convertBooks process via the encodeJmpLinks function, not during the markdown to USFM conversion in convertMarkdown.ts.

Applied to files:

  • convert/convertMarkdown.ts
  • convert/convertBooks.ts
📚 Learning: 2025-06-23T20:54:56.765Z
Learnt from: judah-sotomayor
Repo: sillsdev/appbuilder-pwa PR: 850
File: src/routes/notes/edit/[noteid]/+page.js:0-0
Timestamp: 2025-06-23T20:54:56.765Z
Learning: In SvelteKit projects, verify the specific error handling pattern used in the codebase, as there may be variations from the standard `throw error(status, message)` syntax shown in official documentation.

Applied to files:

  • convert/fileUtils.ts
📚 Learning: 2025-05-07T13:16:04.481Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: The USFM attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) Markdown to USFM conversion in convertMarkdown.ts should NOT encode attributes like title or href, (2) The encoding of these attributes should happen only in the convertBooks.ts process via the encodeJmpLinks function to prevent double-encoding.

Applied to files:

  • convert/convertBooks.ts
📚 Learning: 2025-07-04T18:51:27.821Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 845
File: src/routes/text/+page.svelte:3-3
Timestamp: 2025-07-04T18:51:27.821Z
Learning: In Svelte 5, the `page` store is imported from `$app/state` instead of `$app/stores`. This is part of the migration from Svelte 4 to Svelte 5 where `$app/stores` is being deprecated in favor of `$app/state`.

Applied to files:

  • src/routes/quiz/[collection]/[id]/+page.svelte
📚 Learning: 2025-07-04T18:51:27.821Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-pwa PR: 845
File: src/routes/text/+page.svelte:3-3
Timestamp: 2025-07-04T18:51:27.821Z
Learning: In SvelteKit 2.12 and Svelte 5, `$app/stores` was deprecated in favor of `$app/state`. When migrating from Svelte 4 to Svelte 5, imports should be changed from `import { page } from '$app/stores';` to `import { page } from '$app/state';`. This change aligns with Svelte 5's reactivity model and provides more granular control over state updates.

Applied to files:

  • src/routes/quiz/[collection]/[id]/+page.svelte
🧬 Code graph analysis (7)
convert/convertStyles.ts (1)
convert/fileUtils.ts (1)
  • createHashedFile (31-45)
src/lib/data/stores/lexicon.ts (1)
src/routes/quiz/[collection]/[id]/+page.js (1)
  • response (38-38)
src/lib/data/audio.ts (1)
src/lib/scripts/stringUtils.ts (1)
  • pathJoin (200-214)
src/lib/data/scripture.js (1)
src/lib/data/stores/scripture.js (2)
  • docSet (110-110)
  • docSet (110-110)
convert/convertBooks.ts (2)
convert/fileUtils.ts (2)
  • createHashedFile (31-45)
  • getHashedNameFromContents (20-29)
src/routes/quiz/[collection]/[id]/+page.js (4)
  • collection (14-14)
  • quizzes (4-9)
  • book (17-17)
  • id (13-13)
src/lib/data/catalogData.ts (2)
src/lib/data/scripture.js (3)
  • key (33-33)
  • docSet (20-20)
  • entry (34-34)
src/lib/data/stores/scripture.js (3)
  • docSet (110-110)
  • docSet (110-110)
  • entry (48-48)
convert/convertManifest.ts (1)
convert/fileUtils.ts (2)
  • createHashedFile (31-45)
  • createHashedFileFromContents (47-64)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: test
🔇 Additional comments (24)
.github/workflows/main.yml (2)

1-216: Good: Previous checkout@v2 → v4 concern has been resolved.

All three checkout actions (lines 14, 129, 169) now use actions/checkout@v4, addressing the deprecation concern from the earlier review.


64-74: Docker environment unavailable for container inspection.

The sandbox environment does not have Docker available, preventing verification of the actual container contents and image size. However, code inspection confirms the concern is valid: line 71 copies the entire root filesystem (/) into $HOME/app-builders/, yet line 74 shows only sab.sh is actually used, and line 73 operates only on *.sh files.

To resolve this, manually verify:

  • Whether ghcr.io/sillsdev/app-builders:latest contains significant non-tool files that could be excluded
  • Whether the .sh scripts depend on files outside their own directory (e.g., libraries, config in parent directories)
  • Whether extracting only specific files (e.g., sab.sh, dab.sh) would break runtime dependencies
src/lib/data/stores/lexicon.ts (3)

48-59: Confirm import.meta.glob key format with base option

Because you’re passing a base option into import.meta.glob, the exact shape of the returned keys can differ by Vite version/config (e.g., ./data.sqlite vs /src/gen-assets/data.sqlite). Since later lookups use ./sql-wasm.wasm and ./data.sqlite as keys, please double‑check that the keys produced at build time actually match these literals; otherwise initialization will always hit the “asset not found” error even when the files exist. A quick runtime console.log(Object.keys(sqliteUrl), Object.keys(wasmUrl)) in dev or a small assertion test would make this explicit.


66-72: Explicit wasm asset check closes earlier vague TypeError risk

The wasmKey/wasmEntry pattern with a clear error message if the entry is missing makes the failure mode much more descriptive and avoids the previous .default access TypeError. This is a solid improvement to initialization robustness.


79-84: SQLite asset resolution is now validated before fetch

The analogous dbKey/dbEntry guard for the SQLite file mirrors the wasm handling and ensures missing or misconfigured assets fail fast with a clear error instead of an obscure runtime failure. This keeps the lexicon DB initialization behavior predictable.

src/routes/text/+page.svelte (3)

498-498: LGTM: CSS variable approach aligns with hashed asset strategy.

The dynamic CSS variable injection correctly implements the hashed asset pattern described in the PR objectives. The approach is consistent with similar implementations in other components.

Note: Ensure the border asset verification from the previous comment passes to confirm borders['./border.png'] resolves correctly.


587-587: LGTM: Correct CSS variable usage.

The update to use var(--borderImageSource) correctly references the CSS custom property set on line 498, completing the dynamic border asset implementation.


64-68: The border asset is build-time generated and follows the established pattern; however, verify input data contains the required border file and consider error handling.

The src/gen-assets/borders/ directory is created during the build process by convert/convertMedia.ts, which copies from input data. This is the intended design and follows the same pattern used for images, videos, audio, and other assets throughout the codebase.

However, the concern about missing error handling remains valid: if the input data lacks borders/border.png, then borders['./border.png'] will be undefined, resulting in invalid CSS (url(undefined)). While the code follows codebase conventions, you should:

  1. Verify your input data contains borders/border.png
  2. Consider adding a fallback: const borderUrl = borders['./border.png'] || '';
src/lib/components/BookTabs.svelte (1)

18-22: Glob‑based tab icon loading looks correct

Using import.meta.glob('./*', { import: 'default', eager: true, base: '/src/gen-assets/icons/tabs' }) and indexing with tabIcons[\./${getImageName(...)}`]` aligns with the new generated‑assets layout and should resolve the correct hashed icon URLs at build time. No further issues from this change.

Also applies to: 52-52, 68-68

svelte.config.js (1)

22-25: New $static and $assets aliases align with asset layout

The aliases point to the expected folders and match how assets are being imported elsewhere (e.g., $assets/images/...). This should make the new gen‑assets pipeline easier to use.

convert/convertMarkdown.ts (1)

10-17: Signature change to include context parameter is safe

Adding _ctx: unknown to convertMarkdownsToMilestones extends the API for context passing without changing the conversion behavior, and the underscore naming avoids unused‑parameter noise. The link/USFM logic remains untouched, which is correct given that USFM attribute encoding is handled elsewhere.

convert/convertMedia.ts (1)

63-69: Media copy/removal retargeting to src/gen-assets is consistent

Switching the rimraf targets, clone destinations, and legacy tab‑icon path to src/gen-assets/... keeps this task aligned with the rest of the asset pipeline while preserving existing behavior (including recursive directory copies and logging). The added TODOs on rarely‑used directories are also fine as internal notes.

Also applies to: 111-113, 121-123, 141-153

src/lib/data/scripture.js (1)

8-13: PKF asset globbing and guarded lookup are solid

Preloading PKF files via import.meta.glob('./*.pkf', { import: 'default', eager: true, base: '/src/gen-assets/collections', query: '?url' }) and having getDocSetUrl resolve ./${docSet}.pkf with an explicit error on missing entries makes the docSet URL resolution both build‑time friendly and safer than the prior unchecked access. This integrates well with the hashed gen‑assets setup.

Also applies to: 32-38

src/lib/components/Sidebar.svelte (1)

6-8: New menu icon loading and imageSrcSet implementation look correct

Using menuIcons = import.meta.glob('./*', { import: 'default', eager: true, base: '/src/gen-assets/icons/menu-items' }) and then building srcset entries as ${url} ${multiplier}x (with a separate density suffix and a filtered list) fixes the earlier glob‑key issue and should yield valid responsive icon srcsets. The <img> fallback sourcing from menuIcons[\./${item.images[0].file}`] ?? ''` is also consistent.

Also applies to: 43-47, 81-96, 272-276

convert/convertManifest.ts (1)

14-21: Icon hashing logic is now robust and fail-fast.

The switch to a regex-based "src" match plus an explicit existsSync check and Error on missing icon fixes the earlier fragile parsing and silent failure mode. This should make manifest processing much more predictable and surface missing-icon issues early. No further changes needed here.

Also applies to: 30-49

src/lib/data/audio.ts (1)

19-29: Glob-based asset loading and existence checks look correct; just verify key alignment.

Using import.meta.glob('./*', { import: 'default', eager: true, base: '/src/gen-assets/audio' }) and then resolving audioKey = \./${audio.filename}`/timingKey = `./${audio.timingFile}`is a good fit, and the explicit checks with clearErrormessages address the earlier risk of undefined.defaultaccess. Please double-check at build time that the glob actually emits keys of the form./` so these lookups never miss due to a path mismatch (e.g., stray subdirectories or leading slashes).

Also applies to: 440-457

convert/convertBooks.ts (4)

118-158: JMP encoding is correctly centralized in convertBooks.

Having encodeJmpLinks here in the books conversion pipeline (instead of in convertMarkdown.ts) keeps all USFM attribute encoding in one place and avoids double-encoding of href/title attributes. This matches the earlier workflow guidance about where attribute encoding should happen.

Based on learnings


234-257: Hashed illustration handling and supporting directories look consistent.

updateImgTags now filters out missing images and rewrites surviving <img> tags to use createHashedFile with context.dataDir and illustrations/${fileName}, emitting <img src="${base}/${imagePath}">. Combined with the static/illustrations and static/collections/<collection.id> mkdirs when HTML books are present, this wiring is coherent with the rest of the static asset strategy. Once isImageMissing is updated to honor context.dataDir, this pipeline should be solid.

Also applies to: 409-420, 459-468


332-364: Filter function signature/context wiring is consistent; just confirm convertMarkdownsToMilestones.

Defining FilterFunction as (text, bcId, bookId, context) and updating both usfmFilterFunctions and htmlFilterFunctions to pass context through applyFilters gives a clear, uniform hook for context-dependent transforms (like hashed assets). All the local filters in this file now match that shape; please just confirm that convertMarkdownsToMilestones in convertMarkdown.ts also accepts context: ConvertBookContext as its fourth parameter so everything stays consistent at the type level.

Also applies to: 784-799


409-420: Collection assets now consistently live under src/gen-assets/collections.

The updated paths for quizzes (src/gen-assets/collections/<bcId>/quizzes/*.json), frozen pkf archives (src/gen-assets/collections/<docSet>.pkf), catalog entries, and collections/index.json all line up with the new generated-assets convention. This matches the import patterns elsewhere (e.g., quiz routes using base: '/src/gen-assets/collections'), so the relocation from static/collections looks coherent.

Also applies to: 533-555, 571-581

data-sandbox/index.ts (1)

22-27: Sandbox now reads pkf data from src/gen-assets/collections, matching converters.

Pointing staticDir at src/gen-assets/collections keeps the sandbox loader in sync with where ConvertBooks now writes .pkf and index.json. This preserves the existing behavior with the new asset layout.

convert/convertReverseIndex.ts (1)

33-36: Reversal assets relocated under src/gen-assets/reversal in line with other generated assets.

Updating outputDir to src/gen-assets/reversal/<language> keeps reversal index files consistent with the new generated-assets structure used elsewhere (collections, badges, etc.). The rest of the logic is unchanged and should continue to work as before.

src/lib/data/catalogData.ts (1)

1-6: Nice defensive check on catalog glob lookup

The new key/entry guard before fetching the catalog JSON resolves the earlier risk of accessing a missing glob entry and gives a clear error if the asset is absent. This aligns well with the new asset-loading approach.

Also applies to: 39-45

src/routes/quiz/[collection]/[id]/+page.svelte (1)

4-5: ****

The original review comment is incorrect. In SvelteKit 2.x / Svelte 5, page imported from $app/state is a read-only reactive object (not a Svelte store), so the $ prefix is not needed. Access params directly as page.params rather than $page.params.

The code in this PR correctly uses page.params.collection throughout, which is the proper pattern. The review's assertion that this pattern "will likely not work" is incorrect—ignore this comment and proceed with the current implementation.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b702d70 and 9743e2c.

📒 Files selected for processing (4)
  • .gitignore (1 hunks)
  • convert/convertStyles.ts (3 hunks)
  • package.json (1 hunks)
  • src/routes/quiz/[collection]/[id]/+page.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • src/routes/quiz/[collection]/[id]/+page.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: lint
🔇 Additional comments (2)
.gitignore (2)

18-18: Good pattern choice for hashed manifest files.

The wildcard pattern static/manifest*.json correctly supports hashed versions of the manifest file (e.g., manifest-abc123.json) as part of the cache-busting strategy, while being more maintainable than hardcoding individual filenames.


25-25: Appropriate addition for generated assets directory.

Adding src/gen-assets to gitignore aligns with the PR's migration of generated assets and is correct since generated content should not be committed.

Verify that src/gen-assets contains only generated files and no legitimate source code.

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.

Break files out of cache with hashes

3 participants