From 3e02485973540096a3b55979186a7e9579620f51 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Thu, 23 Jul 2026 09:56:28 +0100 Subject: [PATCH] fix: include filename extension in imports --- jest.config.cjs | 15 +++ src/ComapeoCoreModule.ts | 206 ++++++++++++++++++++------------------- src/index.ts | 4 +- src/sentry-metrics.ts | 15 +-- src/sentry.ts | 22 ++--- tsconfig.json | 4 +- 6 files changed, 142 insertions(+), 124 deletions(-) create mode 100644 jest.config.cjs diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 00000000..88f14041 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,15 @@ +/** @type {import('jest').Config} */ +module.exports = { + // Source uses `.js` extensions on relative imports (required for the + // emitted ESM to resolve at runtime under `moduleResolution: nodenext`), + // but the files on disk are `.ts` and `tsc` rewrites nothing. Jest's + // resolver doesn't apply TypeScript's `.js`→`.ts` remap, so strip the + // extension back off before it resolves. + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + // All Jest tests live under src/. Scoping roots here (rather than the + // default rootDir) keeps the test/haste scan out of backend/, ios/, and + // any transient .claude/ worktrees without naming machine-specific paths. + roots: ["/src"], +}; diff --git a/src/ComapeoCoreModule.ts b/src/ComapeoCoreModule.ts index 2e376610..3e50a7d0 100644 --- a/src/ComapeoCoreModule.ts +++ b/src/ComapeoCoreModule.ts @@ -8,7 +8,7 @@ import { type MessageEventPayload, type NotificationPermissionResponse, type StateChangeEventPayload, -} from "./ComapeoCore.types"; +} from "./ComapeoCore.types.js"; import type { MessagePortLike } from "rpc-reflector"; import { createComapeoCoreClient, @@ -21,8 +21,8 @@ import * as Sentry from "@sentry/react-native"; // `@sentry/react-native@7`; `@sentry/core` is a direct dep of RN so // the import is safe. import { getTraceData, startNewTrace } from "@sentry/core"; -import type { SentryInitConfig } from "./sentry"; -import { rpcClientMetric, rpcStatusFor } from "./sentry-metrics"; +import type { SentryInitConfig } from "./sentry.js"; +import { rpcClientMetric, rpcStatusFor } from "./sentry-metrics.js"; // `onRequestHook` request type derived from `createComapeoCoreClient` so // any hook-signature change up-stream is a compile error here. The @@ -347,109 +347,111 @@ const debugTracingEnabled = (() => { return prefs.diagnosticsEnabled && prefs.debug; })(); -export const comapeo: ComapeoCoreClientApi = createComapeoCoreClient(messagePort, { - timeout: RPC_TIMEOUT_MS, - onRequestHook: (request, next) => { - // Sentry-not-initialised guard. `isInitialized` lives in `@sentry/core` - // and is reachable through the namespace at runtime but isn't on the - // public type surface — defensive accessor in case the helper isn't - // wired through in older SDK releases. - const isInitialized = ( - Sentry as unknown as { - isInitialized?: () => boolean; +export const comapeo: ComapeoCoreClientApi = createComapeoCoreClient( + messagePort, + { + timeout: RPC_TIMEOUT_MS, + onRequestHook: (request, next) => { + // Sentry-not-initialised guard. `isInitialized` lives in `@sentry/core` + // and is reachable through the namespace at runtime but isn't on the + // public type surface — defensive accessor in case the helper isn't + // wired through in older SDK releases. + const isInitialized = ( + Sentry as unknown as { + isInitialized?: () => boolean; + } + ).isInitialized; + const sentryUp = typeof isInitialized !== "function" || isInitialized(); + const method = request.method.join("."); + + // Metrics/tracing only — the hook never captures exceptions. An RPC + // rejection is often expected control flow (e.g. NotFound) that the + // caller may not want reported; deciding what's report-worthy is the + // caller's job, at the call site. The metric layer no-ops when Sentry is + // off. Per-RPC traces (below) only run under `debug`. + const recordMetric = (start: number, status: string) => { + rpcClientMetric(method, status, performance.now() - start); + }; + + if (!sentryUp || !debugTracingEnabled) { + const start = performance.now(); + const responsePromise = next(request); + responsePromise + .then( + () => recordMetric(start, "ok"), + (error: unknown) => recordMetric(start, rpcStatusFor(error)), + ) + .catch(noop); + return; } - ).isInitialized; - const sentryUp = - typeof isInitialized !== "function" || isInitialized(); - const method = request.method.join("."); - - // Metrics/tracing only — the hook never captures exceptions. An RPC - // rejection is often expected control flow (e.g. NotFound) that the - // caller may not want reported; deciding what's report-worthy is the - // caller's job, at the call site. The metric layer no-ops when Sentry is - // off. Per-RPC traces (below) only run under `debug`. - const recordMetric = (start: number, status: string) => { - rpcClientMetric(method, status, performance.now() - start); - }; - - if (!sentryUp || !debugTracingEnabled) { - const start = performance.now(); - const responsePromise = next(request); - responsePromise - .then( - () => recordMetric(start, "ok"), - (error: unknown) => recordMetric(start, rpcStatusFor(error)), - ) - .catch(noop); - return; - } - const runSpan = () => - Sentry.startSpan( - { - name: method, - op: "rpc.client", - forceTransaction: true, - attributes: { - "rpc.system": "comapeo-ipc", - "rpc.method": method, + const runSpan = () => + Sentry.startSpan( + { + name: method, + op: "rpc.client", + forceTransaction: true, + attributes: { + "rpc.system": "comapeo-ipc", + "rpc.method": method, + }, }, - }, - async (span) => { - const { "sentry-trace": sentryTrace, baggage } = getTraceData({ - span, - }); - const tracedRequest: IpcRequestWithMetadata = sentryTrace - ? { - ...request, - metadata: { - "sentry-trace": sentryTrace, - baggage: baggage ?? "", - }, - } - : request; - // Record the metric while the span is active so it links to the - // trace. Duration is measured around the same round-trip the - // span brackets. - const start = performance.now(); - try { - // Split the span duration into "sync send" (JSI hop + UDS write - // to Node) and "await" (entire round-trip incl. response delivery - // back to the JS thread). If the gap between this span and the - // Node-side rpc span is dominated by JS-thread contention on - // cold boot, `rn.send.syncMs` stays small while total stays high. - const sendStart = performance.now(); - const responsePromise = next(tracedRequest); - const sendMs = performance.now() - sendStart; - span.setAttribute?.("rn.send.syncMs", sendMs); - await responsePromise; - span.setStatus?.({ code: 1, message: "ok" }); - recordMetric(start, "ok"); - } catch (error) { - // Mark the span errored for tracing, but do not capture an issue - // — see the metrics-only note on the non-debug path above. - span.setStatus?.({ code: 2, message: "internal_error" }); - recordMetric(start, rpcStatusFor(error)); - } - }, - ); - // Mint a fresh trace_id when there's no caller context worth - // inheriting. Without `startNewTrace`, every standalone RPC pulls - // the trace_id from the isolation-scope's propagation context, - // which is set once at SDK init and never rotates — so unrelated - // RPC calls (across reloads, even across days) end up sharing - // one trace. Skip `app.start.*` parents specifically: the - // `appStartIntegration` keeps its transaction open for ~10s - // post-launch, which would otherwise sweep any RPC fired during - // that window into the App Start trace and make the dashboard - // render them as nested under it. - if (hasInheritableActiveSpan()) { - runSpan(); - } else { - startNewTrace(runSpan); - } + async (span) => { + const { "sentry-trace": sentryTrace, baggage } = getTraceData({ + span, + }); + const tracedRequest: IpcRequestWithMetadata = sentryTrace + ? { + ...request, + metadata: { + "sentry-trace": sentryTrace, + baggage: baggage ?? "", + }, + } + : request; + // Record the metric while the span is active so it links to the + // trace. Duration is measured around the same round-trip the + // span brackets. + const start = performance.now(); + try { + // Split the span duration into "sync send" (JSI hop + UDS write + // to Node) and "await" (entire round-trip incl. response delivery + // back to the JS thread). If the gap between this span and the + // Node-side rpc span is dominated by JS-thread contention on + // cold boot, `rn.send.syncMs` stays small while total stays high. + const sendStart = performance.now(); + const responsePromise = next(tracedRequest); + const sendMs = performance.now() - sendStart; + span.setAttribute?.("rn.send.syncMs", sendMs); + await responsePromise; + span.setStatus?.({ code: 1, message: "ok" }); + recordMetric(start, "ok"); + } catch (error) { + // Mark the span errored for tracing, but do not capture an issue + // — see the metrics-only note on the non-debug path above. + span.setStatus?.({ code: 2, message: "internal_error" }); + recordMetric(start, rpcStatusFor(error)); + } + }, + ); + // Mint a fresh trace_id when there's no caller context worth + // inheriting. Without `startNewTrace`, every standalone RPC pulls + // the trace_id from the isolation-scope's propagation context, + // which is set once at SDK init and never rotates — so unrelated + // RPC calls (across reloads, even across days) end up sharing + // one trace. Skip `app.start.*` parents specifically: the + // `appStartIntegration` keeps its transaction open for ~10s + // post-launch, which would otherwise sweep any RPC fired during + // that window into the App Start trace and make the dashboard + // render them as nested under it. + if (hasInheritableActiveSpan()) { + runSpan(); + } else { + startNewTrace(runSpan); + } + }, }, -}); +); type StateEvents = { stateChange: (state: ComapeoState, error: ComapeoErrorInfo | null) => void; diff --git a/src/index.ts b/src/index.ts index a788c5ee..2b4bffc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,5 +6,5 @@ export { comapeoServicesClient, getNotificationPermissionsAsync, requestNotificationPermissionsAsync, -} from "./ComapeoCoreModule"; -export * from "./ComapeoCore.types"; +} from "./ComapeoCoreModule.js"; +export * from "./ComapeoCore.types.js"; diff --git a/src/sentry-metrics.ts b/src/sentry-metrics.ts index fb204120..f0098d63 100644 --- a/src/sentry-metrics.ts +++ b/src/sentry-metrics.ts @@ -19,9 +19,9 @@ import * as Sentry from "@sentry/react-native"; import { readSentryConfig, readSentryPreferencesAtLaunch, -} from "./ComapeoCoreModule"; -import type { SentryDeviceTags } from "./sentry"; -import { isForbiddenMetric } from "./sentry-scrub"; +} from "./ComapeoCoreModule.js"; +import type { SentryDeviceTags } from "./sentry.js"; +import { isForbiddenMetric } from "./sentry-scrub.js"; type MetricAttributes = Record; @@ -55,9 +55,8 @@ function deviceTags(): { device_class: string; os_major: string } { } function sentryUp(): boolean { - const isInitialized = ( - Sentry as unknown as { isInitialized?: () => boolean } - ).isInitialized; + const isInitialized = (Sentry as unknown as { isInitialized?: () => boolean }) + .isInitialized; return typeof isInitialized !== "function" || isInitialized(); } @@ -127,7 +126,9 @@ function gauge( */ export function rpcStatusFor(error: unknown): string { const name = - error instanceof Error ? error.name : String((error as { name?: string })?.name); + error instanceof Error + ? error.name + : String((error as { name?: string })?.name); if (/timeout/i.test(name)) return "timeout"; return "error"; } diff --git a/src/sentry.ts b/src/sentry.ts index 94b7fdb8..2a5498ba 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -25,14 +25,11 @@ import { setApplicationUsageDataNative, setDebugEnabledNative, type SentryPreferences, -} from "./ComapeoCoreModule"; -import type { ComapeoErrorInfo, ComapeoState } from "./ComapeoCore.types"; -import { SentryTags, SENTRY_OWNED_GLOBAL_KEY } from "./sentry-tags"; -import { scrubEvent, scrubBreadcrumb, scrubLog } from "./sentry-scrub"; -import { - BACKEND_MODULES, - COMAPEO_MODULE_VERSION_LABEL, -} from "./version"; +} from "./ComapeoCoreModule.js"; +import type { ComapeoErrorInfo, ComapeoState } from "./ComapeoCore.types.js"; +import { SentryTags, SENTRY_OWNED_GLOBAL_KEY } from "./sentry-tags.js"; +import { scrubEvent, scrubBreadcrumb, scrubLog } from "./sentry-scrub.js"; +import { BACKEND_MODULES, COMAPEO_MODULE_VERSION_LABEL } from "./version.js"; /** * Subset of `Sentry.init` options that map cleanly from values the @@ -287,9 +284,11 @@ export function initSentry(options: InitSentryOptions = {}): void { // surface (it lives in `@sentry/core`'s utilities and is exposed // through the namespace at runtime). Defensive accessor handles // older SDK releases where the helper isn't wired through. - const maybeIsInitialized = (Sentry as unknown as { - isInitialized?: () => boolean; - }).isInitialized; + const maybeIsInitialized = ( + Sentry as unknown as { + isInitialized?: () => boolean; + } + ).isInitialized; const sdkInitialized = typeof maybeIsInitialized === "function" && maybeIsInitialized(); @@ -555,4 +554,3 @@ function handleAppStateChange(next: AppStateStatus): void { } AppState.addEventListener("change", handleAppStateChange); - diff --git a/tsconfig.json b/tsconfig.json index 8b91b31c..e0bd41e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "expo-module-scripts/tsconfig.base", "compilerOptions": { - "outDir": "./build" + "outDir": "./build", + "module": "nodenext", + "moduleResolution": "nodenext" }, "include": ["./src"], "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]