UNPKG

convex

Version:

Client for the Convex Cloud

238 lines (216 loc) 7.04 kB
import path from "path"; import esbuild, { BuildFailure, LogLevel, Plugin } from "esbuild"; import { Context } from "./context.js"; import { logError, changeSpinner, logFailure, logVerbose, logMessage, } from "./log.js"; import { wasmPlugin } from "./wasm.js"; import dependencyTrackerPlugin from "./depgraph.js"; export async function innerEsbuild({ entryPoints, platform, dir, extraConditions, generateSourceMaps, plugins, chunksFolder, logLevel, }: { entryPoints: string[]; platform: esbuild.Platform; dir: string; extraConditions: string[]; generateSourceMaps: boolean; plugins: Plugin[]; chunksFolder: string; logLevel?: LogLevel; }) { const result = await esbuild.build({ entryPoints, bundle: true, platform: platform, format: "esm", target: "esnext", jsx: "automatic", outdir: "out", outbase: dir, conditions: ["convex", "module", ...extraConditions], plugins, write: false, sourcemap: generateSourceMaps, splitting: true, chunkNames: path.join(chunksFolder, "[hash]"), treeShaking: true, minifySyntax: true, minifyIdentifiers: true, // Enabling minifyWhitespace breaks sourcemaps on convex backends. // The sourcemaps produced are valid on https://evanw.github.io/source-map-visualization // but something we're doing (perhaps involving https://github.com/getsentry/rust-sourcemap) // makes everything map to the same line. minifyWhitespace: false, // false is the default, just showing for clarify. keepNames: true, define: { "process.env.NODE_ENV": '"production"', }, metafile: true, logLevel: logLevel || "warning", }); return result; } export function isEsbuildBuildError(e: any): e is BuildFailure { return ( "errors" in e && "warnings" in e && Array.isArray(e.errors) && Array.isArray(e.warnings) ); } /** * Bundle non-"use node" entry points one at a time to track down the first file with an error * is being imported. */ export async function debugIsolateBundlesSerially( ctx: Context, { entryPoints, extraConditions, dir, }: { entryPoints: string[]; extraConditions: string[]; dir: string; }, ): Promise<void> { logMessage( `Bundling convex entry points one at a time to track down things that can't be bundled for the Convex JS runtime.`, ); let i = 1; for (const entryPoint of entryPoints) { changeSpinner( `bundling entry point ${entryPoint} (${i++}/${entryPoints.length})...`, ); const { plugin, tracer } = dependencyTrackerPlugin(); try { await innerEsbuild({ entryPoints: [entryPoint], platform: "browser", generateSourceMaps: true, chunksFolder: "_deps", extraConditions, dir, plugins: [plugin, wasmPlugin], logLevel: "silent", }); } catch (error) { if (!isEsbuildBuildError(error) || !error.errors[0]) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: null, }); } const buildError = error.errors[0]; const errorFile = buildError.location?.file; if (!errorFile) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: null, }); } const importedPath = buildError.text.match(/"([^"]+)"/)?.[1]; if (!importedPath) continue; const full = path.resolve(errorFile); logError(""); logError( `Bundling ${entryPoint} resulted in ${error.errors.length} esbuild errors.`, ); logError(`One of the bundling errors occurred while bundling ${full}:\n`); logError( esbuild .formatMessagesSync([buildError], { kind: "error", color: true, }) .join("\n"), ); logError("It would help to avoid importing this file."); const chains = tracer.traceImportChains(entryPoint, full); const chain: string[] = chains[0]; chain.reverse(); logError(``); if (chain.length > 0) { const problematicFileRelative = formatFilePath(dir, chain[0]); if (chain.length === 1) { logError(` ${problematicFileRelative}`); } else { logError(` ${problematicFileRelative} is imported by`); for (let i = 1; i < chain.length - 1; i++) { const fileRelative = formatFilePath(dir, chain[i]); logError(` ${fileRelative}, which is imported by`); } const entryPointFile = chain[chain.length - 1]; const entryPointRelative = formatFilePath(dir, entryPointFile); logError(` ${entryPointRelative}, which doesn't use "use node"\n`); logError( ` For registered action functions to use Node.js APIs in any code they run they must be defined\n` + ` in a file with 'use node' at the top. See https://docs.convex.dev/functions/runtimes#nodejs-runtime\n`, ); } } logFailure("Bundling failed"); return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Bundling failed.", }); } logVerbose(`${entryPoint} bundled`); } } // Helper function to format file paths consistently function formatFilePath(baseDir: string, filePath: string): string { // If it's already a relative path like "./shared", just return it if (!path.isAbsolute(filePath)) { // For relative paths, ensure they start with "convex/" if (!filePath.startsWith("convex/")) { // If it's a path like "./subdir/file.ts" or "subdir/file.ts" const cleanPath = filePath.replace(/^\.\//, ""); return `convex/${cleanPath}`; } return filePath; } // Get the path relative to the base directory const relativePath = path.relative(baseDir, filePath); // Remove any leading "./" that path.relative might add const cleanPath = relativePath.replace(/^\.\//, ""); // Check if this is a path within the convex directory const isConvexPath = cleanPath.startsWith("convex/") || cleanPath.includes("/convex/") || path.dirname(cleanPath) === "convex"; if (isConvexPath) { // If it already starts with convex/, return it as is if (cleanPath.startsWith("convex/")) { return cleanPath; } // For files in the convex directory if (path.dirname(cleanPath) === "convex") { const filename = path.basename(cleanPath); return `convex/${filename}`; } // For files in subdirectories of convex const convexIndex = cleanPath.indexOf("convex/"); if (convexIndex >= 0) { return cleanPath.substring(convexIndex); } } // For any other path, assume it's in the convex directory // This handles cases where the file is in a subdirectory of convex // but the path doesn't include "convex/" explicitly return `convex/${cleanPath}`; }