Admin/Frontend Shared Code Extraction
tracker-admin-svelte and tracker-frontend-svelte share a large amount of
code — the entire login/auth stack, most modals, most of the API layer, and
several core components. That shared code lives in packages/tracker-shared,
a workspace package consumed as raw TS/Svelte source directly by Vite (no
build step of its own). This extraction is done; this doc records the
constraints and gotchas for adding to it, not a task list.
At the time of this extraction there was no production build path for either Svelte app — that was added afterward, see svelte-staging-cutover-todo.md.
Workspace setup
- Root
package.jsondeclares"workspaces": ["tracker-admin-svelte", "tracker-frontend-svelte", "packages/tracker-shared"].packages/tracker-shared/package.jsonuses"exports": {"./*": "./src/*"}and declaressvelte-hero-iconsas a real dependency,svelteas a peer dependency. - Both apps need
optimizeDeps.exclude: ["tracker-shared"]andssr.noExternal: ["tracker-shared"]invite.config.ts— the package ships raw source, and SvelteKit still does an SSR pass at build/dev time even underadapter-static's fallback mode. Without these, esbuild's dep pre-bundler chokes on.sveltefiles and the SSR module runner treats the package as an unresolvable external Node import. - Both apps need
@source "../../packages/tracker-shared/src";inapp.css— Tailwind v4's automatic content detection only scans downward from each app's own directory, so it won't see a sibling package without an explicit@source. - Dev-container Compose services (
admin-svelte-dev,frontend-svelte-dev) mount the whole repo root (.:/workspace) with a per-appworking_dir, not just their own app directory — otherwise the container can't seepackages/tracker-sharedor the rootpackage.jsonneeded for npm to recognize the workspace.
Gotchas when adding to tracker-shared
$lib/...doesn't resolve frompackages/tracker-shared— it's a per-app SvelteKit path alias. A file is only safe to move if it has zero$lib/...imports, or its only such imports already point at something already moved.- TypeScript doesn't extension-probe through the wildcard exports map.
svelte-check/TS can't resolve extensionless specifiers (e.g.tracker-shared/pagination) through"./*": "./src/*", even though Vite's own resolution andvitesthandle it fine. Fix: add an explicit non-wildcardexportsentry (literal target,.tsextension included) for any extensionless.tsexport..svelteimports are unaffected since they already include their own extension in the specifier. %sveltekit.env.X%is a build-time string substitution, not a JS expression. When the env var is unset it renders as an empty string, notundefined— so a?? "fallback"in the JS module doesn't help an inline<script>boot snippet that reads the placeholder directly (used for per-appPUBLIC_THEME_STORAGE_KEYvalues, to avoid a theme-flash on reload). Needs its own JS-level fallback around the placeholder itself:"%sveltekit.env.X%" || "tracker-theme".- A workspace package's own further imports (including CSS) resolve
relative to that package's
package.json, not the consuming app's.leaflet/leaflet-geosearchhad to be declared as real dependencies ofpackages/tracker-shared/package.json(not just each app), orleaflet/dist/leaflet.cssfailed to resolve duringvite buildeven though each app already depended onleafletdirectly. - Route files,
app.d.ts, andapp.csscan't move at all — routing is inherently per-app,app.d.tsmust live atsrc/app.d.tsfor SvelteKit's ambient types, andapp.css's@sourcedirective resolves relative to its own location (moving it would break Tailwind's content detection for the rest of that app). Byte-identical isn't the same as shareable.
Left alone — structurally different, not shareable
Documented so nobody re-litigates trying to merge these:
routes/(app)/+layout.svelte— admin adds a role gate (isAdmincheck + "Access denied" screen); frontend has none.routes/(app)/+page.svelte— admin dashboard (calendar heatmap, stats) vs. frontend's live map/table — different features entirely.routes/(app)/trackers/[id]/+page.svelte— admin adds full tracker CRUD; frontend is read-mostly.routes/(app)/locations/+page.svelte— admin has considerably more production-run-scoped logic.
Their shared sub-pieces (API calls, modals, list/card components) are already
in tracker-shared.
Notes
- Files unique to one app (admin: dashboard/users/dark-trackers/CSV import; frontend: map/reports/campaign-management) are genuinely app-specific, not duplication — out of scope for sharing.
- This extraction landed as ~20 independent PRs (#25-#44 on
migration-to-svelte-5), each a self-contained batch (identical files, then trivial-diff reconciliations, then real API/behavior reconciliations, then the higher-risk UI merges likeTrackerCard/TrackerListRow's edit/delete-vs-fetch-status-dot split). That "find what's blocking the most other files, do that first" ordering is worth repeating for any future large duplication cleanup.