trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
300 lines (298 loc) • 12 kB
JavaScript
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