UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

300 lines (298 loc) • 12 kB
import { CORE_VERSION } from "@trigger.dev/core/v3"; import { DEFAULT_RUNTIME } from "@trigger.dev/core/v3/build"; import * as esbuild from "esbuild"; import { createHash } from "node:crypto"; import { join, relative, resolve } from "node:path"; import { createFile, createFileWithStore } from "../utilities/fileSystem.js"; import { logger } from "../utilities/logger.js"; import { resolveFileSources } from "../utilities/sourceFiles.js"; import { VERSION } from "../version.js"; import { createEntryPointManager } from "./entryPoints.js"; import { copyManifestToDir } from "./manifests.js"; import { getIndexControllerForTarget, getIndexWorkerForTarget, getRunControllerForTarget, getRunWorkerForTarget, isIndexControllerForTarget, isIndexWorkerForTarget, isInitEntryPoint, isLoaderEntryPoint, isRunControllerForTarget, isRunWorkerForTarget, shims, } from "./packageModules.js"; import { buildPlugins } from "./plugins.js"; import { cliLink, prettyError } from "../utilities/cliOutput.js"; import { SkipLoggingError } from "../cli/common.js"; export class BundleError extends Error { issues; constructor(message, issues) { super(message); this.issues = issues; } } export async function bundleWorker(options) { const { resolvedConfig } = options; let currentContext; const entryPointManager = await createEntryPointManager(resolvedConfig.dirs, resolvedConfig, options.target, typeof options.watch === "boolean" ? options.watch : false, async (newEntryPoints) => { if (currentContext) { // Rebuild with new entry points await currentContext.cancel(); await currentContext.dispose(); const buildOptions = await createBuildOptions({ ...options, entryPoints: newEntryPoints, }); logger.debug("Rebuilding worker with options", buildOptions); currentContext = await esbuild.context(buildOptions); await currentContext.watch(); } }); if (entryPointManager.entryPoints.length === 0) { const errorMessageBody = ` Dirs config: ${resolvedConfig.dirs.join("\n- ")} Search patterns: ${entryPointManager.patterns.join("\n- ")} Possible solutions: 1. Check if the directory paths in your config are correct 2. Verify that your files match the search patterns 3. Update the search patterns in your config `.replace(/^ {6}/gm, ""); prettyError("No trigger files found", errorMessageBody, cliLink("View the config docs", "https://trigger.dev/docs/config/config-file")); throw new SkipLoggingError(); } let initialBuildResult; const initialBuildResultPromise = new Promise((resolve) => (initialBuildResult = resolve)); const buildResultPlugin = { name: "Initial build result plugin", setup(build) { build.onEnd(initialBuildResult); }, }; const buildOptions = await createBuildOptions({ ...options, entryPoints: entryPointManager.entryPoints, buildResultPlugin, }); let result; let stop; logger.debug("Building worker with options", buildOptions); if (options.watch) { currentContext = await esbuild.context(buildOptions); await currentContext.watch(); result = await initialBuildResultPromise; if (result.errors.length > 0) { throw new BundleError("Failed to build", result.errors); } stop = async function () { await entryPointManager.stop(); await currentContext?.dispose(); }; } else { result = await esbuild.build(buildOptions); stop = async function () { await entryPointManager.stop(); }; } const bundleResult = await getBundleResultFromBuild(options.target, options.cwd, options.resolvedConfig, result, options.storeDir); if (!bundleResult) { throw new Error("Failed to get bundle result"); } return { ...bundleResult, stop }; } // Helper function to create build options async function createBuildOptions(options) { const customConditions = options.resolvedConfig.build?.conditions ?? []; const conditions = [...customConditions, "trigger.dev", "module", "node"]; const keepNames = options.resolvedConfig.build?.keepNames ?? options.resolvedConfig.build?.experimental_keepNames ?? true; const minify = options.resolvedConfig.build?.minify ?? options.resolvedConfig.build?.experimental_minify ?? false; const $buildPlugins = await buildPlugins(options.target, options.resolvedConfig); return { entryPoints: options.entryPoints, outdir: options.destination, absWorkingDir: options.cwd, bundle: true, metafile: true, write: false, minify, splitting: true, charset: "utf8", platform: "node", sourcemap: true, sourcesContent: options.target === "dev", conditions, keepNames, format: "esm", target: ["node20", "es2022"], loader: { ".js": "jsx", ".mjs": "jsx", ".cjs": "jsx", ".wasm": "copy", }, outExtension: { ".js": ".mjs" }, inject: [...shims], // TODO: copy this into the working dir to work with Yarn PnP jsx: options.jsxAutomatic ? "automatic" : undefined, jsxDev: options.jsxAutomatic && options.target === "dev" ? true : undefined, plugins: [ ...$buildPlugins, ...(options.plugins ?? []), ...(options.buildResultPlugin ? [options.buildResultPlugin] : []), ], ...(options.jsxFactory && { jsxFactory: options.jsxFactory }), ...(options.jsxFragment && { jsxFragment: options.jsxFragment }), logLevel: "silent", logOverride: { "empty-glob": "silent", "package.json": "silent", }, }; } export async function getBundleResultFromBuild(target, workingDir, resolvedConfig, result, storeDir) { const hasher = createHash("md5"); const outputHashes = {}; for (const outputFile of result.outputFiles) { hasher.update(outputFile.hash); // Store the hash for each output file (keyed by path) outputHashes[outputFile.path] = outputFile.hash; if (storeDir) { // Use content-addressable store with esbuild's built-in hash for ALL files await createFileWithStore(outputFile.path, outputFile.contents, storeDir, outputFile.hash); } else { await createFile(outputFile.path, outputFile.contents); } } const files = []; let configPath; let loaderEntryPoint; let runWorkerEntryPoint; let runControllerEntryPoint; let indexWorkerEntryPoint; let indexControllerEntryPoint; let initEntryPoint; const configEntryPoint = resolvedConfig.configFile ? relative(resolvedConfig.workingDir, resolvedConfig.configFile) : "trigger.config.ts"; for (const [outputPath, outputMeta] of Object.entries(result.metafile.outputs)) { if (outputPath.endsWith(".mjs")) { const $outputPath = resolve(workingDir, outputPath); if (!outputMeta.entryPoint) { continue; } if (outputMeta.entryPoint.startsWith(configEntryPoint)) { configPath = $outputPath; } else if (isLoaderEntryPoint(outputMeta.entryPoint)) { loaderEntryPoint = $outputPath; } else if (isRunControllerForTarget(outputMeta.entryPoint, target)) { runControllerEntryPoint = $outputPath; } else if (isRunWorkerForTarget(outputMeta.entryPoint, target)) { runWorkerEntryPoint = $outputPath; } else if (isIndexControllerForTarget(outputMeta.entryPoint, target)) { indexControllerEntryPoint = $outputPath; } else if (isIndexWorkerForTarget(outputMeta.entryPoint, target)) { indexWorkerEntryPoint = $outputPath; } else if (isInitEntryPoint(outputMeta.entryPoint, resolvedConfig.dirs)) { initEntryPoint = $outputPath; } else { if (!outputMeta.entryPoint.startsWith("..") && !outputMeta.entryPoint.includes("node_modules")) { files.push({ entry: outputMeta.entryPoint, out: $outputPath, }); } } } } if (!configPath) { return undefined; } return { files, configPath: configPath, loaderEntryPoint, runWorkerEntryPoint, runControllerEntryPoint, indexWorkerEntryPoint, indexControllerEntryPoint, initEntryPoint, contentHash: hasher.digest("hex"), metafile: result.metafile, outputHashes, }; } // Converts a directory to a glob that matches all the entry points in that function dirToEntryPointGlob(dir) { return [ join(dir, "**", "*.ts"), join(dir, "**", "*.tsx"), join(dir, "**", "*.mts"), join(dir, "**", "*.cts"), join(dir, "**", "*.js"), join(dir, "**", "*.jsx"), join(dir, "**", "*.mjs"), join(dir, "**", "*.cjs"), ]; } export function logBuildWarnings(warnings) { const logs = esbuild.formatMessagesSync(warnings, { kind: "warning", color: true }); for (const log of logs) { console.warn(log); } } /** * Logs all errors/warnings associated with an esbuild BuildFailure in the same * style esbuild would. */ export function logBuildFailure(errors, warnings) { const logs = esbuild.formatMessagesSync(errors, { kind: "error", color: true }); for (const log of logs) { console.error(log); } logBuildWarnings(warnings); } export async function createBuildManifestFromBundle({ bundle, destination, resolvedConfig, workerDir, environment, branch, target, envVars, sdkVersion, storeDir, }) { const buildManifest = { contentHash: bundle.contentHash, runtime: resolvedConfig.runtime ?? DEFAULT_RUNTIME, environment: environment, branch, packageVersion: sdkVersion ?? CORE_VERSION, cliPackageVersion: VERSION, target: target, files: bundle.files, sources: await resolveFileSources(bundle.files, resolvedConfig), externals: [], config: { project: resolvedConfig.project, dirs: resolvedConfig.dirs, }, outputPath: destination, indexControllerEntryPoint: bundle.indexControllerEntryPoint ?? getIndexControllerForTarget(target), indexWorkerEntryPoint: bundle.indexWorkerEntryPoint ?? getIndexWorkerForTarget(target), runControllerEntryPoint: bundle.runControllerEntryPoint ?? getRunControllerForTarget(target), runWorkerEntryPoint: bundle.runWorkerEntryPoint ?? getRunWorkerForTarget(target), loaderEntryPoint: bundle.loaderEntryPoint, initEntryPoint: bundle.initEntryPoint, configPath: bundle.configPath, customConditions: resolvedConfig.build.conditions ?? [], deploy: { env: envVars ?? {}, }, build: {}, otelImportHook: { include: resolvedConfig.instrumentedPackageNames ?? [], }, // `outputHashes` is only needed for dev builds for the deduplication mechanism during rebuilds. // For deploys builds, we omit it to ensure deterministic builds outputHashes: target === "dev" ? bundle.outputHashes : {}, }; if (!workerDir) { return buildManifest; } return copyManifestToDir(buildManifest, destination, workerDir, storeDir); } //# sourceMappingURL=bundle.js.map