From 0227dac9f712af593c59fc693f8b4b54c2c477e2 Mon Sep 17 00:00:00 2001 From: Raphael Rouiller Date: Tue, 7 Jul 2026 14:16:58 +0200 Subject: [PATCH] fix(api): add .js extensions to relative imports for ESM runtime resolution Vercel's Node runtime resolves compiled functions as ESM, which requires explicit file extensions on relative imports. The extensionless imports crashed every /api function with ERR_MODULE_NOT_FOUND once the project was rebuilt with the current Vercel builder. Adding .js works for both ESM and CJS and is resolved by vite/esbuild in dev and tests. --- api/_shared/rate-limit.ts | 2 +- api/booking/_lib/d1.ts | 2 +- api/booking/_lib/google-calendar.ts | 2 +- api/booking/_lib/mailer.ts | 2 +- api/booking/_lib/slots.test.ts | 2 +- api/booking/_lib/slots.ts | 2 +- api/booking/_lib/tokens.ts | 2 +- api/booking/availability.ts | 10 +++++----- api/booking/cancel.ts | 14 +++++++------- api/booking/create.ts | 18 +++++++++--------- api/booking/lookup.ts | 8 ++++---- api/booking/reschedule.ts | 16 ++++++++-------- api/newsletter.ts | 4 ++-- api/send.ts | 4 ++-- 14 files changed, 44 insertions(+), 44 deletions(-) diff --git a/api/_shared/rate-limit.ts b/api/_shared/rate-limit.ts index f1ace30..85c03a5 100644 --- a/api/_shared/rate-limit.ts +++ b/api/_shared/rate-limit.ts @@ -1,6 +1,6 @@ import type { IncomingHttpHeaders } from 'http'; -import type { ApiResponse } from './http-types'; +import type { ApiResponse } from './http-types.js'; import recaptcha from './recaptcha.js'; const { extractClientIp } = recaptcha; diff --git a/api/booking/_lib/d1.ts b/api/booking/_lib/d1.ts index 4b137d8..0cdd9db 100644 --- a/api/booking/_lib/d1.ts +++ b/api/booking/_lib/d1.ts @@ -1,4 +1,4 @@ -import { getRuntimeEnv } from './config'; +import { getRuntimeEnv } from './config.js'; // We're on Vercel, not on Cloudflare, so we hit D1's HTTP query API rather // than binding the database. The API accepts a single SQL statement plus an diff --git a/api/booking/_lib/google-calendar.ts b/api/booking/_lib/google-calendar.ts index 4f4d653..5037bcd 100644 --- a/api/booking/_lib/google-calendar.ts +++ b/api/booking/_lib/google-calendar.ts @@ -1,4 +1,4 @@ -import { getRuntimeEnv } from './config'; +import { getRuntimeEnv } from './config.js'; // Thin wrapper around the Google Calendar v3 REST API. We avoid pulling the // `googleapis` package (heavy, slow cold-start) and stay on direct `fetch` diff --git a/api/booking/_lib/mailer.ts b/api/booking/_lib/mailer.ts index d700d7f..a1742d9 100644 --- a/api/booking/_lib/mailer.ts +++ b/api/booking/_lib/mailer.ts @@ -5,7 +5,7 @@ import { MAIL_FROM, NOTIFICATION_EMAIL, getRuntimeEnv, -} from './config'; +} from './config.js'; // Render-side helpers are kept lean and inline: no template engine, no // per-locale dispatch table, just two functions per email kind (subject + html) diff --git a/api/booking/_lib/slots.test.ts b/api/booking/_lib/slots.test.ts index 00dd606..9873088 100644 --- a/api/booking/_lib/slots.test.ts +++ b/api/booking/_lib/slots.test.ts @@ -4,7 +4,7 @@ import { buildAvailableSlots, isSlotValid, type BusyInterval, -} from './slots'; +} from './slots.js'; // All test inputs are anchored in UTC. The slots module renders them against // Europe/Zurich (UTC+1 in winter, UTC+2 in summer), so the expected wall-clock diff --git a/api/booking/_lib/slots.ts b/api/booking/_lib/slots.ts index 2b5a526..a707c95 100644 --- a/api/booking/_lib/slots.ts +++ b/api/booking/_lib/slots.ts @@ -6,7 +6,7 @@ import { MEETING_DURATION_MIN, MIN_NOTICE_MS, SLOT_GRANULARITY_MIN, -} from './config'; +} from './config.js'; // All timestamps in this module are UTC. The "9-18 Europe/Zurich" window is // resolved per-day using Intl.DateTimeFormat in BOOKING_TIMEZONE because that diff --git a/api/booking/_lib/tokens.ts b/api/booking/_lib/tokens.ts index 5d0c311..216c2d3 100644 --- a/api/booking/_lib/tokens.ts +++ b/api/booking/_lib/tokens.ts @@ -1,6 +1,6 @@ import { createHmac, randomUUID, timingSafeEqual } from 'crypto'; -import { getRuntimeEnv } from './config'; +import { getRuntimeEnv } from './config.js'; // Cancellation / reschedule links are stateless-ish: each booking gets a token // derived from `HMAC(secret, bookingId + ":" + purpose + ":" + exp + ":" + version)`. diff --git a/api/booking/availability.ts b/api/booking/availability.ts index 8c95ecb..38a535c 100644 --- a/api/booking/availability.ts +++ b/api/booking/availability.ts @@ -1,13 +1,13 @@ -import type { ApiRequest, ApiResponse } from '../_shared/http-types'; +import type { ApiRequest, ApiResponse } from '../_shared/http-types.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from '../_shared/rate-limit'; +} from '../_shared/rate-limit.js'; -import { getBusyIntervals } from './_lib/google-calendar'; -import { buildAvailableSlots } from './_lib/slots'; -import { parseAvailabilityRange } from './_lib/validators'; +import { getBusyIntervals } from './_lib/google-calendar.js'; +import { buildAvailableSlots } from './_lib/slots.js'; +import { parseAvailabilityRange } from './_lib/validators.js'; const AVAILABILITY_RATE_LIMIT = { limit: 60, diff --git a/api/booking/cancel.ts b/api/booking/cancel.ts index baadeff..b97a1f5 100644 --- a/api/booking/cancel.ts +++ b/api/booking/cancel.ts @@ -1,15 +1,15 @@ -import type { ApiRequest, ApiResponse } from '../_shared/http-types'; +import type { ApiRequest, ApiResponse } from '../_shared/http-types.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from '../_shared/rate-limit'; +} from '../_shared/rate-limit.js'; -import { getRuntimeEnv } from './_lib/config'; -import { exec, queryFirst } from './_lib/d1'; -import { deleteEvent } from './_lib/google-calendar'; -import { sendBookingCancellation } from './_lib/mailer'; -import { verifyToken } from './_lib/tokens'; +import { getRuntimeEnv } from './_lib/config.js'; +import { exec, queryFirst } from './_lib/d1.js'; +import { deleteEvent } from './_lib/google-calendar.js'; +import { sendBookingCancellation } from './_lib/mailer.js'; +import { verifyToken } from './_lib/tokens.js'; const CANCEL_RATE_LIMIT = { limit: 10, diff --git a/api/booking/create.ts b/api/booking/create.ts index dc7979c..a97fcc2 100644 --- a/api/booking/create.ts +++ b/api/booking/create.ts @@ -1,22 +1,22 @@ -import type { ApiRequest, ApiResponse } from '../_shared/http-types'; +import type { ApiRequest, ApiResponse } from '../_shared/http-types.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from '../_shared/rate-limit'; +} from '../_shared/rate-limit.js'; import recaptcha from '../_shared/recaptcha.js'; import { BOOKING_TIMEZONE, MEETING_DURATION_MIN, getRuntimeEnv, -} from './_lib/config'; -import { exec, isUniqueConstraintError, queryFirst } from './_lib/d1'; -import { createEvent, deleteEvent, getBusyIntervals } from './_lib/google-calendar'; -import { sendBookingConfirmation } from './_lib/mailer'; -import { isSlotValid } from './_lib/slots'; -import { generateToken, newBookingId } from './_lib/tokens'; -import { validateCreatePayload } from './_lib/validators'; +} from './_lib/config.js'; +import { exec, isUniqueConstraintError, queryFirst } from './_lib/d1.js'; +import { createEvent, deleteEvent, getBusyIntervals } from './_lib/google-calendar.js'; +import { sendBookingConfirmation } from './_lib/mailer.js'; +import { isSlotValid } from './_lib/slots.js'; +import { generateToken, newBookingId } from './_lib/tokens.js'; +import { validateCreatePayload } from './_lib/validators.js'; const { extractClientIp, verifyRecaptcha } = recaptcha; diff --git a/api/booking/lookup.ts b/api/booking/lookup.ts index eda2841..542b323 100644 --- a/api/booking/lookup.ts +++ b/api/booking/lookup.ts @@ -1,12 +1,12 @@ -import type { ApiRequest, ApiResponse } from '../_shared/http-types'; +import type { ApiRequest, ApiResponse } from '../_shared/http-types.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from '../_shared/rate-limit'; +} from '../_shared/rate-limit.js'; -import { queryFirst } from './_lib/d1'; -import { verifyToken } from './_lib/tokens'; +import { queryFirst } from './_lib/d1.js'; +import { verifyToken } from './_lib/tokens.js'; const LOOKUP_RATE_LIMIT = { limit: 30, diff --git a/api/booking/reschedule.ts b/api/booking/reschedule.ts index 2e629cd..9706866 100644 --- a/api/booking/reschedule.ts +++ b/api/booking/reschedule.ts @@ -1,20 +1,20 @@ -import type { ApiRequest, ApiResponse } from '../_shared/http-types'; +import type { ApiRequest, ApiResponse } from '../_shared/http-types.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from '../_shared/rate-limit'; +} from '../_shared/rate-limit.js'; import { BOOKING_TIMEZONE, MEETING_DURATION_MIN, getRuntimeEnv, -} from './_lib/config'; -import { exec, isUniqueConstraintError, queryFirst } from './_lib/d1'; -import { getBusyIntervals, updateEventTime } from './_lib/google-calendar'; -import { sendBookingConfirmation } from './_lib/mailer'; -import { isSlotValid } from './_lib/slots'; -import { generateToken, verifyToken } from './_lib/tokens'; +} from './_lib/config.js'; +import { exec, isUniqueConstraintError, queryFirst } from './_lib/d1.js'; +import { getBusyIntervals, updateEventTime } from './_lib/google-calendar.js'; +import { sendBookingConfirmation } from './_lib/mailer.js'; +import { isSlotValid } from './_lib/slots.js'; +import { generateToken, verifyToken } from './_lib/tokens.js'; const RESCHEDULE_RATE_LIMIT = { limit: 5, diff --git a/api/newsletter.ts b/api/newsletter.ts index 54a588f..8e946a9 100644 --- a/api/newsletter.ts +++ b/api/newsletter.ts @@ -1,11 +1,11 @@ import { Resend } from 'resend'; -import type { ApiRequest, ApiResponse } from './_shared/http-types'; +import type { ApiRequest, ApiResponse } from './_shared/http-types.js'; import newsletterMailer from './_shared/newsletter-mailer.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from './_shared/rate-limit'; +} from './_shared/rate-limit.js'; import recaptcha from './_shared/recaptcha.js'; const { newsletterApiErrors, sendNewsletterEmail, validateNewsletterPayload } = newsletterMailer; diff --git a/api/send.ts b/api/send.ts index 599f282..224888b 100644 --- a/api/send.ts +++ b/api/send.ts @@ -1,11 +1,11 @@ import { Resend } from 'resend'; -import type { ApiRequest, ApiResponse } from './_shared/http-types'; +import type { ApiRequest, ApiResponse } from './_shared/http-types.js'; import contactMailer from './_shared/contact-mailer.js'; import { applyRateLimitHeaders, checkRateLimit, getRateLimitIdentifier, -} from './_shared/rate-limit'; +} from './_shared/rate-limit.js'; import recaptcha from './_shared/recaptcha.js'; const { contactApiErrors, sendContactEmails, validateContactPayload } = contactMailer;