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.jsonfiles (replaced by root lockfile)