visar.log
Technical notes from building things
← all posts

Monorepo Migration: Extracting Source-Specific Logic from hitomi-frontend/backend

The Problem

hitomi-frontend and hitomi-backend were two separate repos with Hitomi-specific logic scattered throughout: hardcoded paths like gallery-dl/hitomi, thumbnail markers like _thumb_, sprite dimensions 100x300, search namespaces, URL builders, archive paths. Every file that touched gallery data had some Hitomi assumption baked in.

This made it impossible to add a second source without forking the entire codebase.

Target Architecture

A monorepo with a clean separation:

gallery-reader/
├── gallery-sources/          # Provider-specific logic
│   └── src/hitomi/index.ts   # ALL hitomi constants + logic
├── gallery-reader/           # Generic frontend (SvelteKit PWA)
├── gallery-server/           # Generic backend (indexer, streamer, downloader)
└── gallery-index/            # JSON registry of sources

The key insight: everything Hitomi-specific can be expressed as a Source interface — a bag of config values and a few pure functions. The server and frontend just import the source and use its properties.

What Got Extracted

The Source interface captures everything a provider needs to define:

Property Hitomi value Used by
gallerySubdir gallery-dl/hitomi scanner, sprite generator, downloader
thumbnailMarker _thumb_ sprite generator (filtering thumb files)
sprite.thumbWidth/Height 100 / 300 sprite worker, frontend CSS
sprite.maxPerStrip 163 sprite generator, frontend strip loader
searchNamespaces type, language, tag, female, male, artist, group, series, character search parser
checkMatch() Tag matching with namespace-specific logic (e.g., female: prefix handling) search filter
download.archivePath() {mediaRoot}/gallery-dl/hitomi.sqlite3 downloader config
download.parseIdFromUrl() Extract ID from {id}.html download queue marker management
toSourceUrl() Build hitomi.la URLs from IDs or search queries frontend downloader integration

History Preservation with git subtree

Used git subtree add --squash to import both repos into the monorepo. This squashes each repo’s history into a single merge commit while keeping the files intact. The original repos stay browsable (now archived) if you need the full history.

The gallery-dl submodule needed special handling — subtree doesn’t preserve submodule links, so it had to be removed and re-added with git submodule add.

npm Workspaces

Root package.json declares all packages as workspaces:

"workspaces": [
    "gallery-sources",
    "gallery-reader",
    "gallery-server/indexer",
    "gallery-server/streamer",
    "gallery-server/downloader"
]

The backend sub-services (indexer, streamer, downloader) each need their own workspace entry because they have independent package.json files with their own dependencies. npm hoists gallery-sources to the root node_modules/ as a symlink, and Node’s module resolution walks up the directory tree to find it.

Build order matters: gallery-sources must compile first (TypeScript to dist/) before anything that imports it can build.

The Rewiring

Most changes were mechanical — replace a hardcoded constant with an import from gallery-sources:

Before (streamer/sprite.ts):

const MAX_THUMBS_PER_STRIP = 163;
const GALLERY_ROOT = path.join(CONFIG.MEDIA_ROOT, 'gallery-dl/hitomi');
// ...
.filter(f => f.includes('_thumb_'))

After:

import { hitomi } from 'gallery-sources';
const { maxPerStrip, thumbWidth, thumbHeight } = hitomi.sprite;
const GALLERY_ROOT = path.join(CONFIG.MEDIA_ROOT, hitomi.gallerySubdir);
// ...
.filter(f => f.includes(hitomi.thumbnailMarker))

The frontend’s toHitomiUrl() function moved into the source as hitomi.toSourceUrl(). The search engine’s KNOWN_NAMESPACES set and checkMatch() function moved wholesale. The downloader’s archive path and gallery ID extraction became source methods.

One path that needed manual attention: the streamer’s FRONTEND_BUILD_PATH was a hardcoded absolute path. Now it resolves relative to __dirname:

FRONTEND_BUILD_PATH: path.resolve(__dirname, '..', '..', '..', 'gallery-reader', 'build'),

The Types Story

Gallery and ImageDimension were defined identically in three places: the indexer’s types.ts, the frontend’s types.ts, and the Go scanner’s struct. Now there’s one canonical definition in gallery-sources/src/types.ts. The indexer re-exports it, the frontend re-exports it. The Go scanner keeps its own struct (it produces the JSON, doesn’t consume the TypeScript type).

Verification

All four packages build cleanly. All three systemd services start and serve correctly from new paths. Search returns 6,958 galleries, sprite endpoint generates strips, downloader queue accepts jobs. The old repos are archived on GitHub.

Files Changed

  • New: gallery-sources/ (package.json, tsconfig, types.ts, hitomi/index.ts, index.ts)
  • New: gallery-index/index.json
  • New: root package.json (workspaces), .gitignore, package-lock.json
  • Modified: gallery-reader types.ts, config.ts, api.ts, package.json
  • Modified: gallery-server indexer types.ts, search.ts, package.json
  • Modified: gallery-server streamer sprite.ts, config.ts, package.json
  • Modified: gallery-server downloader config.ts, queue.manager.ts, package.json
  • Modified: 3 systemd service files (WorkingDirectory paths)
  • Removed: per-package package-lock.json files (replaced by root lockfile)