|
| 1 | +import path from "path"; |
| 2 | +import esbuild, { BuildFailure, LogLevel, Plugin } from "esbuild"; |
| 3 | +import { |
| 4 | + Context, |
| 5 | + logError, |
| 6 | + changeSpinner, |
| 7 | + logFailure, |
| 8 | + logVerbose, |
| 9 | + logMessage, |
| 10 | +} from "./context.js"; |
| 11 | +import { wasmPlugin } from "./wasm.js"; |
| 12 | +import dependencyTrackerPlugin from "./depgraph.js"; |
| 13 | + |
| 14 | +export async function innerEsbuild({ |
| 15 | + entryPoints, |
| 16 | + platform, |
| 17 | + dir, |
| 18 | + extraConditions, |
| 19 | + generateSourceMaps, |
| 20 | + plugins, |
| 21 | + chunksFolder, |
| 22 | + logLevel, |
| 23 | +}: { |
| 24 | + entryPoints: string[]; |
| 25 | + platform: esbuild.Platform; |
| 26 | + dir: string; |
| 27 | + extraConditions: string[]; |
| 28 | + generateSourceMaps: boolean; |
| 29 | + plugins: Plugin[]; |
| 30 | + chunksFolder: string; |
| 31 | + logLevel?: LogLevel; |
| 32 | +}) { |
| 33 | + const result = await esbuild.build({ |
| 34 | + entryPoints, |
| 35 | + bundle: true, |
| 36 | + platform: platform, |
| 37 | + format: "esm", |
| 38 | + target: "esnext", |
| 39 | + jsx: "automatic", |
| 40 | + outdir: "out", |
| 41 | + outbase: dir, |
| 42 | + conditions: ["convex", "module", ...extraConditions], |
| 43 | + plugins, |
| 44 | + write: false, |
| 45 | + sourcemap: generateSourceMaps, |
| 46 | + splitting: true, |
| 47 | + chunkNames: path.join(chunksFolder, "[hash]"), |
| 48 | + treeShaking: true, |
| 49 | + minifySyntax: true, |
| 50 | + minifyIdentifiers: true, |
| 51 | + // Enabling minifyWhitespace breaks sourcemaps on convex backends. |
| 52 | + // The sourcemaps produced are valid on https://evanw.github.io/source-map-visualization |
| 53 | + // but something we're doing (perhaps involving https://github.com/getsentry/rust-sourcemap) |
| 54 | + // makes everything map to the same line. |
| 55 | + minifyWhitespace: false, // false is the default, just showing for clarify. |
| 56 | + keepNames: true, |
| 57 | + define: { |
| 58 | + "process.env.NODE_ENV": '"production"', |
| 59 | + }, |
| 60 | + metafile: true, |
| 61 | + logLevel: logLevel || "warning", |
| 62 | + }); |
| 63 | + return result; |
| 64 | +} |
| 65 | + |
| 66 | +export function isEsbuildBuildError(e: any): e is BuildFailure { |
| 67 | + return ( |
| 68 | + "errors" in e && |
| 69 | + "warnings" in e && |
| 70 | + Array.isArray(e.errors) && |
| 71 | + Array.isArray(e.warnings) |
| 72 | + ); |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Bundle non-"use node" entry points one at a time to track down the first file with an error |
| 77 | + * is being imported. |
| 78 | + */ |
| 79 | +export async function debugIsolateBundlesSerially( |
| 80 | + ctx: Context, |
| 81 | + { |
| 82 | + entryPoints, |
| 83 | + extraConditions, |
| 84 | + dir, |
| 85 | + }: { |
| 86 | + entryPoints: string[]; |
| 87 | + extraConditions: string[]; |
| 88 | + dir: string; |
| 89 | + }, |
| 90 | +): Promise<void> { |
| 91 | + logMessage( |
| 92 | + ctx, |
| 93 | + `Bundling convex entry points one at a time to track down things that can't be bundled for the Convex JS runtime.`, |
| 94 | + ); |
| 95 | + let i = 1; |
| 96 | + for (const entryPoint of entryPoints) { |
| 97 | + changeSpinner( |
| 98 | + ctx, |
| 99 | + `bundling entry point ${entryPoint} (${i++}/${entryPoints.length})...`, |
| 100 | + ); |
| 101 | + |
| 102 | + const { plugin, tracer } = dependencyTrackerPlugin(); |
| 103 | + try { |
| 104 | + await innerEsbuild({ |
| 105 | + entryPoints: [entryPoint], |
| 106 | + platform: "browser", |
| 107 | + generateSourceMaps: true, |
| 108 | + chunksFolder: "_deps", |
| 109 | + extraConditions, |
| 110 | + dir, |
| 111 | + plugins: [plugin, wasmPlugin], |
| 112 | + logLevel: "silent", |
| 113 | + }); |
| 114 | + } catch (error) { |
| 115 | + if (!isEsbuildBuildError(error) || !error.errors[0]) { |
| 116 | + return await ctx.crash({ |
| 117 | + exitCode: 1, |
| 118 | + errorType: "invalid filesystem data", |
| 119 | + printedMessage: null, |
| 120 | + }); |
| 121 | + } |
| 122 | + |
| 123 | + const buildError = error.errors[0]; |
| 124 | + const errorFile = buildError.location?.file; |
| 125 | + if (!errorFile) { |
| 126 | + return await ctx.crash({ |
| 127 | + exitCode: 1, |
| 128 | + errorType: "invalid filesystem data", |
| 129 | + printedMessage: null, |
| 130 | + }); |
| 131 | + } |
| 132 | + |
| 133 | + const importedPath = buildError.text.match(/"([^"]+)"/)?.[1]; |
| 134 | + if (!importedPath) continue; |
| 135 | + |
| 136 | + const full = path.resolve(errorFile); |
| 137 | + logError(ctx, ""); |
| 138 | + logError( |
| 139 | + ctx, |
| 140 | + `Bundling ${entryPoint} resulted in ${error.errors.length} esbuild errors.`, |
| 141 | + ); |
| 142 | + logError( |
| 143 | + ctx, |
| 144 | + `One of the bundling errors occurred while bundling ${full}:\n`, |
| 145 | + ); |
| 146 | + logError( |
| 147 | + ctx, |
| 148 | + esbuild |
| 149 | + .formatMessagesSync([buildError], { |
| 150 | + kind: "error", |
| 151 | + color: true, |
| 152 | + }) |
| 153 | + .join("\n"), |
| 154 | + ); |
| 155 | + logError(ctx, "It would help to avoid importing this file."); |
| 156 | + const chains = tracer.traceImportChains(entryPoint, full); |
| 157 | + const chain: string[] = chains[0]; |
| 158 | + chain.reverse(); |
| 159 | + |
| 160 | + logError(ctx, ``); |
| 161 | + if (chain.length > 0) { |
| 162 | + const problematicFileRelative = formatFilePath(dir, chain[0]); |
| 163 | + |
| 164 | + if (chain.length === 1) { |
| 165 | + logError(ctx, ` ${problematicFileRelative}`); |
| 166 | + } else { |
| 167 | + logError(ctx, ` ${problematicFileRelative} is imported by`); |
| 168 | + |
| 169 | + for (let i = 1; i < chain.length - 1; i++) { |
| 170 | + const fileRelative = formatFilePath(dir, chain[i]); |
| 171 | + logError(ctx, ` ${fileRelative}, which is imported by`); |
| 172 | + } |
| 173 | + |
| 174 | + const entryPointFile = chain[chain.length - 1]; |
| 175 | + const entryPointRelative = formatFilePath(dir, entryPointFile); |
| 176 | + |
| 177 | + logError( |
| 178 | + ctx, |
| 179 | + ` ${entryPointRelative}, which doesn't use "use node"\n`, |
| 180 | + ); |
| 181 | + logError( |
| 182 | + ctx, |
| 183 | + ` For registered action functions to use Node.js APIs in any code they run they must be defined\n` + |
| 184 | + ` in a file with 'use node' at the top. See https://docs.convex.dev/functions/runtimes#nodejs-runtime\n`, |
| 185 | + ); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + logFailure(ctx, "Bundling failed"); |
| 190 | + return await ctx.crash({ |
| 191 | + exitCode: 1, |
| 192 | + errorType: "invalid filesystem data", |
| 193 | + printedMessage: "Bundling failed.", |
| 194 | + }); |
| 195 | + } |
| 196 | + logVerbose(ctx, `${entryPoint} bundled`); |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +// Helper function to format file paths consistently |
| 201 | +function formatFilePath(baseDir: string, filePath: string): string { |
| 202 | + // If it's already a relative path like "./shared", just return it |
| 203 | + if (!path.isAbsolute(filePath)) { |
| 204 | + // For relative paths, ensure they start with "convex/" |
| 205 | + if (!filePath.startsWith("convex/")) { |
| 206 | + // If it's a path like "./subdir/file.ts" or "subdir/file.ts" |
| 207 | + const cleanPath = filePath.replace(/^\.\//, ""); |
| 208 | + return `convex/${cleanPath}`; |
| 209 | + } |
| 210 | + return filePath; |
| 211 | + } |
| 212 | + |
| 213 | + // Get the path relative to the base directory |
| 214 | + const relativePath = path.relative(baseDir, filePath); |
| 215 | + |
| 216 | + // Remove any leading "./" that path.relative might add |
| 217 | + const cleanPath = relativePath.replace(/^\.\//, ""); |
| 218 | + |
| 219 | + // Check if this is a path within the convex directory |
| 220 | + const isConvexPath = |
| 221 | + cleanPath.startsWith("convex/") || |
| 222 | + cleanPath.includes("/convex/") || |
| 223 | + path.dirname(cleanPath) === "convex"; |
| 224 | + |
| 225 | + if (isConvexPath) { |
| 226 | + // If it already starts with convex/, return it as is |
| 227 | + if (cleanPath.startsWith("convex/")) { |
| 228 | + return cleanPath; |
| 229 | + } |
| 230 | + |
| 231 | + // For files in the convex directory |
| 232 | + if (path.dirname(cleanPath) === "convex") { |
| 233 | + const filename = path.basename(cleanPath); |
| 234 | + return `convex/${filename}`; |
| 235 | + } |
| 236 | + |
| 237 | + // For files in subdirectories of convex |
| 238 | + const convexIndex = cleanPath.indexOf("convex/"); |
| 239 | + if (convexIndex >= 0) { |
| 240 | + return cleanPath.substring(convexIndex); |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + // For any other path, assume it's in the convex directory |
| 245 | + // This handles cases where the file is in a subdirectory of convex |
| 246 | + // but the path doesn't include "convex/" explicitly |
| 247 | + return `convex/${cleanPath}`; |
| 248 | +} |
0 commit comments