Skip to content

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.json declares "workspaces": ["tracker-admin-svelte", "tracker-frontend-svelte", "packages/tracker-shared"]. packages/tracker-shared/package.json uses "exports": {"./*": "./src/*"} and declares svelte-hero-icons as a real dependency, svelte as a peer dependency.
  • Both apps need optimizeDeps.exclude: ["tracker-shared"] and ssr.noExternal: ["tracker-shared"] in vite.config.ts — the package ships raw source, and SvelteKit still does an SSR pass at build/dev time even under adapter-static's fallback mode. Without these, esbuild's dep pre-bundler chokes on .svelte files and the SSR module runner treats the package as an unresolvable external Node import.
  • Both apps need @source "../../packages/tracker-shared/src"; in app.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-app working_dir, not just their own app directory — otherwise the container can't see packages/tracker-shared or the root package.json needed for npm to recognize the workspace.

Gotchas when adding to tracker-shared

  • $lib/... doesn't resolve from packages/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 and vitest handle it fine. Fix: add an explicit non-wildcard exports entry (literal target, .ts extension included) for any extensionless .ts export. .svelte imports 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, not undefined — so a ?? "fallback" in the JS module doesn't help an inline <script> boot snippet that reads the placeholder directly (used for per-app PUBLIC_THEME_STORAGE_KEY values, 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-geosearch had to be declared as real dependencies of packages/tracker-shared/package.json (not just each app), or leaflet/dist/leaflet.css failed to resolve during vite build even though each app already depended on leaflet directly.
  • Route files, app.d.ts, and app.css can't move at all — routing is inherently per-app, app.d.ts must live at src/app.d.ts for SvelteKit's ambient types, and app.css's @source directive 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 (isAdmin check + "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 like TrackerCard/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.