Followup to #447. The audit ran on \`as unknown as\` first; this PR closes out \`as any\` separately. After this, \`grep -rn ' as any\\b' apps/ packages/\` returns 0 (excluding tests and node_modules). ## Sites fixed **\`apps/journal/server.ts\`** + **\`packages/jobs/src/types.ts\`** — \`komootBulkImportJob as any\`. The job had a typed payload (\`JobDefinition<KomootBulkImportData>\`) but the worker's \`JobDefinition[]\` array forced a contravariance cast at every site that mixed typed and untyped jobs. Dropped the generic from \`JobDefinition\` entirely; handlers narrow their own \`job.data\`. Only one job (komoot-bulk-import) used the generic, so the surface is tiny. **\`apps/journal/app/lib/connected-services/fit.ts\`** + same pattern in \`routes/sync.import.\$provider.server.ts\` — \`parser.parse(buffer as any, (err: unknown, d: any) => ...)\`. fit-file-parser's TypeScript types require \`Buffer<ArrayBuffer>\` specifically; a generic Node \`Buffer\` is structurally \`Buffer<ArrayBufferLike>\` (which includes SharedArrayBuffer). The runtime accepts either, so narrowed the cast to \`as Buffer<ArrayBuffer>\` — still a cast, but precise about what we're asserting and why. Removed the \`(err, d: any) => …\` ad-hoc callback typings; the library exports a proper \`FitParserCallback\`. **\`apps/mobile/lib/editor/RouteMap.tsx\`** — \`onLongPress\` handler took \`(event: any)\`. maplibre-react-native v11 generates the event type via React Native codegen but doesn't re-export it as a public TypeScript type. Replaced with a local structural slice of the parts we actually read. Net: 4 \`as any\` sites in prod code → 0. \`pnpm typecheck\` / \`lint\` / \`test\` all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
// Shared FIT file → GPX converter for provider importers.
|
|
//
|
|
// FIT is an open standard (Garmin/ANT+) used by Wahoo, Garmin, Coros, and
|
|
// others. This module is shared across all providers that produce FIT files
|
|
// so the conversion logic lives in one place.
|
|
|
|
import FitParser from "fit-file-parser";
|
|
import { generateGpx } from "@trails-cool/gpx";
|
|
|
|
export async function fitToGpx(buffer: Buffer, name: string): Promise<string | null> {
|
|
const parsed = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
const parser = new FitParser({ force: true });
|
|
// fit-file-parser's typing requires `Buffer<ArrayBuffer>` specifically;
|
|
// a generic Node `Buffer` is structurally `Buffer<ArrayBufferLike>`.
|
|
// The runtime accepts either, so coerce the underlying buffer slot.
|
|
parser.parse(buffer as Buffer<ArrayBuffer>, (error, data) => {
|
|
if (error) reject(error);
|
|
else resolve((data ?? {}) as Record<string, unknown>);
|
|
});
|
|
});
|
|
|
|
const records = (parsed.records ?? []) as Array<{
|
|
position_lat?: number;
|
|
position_long?: number;
|
|
altitude?: number;
|
|
timestamp?: string | Date;
|
|
}>;
|
|
|
|
const trackPoints = records
|
|
.filter((r) => r.position_lat != null && r.position_long != null)
|
|
.map((r) => ({
|
|
lat: r.position_lat!,
|
|
lon: r.position_long!,
|
|
ele: r.altitude,
|
|
time: r.timestamp instanceof Date ? r.timestamp.toISOString() : r.timestamp,
|
|
}));
|
|
|
|
if (trackPoints.length < 2) return null;
|
|
return generateGpx({ name, tracks: [trackPoints] });
|
|
}
|