From 9c6407423ad49c3c1c09b864469da642f4b5de8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 24 May 2026 12:05:08 +0200 Subject: [PATCH] fix(journal): remove ineffective dynamic imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rollup was warning on 5 modules that were both dynamically and statically imported. With static importers in the same chunk, the dynamic forms buy no chunking benefit — they were leftovers from earlier cycle-avoidance workarounds that no longer apply. Converted to static: - @trails-cool/gpx (routes.\$id.server.ts, sync.import.\$provider.server.ts) - logger.server (boss.server.ts — comment claimed test cycles, but tests pass) - boss.server (activities.server.ts at two sites) - connected-services/manager (komoot/importer.ts, wahoo/importer.ts) - notifications.server (root.tsx) Kept dynamic: fit-file-parser in sync.import.\$provider.server.ts — it's heavy and only the FIT ingestion path needs it, no other static importers exist, so the dynamic actually does chunk-split it. Build is now warning-free. Full repo: pnpm typecheck, pnpm lint, pnpm test, pnpm build all green. 192 tests passed (up from 181 — rate-limit test from #424 + others). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/journal/app/lib/activities.server.ts | 3 +-- apps/journal/app/lib/boss.server.ts | 5 ++--- .../lib/connected-services/providers/komoot/importer.ts | 2 +- .../app/lib/connected-services/providers/wahoo/importer.ts | 2 +- apps/journal/app/root.tsx | 6 ++---- apps/journal/app/routes/routes.$id.server.ts | 2 +- apps/journal/app/routes/sync.import.$provider.server.ts | 7 +++++-- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/journal/app/lib/activities.server.ts b/apps/journal/app/lib/activities.server.ts index 35c85e5..4c66a4c 100644 --- a/apps/journal/app/lib/activities.server.ts +++ b/apps/journal/app/lib/activities.server.ts @@ -5,6 +5,7 @@ import { activities, routes, syncImports, users, follows } from "@trails-cool/db import type { Visibility } from "@trails-cool/db/schema/journal"; import { validateGpx, writeGeom } from "./gpx-save.server.ts"; import type { GpxData } from "./gpx-save.server.ts"; +import { enqueueOptional } from "./boss.server.ts"; export interface ActivityInput { name: string; @@ -36,7 +37,6 @@ export async function updateActivityVisibility( // idempotent, so toggling private→public→private→public won't spam // followers (only the first transition per activity emits). if (visibility === "public") { - const { enqueueOptional } = await import("./boss.server.ts"); await enqueueOptional("notifications-fanout", { activityId: id }, { source: "updateActivityVisibility" }); } @@ -91,7 +91,6 @@ export async function createActivity(ownerId: string, input: ActivityInput) { // updateActivityVisibility path for the case where visibility is set // up-front rather than flipped later). if (input.visibility === "public") { - const { enqueueOptional } = await import("./boss.server.ts"); await enqueueOptional("notifications-fanout", { activityId: id }, { source: "createActivity" }); } diff --git a/apps/journal/app/lib/boss.server.ts b/apps/journal/app/lib/boss.server.ts index 7e4b654..2ebeb1c 100644 --- a/apps/journal/app/lib/boss.server.ts +++ b/apps/journal/app/lib/boss.server.ts @@ -4,6 +4,8 @@ // is bound to the Node process — startWorker calls boss.start(); the // SIGTERM handler stops it. +import { logger } from "./logger.server.ts"; + // Structurally typed (we only need `send`) so we don't have to pull // pg-boss into the journal app's dep graph just for the typedef. interface BossLike { @@ -43,9 +45,6 @@ export async function enqueueOptional( const boss = getBoss(); await boss.send(queue, data); } catch (err) { - // Lazy import to avoid cycles in test environments where logger.server - // pulls in env-dependent setup. - const { logger } = await import("./logger.server.ts"); logger.warn({ err, queue, ...ctx }, "boss.send failed; continuing"); } } diff --git a/apps/journal/app/lib/connected-services/providers/komoot/importer.ts b/apps/journal/app/lib/connected-services/providers/komoot/importer.ts index 713d70a..0d89f42 100644 --- a/apps/journal/app/lib/connected-services/providers/komoot/importer.ts +++ b/apps/journal/app/lib/connected-services/providers/komoot/importer.ts @@ -8,6 +8,7 @@ import { decrypt } from "../../../crypto.server.ts"; import { fetchKomootTours, fetchKomootTourGpx } from "../../../komoot.server.ts"; import { isAlreadyImported, importActivity } from "../../../sync/imports.server.ts"; +import { getServiceById } from "../../manager.ts"; import type { CapabilityContext, ImportableList, @@ -58,7 +59,6 @@ export const komootImporter: Importer = { return ctx.withFreshCredentials(async (rawCreds) => { const creds = rawCreds as KomootCreds; - const { getServiceById } = await import("../../manager.ts"); const service = await getServiceById(ctx.serviceId); if (!service) throw new Error(`Connected service ${ctx.serviceId} not found`); diff --git a/apps/journal/app/lib/connected-services/providers/wahoo/importer.ts b/apps/journal/app/lib/connected-services/providers/wahoo/importer.ts index 63d1968..69637fa 100644 --- a/apps/journal/app/lib/connected-services/providers/wahoo/importer.ts +++ b/apps/journal/app/lib/connected-services/providers/wahoo/importer.ts @@ -7,6 +7,7 @@ import { fitToGpx } from "../../fit.ts"; import { fetchWithTimeout } from "../../../http.server.ts"; import { importActivity, isAlreadyImported } from "../../../sync/imports.server.ts"; +import { getServiceById } from "../../manager.ts"; import type { CapabilityContext, ImportableList, @@ -116,7 +117,6 @@ export const wahooImporter: Importer = { // Resolve the connected service's user id via the capability context. // The caller (route handler) supplies userId out-of-band — for now the // route handler bridges the gap. We use the manager's getServiceById. - const { getServiceById } = await import("../../manager.ts"); const service = await getServiceById(ctx.serviceId); if (!service) throw new Error(`Connected service ${ctx.serviceId} not found`); diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx index 65ff76a..31e6782 100644 --- a/apps/journal/app/root.tsx +++ b/apps/journal/app/root.tsx @@ -9,6 +9,7 @@ import { getSessionUser } from "~/lib/auth/session.server"; import { LocaleProvider } from "~/components/LocaleContext"; import { AlphaBanner } from "~/components/AlphaBanner"; import { useUnreadNotifications } from "~/hooks/useUnreadNotifications"; +import { countUnread } from "~/lib/notifications.server"; import { Footer } from "~/components/Footer"; import { AccountDropdown } from "~/components/AccountDropdown"; import { MobileNavMenu } from "~/components/MobileNavMenu"; @@ -69,12 +70,9 @@ export async function loader({ request }: Route.LoaderArgs) { // Unread-notification count for the navbar bell badge. Pending follow // requests are not separately counted here — each pending request // creates an unread `follow_request_received` notification, so the - // unread count already covers them. Hidden behind a dynamic import so - // the root layout doesn't pull in the notifications module on - // anonymous renders. + // unread count already covers them. let unreadNotifications = 0; if (user) { - const { countUnread } = await import("./lib/notifications.server.ts"); unreadNotifications = await countUnread(user.id); } diff --git a/apps/journal/app/routes/routes.$id.server.ts b/apps/journal/app/routes/routes.$id.server.ts index 7edaddb..f4fe947 100644 --- a/apps/journal/app/routes/routes.$id.server.ts +++ b/apps/journal/app/routes/routes.$id.server.ts @@ -8,6 +8,7 @@ import { getRoute, getRouteWithVersions, deleteRoute, updateRoute } from "~/lib/ import { getDb } from "~/lib/db"; import { syncPushes } from "@trails-cool/db/schema/journal"; import { getService } from "~/lib/connected-services"; +import { computeDays, parseGpxAsync } from "@trails-cool/gpx"; export async function loadRouteDetail(request: Request, id: string | undefined) { const routeId = id ?? ""; @@ -32,7 +33,6 @@ export async function loadRouteDetail(request: Request, id: string | undefined) let waypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string; osmId?: number; poiTags?: Record }> = []; if (route.gpx) { try { - const { computeDays, parseGpxAsync } = await import("@trails-cool/gpx"); const gpxData = await parseGpxAsync(route.gpx); waypoints = gpxData.waypoints.map((w) => ({ lat: w.lat, diff --git a/apps/journal/app/routes/sync.import.$provider.server.ts b/apps/journal/app/routes/sync.import.$provider.server.ts index ba4a6a9..7b51104 100644 --- a/apps/journal/app/routes/sync.import.$provider.server.ts +++ b/apps/journal/app/routes/sync.import.$provider.server.ts @@ -9,6 +9,7 @@ import { } from "~/lib/connected-services"; import { getImportedIds, recordImport } from "~/lib/sync/imports.server"; import { createActivity } from "~/lib/activities.server"; +import { generateGpx } from "@trails-cool/gpx"; export async function loadSyncImportProvider(request: Request, provider: string | undefined) { const user = await requireSessionUser(request); @@ -77,9 +78,11 @@ export async function syncImportProviderAction(request: Request, provider: strin if (!resp.ok) throw new Error(`Download failed: ${resp.status}`); return Buffer.from(await resp.arrayBuffer()); }); - // Lazy-load to avoid bundling fit-file-parser into all routes. + // Lazy-load fit-file-parser only — it's heavy and only the FIT + // ingestion path needs it. @trails-cool/gpx is already pulled in + // statically by other modules in the chunk, so the dynamic import + // there was ineffective. const { default: FitParser } = await import("fit-file-parser"); - const { generateGpx } = await import("@trails-cool/gpx"); const parsed = await new Promise>((resolve, reject) => { const parser = new FitParser({ force: true }); // eslint-disable-next-line @typescript-eslint/no-explicit-any