From 64526687e0dbda3367372d9da05cc814528d6080 Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Tue, 8 Sep 2026 19:35:26 +0200 Subject: [PATCH 1/2] Fix Node ESM imports in Petrinaut Vercel Functions --- apps/petrinaut-website/api/oembed.ts | 4 +- apps/petrinaut-website/api/voice/config.ts | 2 +- .../api/voice/realtime-call.ts | 4 +- apps/petrinaut-website/package.json | 1 + .../vercel-function-entrypoints.test.ts | 131 ++++++++++++++++++ .../src/server/voice/openai-realtime-call.ts | 6 +- .../src/server/voice/openai-voice-config.ts | 2 +- .../server/voice/voice-request-diagnostics.ts | 2 +- yarn.lock | 1 + 9 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts diff --git a/apps/petrinaut-website/api/oembed.ts b/apps/petrinaut-website/api/oembed.ts index d7630168b55..8f28b3c5a47 100644 --- a/apps/petrinaut-website/api/oembed.ts +++ b/apps/petrinaut-website/api/oembed.ts @@ -9,11 +9,11 @@ import { isExampleSlug, PETRINAUT_DEMO_ORIGIN, type ExampleSlug, -} from "../src/examples/catalog-metadata"; +} from "../src/examples/catalog-metadata.js"; import { canonicalSearchString, validateSharedExampleSearch, -} from "../src/examples/example-search"; +} from "../src/examples/example-search.js"; const DEFAULT_WIDTH = 800; const DEFAULT_HEIGHT = 450; diff --git a/apps/petrinaut-website/api/voice/config.ts b/apps/petrinaut-website/api/voice/config.ts index c6239de88db..07651eef807 100644 --- a/apps/petrinaut-website/api/voice/config.ts +++ b/apps/petrinaut-website/api/voice/config.ts @@ -1,4 +1,4 @@ -import { createOpenAIVoiceConfigHandler } from "../../src/server/voice/openai-voice-config"; +import { createOpenAIVoiceConfigHandler } from "../../src/server/voice/openai-voice-config.js"; declare const process: { env: Record; diff --git a/apps/petrinaut-website/api/voice/realtime-call.ts b/apps/petrinaut-website/api/voice/realtime-call.ts index dd6dee68187..7f047f5dcc0 100644 --- a/apps/petrinaut-website/api/voice/realtime-call.ts +++ b/apps/petrinaut-website/api/voice/realtime-call.ts @@ -1,5 +1,5 @@ -import { createOpenAIRealtimeCallHandler } from "../../src/server/voice/openai-realtime-call"; -import { reportVoiceDiagnostic } from "../../src/voice-diagnostics"; +import { createOpenAIRealtimeCallHandler } from "../../src/server/voice/openai-realtime-call.js"; +import { reportVoiceDiagnostic } from "../../src/voice-diagnostics.js"; declare const process: { env: Record; diff --git a/apps/petrinaut-website/package.json b/apps/petrinaut-website/package.json index d90b45168e0..a6d24ef3723 100644 --- a/apps/petrinaut-website/package.json +++ b/apps/petrinaut-website/package.json @@ -42,6 +42,7 @@ "@fast-check/vitest": "0.4.1", "@tanstack/router-generator": "1.167.32", "@tanstack/router-plugin": "1.168.34", + "@types/node": "22.18.13", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@typescript/native-preview": "7.0.0-dev.20260511.1", diff --git a/apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts b/apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts new file mode 100644 index 00000000000..f9810f647ab --- /dev/null +++ b/apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts @@ -0,0 +1,131 @@ +/// + +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { stripTypeScriptTypes } from "node:module"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { describe, expect, it } from "vitest"; + +const appRoot = fileURLToPath(new URL("../..", import.meta.url)); +const entrypointPaths = [ + "api/chat.ts", + "api/oembed.ts", + "api/voice/config.ts", + "api/voice/realtime-call.ts", +] as const; + +const importDeclarationPattern = + /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["'];?/gu; +const exportDeclarationPattern = + /(?:^|\n)\s*export\s+(?:type\s+)?(?:\*|\{[\s\S]*?\})\s+from\s+["']([^"']+)["'];?/gu; + +const staticModuleSpecifiers = (source: string): string[] => { + const specifiers: string[] = []; + + for (const pattern of [importDeclarationPattern, exportDeclarationPattern]) { + for (const match of source.matchAll(pattern)) { + const specifier = match[1]; + if (specifier !== undefined) { + specifiers.push(specifier); + } + } + } + + return specifiers; +}; + +const emittedPath = (outputRoot: string, sourcePath: string): string => + join(outputRoot, relative(appRoot, sourcePath).replace(/\.ts$/u, ".js")); + +const emitNativeEsmGraph = async ( + outputRoot: string, + sourcePath: string, + emittedSourcePaths: Set, +): Promise => { + const absoluteSourcePath = resolve(sourcePath); + if (emittedSourcePaths.has(absoluteSourcePath)) { + return; + } + emittedSourcePaths.add(absoluteSourcePath); + + const source = await readFile(absoluteSourcePath, "utf8"); + const javascript = stripTypeScriptTypes(source, { + mode: "strip", + }); + const outputPath = emittedPath(outputRoot, absoluteSourcePath); + await mkdir(dirname(outputPath), { recursive: true }); + await writeFile(outputPath, javascript); + + for (const specifier of staticModuleSpecifiers(javascript)) { + if (!specifier.startsWith(".")) { + continue; + } + if (!specifier.endsWith(".js")) { + throw new Error( + `${relative(appRoot, absoluteSourcePath)} imports ${specifier} without a .js extension`, + ); + } + + const importedSourcePath = resolve( + dirname(absoluteSourcePath), + specifier.replace(/\.js$/u, ".ts"), + ); + await emitNativeEsmGraph( + outputRoot, + importedSourcePath, + emittedSourcePaths, + ); + } +}; + +describe("Petrinaut Vercel Function entrypoints", () => { + it("loads every local import graph with native Node ESM resolution", async () => { + const temporaryParent = join(appRoot, ".turbo"); + await mkdir(temporaryParent, { recursive: true }); + const outputRoot = await mkdtemp( + join(temporaryParent, "vercel-function-entrypoints-"), + ); + + try { + await writeFile( + join(outputRoot, "package.json"), + `${JSON.stringify({ type: "module" })}\n`, + ); + + const emittedSourcePaths = new Set(); + for (const entrypointPath of entrypointPaths) { + await emitNativeEsmGraph( + outputRoot, + resolve(appRoot, entrypointPath), + emittedSourcePaths, + ); + } + + const importScript = entrypointPaths + .map((entrypointPath) => { + const outputPath = emittedPath( + outputRoot, + resolve(appRoot, entrypointPath), + ); + return `await import(${JSON.stringify(pathToFileURL(outputPath).href)});`; + }) + .join("\n"); + const result = spawnSync( + process.execPath, + ["--input-type=module", "--eval", importScript], + { + cwd: appRoot, + encoding: "utf8", + env: { ...process.env, NODE_NO_WARNINGS: "1" }, + }, + ); + + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + } finally { + await rm(outputRoot, { force: true, recursive: true }); + } + }); +}); diff --git a/apps/petrinaut-website/src/server/voice/openai-realtime-call.ts b/apps/petrinaut-website/src/server/voice/openai-realtime-call.ts index db8aac05f1b..ee8d0e394b3 100644 --- a/apps/petrinaut-website/src/server/voice/openai-realtime-call.ts +++ b/apps/petrinaut-website/src/server/voice/openai-realtime-call.ts @@ -2,13 +2,13 @@ import { voiceErrorMessage, type VoiceDiagnosticReporter, type VoiceErrorCode, -} from "../../voice-diagnostics"; +} from "../../voice-diagnostics.js"; import { createOpenAIRealtimeSession, getOpenAIVoiceAvailability, OPENAI_REALTIME_CONNECTION_TIMEOUT_MS, -} from "./openai-voice-policy"; -import { createVoiceRequestDiagnostics } from "./voice-request-diagnostics"; +} from "./openai-voice-policy.js"; +import { createVoiceRequestDiagnostics } from "./voice-request-diagnostics.js"; const OPENAI_REALTIME_CALLS_ENDPOINT = "https://api.openai.com/v1/realtime/calls"; diff --git a/apps/petrinaut-website/src/server/voice/openai-voice-config.ts b/apps/petrinaut-website/src/server/voice/openai-voice-config.ts index f43ce99bc14..699002753df 100644 --- a/apps/petrinaut-website/src/server/voice/openai-voice-config.ts +++ b/apps/petrinaut-website/src/server/voice/openai-voice-config.ts @@ -1,4 +1,4 @@ -import { getOpenAIVoiceAvailability } from "./openai-voice-policy"; +import { getOpenAIVoiceAvailability } from "./openai-voice-policy.js"; interface VoiceEnvironment { readonly OPENAI_VOICE_API_KEY?: string; diff --git a/apps/petrinaut-website/src/server/voice/voice-request-diagnostics.ts b/apps/petrinaut-website/src/server/voice/voice-request-diagnostics.ts index 0673aa2e170..72191f0d67e 100644 --- a/apps/petrinaut-website/src/server/voice/voice-request-diagnostics.ts +++ b/apps/petrinaut-website/src/server/voice/voice-request-diagnostics.ts @@ -8,7 +8,7 @@ import { type VoiceDiagnosticReporter, type VoiceErrorCode, type VoiceOperation, -} from "../../voice-diagnostics"; +} from "../../voice-diagnostics.js"; interface VoiceRequestDiagnosticDependencies { readonly createRequestId?: () => string; diff --git a/yarn.lock b/yarn.lock index cc5c5c0c50e..14b46c4d473 100644 --- a/yarn.lock +++ b/yarn.lock @@ -955,6 +955,7 @@ __metadata: "@tanstack/react-router": "npm:1.170.31" "@tanstack/router-generator": "npm:1.167.32" "@tanstack/router-plugin": "npm:1.168.34" + "@types/node": "npm:22.18.13" "@types/react": "npm:19.2.14" "@types/react-dom": "npm:19.2.3" "@typescript/native-preview": "npm:7.0.0-dev.20260511.1" From 7addf294a84dd91d2b1e95f4a8dd02982009325a Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Tue, 8 Sep 2026 22:39:28 +0200 Subject: [PATCH 2/2] Use NodeNext checks for Petrinaut API functions Co-authored-by: Cursor --- apps/petrinaut-website/api/tsconfig.json | 10 ++ apps/petrinaut-website/package.json | 2 +- .../vercel-function-entrypoints.test.ts | 131 ------------------ apps/petrinaut-website/tsconfig.json | 5 +- 4 files changed, 14 insertions(+), 134 deletions(-) create mode 100644 apps/petrinaut-website/api/tsconfig.json delete mode 100644 apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts diff --git a/apps/petrinaut-website/api/tsconfig.json b/apps/petrinaut-website/api/tsconfig.json new file mode 100644 index 00000000000..3114db80a74 --- /dev/null +++ b/apps/petrinaut-website/api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node"], + "verbatimModuleSyntax": true + }, + "include": ["./**/*.ts"] +} diff --git a/apps/petrinaut-website/package.json b/apps/petrinaut-website/package.json index a6d24ef3723..9c270d2c0b3 100644 --- a/apps/petrinaut-website/package.json +++ b/apps/petrinaut-website/package.json @@ -12,7 +12,7 @@ "fix:eslint": "oxlint --fix --type-aware --report-unused-disable-directives-severity=error .", "generate:icons": "node scripts/generate-favicons.mjs", "lint:eslint": "oxlint --type-aware --report-unused-disable-directives-severity=error .", - "lint:tsc": "tsgo --noEmit", + "lint:tsc": "tsgo --noEmit && tsgo --noEmit --project api/tsconfig.json", "preview": "vite preview", "test:unit": "vitest run --passWithNoTests" }, diff --git a/apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts b/apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts deleted file mode 100644 index f9810f647ab..00000000000 --- a/apps/petrinaut-website/src/server/vercel-function-entrypoints.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/// - -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { stripTypeScriptTypes } from "node:module"; -import { dirname, join, relative, resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -import { describe, expect, it } from "vitest"; - -const appRoot = fileURLToPath(new URL("../..", import.meta.url)); -const entrypointPaths = [ - "api/chat.ts", - "api/oembed.ts", - "api/voice/config.ts", - "api/voice/realtime-call.ts", -] as const; - -const importDeclarationPattern = - /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["'];?/gu; -const exportDeclarationPattern = - /(?:^|\n)\s*export\s+(?:type\s+)?(?:\*|\{[\s\S]*?\})\s+from\s+["']([^"']+)["'];?/gu; - -const staticModuleSpecifiers = (source: string): string[] => { - const specifiers: string[] = []; - - for (const pattern of [importDeclarationPattern, exportDeclarationPattern]) { - for (const match of source.matchAll(pattern)) { - const specifier = match[1]; - if (specifier !== undefined) { - specifiers.push(specifier); - } - } - } - - return specifiers; -}; - -const emittedPath = (outputRoot: string, sourcePath: string): string => - join(outputRoot, relative(appRoot, sourcePath).replace(/\.ts$/u, ".js")); - -const emitNativeEsmGraph = async ( - outputRoot: string, - sourcePath: string, - emittedSourcePaths: Set, -): Promise => { - const absoluteSourcePath = resolve(sourcePath); - if (emittedSourcePaths.has(absoluteSourcePath)) { - return; - } - emittedSourcePaths.add(absoluteSourcePath); - - const source = await readFile(absoluteSourcePath, "utf8"); - const javascript = stripTypeScriptTypes(source, { - mode: "strip", - }); - const outputPath = emittedPath(outputRoot, absoluteSourcePath); - await mkdir(dirname(outputPath), { recursive: true }); - await writeFile(outputPath, javascript); - - for (const specifier of staticModuleSpecifiers(javascript)) { - if (!specifier.startsWith(".")) { - continue; - } - if (!specifier.endsWith(".js")) { - throw new Error( - `${relative(appRoot, absoluteSourcePath)} imports ${specifier} without a .js extension`, - ); - } - - const importedSourcePath = resolve( - dirname(absoluteSourcePath), - specifier.replace(/\.js$/u, ".ts"), - ); - await emitNativeEsmGraph( - outputRoot, - importedSourcePath, - emittedSourcePaths, - ); - } -}; - -describe("Petrinaut Vercel Function entrypoints", () => { - it("loads every local import graph with native Node ESM resolution", async () => { - const temporaryParent = join(appRoot, ".turbo"); - await mkdir(temporaryParent, { recursive: true }); - const outputRoot = await mkdtemp( - join(temporaryParent, "vercel-function-entrypoints-"), - ); - - try { - await writeFile( - join(outputRoot, "package.json"), - `${JSON.stringify({ type: "module" })}\n`, - ); - - const emittedSourcePaths = new Set(); - for (const entrypointPath of entrypointPaths) { - await emitNativeEsmGraph( - outputRoot, - resolve(appRoot, entrypointPath), - emittedSourcePaths, - ); - } - - const importScript = entrypointPaths - .map((entrypointPath) => { - const outputPath = emittedPath( - outputRoot, - resolve(appRoot, entrypointPath), - ); - return `await import(${JSON.stringify(pathToFileURL(outputPath).href)});`; - }) - .join("\n"); - const result = spawnSync( - process.execPath, - ["--input-type=module", "--eval", importScript], - { - cwd: appRoot, - encoding: "utf8", - env: { ...process.env, NODE_NO_WARNINGS: "1" }, - }, - ); - - expect(result.stderr).toBe(""); - expect(result.status).toBe(0); - } finally { - await rm(outputRoot, { force: true, recursive: true }); - } - }); -}); diff --git a/apps/petrinaut-website/tsconfig.json b/apps/petrinaut-website/tsconfig.json index 60cf1627251..422ea6b82f3 100644 --- a/apps/petrinaut-website/tsconfig.json +++ b/apps/petrinaut-website/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "noEmit": true, "skipLibCheck": true, - "isolatedModules": true + "isolatedModules": true, + "types": [] }, - "include": ["api", "src", "router-codegen-config.ts"] + "include": ["src", "router-codegen-config.ts"] }