cli now building and loadable

This commit is contained in:
Eric Allam
2024-08-06 16:12:21 +01:00
committed by Eric Allam
parent 1125d61bdc
commit a2eb896218
65 changed files with 1387 additions and 8214 deletions
-1
View File
@@ -12,7 +12,6 @@ coverage
# next.js
.next/
out/
build
dist
packages/**/dist
+2 -2
View File
@@ -15,7 +15,7 @@
"@aws-sdk/client-sqs": "^3.445.0",
"@trigger.dev/core": "workspace:*",
"ulidx": "^2.2.1",
"zod": "3.22.3",
"zod": "3.23.8",
"zod-error": "1.5.0"
}
}
}
+1 -1
View File
@@ -175,7 +175,7 @@
"ulid": "^2.3.0",
"ulidx": "^2.2.1",
"ws": "^8.11.0",
"zod": "3.22.3",
"zod": "3.23.8",
"zod-error": "1.5.0",
"zod-validation-error": "^1.5.0"
},
+35 -11
View File
@@ -29,7 +29,7 @@
"dist"
],
"bin": {
"triggerdev": "./dist/index.js"
"triggerdev": "./dist/esm/index.js"
},
"tshy": {
"selfLink": false,
@@ -62,10 +62,12 @@
"typescript": "^5.5.4",
"vitest": "^1.6.0",
"xdg-app-paths": "^8.3.0",
"tshy": "^3.0.2"
"tshy": "^3.0.2",
"ts-essentials": "10.0.1"
},
"scripts": {
"typecheck": "tsc",
"prepare": "tshy",
"build": "tshy",
"dev": "tshy --watch",
"test": "vitest",
@@ -94,14 +96,9 @@
"cli-table3": "^0.6.3",
"commander": "^9.4.1",
"degit": "^2.8.4",
"dotenv": "^16.4.4",
"esbuild": "^0.19.11",
"evt": "^2.4.13",
"execa": "^9.1.0",
"find-up": "^7.0.0",
"glob": "^10.3.10",
"gradient-string": "^2.0.2",
"import-meta-resolve": "^4.0.0",
"ink": "^4.4.1",
"jsonc-parser": "3.2.1",
"liquidjs": "^10.9.2",
@@ -117,7 +114,6 @@
"react-error-boundary": "^4.0.12",
"semver": "^7.5.0",
"simple-git": "^3.19.0",
"source-map-support": "^0.5.21",
"terminal-link": "^3.0.0",
"tiny-invariant": "^1.2.0",
"tsconfig-paths": "^4.2.0",
@@ -125,10 +121,38 @@
"update-check": "^1.5.4",
"url": "^0.11.1",
"ws": "^8.12.0",
"zod": "3.22.3",
"zod-validation-error": "^1.5.0"
"zod-validation-error": "^1.5.0",
"async-sema": "^3.1.1",
"c12": "^1.11.1",
"defu": "^6.1.4",
"dotenv": "^16.4.5",
"esbuild": "^0.23.0",
"find-up": "^7.0.0",
"glob": "^11.0.0",
"glob-to-regexp": "^0.4.1",
"hono": "^4.4.13",
"import-in-the-middle": "1.9.1",
"import-meta-resolve": "^4.1.0",
"magicast": "^0.3.4",
"mlly": "^1.7.1",
"package-json-from-dist": "^1.0.0",
"pkg-types": "^1.1.3",
"resolve": "^1.22.8",
"signal-exit": "^4.1.0",
"source-map-support": "0.5.21",
"unplugin": "^1.12.0",
"zod": "3.23.8"
},
"engines": {
"node": ">=18.20.0"
},
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
}
}
}
}
}
-58
View File
@@ -1,58 +0,0 @@
# syntax=docker/dockerfile:labs
FROM node:21-bookworm-slim@sha256:fb82287cf66ca32d854c05f54251fca8b572149163f154248df7e800003c90b5 AS base
ARG AUDIOWAVEFORM_VERSION=1.10.1
ARG AUDIOWAVEFORM_CHECKSUM=sha256:00b41ea4d6e7a5b4affcfe4ac99951ec89da81a8cba40af19e9b98c3a8f9b4b8
ADD --checksum=${AUDIOWAVEFORM_CHECKSUM} \
# on debian major version upgrades, this url will need to be updated
https://github.com/bbc/audiowaveform/releases/download/${AUDIOWAVEFORM_VERSION}/audiowaveform_${AUDIOWAVEFORM_VERSION}-1-12_amd64.deb .
# errors due to missing deps are expected here, these will get fixed in the apt install step
RUN dpkg -i audiowaveform_${AUDIOWAVEFORM_VERSION}-1-12_amd64.deb || true
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
# required for audiowaveform
apt-get --fix-broken install -y && \
apt-get install -y --no-install-recommends \
busybox \
ca-certificates \
dumb-init \
git \
openssl \
sox \
&& \
rm -rf /var/lib/apt/lists/* audiowaveform*.deb
# Create and set workdir with appropriate permissions
RUN mkdir /app && chown node:node /app
WORKDIR /app
# copy all the files just in case anything is needed in postinstall
COPY --chown=node:node . .
USER node
RUN npm ci --no-fund --no-audit && npm cache clean --force
# Development or production stage builds upon the base stage
FROM base AS final
# Use ARG for build-time variables
ARG TRIGGER_PROJECT_ID
ARG TRIGGER_DEPLOYMENT_ID
ARG TRIGGER_DEPLOYMENT_VERSION
ARG TRIGGER_CONTENT_HASH
ARG TRIGGER_PROJECT_REF
ARG NODE_EXTRA_CA_CERTS
ENV TRIGGER_PROJECT_ID=${TRIGGER_PROJECT_ID} \
TRIGGER_DEPLOYMENT_ID=${TRIGGER_DEPLOYMENT_ID} \
TRIGGER_DEPLOYMENT_VERSION=${TRIGGER_DEPLOYMENT_VERSION} \
TRIGGER_CONTENT_HASH=${TRIGGER_CONTENT_HASH} \
TRIGGER_PROJECT_REF=${TRIGGER_PROJECT_REF} \
NODE_EXTRA_CA_CERTS=${NODE_EXTRA_CA_CERTS} \
NODE_ENV=production
USER node
CMD [ "dumb-init", "node", "index.js" ]
+2 -5
View File
@@ -1,12 +1,10 @@
import { Command } from "commander";
import { configureDeployCommand } from "../commands/deploy.js";
import { configureDevCommand } from "../commands/dev.js";
import { configureInitCommand } from "../commands/init.js";
import { configureLoginCommand } from "../commands/login.js";
import { configureLogoutCommand } from "../commands/logout.js";
import { configureWhoamiCommand } from "../commands/whoami.js";
import { COMMAND_NAME } from "../consts.js";
import { getVersion } from "../utilities/getVersion.js";
import { COMMAND_NAME, VERSION } from "../consts.js";
import { configureListProfilesCommand } from "../commands/list-profiles.js";
import { configureUpdateCommand } from "../commands/update.js";
@@ -15,12 +13,11 @@ export const program = new Command();
program
.name(COMMAND_NAME)
.description("Create, run locally and deploy Trigger.dev background tasks.")
.version(getVersion(), "-v, --version", "Display the version number");
.version(VERSION, "-v, --version", "Display the version number");
configureLoginCommand(program);
configureInitCommand(program);
configureDevCommand(program);
configureDeployCommand(program);
configureWhoamiCommand(program);
configureLogoutCommand(program);
configureListProfilesCommand(program);
File diff suppressed because it is too large Load Diff
+3 -924
View File
@@ -1,69 +1,11 @@
import {
CreateBackgroundWorkerRequestBody,
ResolvedConfig,
TaskResource,
clientWebsocketMessages,
detectDependencyVersion,
serverWebsocketMessages,
} from "@trigger.dev/core/v3";
import { ZodMessageHandler, ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
import { watch } from "chokidar";
import { Command } from "commander";
import { BuildContext, Metafile, context } from "esbuild";
import { render, useInput } from "ink";
import { createHash } from "node:crypto";
import fs, { readFileSync } from "node:fs";
import { ClientRequestArgs } from "node:http";
import { basename, dirname, join, normalize } from "node:path";
import pDebounce from "p-debounce";
import { WebSocket } from "partysocket";
import React, { Suspense, useEffect } from "react";
import { ClientOptions, WebSocket as wsWebSocket } from "ws";
import { z } from "zod";
import * as packageJson from "../../package.json";
import { CliApiClient } from "../apiClient.js";
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
import {
bundleDependenciesPlugin,
bundleTriggerDevCore,
mockServerOnlyPlugin,
workerSetupImportConfigPlugin,
} from "../utilities/build.js";
import {
chalkError,
chalkGrey,
chalkLink,
chalkPurple,
chalkTask,
chalkWorker,
cliLink,
} from "../utilities/cliOutput.js";
import { readConfig } from "../utilities/configFiles.js";
import { readJSONFile } from "../utilities/fileSystem.js";
import { printDevBanner, printStandloneInitialBanner } from "../utilities/initialBanner.js";
import {
detectPackageNameFromImportPath,
parsePackageName,
stripWorkspaceFromVersion,
} from "../utilities/installPackages.js";
import { chalkError } from "../utilities/cliOutput.js";
import { logger } from "../utilities/logger.js";
import { runtimeCheck } from "../utilities/runtimeCheck.js";
import { isLoggedIn } from "../utilities/session.js";
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
import { TaskMetadataParseError, UncaughtExceptionError } from "../workers/common/errors";
import { BackgroundWorker, BackgroundWorkerCoordinator } from "../workers/dev/backgroundWorker.js";
import { runtimeCheck } from "../utilities/runtimeCheck";
import {
logESMRequireError,
logTaskMetadataParseError,
parseBuildErrorStack,
parseNpmInstallError,
} from "../utilities/deployErrors";
import { findUp, pathExists } from "find-up";
import { cliRootPath } from "../utilities/resolveInternalFilePath";
import { escapeImportPath } from "../utilities/windows";
import { updateTriggerPackages } from "./update";
import { esbuildDecorators } from "@anatine/esbuild-decorators";
import { callResolveEnvVars } from "../utilities/resolveEnvVars";
let apiClient: CliApiClient | undefined;
@@ -99,7 +41,7 @@ export function configureDevCommand(program: Command) {
}
const MINIMUM_NODE_MAJOR = 18;
const MINIMUM_NODE_MINOR = 16;
const MINIMUM_NODE_MINOR = 20;
export async function devCommand(dir: string, options: DevCommandOptions) {
try {
@@ -129,867 +71,4 @@ export async function devCommand(dir: string, options: DevCommandOptions) {
process.exitCode = 1;
return;
}
const devInstance = await startDev(dir, options, authorization.auth, authorization.dashboardUrl);
const { waitUntilExit } = devInstance.devReactElement;
await waitUntilExit();
}
async function startDev(
dir: string,
options: DevCommandOptions,
authorization: { apiUrl: string; accessToken: string },
dashboardUrl: string
) {
let rerender: (node: React.ReactNode) => void | undefined;
try {
if (options.logLevel) {
logger.loggerLevel = options.logLevel;
}
await printStandloneInitialBanner(true);
let displayedUpdateMessage = false;
if (!options.skipUpdateCheck) {
displayedUpdateMessage = await updateTriggerPackages(dir, { ...options }, true, true);
}
printDevBanner(displayedUpdateMessage);
logger.debug("Starting dev session", { dir, options, authorization });
let config = await readConfig(dir, {
projectRef: options.projectRef,
configFile: options.config,
});
logger.debug("Initial config", { config });
if (config.status === "error") {
logger.error("Failed to read config", config.error);
process.exit(1);
}
async function getDevReactElement(
configParam: ResolvedConfig,
authorization: { apiUrl: string; accessToken: string },
configPath?: string,
configModule?: any
) {
const accessToken = authorization.accessToken;
const apiUrl = authorization.apiUrl;
apiClient = new CliApiClient(apiUrl, accessToken);
const devEnv = await apiClient.getProjectEnv({
projectRef: configParam.project,
env: "dev",
});
if (!devEnv.success) {
if (devEnv.error === "Project not found") {
logger.error(
`Project not found: ${configParam.project}. Ensure you are using the correct project ref and CLI profile (use --profile). Currently using the "${options.profile}" profile, which points to ${authorization.apiUrl}`
);
} else {
logger.error(
`Failed to initialize dev environment: ${devEnv.error}. Using project ref ${configParam.project}`
);
}
process.exit(1);
}
const environmentClient = new CliApiClient(apiUrl, devEnv.data.apiKey);
return (
<DevUI
dashboardUrl={dashboardUrl}
config={configParam}
apiUrl={apiUrl}
apiKey={devEnv.data.apiKey}
environmentClient={environmentClient}
projectName={devEnv.data.name}
debuggerOn={options.debugger}
debugOtel={options.debugOtel}
configPath={configPath}
configModule={configModule}
/>
);
}
const devReactElement = render(
await getDevReactElement(
config.config,
authorization,
config.status === "file" ? config.path : undefined,
config.status === "file" ? config.module : undefined
)
);
rerender = devReactElement.rerender;
return {
devReactElement,
stop: async () => {
devReactElement.unmount();
},
};
} catch (e) {
throw e;
}
}
type DevProps = {
config: ResolvedConfig;
dashboardUrl: string;
apiUrl: string;
apiKey: string;
environmentClient: CliApiClient;
projectName: string;
debuggerOn: boolean;
debugOtel: boolean;
configPath?: string;
configModule?: any;
};
function useDev({
config,
dashboardUrl,
apiUrl,
apiKey,
environmentClient,
projectName,
debuggerOn,
debugOtel,
configPath,
configModule,
}: DevProps) {
useEffect(() => {
const websocketUrl = new URL(apiUrl);
websocketUrl.protocol = websocketUrl.protocol.replace("http", "ws");
websocketUrl.pathname = `/ws`;
const websocket = new WebSocket(websocketUrl.href, [], {
WebSocket: WebsocketFactory(apiKey),
connectionTimeout: 10000,
maxRetries: 10,
minReconnectionDelay: 1000,
maxReconnectionDelay: 30000,
reconnectionDelayGrowFactor: 1.4, // This leads to the following retry times: 1, 1.4, 1.96, 2.74, 3.84, 5.38, 7.53, 10.54, 14.76, 20.66
maxEnqueuedMessages: 250,
});
const sender = new ZodMessageSender({
schema: clientWebsocketMessages,
sender: async (message) => {
websocket.send(JSON.stringify(message));
},
});
const backgroundWorkerCoordinator = new BackgroundWorkerCoordinator(
`${dashboardUrl}/projects/v3/${config.project}`
);
websocket.addEventListener("open", async (event) => {
logger.debug("WebSocket opened", { event });
});
websocket.addEventListener("close", (event) => {
logger.debug("WebSocket closed", { event });
});
websocket.addEventListener("error", (event) => {
logger.log(`${chalkError("WebSocketError:")} ${event.error.message}`);
logger.debug("WebSocket error", { event, rawError: event.error });
});
// This is the deprecated task heart beat that uses the friendly attempt ID
// It will only be used if the worker does not support lazy attempts
backgroundWorkerCoordinator.onWorkerTaskHeartbeat.attach(
async ({ worker, backgroundWorkerId, id }) => {
await sender.send("BACKGROUND_WORKER_MESSAGE", {
backgroundWorkerId,
data: {
type: "TASK_HEARTBEAT",
id,
},
});
}
);
// "Task Run Heartbeat" id is the actual run ID that corresponds to the MarQS message ID
backgroundWorkerCoordinator.onWorkerTaskRunHeartbeat.attach(
async ({ worker, backgroundWorkerId, id }) => {
await sender.send("BACKGROUND_WORKER_MESSAGE", {
backgroundWorkerId,
data: {
type: "TASK_RUN_HEARTBEAT",
id,
},
});
}
);
backgroundWorkerCoordinator.onTaskCompleted.attach(
async ({ backgroundWorkerId, completion, execution }) => {
await sender.send("BACKGROUND_WORKER_MESSAGE", {
backgroundWorkerId,
data: {
type: "TASK_RUN_COMPLETED",
completion,
execution,
},
});
}
);
backgroundWorkerCoordinator.onTaskFailedToRun.attach(
async ({ backgroundWorkerId, completion }) => {
await sender.send("BACKGROUND_WORKER_MESSAGE", {
backgroundWorkerId,
data: {
type: "TASK_RUN_FAILED_TO_RUN",
completion,
},
});
}
);
backgroundWorkerCoordinator.onWorkerRegistered.attach(async ({ id, worker, record }) => {
await sender.send("READY_FOR_TASKS", {
backgroundWorkerId: id,
});
});
backgroundWorkerCoordinator.onWorkerDeprecated.attach(async ({ id, worker }) => {
await sender.send("BACKGROUND_WORKER_DEPRECATED", {
backgroundWorkerId: id,
});
});
websocket.addEventListener("message", async (event) => {
try {
const data = JSON.parse(
typeof event.data === "string" ? event.data : new TextDecoder("utf-8").decode(event.data)
);
const messageHandler = new ZodMessageHandler({
schema: serverWebsocketMessages,
messages: {
SERVER_READY: async (payload) => {
for (const worker of backgroundWorkerCoordinator.currentWorkers) {
await sender.send("READY_FOR_TASKS", {
backgroundWorkerId: worker.id,
inProgressRuns: worker.worker.inProgressRuns,
});
}
},
BACKGROUND_WORKER_MESSAGE: async (payload) => {
await backgroundWorkerCoordinator.handleMessage(
payload.backgroundWorkerId,
payload.data
);
},
},
});
await messageHandler.handleMessage(data);
} catch (error) {
if (error instanceof Error) {
logger.error("Error while handling websocket message", { error: error.message });
} else {
logger.error(
"Unkown error while handling websocket message, use `-l debug` for additional output"
);
logger.debug("Error while handling websocket message", { error });
}
}
});
let ctx: BuildContext | undefined;
let firstBuild = true;
async function runBuild() {
if (ctx) {
// This will stop the watching
await ctx.dispose();
}
let latestWorkerContentHash: string | undefined;
const taskFiles = await gatherTaskFiles(config);
const workerFacadePath = join(cliRootPath(), "workers", "dev", "worker-facade.js");
const workerFacade = readFileSync(workerFacadePath, "utf-8");
const workerSetupPath = join(cliRootPath(), "workers", "dev", "worker-setup.js");
let entryPointContents = workerFacade
.replace("__TASKS__", createTaskFileImports(taskFiles))
.replace(
"__WORKER_SETUP__",
`import { tracingSDK, otelTracer, otelLogger, sender } from "${escapeImportPath(
workerSetupPath
)}";`
);
if (configPath) {
configPath = normalize(configPath);
logger.debug("Importing project config from", { configPath });
entryPointContents = entryPointContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`import * as importedConfigExports from "${escapeImportPath(
configPath
)}"; const importedConfig = importedConfigExports.config; const handleError = importedConfigExports.handleError;`
);
} else {
entryPointContents = entryPointContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`const importedConfig = undefined; const handleError = undefined;`
);
}
logger.log(chalkGrey("○ Building background worker…"));
ctx = await context({
stdin: {
contents: entryPointContents,
resolveDir: process.cwd(),
sourcefile: "__entryPoint.ts",
},
banner: {
js: `process.on("uncaughtException", function(error, origin) { if (error instanceof Error) { process.send && process.send({ type: "UNCAUGHT_EXCEPTION", payload: { error: { name: error.name, message: error.message, stack: error.stack }, origin }, version: "v1" }); } else { process.send && process.send({ type: "UNCAUGHT_EXCEPTION", payload: { error: { name: "Error", message: typeof error === "string" ? error : JSON.stringify(error) }, origin }, version: "v1" }); } });`,
},
bundle: true,
metafile: true,
write: false,
minify: false,
sourcemap: "external", // does not set the //# sourceMappingURL= comment in the file, we handle it ourselves
logLevel: "error",
platform: "node",
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
target: ["node18", "es2020"],
outdir: "out",
define: {
TRIGGER_API_URL: `"${config.triggerUrl}"`,
__PROJECT_CONFIG__: JSON.stringify(config),
},
plugins: [
mockServerOnlyPlugin(),
bundleTriggerDevCore("workerFacade", config.tsconfigPath),
bundleDependenciesPlugin(
"workerFacade",
{},
(config.dependenciesToBundle ?? []).concat([/^@trigger.dev/]),
config.tsconfigPath
),
workerSetupImportConfigPlugin(configPath),
esbuildDecorators({
tsconfig: config.tsconfigPath,
tsx: true,
force: false,
}),
{
name: "trigger.dev v3",
setup(build) {
build.onEnd(async (result) => {
if (result.errors.length > 0) return;
if (!result || !result.outputFiles) {
logger.error("Build failed: no result");
return;
}
if (!firstBuild) {
logger.log(chalkGrey("○ Building background worker…"));
}
const metaOutputKey = join("out", `stdin.js`).replace(/\\/g, "/");
const metaOutput = result.metafile!.outputs[metaOutputKey];
if (!metaOutput) {
throw new Error(`Could not find metafile`);
}
const outputFileKey = join(config.projectDir, metaOutputKey);
const outputFile = result.outputFiles.find((file) => file.path === outputFileKey);
if (!outputFile) {
throw new Error(
`Could not find output file for entry point ${metaOutput.entryPoint}`
);
}
const sourceMapFileKey = join(config.projectDir, `${metaOutputKey}.map`);
const sourceMapFile = result.outputFiles.find(
(file) => file.path === sourceMapFileKey
);
const md5Hasher = createHash("md5");
md5Hasher.update(Buffer.from(outputFile.contents.buffer));
const contentHash = md5Hasher.digest("hex");
if (latestWorkerContentHash === contentHash) {
logger.log(chalkGrey("○ No changes detected, skipping build…"));
return;
}
// Create a file at join(dir, ".trigger", path) with the fileContents
const fullPath = join(config.projectDir, ".trigger", `${contentHash}.js`);
const sourceMapPath = `${fullPath}.map`;
const outputFileWithSourceMap = `${
outputFile.text
}\n//# sourceMappingURL=${basename(sourceMapPath)}`;
await fs.promises.mkdir(dirname(fullPath), { recursive: true });
await fs.promises.writeFile(fullPath, outputFileWithSourceMap);
logger.debug(`Wrote background worker to ${fullPath}`);
const dependencies = await gatherRequiredDependencies(metaOutput, config);
if (sourceMapFile) {
const sourceMapPath = `${fullPath}.map`;
await fs.promises.writeFile(sourceMapPath, sourceMapFile.text);
}
const environmentVariablesResponse =
await environmentClient.getEnvironmentVariables(config.project);
const processEnv = await gatherProcessEnv();
const backgroundWorker = new BackgroundWorker(
fullPath,
{
projectConfig: config,
dependencies,
env: {
...processEnv,
TRIGGER_API_URL: apiUrl,
TRIGGER_SECRET_KEY: apiKey,
...(environmentVariablesResponse.success
? environmentVariablesResponse.data.variables
: {}),
},
debuggerOn,
debugOtel,
resolveEnvVariables: createResolveEnvironmentVariablesFunction(configModule),
},
environmentClient
);
try {
await backgroundWorker.initialize();
latestWorkerContentHash = contentHash;
let packageVersion: string | undefined;
const taskResources: Array<TaskResource> = [];
if (!backgroundWorker.tasks || backgroundWorker.tasks.length === 0) {
logger.log(
`${chalkError(
"X Error:"
)} Worker failed to build: no tasks found. Searched in ${config.triggerDirectories.join(
", "
)}`
);
return;
}
for (const task of backgroundWorker.tasks) {
taskResources.push(task);
packageVersion = task.packageVersion;
}
if (!packageVersion) {
throw new Error(`Background Worker started without package version`);
}
// Check for any duplicate task ids
const taskIds = taskResources.map((task) => task.id);
const duplicateTaskIds = taskIds.filter(
(id, index) => taskIds.indexOf(id) !== index
);
if (duplicateTaskIds.length > 0) {
logger.error(
createDuplicateTaskIdOutputErrorMessage(duplicateTaskIds, taskResources)
);
return;
}
logger.debug("Creating background worker with tasks", {
tasks: taskResources,
});
const backgroundWorkerBody: CreateBackgroundWorkerRequestBody = {
localOnly: true,
metadata: {
packageVersion,
cliPackageVersion: packageJson.version,
tasks: taskResources,
contentHash: contentHash,
},
supportsLazyAttempts: true,
};
const backgroundWorkerRecord = await environmentClient.createBackgroundWorker(
config.project,
backgroundWorkerBody
);
if (!backgroundWorkerRecord.success) {
throw new Error(backgroundWorkerRecord.error);
}
backgroundWorker.metadata = backgroundWorkerRecord.data;
backgroundWorker;
const testUrl = `${dashboardUrl}/projects/v3/${config.project}/test?environment=dev`;
const runsUrl = `${dashboardUrl}/projects/v3/${config.project}/runs?envSlug=dev`;
const pipe = chalkGrey("|");
const bullet = chalkGrey("○");
const arrow = chalkGrey("->");
const testLink = chalkLink(cliLink("Test tasks", testUrl));
const runsLink = chalkLink(cliLink("View runs", runsUrl));
const workerStarted = chalkGrey("Background worker started");
const workerVersion = chalkWorker(backgroundWorkerRecord.data.version);
logger.log(
`${bullet} ${workerStarted} ${arrow} ${workerVersion} ${pipe} ${testLink} ${pipe} ${runsLink}`
);
firstBuild = false;
await backgroundWorkerCoordinator.registerWorker(
backgroundWorkerRecord.data,
backgroundWorker
);
} catch (e) {
logger.debug("Error starting background worker", {
error: e,
});
if (e instanceof TaskMetadataParseError) {
logTaskMetadataParseError(e.zodIssues, e.tasks);
return;
} else if (e instanceof UncaughtExceptionError) {
const parsedBuildError = parseBuildErrorStack(e.originalError);
if (parsedBuildError && typeof parsedBuildError !== "string") {
logESMRequireError(
parsedBuildError,
configPath
? { status: "file", path: configPath, config }
: { status: "in-memory", config }
);
return;
} else {
}
if (e.originalError.message || e.originalError.stack) {
logger.log(
`${chalkError("X Error:")} Worker failed to start`,
e.originalError.stack ?? e.originalError.message
);
}
return;
}
const parsedError = parseNpmInstallError(e);
if (typeof parsedError === "string") {
logger.log(`\n${chalkError("X Error:")} ${parsedError}`);
} else {
switch (parsedError.type) {
case "package-not-found-error": {
logger.log(
`\n${chalkError("X Error:")} The package ${chalkPurple(
parsedError.packageName
)} could not be found in the npm registry.`
);
break;
}
case "no-matching-version-error": {
logger.log(
`\n${chalkError("X Error:")} The package ${chalkPurple(
parsedError.packageName
)} could not resolve because the version doesn't exist`
);
break;
}
}
}
const stderr = backgroundWorker.stderr
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join("\n");
if (stderr) {
logger.log(`\n${chalkError("X Error logs:")}\n${stderr}`);
}
}
});
},
},
],
});
await ctx.watch();
}
const throttledRebuild = pDebounce(runBuild, 250, { before: true });
const taskFileWatcher = watch(
config.triggerDirectories.map((triggerDir) => `${triggerDir}/**/*.ts`),
{
ignoreInitial: true,
}
);
taskFileWatcher.on("add", async (path) => {
throttledRebuild().catch((error) => {
logger.error(error);
});
});
taskFileWatcher.on("unlink", async (path) => {
throttledRebuild().catch((error) => {
logger.error(error);
});
});
throttledRebuild().catch((error) => {
logger.error(error);
});
return () => {
const cleanup = async () => {
logger.debug(`Shutting down dev session for ${config.project}`);
const start = Date.now();
await taskFileWatcher.close();
websocket?.close();
backgroundWorkerCoordinator.close();
ctx?.dispose().catch((error) => {
console.error(error);
});
logger.debug(`Shutdown completed in ${Date.now() - start}ms`);
};
cleanup();
};
}, [config, apiUrl, apiKey, environmentClient]);
}
function DevUI(props: DevProps) {
return (
<Suspense>
<DevUIImp {...props} />
</Suspense>
);
}
function DevUIImp(props: DevProps) {
const dev = useDev(props);
return (
<>
<HotKeys />
</>
);
}
function useHotkeys() {
useInput(async (input, key) => {});
}
function HotKeys() {
useHotkeys();
return <></>;
}
function WebsocketFactory(apiKey: string) {
return class extends wsWebSocket {
constructor(address: string | URL, options?: ClientOptions | ClientRequestArgs) {
super(address, { ...(options ?? {}), headers: { Authorization: `Bearer ${apiKey}` } });
}
};
}
// Returns the dependencies that are required by the output that are found in output and the CLI package dependencies
// Returns the dependency names and the version to use (taken from the CLI deps package.json)
async function gatherRequiredDependencies(
outputMeta: Metafile["outputs"][string],
config: ResolvedConfig
) {
const dependencies: Record<string, string> = {};
logger.debug("Gathering required dependencies from imports", {
imports: outputMeta.imports,
});
for (const file of outputMeta.imports) {
if ((file.kind !== "require-call" && file.kind !== "dynamic-import") || !file.external) {
continue;
}
const packageName = detectPackageNameFromImportPath(file.path);
if (dependencies[packageName]) {
continue;
}
const internalDependencyVersion =
(packageJson.dependencies as Record<string, string>)[packageName] ??
detectDependencyVersion(packageName);
if (internalDependencyVersion) {
dependencies[packageName] = stripWorkspaceFromVersion(internalDependencyVersion);
}
}
if (config.additionalPackages) {
const projectPackageJson = await readJSONFile(join(config.projectDir, "package.json"));
for (const packageName of config.additionalPackages) {
if (dependencies[packageName]) {
continue;
}
const packageParts = parsePackageName(packageName);
if (packageParts.version) {
dependencies[packageParts.name] = packageParts.version;
continue;
} else {
const externalDependencyVersion = {
...projectPackageJson?.devDependencies,
...projectPackageJson?.dependencies,
}[packageName];
if (externalDependencyVersion) {
dependencies[packageParts.name] = externalDependencyVersion;
continue;
} else {
logger.warn(
`Could not find version for package ${packageName}, add a version specifier to the package name (e.g. ${packageParts.name}@latest) or add it to your project's package.json`
);
}
}
}
}
return dependencies;
}
function createDuplicateTaskIdOutputErrorMessage(
duplicateTaskIds: Array<string>,
taskResources: Array<TaskResource>
) {
const duplicateTable = duplicateTaskIds
.map((id) => {
const tasks = taskResources.filter((task) => task.id === id);
return `\n\n${chalkTask(id)} was found in:${tasks
.map((task) => `\n${task.filePath} -> ${task.exportName}`)
.join("")}`;
})
.join("");
return `Duplicate ${chalkTask("task id")} detected:${duplicateTable}`;
}
async function gatherProcessEnv() {
const env = {
...process.env,
NODE_ENV: process.env.NODE_ENV ?? "development",
NODE_PATH: await amendNodePathWithPnpmNodeModules(process.env.NODE_PATH),
};
// Filter out undefined values
return Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== undefined));
}
async function amendNodePathWithPnpmNodeModules(nodePath?: string): Promise<string | undefined> {
const pnpmModulesPath = await findPnpmNodeModulesPath();
if (!pnpmModulesPath) {
return nodePath;
}
if (nodePath) {
if (nodePath.includes(pnpmModulesPath)) {
return nodePath;
}
return `${nodePath}:${pnpmModulesPath}`;
}
return pnpmModulesPath;
}
async function findPnpmNodeModulesPath(): Promise<string | undefined> {
return await findUp(
async (directory) => {
const pnpmModules = join(directory, "node_modules", ".pnpm", "node_modules");
const hasPnpmNodeModules = await pathExists(pnpmModules);
if (hasPnpmNodeModules) {
return pnpmModules;
}
},
{ type: "directory" }
);
}
let hasResolvedEnvVars = false;
let resolvedEnvVars: Record<string, string> = {};
function createResolveEnvironmentVariablesFunction(configModule?: any) {
return async (
env: Record<string, string>,
worker: BackgroundWorker
): Promise<Record<string, string> | undefined> => {
if (hasResolvedEnvVars) {
return resolvedEnvVars;
}
const $resolvedEnvVars = await callResolveEnvVars(
configModule,
env,
"dev",
worker.params.projectConfig.project
);
if ($resolvedEnvVars) {
resolvedEnvVars = $resolvedEnvVars.variables;
hasResolvedEnvVars = true;
}
return resolvedEnvVars;
};
}
+5 -6
View File
@@ -20,7 +20,6 @@ import {
tracer,
wrapCommandAction,
} from "../cli/common.js";
import { readConfig } from "../utilities/configFiles.js";
import { createFileFromTemplate } from "../utilities/createFileFromTemplate.js";
import { createFile, pathExists, readFile } from "../utilities/fileSystem.js";
import { PackageManager, getUserPackageManager } from "../utilities/getUserPackageManager.js";
@@ -30,8 +29,8 @@ import { cliRootPath } from "../utilities/resolveInternalFilePath.js";
import { login } from "./login.js";
import { spinner } from "../utilities/windows.js";
import { CLOUD_API_URL } from "../consts.js";
import { version } from "../../package.json";
import { cliLink, prettyError } from "../utilities/cliOutput.js";
import { loadConfig } from "../config.js";
const InitCommandOptions = CommonCommandOptions.extend({
projectRef: z.string().optional(),
@@ -56,7 +55,7 @@ export function configureInitCommand(program: Command) {
.option(
"-t, --tag <package tag>",
"The version of the @trigger.dev/sdk package to install",
version
"latest"
)
.option("--skip-package-install", "Skip installing the @trigger.dev/sdk package")
.option("--override-config", "Override the existing config file if it exists")
@@ -112,11 +111,11 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
if (!options.overrideConfig) {
try {
// check to see if there is an existing trigger.dev config file in the project directory
const result = await readConfig(dir);
const result = await loadConfig({ cwd: dir });
outro(
result.status === "file"
? `Project already initialized: Found config file at ${result.path}. Pass --override-config to override`
result.configFile
? `Project already initialized: Found config file at ${result.configFile}. Pass --override-config to override`
: "Project already initialized"
);
+2 -2
View File
@@ -15,13 +15,13 @@ import {
} from "../cli/common.js";
import { chalkLink, prettyError } from "../utilities/cliOutput.js";
import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js";
import { getVersion } from "../utilities/getVersion.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { LoginResult } from "../utilities/session.js";
import { whoAmI } from "./whoami.js";
import { logger } from "../utilities/logger.js";
import { spinner } from "../utilities/windows.js";
import { isLinuxServer } from "../utilities/linux.js";
import { VERSION } from "../consts.js";
export const LoginCommandOptions = CommonCommandOptions.extend({
apiUrl: z.string(),
@@ -35,7 +35,7 @@ export function configureLoginCommand(program: Command) {
.command("login")
.description("Login with Trigger.dev so you can perform authenticated actions")
)
.version(getVersion(), "-v, --version", "Display the version number")
.version(VERSION, "-v, --version", "Display the version number")
.action(async (options) => {
await handleTelemetry(async () => {
await printInitialBanner(false);
+2 -2
View File
@@ -10,8 +10,8 @@ import { printStandloneInitialBanner, updateCheck } from "../utilities/initialBa
import { join, resolve } from "path";
import { JavascriptProject } from "../utilities/javascriptProject.js";
import { PackageManager } from "../utilities/getUserPackageManager.js";
import { getVersion } from "../utilities/getVersion.js";
import { chalkError, prettyError, prettyWarning } from "../utilities/cliOutput.js";
import { VERSION } from "../consts.js";
export const UpdateCommandOptions = CommonCommandOptions.pick({
logLevel: true,
@@ -66,7 +66,7 @@ export async function updateTriggerPackages(
return false;
}
const cliVersion = getVersion();
const cliVersion = VERSION;
const newCliVersion = await updateCheck();
if (newCliVersion) {
+201
View File
@@ -0,0 +1,201 @@
import { TriggerConfig } from "@trigger.dev/core/v3";
import { DEFAULT_RUNTIME, ResolvedConfig } from "@trigger.dev/core/v3/build";
import * as c12 from "c12";
import { defu } from "defu";
import * as esbuild from "esbuild";
import { readdir } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative } from "node:path";
import { findWorkspaceDir, resolveLockfile, resolvePackageJSON, resolveTSConfig } from "pkg-types";
import { generateCode, loadFile } from "./utilities/importMagicast.js";
export type ResolveConfigOptions = {
cwd?: string;
};
export async function loadConfig({
cwd = process.cwd(),
}: ResolveConfigOptions = {}): Promise<ResolvedConfig> {
const result = await c12.loadConfig<TriggerConfig>({
name: "trigger",
cwd,
});
return await resolveConfig(cwd, result);
}
type ResolveWatchConfigOptions = ResolveConfigOptions & {
onUpdate: (config: ResolvedConfig) => void;
debounce?: number;
ignoreInitial?: boolean;
};
type ResolveWatchConfigResult = {
config: ResolvedConfig;
files: string[];
stop: () => Promise<void>;
};
export async function watchConfig({
cwd = process.cwd(),
onUpdate,
debounce = 100,
ignoreInitial = true,
}: ResolveWatchConfigOptions): Promise<ResolveWatchConfigResult> {
const result = await c12.watchConfig<TriggerConfig>({
name: "trigger",
cwd,
debounce,
chokidarOptions: { ignoreInitial },
acceptHMR: async ({ oldConfig, newConfig, getDiff }) => {
const diff = getDiff();
console.log("watchConfig.acceptHMR", { diff, oldConfig, newConfig });
if (diff.length === 0) {
console.log("No config changed detected!");
return true; // No changes!
}
return false;
},
onUpdate: async ({ newConfig, getDiff }) => {
const diff = getDiff();
if (diff.length === 0) {
console.log("No config changed detected!");
return;
}
const resolvedConfig = await resolveConfig(cwd, newConfig);
onUpdate(resolvedConfig);
},
});
const config = await resolveConfig(cwd, result);
return {
config,
files: result.watchingFiles,
stop: result.unwatch,
};
}
export function configPlugin(resolvedConfig: ResolvedConfig): esbuild.Plugin | undefined {
const configFile = resolvedConfig.configFile;
if (!configFile) {
return;
}
// We need to strip the "build" key from the config file, so build dependencies don't make it into the final bundle
return {
name: "trigger-config-strip",
setup(build) {
const filename = basename(configFile);
// Convert the filename to a regex to filter against
const filter = new RegExp(`${filename.replace(/\./g, "\\.")}$`);
console.log("trigger-config-strip.filter", filter);
build.onLoad({ filter }, async (args) => {
console.log("trigger-config-strip.onLoad", args);
const $mod = await loadFile(args.path);
// Support for both bare object export and `defineConfig` wrapper
const options =
$mod.exports.default.$type === "function-call"
? $mod.exports.default.$args[0]
: $mod.exports.default;
options.build = {};
const contents = generateCode($mod);
console.log("trigger-config-strip.onLoad.contents", contents);
return {
contents: contents.code,
loader: "ts",
resolveDir: dirname(args.path),
};
});
},
};
}
async function resolveConfig(
cwd: string,
result: c12.ResolvedConfig<TriggerConfig>
): Promise<ResolvedConfig> {
const packageJsonPath = await resolvePackageJSON(cwd);
const tsconfigPath = await resolveTSConfig(cwd);
const lockfilePath = await resolveLockfile(cwd);
const workspaceDir = await findWorkspaceDir(cwd);
const workingDir = packageJsonPath ? dirname(packageJsonPath) : cwd;
let dirs = result.config.dirs ? result.config.dirs : await autoDetectDirs(workingDir);
dirs = dirs.map((dir) => (isAbsolute(dir) ? relative(workingDir, dir) : dir));
const mergedConfig = defu(
{
workingDir: packageJsonPath ? dirname(packageJsonPath) : cwd,
configFile: result.configFile,
packageJsonPath,
tsconfigPath,
lockfilePath,
workspaceDir,
},
result.config,
{
dirs,
runtime: DEFAULT_RUNTIME,
tsconfig: tsconfigPath,
build: {
jsx: {
factory: "React.createElement",
fragment: "React.Fragment",
automatic: true,
},
extensions: [],
external: [],
},
}
);
return {
...mergedConfig,
dirs: Array.from(new Set(mergedConfig.dirs)),
};
}
const IGNORED_DIRS = ["node_modules", ".git", "dist", "out", "build"];
async function autoDetectDirs(workingDir: string): Promise<string[]> {
const entries = await readdir(workingDir, { withFileTypes: true });
const dirs: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory() || IGNORED_DIRS.includes(entry.name) || entry.name.startsWith("."))
continue;
const fullPath = join(workingDir, entry.name);
// Ignore the directory if it's <any>/app/api/trigger
if (fullPath.endsWith("app/api/trigger")) {
continue;
}
if (entry.name === "trigger") {
dirs.push(fullPath);
}
dirs.push(...(await autoDetectDirs(fullPath)));
}
return dirs;
}
+1 -9
View File
@@ -1,13 +1,5 @@
import path from "path";
import { fileURLToPath } from "url";
// With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily.
// Path is in relation to a single index.js file inside ./dist
const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename);
export const PKG_ROOT = path.join(distPath, "../");
export const COMMAND_NAME = "trigger.dev";
export const CLOUD_WEB_URL = "https://cloud.trigger.dev";
export const CLOUD_API_URL = "https://api.trigger.dev";
export const CONFIG_FILES = ["trigger.config.ts", "trigger.config.js", "trigger.config.mjs"];
export const VERSION = "0.0.1"; // This is replaced by the build script
+3
View File
@@ -0,0 +1,3 @@
import { pathToFileURL } from "node:url";
//@ts-ignore - Have to ignore because TSC thinks this is ESM
export const packageDir = pathToFileURL(__dirname).pathname;
+2
View File
@@ -0,0 +1,2 @@
//@ts-ignore
export const packageDir = new URL(".", import.meta.url).pathname;
+3 -3
View File
@@ -4,12 +4,12 @@ import { Resource, detectResourcesSync, processDetectorSync } from "@opentelemet
import { NodeTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
import { DiagConsoleLogger, DiagLogLevel, diag, trace } from "@opentelemetry/api";
import { version } from "../../package.json";
import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
import { logger } from "../utilities/logger.js";
import { VERSION } from "../consts.js";
function initializeTracing(): NodeTracerProvider | undefined {
if (
@@ -30,7 +30,7 @@ function initializeTracing(): NodeTracerProvider | undefined {
}).merge(
new Resource({
[SEMRESATTRS_SERVICE_NAME]: "trigger.dev cli v3",
[SEMRESATTRS_SERVICE_VERSION]: version,
[SEMRESATTRS_SERVICE_VERSION]: VERSION,
})
);
@@ -70,5 +70,5 @@ function initializeTracing(): NodeTracerProvider | undefined {
export const provider = initializeTracing();
export function getTracer() {
return trace.getTracer("trigger.dev cli v3", version);
return trace.getTracer("trigger.dev cli v3", VERSION);
}
-258
View File
@@ -1,258 +0,0 @@
import type * as esbuild from "esbuild";
import type { Plugin } from "esbuild";
import { readFileSync } from "node:fs";
import { extname, isAbsolute } from "node:path";
import tsConfigPaths from "tsconfig-paths";
import { logger } from "./logger";
import { escapeImportPath } from "./windows";
import { DependencyMeta } from "./javascriptProject";
export function mockServerOnlyPlugin(): Plugin {
return {
name: "trigger-mock-server-only",
setup(build) {
build.onResolve({ filter: /server-only/ }, (args) => {
if (args.path !== "server-only") {
return undefined;
}
logger.debug(`[trigger-mock-server-only] Bundling ${args.path}`, {
...args,
});
return {
path: args.path,
external: false,
namespace: "server-only-mock",
};
});
build.onLoad({ filter: /server-only/, namespace: "server-only-mock" }, (args) => {
return {
contents: `export default true;`,
loader: "js",
};
});
},
};
}
export function bundleTriggerDevCore(buildIdentifier: string, tsconfigPath?: string): Plugin {
return {
name: "trigger-bundle-core",
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
if (!args.path.startsWith("@trigger.dev/core/v3")) {
return undefined;
}
const triggerSdkPath = require.resolve("@trigger.dev/sdk/v3", { paths: [process.cwd()] });
logger.debug(`[${buildIdentifier}][trigger-bundle-core] Resolved @trigger.dev/sdk/v3`, {
...args,
triggerSdkPath,
});
const resolvedPath = require.resolve(args.path, {
paths: [triggerSdkPath],
});
logger.debug(`[${buildIdentifier}][trigger-bundle-core] Externalizing ${args.path}`, {
...args,
triggerSdkPath,
resolvedPath,
});
return {
path: resolvedPath,
external: false,
};
});
},
};
}
export function workerSetupImportConfigPlugin(configPath?: string): Plugin {
return {
name: "trigger-worker-setup",
setup(build) {
if (!configPath) {
return;
}
build.onLoad({ filter: /worker-setup\.js$/ }, async (args) => {
let workerSetupContents = readFileSync(args.path, "utf-8");
workerSetupContents = workerSetupContents.replace(
"__SETUP_IMPORTED_PROJECT_CONFIG__",
`import * as setupImportedConfigExports from "${escapeImportPath(
configPath
)}"; const setupImportedConfig = setupImportedConfigExports.config;`
);
logger.debug("Loading worker setup", {
args,
workerSetupContents,
configPath,
});
return {
contents: workerSetupContents,
loader: "js",
};
});
},
};
}
export function bundleDependenciesPlugin(
buildIdentifier: string,
dependencies: Record<string, DependencyMeta>,
dependenciesToBundle?: Array<string | RegExp>,
tsconfigPath?: string
): Plugin {
const matchPath = tsconfigPath ? createMatchPath(tsconfigPath) : undefined;
function resolvePath(id: string) {
if (!matchPath) {
return id;
}
return matchPath(id, undefined, undefined, [".ts", ".tsx", ".js", ".jsx"]) || id;
}
return {
name: "trigger-bundle-dependencies",
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
const resolvedPath = resolvePath(args.path);
if (!isBareModuleId(resolvedPath)) {
return undefined; // let esbuild handle it
}
// Skip assets that are treated as files (.css, .svg, .png, etc.).
// Otherwise, esbuild would emit code that would attempt to require()
// or import these files --- which aren't JavaScript!
let loader;
try {
loader = getLoaderForFile(args.path);
} catch (e) {
if (!(e instanceof Error && e.message.startsWith("Cannot get loader for file"))) {
throw e;
}
}
if (loader === "file") {
return undefined;
}
for (let pattern of dependenciesToBundle ?? []) {
if (typeof pattern === "string" ? args.path === pattern : pattern.test(args.path)) {
return undefined; // let esbuild bundle it
}
}
if (dependencies[args.path] && !dependencies[args.path]!.external) {
return undefined; // let esbuild bundle it
}
logger.debug(`[${buildIdentifier}] Externalizing ${args.path}`, {
...args,
});
// Everything else should be external
return {
path: args.path,
external: true,
};
});
},
};
}
function isBareModuleId(id: string): boolean {
return !id.startsWith("node:") && !id.startsWith(".") && !isAbsolute(id);
}
export function createMatchPath(tsconfigPath: string | undefined) {
// There is no tsconfig to match paths against.
if (!tsconfigPath) {
return undefined;
}
// When passing a absolute path, loadConfig assumes that the path contains
// a tsconfig file.
// Ref.: https://github.com/dividab/tsconfig-paths/blob/v4.0.0/src/__tests__/config-loader.test.ts#L74
let configLoaderResult = tsConfigPaths.loadConfig(tsconfigPath);
if (configLoaderResult.resultType === "failed") {
if (configLoaderResult.message === "Missing baseUrl in compilerOptions") {
throw new Error(
`🚨 Oops! No baseUrl found, please set compilerOptions.baseUrl in your tsconfig or jsconfig`
);
}
return undefined;
}
return tsConfigPaths.createMatchPath(
configLoaderResult.absoluteBaseUrl,
configLoaderResult.paths,
configLoaderResult.mainFields,
configLoaderResult.addMatchAll
);
}
const loaders: { [ext: string]: esbuild.Loader } = {
".aac": "file",
".avif": "file",
".css": "file",
".csv": "file",
".eot": "file",
".fbx": "file",
".flac": "file",
".gif": "file",
".glb": "file",
".gltf": "file",
".gql": "text",
".graphql": "text",
".hdr": "file",
".ico": "file",
".jpeg": "file",
".jpg": "file",
".js": "jsx",
".jsx": "jsx",
".json": "json",
// We preprocess md and mdx files using @mdx-js/mdx and send through
// the JSX for esbuild to handle
".md": "jsx",
".mdx": "jsx",
".mov": "file",
".mp3": "file",
".mp4": "file",
".node": "copy",
".ogg": "file",
".otf": "file",
".png": "file",
".psd": "file",
".sql": "text",
".svg": "file",
".ts": "ts",
".tsx": "tsx",
".ttf": "file",
".wasm": "file",
".wav": "file",
".webm": "file",
".webmanifest": "file",
".webp": "file",
".woff": "file",
".woff2": "file",
".zip": "file",
};
export function getLoaderForFile(file: string): esbuild.Loader {
const ext = extname(file);
const loader = loaders[ext];
if (loader) return loader;
throw new Error(`Cannot get loader for file ${file}`);
}
+4 -168
View File
@@ -1,19 +1,14 @@
import { Config, ResolvedConfig } from "@trigger.dev/core/v3";
import { findUp } from "find-up";
import { mkdirSync, writeFileSync } from "node:fs";
import path, { join } from "node:path";
import { pathToFileURL } from "node:url";
import path from "node:path";
import xdgAppPaths from "xdg-app-paths";
import { z } from "zod";
import { CLOUD_API_URL, CONFIG_FILES } from "../consts.js";
import { createTempDir, readJSONFileSync } from "./fileSystem.js";
import { CONFIG_FILES } from "../consts.js";
import { readJSONFileSync } from "./fileSystem.js";
import { logger } from "./logger.js";
import { findTriggerDirectories, resolveTriggerDirectories } from "./taskFiles.js";
import { build } from "esbuild";
import { esbuildDecorators } from "@anatine/esbuild-decorators";
function getGlobalConfigFolderPath() {
const configDir = xdgAppPaths("trigger").config();
const configDir = xdgAppPaths.default("trigger").config();
return configDir;
}
@@ -111,162 +106,3 @@ async function findFilePath(dir: string, fileName: string): Promise<string | und
return result;
}
export type ReadConfigOptions = {
projectRef?: string;
configFile?: string;
cwd?: string;
};
export type ReadConfigFileResult = {
status: "file";
config: ResolvedConfig;
path: string;
module?: any;
};
export type ReadConfigResult =
| ReadConfigFileResult
| {
status: "in-memory";
config: ResolvedConfig;
}
| {
status: "error";
error: unknown;
};
export async function readConfig(
dir: string,
options?: ReadConfigOptions
): Promise<ReadConfigResult> {
const absoluteDir = path.resolve(options?.cwd || process.cwd(), dir);
const configPath = await getConfigPath(dir, options?.configFile);
if (!configPath) {
if (options?.projectRef) {
const rawConfig = await normalizeConfig({ project: options.projectRef });
const config = Config.parse(rawConfig);
return {
status: "in-memory",
config: await resolveConfig(absoluteDir, config),
};
} else {
throw new Error(`Config file not found in ${absoluteDir} or any parent directory.`);
}
}
const tempDir = await createTempDir();
const builtConfigFilePath = join(tempDir, "config.js");
const builtConfigFileHref = pathToFileURL(builtConfigFilePath).href;
logger.debug("Building config file", {
configPath,
builtConfigFileHref,
builtConfigFilePath,
});
// We need to build the path to the config file, and then import it?
await build({
entryPoints: [configPath],
bundle: true,
metafile: true,
minify: false,
write: true,
format: "cjs",
platform: "node",
target: ["es2020", "node18"],
outfile: builtConfigFilePath,
logLevel: "silent",
plugins: [
esbuildDecorators({
cwd: absoluteDir,
tsx: false,
force: false,
}),
{
name: "native-node-modules",
setup(build) {
const opts = build.initialOptions;
opts.loader = opts.loader || {};
opts.loader[".node"] = "copy";
},
},
],
});
try {
// import the config file
const userConfigModule = await import(builtConfigFileHref);
// The --project-ref CLI arg will always override the project specified in the config file
const rawConfig = await normalizeConfig(
userConfigModule?.config,
options?.projectRef ? { project: options?.projectRef } : undefined
);
const config = Config.parse(rawConfig);
return {
status: "file",
config: await resolveConfig(absoluteDir, config),
path: configPath,
module: userConfigModule,
};
} catch (error) {
return {
status: "error",
error,
};
}
}
export async function resolveConfig(path: string, config: Config): Promise<ResolvedConfig> {
if (!config.triggerDirectories) {
config.triggerDirectories = await findTriggerDirectories(path);
// TODO trigger-dir-missing: throw error if no trigger directory is found
}
config.triggerDirectories = resolveTriggerDirectories(path, config.triggerDirectories);
// TODO trigger-dir-not-found: throw error if trigger directories do not exist
logger.debug("Resolved trigger directories", { triggerDirectories: config.triggerDirectories });
if (!config.triggerUrl) {
config.triggerUrl = CLOUD_API_URL;
}
if (!config.projectDir) {
config.projectDir = path;
}
if (!config.tsconfigPath) {
config.tsconfigPath = await findFilePath(path, "tsconfig.json");
}
if (!config.additionalFiles) {
config.additionalFiles = [];
}
if (config.extraCACerts) {
config.additionalFiles.push(config.extraCACerts);
config.extraCACerts = config.extraCACerts.replace(/^(\.[.]?\/)+/, "");
}
return config as ResolvedConfig;
}
export async function normalizeConfig(config: any, overrides?: Record<string, any>): Promise<any> {
let normalized = config;
if (typeof config === "function") {
normalized = await config();
}
normalized = { ...normalized, ...overrides };
return normalized;
}
@@ -1,5 +1,5 @@
import fs from "fs/promises";
import { pathExists, readFile } from "./fileSystem";
import { pathExists, readFile } from "./fileSystem.js";
import path from "path";
type Result =
+20 -27
View File
@@ -1,11 +1,17 @@
import chalk from "chalk";
import { relative } from "node:path";
import { chalkError, chalkPurple, chalkGrey, chalkGreen, chalkWarning, cliLink } from "./cliOutput";
import { logger } from "./logger";
import { ReadConfigResult } from "./configFiles";
import {
chalkError,
chalkPurple,
chalkGrey,
chalkGreen,
chalkWarning,
cliLink,
} from "./cliOutput.js";
import { logger } from "./logger.js";
import { z } from "zod";
import { groupTaskMetadataIssuesByTask } from "@trigger.dev/core/v3";
import { docs } from "./links";
import { docs } from "./links.js";
export type ESMRequireError = {
type: "esm-require-error";
@@ -41,6 +47,8 @@ export function parseBuildErrorStack(error: unknown): BuildError | undefined {
return error.message;
}
}
return;
}
function getPackageNameFromEsmRequireError(stack: string): string | undefined {
@@ -75,7 +83,7 @@ function getPackageNameFromEsmRequireError(stack: string): string | undefined {
return match[1];
}
export function logESMRequireError(parsedError: ESMRequireError, resolvedConfig: ReadConfigResult) {
export function logESMRequireError(parsedError: ESMRequireError) {
logger.log(
`\n${chalkError("X Error:")} The ${chalkPurple(
parsedError.moduleName
@@ -89,28 +97,13 @@ export function logESMRequireError(parsedError: ESMRequireError, resolvedConfig:
)}`
);
if (resolvedConfig.status === "file") {
const relativePath = relative(resolvedConfig.config.projectDir, resolvedConfig.path).replace(
/\\/g,
"/"
);
logger.log(
`${chalkGrey("○")} ${chalk.underline("Or")} add ${chalkPurple(
parsedError.moduleName
)} to the ${chalkGreen("dependenciesToBundle")} array in your config file ${chalkGrey(
`(${relativePath})`
)}. This will bundle the module with your code.\n`
);
} else {
logger.log(
`${chalkGrey("○")} ${chalk.underline("Or")} add ${chalkPurple(
parsedError.moduleName
)} to the ${chalkGreen("dependenciesToBundle")} array in your config file ${chalkGrey(
"(you'll need to create one)"
)}. This will bundle the module with your code.\n`
);
}
logger.log(
`${chalkGrey("○")} ${chalk.underline("Or")} add ${chalkPurple(
parsedError.moduleName
)} to the ${chalkGreen("dependenciesToBundle")} array in your config file ${chalkGrey(
"(you'll need to create one)"
)}. This will bundle the module with your code.\n`
);
logger.log(
`${chalkGrey("○")} For more info see the ${cliLink("relevant docs", docs.config.esm)}.\n`
@@ -1,32 +0,0 @@
import { checkApiKeyIsDevServer } from "./getApiKeyType.js";
describe("Test API keys", () => {
test("dev server succeeds", async () => {
const result = checkApiKeyIsDevServer("tr_dev_12345");
expect(result.success).toEqual(true);
});
test("dev public fails", async () => {
const result = checkApiKeyIsDevServer("pk_dev_12345");
expect(result.success).toEqual(false);
if (result.success) return;
expect(result.type?.environment).toEqual("dev");
expect(result.type?.type).toEqual("public");
});
test("prod server fails", async () => {
const result = checkApiKeyIsDevServer("tr_prod_12345");
expect(result.success).toEqual(false);
if (result.success) return;
expect(result.type?.environment).toEqual("prod");
expect(result.type?.type).toEqual("server");
});
test("prod public fails", async () => {
const result = checkApiKeyIsDevServer("pk_prod_12345");
expect(result.success).toEqual(false);
if (result.success) return;
expect(result.type?.environment).toEqual("prod");
expect(result.type?.type).toEqual("public");
});
});
@@ -1,6 +1,6 @@
import { findUp } from "find-up";
import { basename } from "path";
import { logger } from "./logger";
import { logger } from "./logger.js";
export type PackageManager = "npm" | "pnpm" | "yarn";
export const LOCKFILES = {
@@ -1,12 +0,0 @@
import { type PackageJson } from "type-fest";
import path from "path";
import { PKG_ROOT } from "../consts.js";
import { readJSONFileSync } from "./fileSystem.js";
export function getVersion() {
const packageJsonPath = path.join(PKG_ROOT, "package.json");
const packageJsonContent = readJSONFileSync(packageJsonPath) as PackageJson;
return packageJsonContent.version ?? "1.0.0";
}
@@ -0,0 +1,7 @@
// @ts-ignore
const { loadFile, generateCode } = require("magicast");
// @ts-ignore
module.exports.loadFile = loadFile;
// @ts-ignore
module.exports.generateCode = generateCode;
@@ -0,0 +1,5 @@
// @ts-ignore
import { loadFile, generateCode } from "magicast";
// @ts-ignore
export { loadFile, generateCode };
+11 -6
View File
@@ -1,14 +1,13 @@
import chalk from "chalk";
import type { Result } from "update-check";
import checkForUpdate from "update-check";
import pkg from "../../package.json";
import { chalkGrey, chalkRun, chalkTask, chalkWorker, green, logo } from "./cliOutput.js";
import { getVersion } from "./getVersion.js";
import { logger } from "./logger.js";
import { spinner } from "./windows.js";
import { readPackageJson } from "./packageJson.js";
export async function printInitialBanner(performUpdateCheck = true) {
const cliVersion = getVersion();
const cliVersion = await getVersion();
const text = `\n${logo()} ${chalkGrey(`(${cliVersion})`)}\n`;
logger.info(text);
@@ -38,7 +37,7 @@ After installation, run Trigger.dev with \`npx trigger.dev\`.`
}
export async function printStandloneInitialBanner(performUpdateCheck = true) {
const cliVersion = getVersion();
const cliVersion = await getVersion();
if (performUpdateCheck) {
const maybeNewVersion = await updateCheck();
@@ -72,9 +71,10 @@ export function printDevBanner(printTopBorder = true) {
async function doUpdateCheck(): Promise<string | undefined> {
let update: Result | null = null;
try {
const pkg = await readPackageJson();
// default cache for update check is 1 day
update = await checkForUpdate(pkg, {
distTag: pkg.version.startsWith("3.0.0-beta") ? "beta" : "latest",
update = await checkForUpdate.default(pkg, {
distTag: pkg.version?.startsWith("3.0.0-beta") ? "beta" : "latest",
});
} catch (err) {
// ignore error
@@ -87,3 +87,8 @@ let updateCheckPromise: Promise<string | undefined>;
export function updateCheck(): Promise<string | undefined> {
return (updateCheckPromise ??= doUpdateCheck());
}
async function getVersion() {
const packageJson = await readPackageJson();
return packageJson.version ?? "unknown";
}
@@ -1,110 +0,0 @@
import { execa } from "execa";
import { join } from "node:path";
import { readJSONFile, writeJSONFile } from "./fileSystem";
import { logger } from "./logger";
export type InstallPackagesOptions = { cwd?: string };
export async function installPackages(
packages: Record<string, string>,
options?: InstallPackagesOptions
) {
const cwd = options?.cwd ?? process.cwd();
logger.debug("Installing packages", { packages });
await setPackageJsonDeps(join(cwd, "package.json"), packages);
await execa(
"npm",
["install", "--install-strategy", "nested", "--ignore-scripts", "--no-audit", "--no-fund"],
{
cwd,
stderr: "pipe",
}
);
}
// Expects path to be in the format:
// - source-map-support/register.js
// - @opentelemetry/api
// - zod
//
// With the result being:
// - source-map-support
// - @opentelemetry/api
// - zod
export function detectPackageNameFromImportPath(path: string): string {
if (path.startsWith("@")) {
return path.split("/").slice(0, 2).join("/");
} else {
return path.split("/")[0] as string;
}
}
/**
* Removes the workspace prefix from a version string.
* @param version - The version string to strip the workspace prefix from.
* @returns The version string without the workspace prefix.
* @example
* stripWorkspaceFromVersion("workspace:1.0.0") // "1.0.0"
* stripWorkspaceFromVersion("1.0.0") // "1.0.0"
*/
export function stripWorkspaceFromVersion(version: string) {
return version.replace(/^workspace:/, "");
}
export function parsePackageName(packageSpecifier: string): { name: string; version?: string } {
let name: string | undefined;
let version: string | undefined;
// Check if the package is scoped
if (packageSpecifier.startsWith("@")) {
const atIndex = packageSpecifier.indexOf("@", 1);
// If a version is included
if (atIndex !== -1) {
name = packageSpecifier.slice(0, atIndex);
version = packageSpecifier.slice(atIndex + 1);
} else {
name = packageSpecifier;
}
} else {
const [packageName, packageVersion] = packageSpecifier.split("@");
if (typeof packageName === "string") {
name = packageName;
}
version = packageVersion;
}
if (!name) {
return { name: packageSpecifier };
}
return { name, version };
}
async function setPackageJsonDeps(path: string, deps: Record<string, string>) {
try {
const existingPackageJson = await readJSONFile(path);
const newPackageJson = {
...existingPackageJson,
dependencies: {
...deps,
},
};
await writeJSONFile(path, newPackageJson);
} catch (error) {
const defaultPackageJson = {
name: "temp",
version: "1.0.0",
description: "",
dependencies: deps,
};
await writeJSONFile(path, defaultPackageJson);
}
}
@@ -1,12 +1,12 @@
import { $, ExecaError } from "execa";
import { join } from "node:path";
import { readJSONFileSync } from "./fileSystem";
import { logger } from "./logger";
import { PackageManager, getUserPackageManager } from "./getUserPackageManager";
import { readJSONFileSync } from "./fileSystem.js";
import { logger } from "./logger.js";
import { PackageManager, getUserPackageManager } from "./getUserPackageManager.js";
import { PackageJson } from "type-fest";
import { assertExhaustive } from "./assertExhaustive";
import { assertExhaustive } from "./assertExhaustive.js";
import { builtinModules } from "node:module";
import { tracer } from "../cli/common";
import { tracer } from "../cli/common.js";
import { recordSpanException } from "@trigger.dev/core/v3/otel";
import { flattenAttributes } from "@trigger.dev/core/v3";
@@ -233,6 +233,8 @@ export class JavascriptProject {
error,
});
}
return;
}
async #getCommand(): Promise<PackageManagerCommands> {
@@ -328,6 +330,8 @@ class PNPMCommands implements PackageManagerCommands {
return dependency.version;
}
}
return;
}
async resolveDependencyVersions(
@@ -524,6 +528,8 @@ class NPMCommands implements PackageManagerCommands {
}
}
}
return;
}
#flattenDependenciesMeta(
+1 -1
View File
@@ -1,5 +1,5 @@
import { spawn } from "child_process";
import { logger } from "./logger";
import { logger } from "./logger.js";
export const isLinuxServer = async () => {
if (process.platform !== "linux") {
@@ -0,0 +1,6 @@
import { readPackageJSON } from "pkg-types";
import { packageDir } from "../packageDir.js";
export async function readPackageJson() {
return await readPackageJSON(packageDir);
}
@@ -1,11 +1,11 @@
import pathModule from "path";
import pathModule from "node:path";
// Takes a relative path (like .) and resolves it to a full path (like /Users/username/Projects/my-triggers)
export const resolvePath = (input: string) => {
return pathModule.resolve(process.cwd(), input);
};
// Takes an absolute path and derives the relative path from the current working directory
// Takes an absolute path and derives the relative path from the current working directory
export const relativePath = (input: string) => {
return pathModule.relative(process.cwd(), input);
};
@@ -1,4 +1,4 @@
import { logger } from "./logger";
import { logger } from "./logger.js";
export async function callResolveEnvVars(
configModule: any,
@@ -59,4 +59,6 @@ export async function callResolveEnvVars(
logger.error(error);
}
}
return;
}
@@ -1,4 +1,4 @@
import { logger } from "./logger";
import { logger } from "./logger.js";
/**
* This function is used by the dev CLI to make sure that the runtime is compatible
+1 -1
View File
@@ -1,7 +1,7 @@
import { ResolvedConfig } from "@trigger.dev/core/v3";
import fs from "node:fs";
import { join, relative, resolve } from "node:path";
import { TaskFile } from "../types";
import { TaskFile } from "../types.js";
export function createTaskFileImports(taskFiles: TaskFile[]) {
return taskFiles
@@ -1,103 +0,0 @@
import { z } from "zod";
export class UncaughtExceptionError extends Error {
constructor(
public readonly originalError: { name: string; message: string; stack?: string },
public readonly origin: "uncaughtException" | "unhandledRejection"
) {
super(`Uncaught exception: ${originalError.message}`);
this.name = "UncaughtExceptionError";
}
}
export class TaskMetadataParseError extends Error {
constructor(
public readonly zodIssues: z.ZodIssue[],
public readonly tasks: any
) {
super(`Failed to parse task metadata`);
this.name = "TaskMetadataParseError";
}
}
export class UnexpectedExitError extends Error {
constructor(
public code: number,
public signal: NodeJS.Signals | null,
public stderr: string | undefined
) {
super(`Unexpected exit with code ${code}`);
this.name = "UnexpectedExitError";
}
}
export class CleanupProcessError extends Error {
constructor() {
super("Cancelled");
this.name = "CleanupProcessError";
}
}
export class CancelledProcessError extends Error {
constructor() {
super("Cancelled");
this.name = "CancelledProcessError";
}
}
export class SigKillTimeoutProcessError extends Error {
constructor() {
super("Process kill timeout");
this.name = "SigKillTimeoutProcessError";
}
}
export class GracefulExitTimeoutError extends Error {
constructor() {
super("Graceful exit timeout");
this.name = "GracefulExitTimeoutError";
}
}
export function getFriendlyErrorMessage(
code: number,
signal: NodeJS.Signals | null,
stderr: string | undefined,
dockerMode = true
) {
const message = (text: string) => {
if (signal) {
return `[${signal}] ${text}`;
} else {
return text;
}
};
if (code === 137) {
if (dockerMode) {
return message(
"Process ran out of memory! Try choosing a machine preset with more memory for this task."
);
} else {
// Note: containerState reason and message should be checked to clarify the error
return message(
"Process most likely ran out of memory, but we can't be certain. Try choosing a machine preset with more memory for this task."
);
}
}
if (stderr?.includes("OOMErrorHandler")) {
return message(
"Process ran out of memory! Try choosing a machine preset with more memory for this task."
);
}
return message(`Process exited with code ${code}.`);
}
File diff suppressed because it is too large Load Diff
@@ -1,271 +0,0 @@
import {
Config,
LogLevel,
ProjectConfig,
clock,
taskCatalog,
type HandleErrorFunction,
} from "@trigger.dev/core/v3";
import {
TaskExecutor,
DurableClock,
getEnvVar,
logLevels,
OtelTaskLogger,
ConsoleInterceptor,
type TracingSDK,
usage,
DevUsageManager,
} from "@trigger.dev/core/v3/workers";
__WORKER_SETUP__;
declare const __WORKER_SETUP__: unknown;
__IMPORTED_PROJECT_CONFIG__;
declare const __IMPORTED_PROJECT_CONFIG__: unknown;
declare const importedConfig: ProjectConfig | undefined;
declare const handleError: HandleErrorFunction | undefined;
declare const __PROJECT_CONFIG__: Config;
declare const tracingSDK: TracingSDK;
declare const otelTracer: Tracer;
declare const otelLogger: Logger;
import {
TaskRunErrorCodes,
TaskRunExecution,
TriggerTracer,
childToWorkerMessages,
logger,
runtime,
workerToChildMessages,
} from "@trigger.dev/core/v3";
import { DevRuntimeManager } from "@trigger.dev/core/v3/dev";
import {
ZodMessageHandler,
ZodMessageSender,
ZodSchemaParsedError,
} from "@trigger.dev/core/v3/zodMessageHandler";
import type { Tracer } from "@opentelemetry/api";
import type { Logger } from "@opentelemetry/api-logs";
declare const sender: ZodMessageSender<typeof childToWorkerMessages>;
const durableClock = new DurableClock();
clock.setGlobalClock(durableClock);
usage.setGlobalUsageManager(new DevUsageManager());
const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger });
const consoleInterceptor = new ConsoleInterceptor(
otelLogger,
typeof __PROJECT_CONFIG__.enableConsoleLogging === "boolean"
? __PROJECT_CONFIG__.enableConsoleLogging
: true
);
const devRuntimeManager = new DevRuntimeManager();
runtime.setGlobalRuntimeManager(devRuntimeManager);
const triggerLogLevel = getEnvVar("TRIGGER_LOG_LEVEL");
const configLogLevel = triggerLogLevel
? triggerLogLevel
: importedConfig
? importedConfig.logLevel
: __PROJECT_CONFIG__.logLevel;
const otelTaskLogger = new OtelTaskLogger({
logger: otelLogger,
tracer: tracer,
level: logLevels.includes(configLogLevel as any) ? (configLogLevel as LogLevel) : "info",
});
logger.setGlobalTaskLogger(otelTaskLogger);
type TaskFileImport = Record<string, unknown>;
const TaskFileImports: Record<string, TaskFileImport> = {};
const TaskFiles: Record<string, string> = {};
__TASKS__;
declare const __TASKS__: Record<string, string>;
// Register the task file metadata (fileName and exportName) for each task
(() => {
for (const [importName, taskFile] of Object.entries(TaskFiles)) {
const fileImports = TaskFileImports[importName];
for (const [exportName, task] of Object.entries(fileImports ?? {})) {
if (
typeof task === "object" &&
task !== null &&
"id" in task &&
typeof task.id === "string"
) {
if (taskCatalog.taskExists(task.id)) {
taskCatalog.registerTaskFileMetadata(task.id, {
exportName,
filePath: (taskFile as any).filePath,
});
}
}
}
}
})();
let _execution: TaskRunExecution | undefined;
let _isRunning = false;
const handler = new ZodMessageHandler({
schema: workerToChildMessages,
messages: {
EXECUTE_TASK_RUN: async ({ execution, traceContext, metadata }) => {
if (_isRunning) {
console.error("Worker is already running a task");
await sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
ok: false,
id: execution.run.id,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_ALREADY_RUNNING,
},
usage: {
durationMs: 0,
},
},
});
return;
}
process.title = `trigger-dev-worker: ${execution.task.id} ${execution.run.id}`;
const task = taskCatalog.getTask(execution.task.id);
if (!task) {
console.error(`Could not find task ${execution.task.id}`);
await sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
ok: false,
id: execution.run.id,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.COULD_NOT_FIND_EXECUTOR,
},
usage: {
durationMs: 0,
},
},
});
return;
}
const executor = new TaskExecutor(task, {
tracer,
tracingSDK,
consoleInterceptor,
projectConfig: __PROJECT_CONFIG__,
importedConfig,
handleErrorFn: handleError,
});
try {
_execution = execution;
_isRunning = true;
const measurement = usage.start();
const { result } = await executor.execute(execution, metadata, traceContext, measurement);
const usageSample = usage.stop(measurement);
return sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
...result,
usage: {
durationMs: usageSample.cpuTime,
},
},
});
} finally {
_execution = undefined;
_isRunning = false;
}
},
TASK_RUN_COMPLETED_NOTIFICATION: async (payload) => {
switch (payload.version) {
case "v1": {
devRuntimeManager.resumeTask(payload.completion, payload.execution.run.id);
break;
}
case "v2": {
devRuntimeManager.resumeTask(payload.completion, payload.completion.id);
break;
}
}
},
CLEANUP: async ({ flush, kill }) => {
if (kill) {
await tracingSDK.flush();
// Now we need to exit the process
await sender.send("READY_TO_DISPOSE", undefined);
} else {
if (flush) {
await tracingSDK.flush();
}
}
},
},
});
process.on("message", async (msg: any) => {
await handler.handleMessage(msg);
});
const TASK_METADATA = taskCatalog.getAllTaskMetadata();
sender.send("TASKS_READY", { tasks: TASK_METADATA }).catch((err) => {
if (err instanceof ZodSchemaParsedError) {
sender.send("TASKS_FAILED_TO_PARSE", { zodIssues: err.error.issues, tasks: TASK_METADATA });
} else {
console.error("Failed to send TASKS_READY message", err);
}
});
process.title = "trigger-dev-worker";
async function asyncHeartbeat(initialDelayInSeconds: number = 30, intervalInSeconds: number = 30) {
async function _doHeartbeat() {
while (true) {
if (_isRunning && _execution) {
try {
await sender.send("TASK_HEARTBEAT", { id: _execution.attempt.id });
} catch (err) {
console.error("Failed to send HEARTBEAT message", err);
}
}
await new Promise((resolve) => setTimeout(resolve, 1000 * intervalInSeconds));
}
}
// Wait for the initial delay
await new Promise((resolve) => setTimeout(resolve, 1000 * initialDelayInSeconds));
// Wait for 5 seconds before the next execution
return _doHeartbeat();
}
// Start the async interval after 30 seconds
asyncHeartbeat().catch((err) => {
console.error("Failed to start asyncHeartbeat", err);
});
@@ -1,34 +0,0 @@
import type { Tracer } from "@opentelemetry/api";
import type { Logger } from "@opentelemetry/api-logs";
import { ProjectConfig, childToWorkerMessages, taskCatalog } from "@trigger.dev/core/v3";
import {
StandardTaskCatalog,
TracingDiagnosticLogLevel,
TracingSDK,
} from "@trigger.dev/core/v3/workers";
import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
import "source-map-support/register.js";
import * as packageJson from "../../../package.json";
__SETUP_IMPORTED_PROJECT_CONFIG__;
declare const __SETUP_IMPORTED_PROJECT_CONFIG__: unknown;
declare const setupImportedConfig: ProjectConfig | undefined;
export const tracingSDK = new TracingSDK({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
instrumentations: setupImportedConfig?.instrumentations ?? [],
diagLogLevel: (process.env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
forceFlushTimeoutMillis: 5_000,
});
export const otelTracer: Tracer = tracingSDK.getTracer("trigger-dev-worker", packageJson.version);
export const otelLogger: Logger = tracingSDK.getLogger("trigger-dev-worker", packageJson.version);
export const sender = new ZodMessageSender({
schema: childToWorkerMessages,
sender: async (message) => {
process.send?.(message);
},
});
taskCatalog.setGlobalTaskCatalog(new StandardTaskCatalog());
@@ -1,873 +0,0 @@
import {
BackgroundWorkerProperties,
Config,
CreateBackgroundWorkerResponse,
ProdChildToWorkerMessages,
ProdTaskRunExecution,
ProdTaskRunExecutionPayload,
ProdWorkerToChildMessages,
SemanticInternalAttributes,
TaskMetadataWithFilePath,
TaskRunBuiltInError,
TaskRunErrorCodes,
TaskRunExecution,
TaskRunExecutionLazyAttemptPayload,
TaskRunExecutionPayload,
TaskRunExecutionResult,
correctErrorStackTrace,
} from "@trigger.dev/core/v3";
import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc";
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
import { Evt } from "evt";
import { ChildProcess, fork } from "node:child_process";
import {
CancelledProcessError,
CleanupProcessError,
GracefulExitTimeoutError,
SigKillTimeoutProcessError,
TaskMetadataParseError,
UncaughtExceptionError,
UnexpectedExitError,
getFriendlyErrorMessage,
} from "../common/errors.js";
type BackgroundWorkerParams = {
env: Record<string, string>;
projectConfig: Config;
contentHash: string;
debugOtel?: boolean;
};
export type OnWaitForDurationMessage = InferSocketMessageSchema<
typeof ProdChildToWorkerMessages,
"WAIT_FOR_DURATION"
>;
export type OnWaitForTaskMessage = InferSocketMessageSchema<
typeof ProdChildToWorkerMessages,
"WAIT_FOR_TASK"
>;
export type OnWaitForBatchMessage = InferSocketMessageSchema<
typeof ProdChildToWorkerMessages,
"WAIT_FOR_BATCH"
>;
export class ProdBackgroundWorker {
private _initialized: boolean = false;
/**
* @deprecated use onTaskRunHeartbeat instead
*/
public onTaskHeartbeat: Evt<string> = new Evt();
public onTaskRunHeartbeat: Evt<string> = new Evt();
public onWaitForDuration: Evt<OnWaitForDurationMessage> = new Evt();
public onWaitForTask: Evt<OnWaitForTaskMessage> = new Evt();
public onWaitForBatch: Evt<OnWaitForBatchMessage> = new Evt();
public onCreateTaskRunAttempt = Evt.create<{ version?: "v1"; runId: string }>();
public attemptCreatedNotification = Evt.create<
| {
success: false;
reason?: string;
}
| {
success: true;
execution: ProdTaskRunExecution;
}
>();
private _onClose: Evt<void> = new Evt();
public tasks: Array<TaskMetadataWithFilePath> = [];
public stderr: Array<string> = [];
_taskRunProcess: TaskRunProcess | undefined;
private _taskRunProcessesBeingKilled: Map<number, TaskRunProcess> = new Map();
private _closed: boolean = false;
constructor(
public path: string,
private params: BackgroundWorkerParams
) {}
async close(gracefulExitTimeoutElapsed = false) {
console.log("Closing worker", { gracefulExitTimeoutElapsed, closed: this._closed });
if (this._closed) {
return;
}
this._closed = true;
this.onTaskHeartbeat.detach();
this.onTaskRunHeartbeat.detach();
// We need to close the task run process
await this._taskRunProcess?.cleanup(true, gracefulExitTimeoutElapsed);
}
async #killTaskRunProcess(flush = true, initialSignal: number | NodeJS.Signals = "SIGTERM") {
console.log("Killing task run process", { flush, initialSignal, closed: this._closed });
if (this._closed || !this._taskRunProcess) {
return;
}
if (flush) {
await this.flushTelemetry();
}
const currentTaskRunProcess = this._taskRunProcess;
// Try graceful exit but don't wait. We limit the amount of processes during creation instead.
this.#tryGracefulExit(currentTaskRunProcess, true, initialSignal).catch((error) => {
console.error("Error while trying graceful exit", error);
});
console.log("Killed task run process, setting closed to true", {
closed: this._closed,
pid: currentTaskRunProcess.pid,
});
this._closed = true;
}
async flushTelemetry() {
console.log("Flushing telemetry");
const start = performance.now();
await this._taskRunProcess?.cleanup(false);
console.log("Flushed telemetry", { duration: performance.now() - start });
}
async initialize(options?: { env?: Record<string, string> }) {
if (this._initialized) {
throw new Error("Worker already initialized");
}
let resolved = false;
this.tasks = await new Promise<Array<TaskMetadataWithFilePath>>((resolve, reject) => {
const child = fork(this.path, {
stdio: [/*stdin*/ "ignore", /*stdout*/ "pipe", /*stderr*/ "pipe", "ipc"],
env: {
...this.params.env,
...options?.env,
},
});
// Set a timeout to kill the child process if it doesn't respond
const timeout = setTimeout(() => {
if (resolved) {
return;
}
resolved = true;
child.kill();
reject(new Error("Worker timed out"));
}, 10_000);
child.stdout?.on("data", (data) => {
console.log(data.toString());
});
child.stderr?.on("data", (data) => {
console.error(data.toString());
this.stderr.push(data.toString());
});
child.on("exit", (code) => {
if (!resolved) {
clearTimeout(timeout);
resolved = true;
reject(new Error(`Worker exited with code ${code}`));
}
});
new ZodIpcConnection({
listenSchema: ProdChildToWorkerMessages,
emitSchema: ProdWorkerToChildMessages,
process: child,
handlers: {
TASKS_READY: async (message) => {
if (!resolved) {
clearTimeout(timeout);
resolved = true;
resolve(message.tasks);
child.kill();
}
},
UNCAUGHT_EXCEPTION: async (message) => {
if (!resolved) {
clearTimeout(timeout);
resolved = true;
reject(new UncaughtExceptionError(message.error, message.origin));
child.kill();
}
},
TASKS_FAILED_TO_PARSE: async (message) => {
if (!resolved) {
clearTimeout(timeout);
resolved = true;
reject(new TaskMetadataParseError(message.zodIssues, message.tasks));
child.kill();
}
},
},
});
});
this._initialized = true;
}
getMetadata(workerId: string, version: string): CreateBackgroundWorkerResponse {
return {
contentHash: this.params.contentHash,
id: workerId,
version: version,
};
}
// We need to notify all the task run processes that a task run has completed,
// in case they are waiting for it through triggerAndWait
async taskRunCompletedNotification(completion: TaskRunExecutionResult) {
this._taskRunProcess?.taskRunCompletedNotification(completion);
}
async waitCompletedNotification() {
this._taskRunProcess?.waitCompletedNotification();
}
async #getFreshTaskRunProcess(
payload: ProdTaskRunExecutionPayload,
messageId?: string
): Promise<TaskRunProcess> {
const metadata = this.getMetadata(
payload.execution.worker.id,
payload.execution.worker.version
);
console.log("Getting fresh task run process, setting closed to false", {
closed: this._closed,
});
this._closed = false;
await this.#killCurrentTaskRunProcessBeforeAttempt();
const taskRunProcess = new TaskRunProcess(
payload.execution.run.id,
payload.execution.run.isTest,
this.path,
{
...this.params.env,
...(payload.environment ?? {}),
},
metadata,
this.params,
messageId
);
taskRunProcess.onExit.attach(({ pid }) => {
console.log("Task run process exited", { pid });
// Only delete the task run process if the pid matches
if (this._taskRunProcess?.pid === pid) {
this._taskRunProcess = undefined;
}
if (pid) {
this._taskRunProcessesBeingKilled.delete(pid);
}
});
taskRunProcess.onIsBeingKilled.attach((taskRunProcess) => {
if (taskRunProcess?.pid) {
this._taskRunProcessesBeingKilled.set(taskRunProcess.pid, taskRunProcess);
}
});
taskRunProcess.onTaskHeartbeat.attach((id) => {
this.onTaskHeartbeat.post(id);
});
taskRunProcess.onTaskRunHeartbeat.attach((id) => {
this.onTaskRunHeartbeat.post(id);
});
taskRunProcess.onWaitForBatch.attach((message) => {
this.onWaitForBatch.post(message);
});
taskRunProcess.onWaitForDuration.attach((message) => {
this.onWaitForDuration.post(message);
});
taskRunProcess.onWaitForTask.attach((message) => {
this.onWaitForTask.post(message);
});
await taskRunProcess.initialize();
this._taskRunProcess = taskRunProcess;
return this._taskRunProcess;
}
async forceKillOldTaskRunProcesses() {
for (const taskRunProcess of this._taskRunProcessesBeingKilled.values()) {
try {
await taskRunProcess.kill("SIGKILL");
} catch (error) {
console.error("Error while force killing old task run processes", error);
}
}
}
async #killCurrentTaskRunProcessBeforeAttempt() {
console.log("killCurrentTaskRunProcessBeforeAttempt()", {
hasTaskRunProcess: !!this._taskRunProcess,
});
if (!this._taskRunProcess) {
return;
}
const currentTaskRunProcess = this._taskRunProcess;
console.log("Killing current task run process", {
isBeingKilled: currentTaskRunProcess?.isBeingKilled,
totalBeingKilled: this._taskRunProcessesBeingKilled.size,
});
if (currentTaskRunProcess.isBeingKilled) {
if (this._taskRunProcessesBeingKilled.size > 1) {
await this.#tryGracefulExit(currentTaskRunProcess);
} else {
// If there's only one or none being killed, don't do anything so we can create a fresh one in parallel
}
} else {
// It's not being killed, so kill it
if (this._taskRunProcessesBeingKilled.size > 0) {
await this.#tryGracefulExit(currentTaskRunProcess);
} else {
// There's none being killed yet, so we can kill it without waiting. We still set a timeout to kill it forcefully just in case it sticks around.
currentTaskRunProcess.kill("SIGTERM", 5_000).catch(() => {});
}
}
}
async #tryGracefulExit(
taskRunProcess: TaskRunProcess,
kill = false,
initialSignal: number | NodeJS.Signals = "SIGTERM"
) {
console.log("Trying graceful exit", { kill, initialSignal });
try {
const initialExit = taskRunProcess.onExit.waitFor(5_000);
if (kill) {
taskRunProcess.kill(initialSignal);
}
await initialExit;
} catch (error) {
console.error("TaskRunProcess graceful kill timeout exceeded", error);
this.#tryForcefulExit(taskRunProcess);
}
}
async #tryForcefulExit(taskRunProcess: TaskRunProcess) {
console.log("Trying forceful exit");
try {
const forcedKill = taskRunProcess.onExit.waitFor(5_000);
taskRunProcess.kill("SIGKILL");
await forcedKill;
} catch (error) {
console.error("TaskRunProcess forced kill timeout exceeded", error);
throw new SigKillTimeoutProcessError();
}
}
// We need to fork the process before we can execute any tasks, use a fresh process for each execution
async executeTaskRun(
payload: ProdTaskRunExecutionPayload,
messageId?: string
): Promise<TaskRunExecutionResult> {
try {
const taskRunProcess = await this.#getFreshTaskRunProcess(payload, messageId);
console.log("executing task run", {
attempt: payload.execution.attempt.id,
taskRunPid: taskRunProcess.pid,
});
const result = await taskRunProcess.executeTaskRun(payload);
if (result.ok) {
return result;
}
const error = result.error;
if (error.type === "BUILT_IN_ERROR") {
const mappedError = await this.#correctError(error, payload.execution);
return {
...result,
error: mappedError,
};
}
return result;
} catch (e) {
if (e instanceof CancelledProcessError) {
return {
id: payload.execution.attempt.id,
ok: false,
retry: undefined,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_RUN_CANCELLED,
},
};
}
if (e instanceof CleanupProcessError) {
return {
id: payload.execution.attempt.id,
ok: false,
retry: undefined,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_EXECUTION_ABORTED,
},
};
}
if (e instanceof UnexpectedExitError) {
return {
id: payload.execution.attempt.id,
ok: false,
retry: undefined,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE,
message: getFriendlyErrorMessage(e.code, e.signal, e.stderr),
stackTrace: e.stderr,
},
};
}
if (e instanceof SigKillTimeoutProcessError) {
return {
id: payload.execution.attempt.id,
ok: false,
retry: undefined,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_PROCESS_SIGKILL_TIMEOUT,
},
};
}
if (e instanceof GracefulExitTimeoutError) {
return {
id: payload.execution.attempt.id,
ok: false,
retry: undefined,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.GRACEFUL_EXIT_TIMEOUT,
message: "Worker process killed while attempt in progress.",
},
};
}
return {
id: payload.execution.attempt.id,
ok: false,
retry: undefined,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_EXECUTION_FAILED,
},
};
} finally {
await this.#killTaskRunProcess();
}
}
async cancelAttempt(attemptId: string) {
if (!this._taskRunProcess) {
console.error("No task run process to cancel attempt", { attemptId });
return;
}
await this._taskRunProcess.cancel();
}
async executeTaskRunLazyAttempt(payload: TaskRunExecutionLazyAttemptPayload) {
// Post to coordinator
this.onCreateTaskRunAttempt.post({ runId: payload.runId });
let execution: ProdTaskRunExecution;
try {
const start = performance.now();
// ..and wait for response
const attemptCreated = await this.attemptCreatedNotification.waitFor(120_000);
if (!attemptCreated.success) {
throw new Error(`${attemptCreated.reason ?? "Unknown error"}`);
}
console.log("Attempt created", {
number: attemptCreated.execution.attempt.number,
duration: performance.now() - start,
});
execution = attemptCreated.execution;
} catch (error) {
console.error("Error while creating attempt", error);
throw new Error(`Failed to create attempt: ${error}`);
}
const completion = await this.executeTaskRun(
{
execution,
traceContext: payload.traceContext,
environment: payload.environment,
},
payload.messageId
);
return { execution, completion };
}
async #correctError(
error: TaskRunBuiltInError,
execution: TaskRunExecution
): Promise<TaskRunBuiltInError> {
return {
...error,
stackTrace: correctErrorStackTrace(error.stackTrace, this.params.projectConfig.projectDir),
};
}
}
class TaskRunProcess {
private _ipc?: ZodIpcConnection<
typeof ProdChildToWorkerMessages,
typeof ProdWorkerToChildMessages
>;
private _child?: ChildProcess;
private _childPid?: number;
private _attemptPromises: Map<
string,
{ resolver: (value: TaskRunExecutionResult) => void; rejecter: (err?: any) => void }
> = new Map();
private _attemptStatuses: Map<string, "PENDING" | "REJECTED" | "RESOLVED"> = new Map();
private _currentExecution: TaskRunExecution | undefined;
private _isBeingKilled: boolean = false;
private _isBeingCancelled: boolean = false;
private _gracefulExitTimeoutElapsed: boolean = false;
private _stderr: Array<string> = [];
/**
* @deprecated use onTaskRunHeartbeat instead
*/
public onTaskHeartbeat: Evt<string> = new Evt();
public onTaskRunHeartbeat: Evt<string> = new Evt();
public onExit: Evt<{ code: number | null; signal: NodeJS.Signals | null; pid?: number }> =
new Evt();
public onIsBeingKilled: Evt<TaskRunProcess> = new Evt();
public onWaitForDuration: Evt<OnWaitForDurationMessage> = new Evt();
public onWaitForTask: Evt<OnWaitForTaskMessage> = new Evt();
public onWaitForBatch: Evt<OnWaitForBatchMessage> = new Evt();
public preCheckpointNotification = Evt.create<{ willCheckpointAndRestore: boolean }>();
constructor(
private runId: string,
private isTest: boolean,
private path: string,
private env: NodeJS.ProcessEnv,
private metadata: BackgroundWorkerProperties,
private worker: BackgroundWorkerParams,
private messageId?: string
) {}
async initialize() {
this._child = fork(this.path, {
stdio: [/*stdin*/ "ignore", /*stdout*/ "pipe", /*stderr*/ "pipe", "ipc"],
env: {
...(this.isTest ? { TRIGGER_LOG_LEVEL: "debug" } : {}),
...this.env,
OTEL_RESOURCE_ATTRIBUTES: JSON.stringify({
[SemanticInternalAttributes.PROJECT_DIR]: this.worker.projectConfig.projectDir,
}),
...(this.worker.debugOtel ? { OTEL_LOG_LEVEL: "debug" } : {}),
},
});
this._childPid = this._child?.pid;
this._ipc = new ZodIpcConnection({
listenSchema: ProdChildToWorkerMessages,
emitSchema: ProdWorkerToChildMessages,
process: this._child,
handlers: {
TASK_RUN_COMPLETED: async (message) => {
const { result, execution } = message;
const promiseStatus = this._attemptStatuses.get(execution.attempt.id);
if (promiseStatus !== "PENDING") {
return;
}
this._attemptStatuses.set(execution.attempt.id, "RESOLVED");
const attemptPromise = this._attemptPromises.get(execution.attempt.id);
if (!attemptPromise) {
return;
}
const { resolver } = attemptPromise;
resolver(result);
},
READY_TO_DISPOSE: async (message) => {
process.exit(0);
},
TASK_HEARTBEAT: async (message) => {
if (this.messageId) {
this.onTaskRunHeartbeat.post(this.messageId);
} else {
console.error(
"No message id for task heartbeat, falling back to (deprecated) attempt heartbeat",
{ id: message.id }
);
this.onTaskHeartbeat.post(message.id);
}
},
TASKS_READY: async (message) => {},
WAIT_FOR_TASK: async (message) => {
this.onWaitForTask.post(message);
},
WAIT_FOR_BATCH: async (message) => {
this.onWaitForBatch.post(message);
},
WAIT_FOR_DURATION: async (message) => {
this.onWaitForDuration.post(message);
},
},
});
this._child.on("exit", this.#handleExit.bind(this));
this._child.stdout?.on("data", this.#handleLog.bind(this));
this._child.stderr?.on("data", this.#handleStdErr.bind(this));
}
async cancel() {
this._isBeingCancelled = true;
await this.cleanup(true);
}
async cleanup(kill = false, gracefulExitTimeoutElapsed = false) {
console.log("cleanup()", { kill, gracefulExitTimeoutElapsed });
if (kill && this._isBeingKilled) {
return;
}
if (kill) {
this._isBeingKilled = true;
this.onIsBeingKilled.post(this);
}
const killChildProcess = gracefulExitTimeoutElapsed && !!this._currentExecution;
// Kill parent unless graceful exit timeout has elapsed and we're in the middle of an execution
const killParentProcess = kill && !killChildProcess;
console.log("Cleaning up task run process", {
killChildProcess,
killParentProcess,
ipc: this._ipc,
childPid: this._childPid,
realChildPid: this._child?.pid,
});
try {
await this._ipc?.sendWithAck(
"CLEANUP",
{
flush: true,
kill: killParentProcess,
},
30_000
);
} catch (error) {
console.error("Error while cleaning up task run process", error);
if (killParentProcess) {
process.exit(0);
}
}
if (killChildProcess) {
this._gracefulExitTimeoutElapsed = true;
// Kill the child process
await this.kill("SIGKILL");
}
}
async executeTaskRun(payload: TaskRunExecutionPayload): Promise<TaskRunExecutionResult> {
let resolver: (value: TaskRunExecutionResult) => void;
let rejecter: (err?: any) => void;
const promise = new Promise<TaskRunExecutionResult>((resolve, reject) => {
resolver = resolve;
rejecter = reject;
});
this._attemptStatuses.set(payload.execution.attempt.id, "PENDING");
// @ts-expect-error - We know that the resolver and rejecter are defined
this._attemptPromises.set(payload.execution.attempt.id, { resolver, rejecter });
const { execution, traceContext } = payload;
this._currentExecution = execution;
if (this._child?.connected && !this._isBeingKilled && !this._child.killed) {
await this._ipc?.send("EXECUTE_TASK_RUN", {
execution,
traceContext,
metadata: this.metadata,
});
}
const result = await promise;
this._currentExecution = undefined;
return result;
}
taskRunCompletedNotification(completion: TaskRunExecutionResult) {
if (!completion.ok && typeof completion.retry !== "undefined") {
console.error(
"Task run completed with error and wants to retry, won't send task run completed notification"
);
return;
}
if (!this._child?.connected || this._isBeingKilled || this._child.killed) {
console.error(
"Child process not connected or being killed, can't send task run completed notification"
);
return;
}
this._ipc?.send("TASK_RUN_COMPLETED_NOTIFICATION", {
version: "v2",
completion,
});
}
waitCompletedNotification() {
if (!this._child?.connected || this._isBeingKilled || this._child.killed) {
console.error(
"Child process not connected or being killed, can't send wait completed notification"
);
return;
}
this._ipc?.send("WAIT_COMPLETED_NOTIFICATION", {});
}
async #handleExit(code: number | null, signal: NodeJS.Signals | null) {
console.log("handling child exit", { code, signal });
// Go through all the attempts currently pending and reject them
for (const [id, status] of this._attemptStatuses.entries()) {
if (status === "PENDING") {
console.log("found pending attempt", { id });
this._attemptStatuses.set(id, "REJECTED");
const attemptPromise = this._attemptPromises.get(id);
if (!attemptPromise) {
continue;
}
const { rejecter } = attemptPromise;
if (this._isBeingCancelled) {
rejecter(new CancelledProcessError());
} else if (this._gracefulExitTimeoutElapsed) {
// Order matters, this has to be before the graceful exit timeout
rejecter(new GracefulExitTimeoutError());
} else if (this._isBeingKilled) {
rejecter(new CleanupProcessError());
} else {
rejecter(
new UnexpectedExitError(
code ?? -1,
signal,
this._stderr.length ? this._stderr.join("\n") : undefined
)
);
}
}
}
this.onExit.post({ code, signal, pid: this.pid });
}
#handleLog(data: Buffer) {
console.log(data.toString());
}
#handleStdErr(data: Buffer) {
const text = data.toString();
console.error(text);
if (this._stderr.length > 100) {
this._stderr.shift();
}
this._stderr.push(text);
}
async kill(signal?: number | NodeJS.Signals, timeoutInMs?: number) {
this._isBeingKilled = true;
const killTimeout = this.onExit.waitFor(timeoutInMs);
this.onIsBeingKilled.post(this);
this._child?.kill(signal);
if (timeoutInMs) {
await killTimeout;
}
}
get isBeingKilled() {
return this._isBeingKilled || this._child?.killed;
}
get pid() {
return this._childPid;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,316 +0,0 @@
import {
Config,
HandleErrorFunction,
LogLevel,
ProdChildToWorkerMessages,
ProdWorkerToChildMessages,
ProjectConfig,
clock,
taskCatalog,
} from "@trigger.dev/core/v3";
import {
ConsoleInterceptor,
DevUsageManager,
DurableClock,
OtelTaskLogger,
ProdUsageManager,
TaskExecutor,
getEnvVar,
logLevels,
usage,
type TracingSDK,
} from "@trigger.dev/core/v3/workers";
import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc";
import { ZodSchemaParsedError } from "@trigger.dev/core/v3/zodMessageHandler";
import "source-map-support/register.js";
__WORKER_SETUP__;
declare const __WORKER_SETUP__: unknown;
__IMPORTED_PROJECT_CONFIG__;
declare const __IMPORTED_PROJECT_CONFIG__: unknown;
declare const importedConfig: ProjectConfig | undefined;
declare const handleError: HandleErrorFunction | undefined;
declare const __PROJECT_CONFIG__: Config;
declare const tracingSDK: TracingSDK;
declare const otelTracer: Tracer;
declare const otelLogger: Logger;
import type { Tracer } from "@opentelemetry/api";
import type { Logger } from "@opentelemetry/api-logs";
import {
TaskRunErrorCodes,
TaskRunExecution,
TriggerTracer,
logger,
runtime,
} from "@trigger.dev/core/v3";
import { ProdRuntimeManager } from "@trigger.dev/core/v3/prod";
const heartbeatIntervalMs = getEnvVar("USAGE_HEARTBEAT_INTERVAL_MS");
const usageEventUrl = getEnvVar("USAGE_EVENT_URL");
const triggerJWT = getEnvVar("TRIGGER_JWT");
const prodUsageManager = new ProdUsageManager(new DevUsageManager(), {
heartbeatIntervalMs: heartbeatIntervalMs ? parseInt(heartbeatIntervalMs, 10) : undefined,
url: usageEventUrl,
jwt: triggerJWT,
});
usage.setGlobalUsageManager(prodUsageManager);
const durableClock = new DurableClock();
clock.setGlobalClock(durableClock);
const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger });
const consoleInterceptor = new ConsoleInterceptor(otelLogger, true);
const triggerLogLevel = getEnvVar("TRIGGER_LOG_LEVEL");
const configLogLevel = triggerLogLevel
? triggerLogLevel
: importedConfig
? importedConfig.logLevel
: __PROJECT_CONFIG__.logLevel;
const otelTaskLogger = new OtelTaskLogger({
logger: otelLogger,
tracer: tracer,
level: logLevels.includes(configLogLevel as any) ? (configLogLevel as LogLevel) : "info",
});
logger.setGlobalTaskLogger(otelTaskLogger);
type TaskFileImport = Record<string, unknown>;
const TaskFileImports: Record<string, TaskFileImport> = {};
const TaskFiles: Record<string, string> = {};
__TASKS__;
declare const __TASKS__: Record<string, string>;
// Register the task file metadata (fileName and exportName) for each task
(() => {
for (const [importName, taskFile] of Object.entries(TaskFiles)) {
const fileImports = TaskFileImports[importName];
for (const [exportName, task] of Object.entries(fileImports ?? {})) {
if (
typeof task === "object" &&
task !== null &&
"id" in task &&
typeof task.id === "string"
) {
if (taskCatalog.taskExists(task.id)) {
taskCatalog.registerTaskFileMetadata(task.id, {
exportName,
filePath: (taskFile as any).filePath,
});
}
}
}
}
})();
let _execution: TaskRunExecution | undefined;
let _isRunning = false;
const zodIpc = new ZodIpcConnection({
listenSchema: ProdWorkerToChildMessages,
emitSchema: ProdChildToWorkerMessages,
process,
handlers: {
EXECUTE_TASK_RUN: async ({ execution, traceContext, metadata }, sender) => {
if (_isRunning) {
console.error("Worker is already running a task");
await sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
ok: false,
id: execution.run.id,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_ALREADY_RUNNING,
},
},
});
return;
}
process.title = `trigger-prod-worker: ${execution.task.id} ${execution.run.id}`;
const task = taskCatalog.getTask(execution.task.id);
if (!task) {
console.error(`Could not find task ${execution.task.id}`);
await sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
ok: false,
id: execution.run.id,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.COULD_NOT_FIND_EXECUTOR,
},
},
});
return;
}
const executor = new TaskExecutor(task, {
tracer,
tracingSDK,
consoleInterceptor,
projectConfig: __PROJECT_CONFIG__,
importedConfig,
handleErrorFn: handleError,
});
try {
_execution = execution;
_isRunning = true;
const measurement = usage.start();
const { result } = await executor.execute(execution, metadata, traceContext, measurement);
const usageSample = usage.stop(measurement);
return await sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
...result,
usage: {
durationMs: usageSample.cpuTime,
},
},
});
} finally {
_execution = undefined;
_isRunning = false;
}
},
TASK_RUN_COMPLETED_NOTIFICATION: async ({ completion }) => {
prodRuntimeManager.resumeTask(completion);
},
WAIT_COMPLETED_NOTIFICATION: async () => {
prodRuntimeManager.resumeAfterDuration();
},
CLEANUP: async ({ flush, kill }, sender) => {
if (kill) {
await flushAll();
// Now we need to exit the process
await sender.send("READY_TO_DISPOSE", undefined);
} else {
if (flush) {
await flushAll();
}
}
},
},
});
async function flushAll(timeoutInMs: number = 10_000) {
const now = performance.now();
console.log(`Flushing at ${now}`);
await Promise.all([flushUsage(), flushTracingSDK()]);
const duration = performance.now() - now;
console.log(`Flushed in ${duration}ms`);
}
async function flushUsage() {
const now = performance.now();
console.log(`Flushing usage at ${now}`);
await prodUsageManager.flush();
const duration = performance.now() - now;
console.log(`Flushed usage in ${duration}ms`);
}
async function flushTracingSDK() {
const now = performance.now();
console.log(`Flushing tracingSDK at ${now}`);
await tracingSDK.flush();
const duration = performance.now() - now;
console.log(`Flushed tracingSDK in ${duration}ms`);
}
// Ignore SIGTERM, handled by entry point
process.on("SIGTERM", async () => {});
const prodRuntimeManager = new ProdRuntimeManager(zodIpc, {
waitThresholdInMs: parseInt(process.env.TRIGGER_RUNTIME_WAIT_THRESHOLD_IN_MS ?? "30000", 10),
});
runtime.setGlobalRuntimeManager(prodRuntimeManager);
let taskMetadata = taskCatalog.getAllTaskMetadata();
if (typeof importedConfig?.machine === "string") {
// Set the machine preset on all tasks that don't have it
taskMetadata = taskMetadata.map((task) => {
if (typeof task.machine?.preset !== "string") {
return {
...task,
machine: {
preset: importedConfig.machine,
},
};
}
return task;
});
}
zodIpc.send("TASKS_READY", { tasks: taskMetadata }).catch((err) => {
if (err instanceof ZodSchemaParsedError) {
zodIpc.send("TASKS_FAILED_TO_PARSE", { zodIssues: err.error.issues, tasks: taskMetadata });
} else {
console.error("Failed to send TASKS_READY message", err);
}
});
process.title = "trigger-prod-worker";
async function asyncHeartbeat(initialDelayInSeconds: number = 30, intervalInSeconds: number = 20) {
async function _doHeartbeat() {
while (true) {
if (_isRunning && _execution) {
try {
// The attempt ID will only be used to heartbeat if the message (run) ID isn't set on the TaskRunProcess
await zodIpc.send("TASK_HEARTBEAT", { id: _execution.attempt.id });
} catch (err) {
console.error("Failed to send HEARTBEAT message", err);
}
}
await new Promise((resolve) => setTimeout(resolve, 1000 * intervalInSeconds));
}
}
// Wait for the initial delay
await new Promise((resolve) => setTimeout(resolve, 1000 * initialDelayInSeconds));
// Wait for 5 seconds before the next execution
return _doHeartbeat();
}
// Start the async interval after initial delay
asyncHeartbeat(5).catch((err) => {
console.error("Failed to start asyncHeartbeat", err);
});
@@ -1,27 +0,0 @@
import type { Tracer } from "@opentelemetry/api";
import * as packageJson from "../../../package.json";
import { ProjectConfig, taskCatalog } from "@trigger.dev/core/v3";
import {
TracingDiagnosticLogLevel,
TracingSDK,
StandardTaskCatalog,
} from "@trigger.dev/core/v3/workers";
import type { Logger } from "@opentelemetry/api-logs";
__SETUP_IMPORTED_PROJECT_CONFIG__;
declare const __SETUP_IMPORTED_PROJECT_CONFIG__: unknown;
declare const setupImportedConfig: ProjectConfig | undefined;
export const tracingSDK = new TracingSDK({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
instrumentations: setupImportedConfig?.instrumentations ?? [],
diagLogLevel: (process.env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
forceFlushTimeoutMillis: process.env.OTEL_FORCE_FLUSH_TIMEOUT
? parseInt(process.env.OTEL_FORCE_FLUSH_TIMEOUT, 10)
: 5_000,
});
export const otelTracer: Tracer = tracingSDK.getTracer("trigger-prod-worker", packageJson.version);
export const otelLogger: Logger = tracingSDK.getLogger("trigger-prod-worker", packageJson.version);
taskCatalog.setGlobalTaskCatalog(new StandardTaskCatalog());
+18 -3
View File
@@ -28,6 +28,7 @@
"./types": "./src/types.ts",
"./versions": "./src/versions.ts",
"./v3": "./src/v3/index.ts",
"./v3/build": "./src/v3/build/index.ts",
"./v3/apps": "./src/v3/apps/index.ts",
"./v3/errors": "./src/v3/errors.ts",
"./v3/logger-api": "./src/v3/logger-api.ts",
@@ -79,7 +80,7 @@
"humanize-duration": "^3.27.3",
"socket.io-client": "4.7.5",
"superjson": "^2.2.1",
"zod": "3.22.3",
"zod": "3.23.8",
"zod-error": "1.5.0",
"zod-validation-error": "^1.5.0",
"execa": "^8.0.1"
@@ -92,7 +93,10 @@
"rimraf": "^3.0.2",
"socket.io": "4.7.4",
"tshy": "^3.0.2",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"esbuild": "^0.23.0",
"defu": "^6.1.4",
"ts-essentials": "10.0.1"
},
"engines": {
"node": ">=18.20.0"
@@ -231,6 +235,17 @@
"default": "./dist/commonjs/v3/index.js"
}
},
"./v3/build": {
"import": {
"triggerdotdev-source": "./src/v3/build/index.ts",
"types": "./dist/esm/v3/build/index.d.ts",
"default": "./dist/esm/v3/build/index.js"
},
"require": {
"types": "./dist/commonjs/v3/build/index.d.ts",
"default": "./dist/commonjs/v3/build/index.js"
}
},
"./v3/apps": {
"import": {
"triggerdotdev-source": "./src/v3/apps/index.ts",
@@ -464,4 +479,4 @@
}
},
"type": "module"
}
}
+1
View File
@@ -0,0 +1 @@
export const VERSION = "0.0.1"; // This is replaced by the build script
+2 -2
View File
@@ -1,6 +1,5 @@
import { context, propagation } from "@opentelemetry/api";
import { z } from "zod";
import { version } from "../../../package.json";
import {
AddTagsRequestBody,
BatchTaskRunExecutionResult,
@@ -44,6 +43,7 @@ import {
ListRunsQueryParams,
UpdateEnvironmentVariableParams,
} from "./types.js";
import { VERSION } from "../../consts.js";
export type {
CreateEnvironmentVariableParams,
@@ -503,7 +503,7 @@ export class ApiClient {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${this.accessToken}`,
"trigger-version": version,
"trigger-version": VERSION,
};
// Only inject the context if we are inside a task
+51
View File
@@ -0,0 +1,51 @@
import { BuildManifest, BuildTarget } from "../schemas/build.js";
import type { Plugin } from "esbuild";
import { ResolvedConfig } from "./resolvedConfig.js";
export interface BuildExtension {
name: string;
externalsForTarget?: (target: BuildTarget) => string[] | undefined;
onBuildStart?: (context: BuildContext) => Promise<void> | void;
onBuildComplete?: (
context: BuildContext,
manifest: BuildManifest
) => Promise<undefined | void> | undefined | void;
}
export interface BuildContext {
target: BuildTarget;
config: ResolvedConfig;
workingDir: string;
addLayer(layer: BuildLayer): void;
registerPlugin(plugin: Plugin, options?: RegisterPluginOptions): void;
/*
* Resolve a path relative to the working directory
*/
resolvePath(path: string): Promise<string | undefined>;
}
export interface BuildLayer {
id: string;
commands?: string[];
files?: Record<string, string>;
build?: {
env?: Record<string, string | undefined>;
};
deploy?: {
env?: Record<string, string | undefined>;
};
dependencies?: Record<string, string>;
}
export type PluginPlacement = "first" | "last";
export type RegisterPluginOptions = {
target?: BuildTarget;
placement?: PluginPlacement;
};
export type RegisteredPlugin = RegisterPluginOptions & {
plugin: Plugin;
};
+3
View File
@@ -0,0 +1,3 @@
export * from "./extensions.js";
export * from "./resolvedConfig.js"
export * from "./runtime.js";
@@ -0,0 +1,27 @@
import { type Defu } from "defu";
import type { Prettify } from "ts-essentials";
import { TriggerConfig } from "../config.js";
import { BuildRuntime } from "../schemas/config.js";
export type ResolvedConfig = Prettify<
Defu<
TriggerConfig,
[
{},
{
runtime: BuildRuntime;
dirs: string[];
tsconfig: string;
build: {
jsx: { factory: string; fragment: string; automatic: true };
} & Omit<NonNullable<TriggerConfig["build"]>, "jsx">;
},
]
> & {
workingDir: string;
workspaceDir: string;
packageJsonPath: string;
lockfilePath: string;
configFile?: string;
}
>;
+14
View File
@@ -0,0 +1,14 @@
import { BuildRuntime } from "../schemas/config.js";
export const DEFAULT_RUNTIME: BuildRuntime = "node20";
export function binaryForRuntime(runtime: BuildRuntime): string {
switch (runtime) {
case "node20":
return "node";
case "bun":
return "bun";
default:
throw new Error(`Unsupported runtime ${runtime}`);
}
}
@@ -1,47 +1,29 @@
import { FailureFnParams, InitFnParams, StartFnParams, SuccessFnParams } from "./index.js";
import { LogLevel } from "../logger/taskLogger.js";
import { MachinePresetName, RetryOptions } from "../schemas/index.js";
import type { Instrumentation } from "@opentelemetry/instrumentation";
import { Instrumentation } from "@opentelemetry/instrumentation";
import { BuildRuntime } from "./schemas/config.js";
import { BuildExtension } from "./build/extensions.js";
import { MachinePresetName } from "./schemas/common.js";
import { LogLevel } from "./logger/taskLogger.js";
import { FailureFnParams, InitFnParams, StartFnParams, SuccessFnParams } from "./types/index.js";
import { RetryOptions } from "./index.js";
export interface ProjectConfig {
export type TriggerConfig = {
/**
* @default "node20"
*/
runtime?: BuildRuntime;
project: string;
triggerDirectories?: string | string[];
triggerUrl?: string;
dirs?: string[];
instrumentations?: Array<Instrumentation>;
tsconfig?: string;
retries?: {
enabledInDev?: boolean;
default?: RetryOptions;
};
additionalPackages?: string[];
/**
* The default machine preset to use for your deployed trigger.dev tasks. You can override this on a per-task basis.
* @default "small-1x"
*/
machine?: MachinePresetName;
/**
* List of additional files to include in your trigger.dev bundle. e.g. ["./prisma/schema.prisma"]
*
* Supports glob patterns.
*
* Note: The path separator for glob patterns is `/`, even on Windows!
*/
additionalFiles?: string[];
/**
* List of patterns that determine if a module is included in your trigger.dev bundle. This is needed when consuming ESM only packages, since the trigger.dev bundle is currently built as a CJS module.
*/
dependenciesToBundle?: Array<string | RegExp>;
/**
* The path to your project's tsconfig.json file. Will use tsconfig.json in the project directory if not provided.
*/
tsconfigPath?: string;
/**
* The OpenTelemetry instrumentations to enable
*/
instrumentations?: Instrumentation[];
/**
* Set the log level for the logger. Defaults to "info", so you will see "log", "info", "warn", and "error" messages, but not "debug" messages.
*
@@ -55,6 +37,64 @@ export interface ProjectConfig {
* Enable console logging while running the dev CLI. This will print out logs from console.log, console.warn, and console.error. By default all logs are sent to the trigger.dev backend, and not logged to the console.
*/
enableConsoleLogging?: boolean;
build?: {
extensions?: BuildExtension[];
external?: string[];
jsx?: {
/**
* @default "React.createElement"
*/
factory?: string;
/**
* @default "React.Fragment"
*/
fragment?: string;
/**
* @default true
* @description Set the esbuild jsx option to automatic. Set this to false if you aren't using React.
* @see https://esbuild.github.io/api/#jsx
*/
automatic?: boolean;
};
};
deploy?: {
env?: Record<string, string>;
};
/**
* @deprecated Use `dirs` instead
*/
triggerDirectories?: string[];
/**
* @deprecated Use the `additionalPackages` extension instead.
*/
additionalPackages?: string[];
/**
* @deprecated Use the `additionalFiles` extension instead.
*/
additionalFiles?: string[];
/**
* @deprecated Dependencies are now bundled by default. If you want to exclude some dependencies from the bundle, use the `build.external` option.
*/
dependenciesToBundle?: Array<string | RegExp>;
/**
* @deprecated Use `tsconfig` instead.
*/
tsconfigPath?: string;
/**
* CA Cert file to be added to NODE_EXTRA_CA_CERT environment variable in, useful in use with self signed cert in the trigger.dev environment.
*
* @example "./certs/ca.crt"
* Note: must start with "./" and be relative to the project root.
*
*/
extraCACerts?: string;
/**
* Run before a task is executed, for all tasks. This is useful for setting up any global state that is needed for all tasks.
@@ -77,18 +117,7 @@ export interface ProjectConfig {
onStart?: (payload: unknown, params: StartFnParams) => Promise<void>;
/**
* postInstall will run during the deploy build step, after all the dependencies have been installed.
*
* @example "prisma generate"
* @deprecated Use a custom build extension to add post install commands
*/
postInstall?: string;
/**
* CA Cert file to be added to NODE_EXTRA_CA_CERT environment variable in, useful in use with self signed cert in the trigger.dev environment.
*
* @example "./certs/ca.crt"
* Note: must start with "./" and be relative to the project root.
*
*/
extraCACerts?: string;
}
};
+2 -1
View File
@@ -44,7 +44,6 @@ export {
} from "./utils/retries.js";
export { accessoryAttributes } from "./utils/styleAttributes.js";
export { detectDependencyVersion } from "./utils/detectDependencyVersion.js";
export {
conditionallyExportPacket,
conditionallyImportPacket,
@@ -56,3 +55,5 @@ export {
stringifyIO,
type IOPacket,
} from "./utils/ioSerialization.js";
export * from "./config.js";
+2 -2
View File
@@ -40,7 +40,7 @@ import {
TaskContextSpanProcessor,
} from "../taskContext/otelProcessors.js";
import { getEnvVar } from "../utils/getEnv.js";
import { version } from "../../../package.json";
import { VERSION } from "../../consts.js";
class AsyncResourceDetector implements DetectorSync {
private _promise: Promise<ResourceAttributes>;
@@ -112,7 +112,7 @@ export class TracingSDK {
new Resource({
[SemanticResourceAttributes.CLOUD_PROVIDER]: "trigger.dev",
[SemanticInternalAttributes.TRIGGER]: true,
[SemanticInternalAttributes.CLI_VERSION]: version,
[SemanticInternalAttributes.CLI_VERSION]: VERSION,
})
)
.merge(config.resource ?? new Resource({}))
+170
View File
@@ -0,0 +1,170 @@
import { z } from "zod";
import { ConfigManifest } from "./config.js";
export const TaskFile = z.object({
entry: z.string(),
out: z.string(),
});
export type TaskFile = z.infer<typeof TaskFile>;
export const BuildExternal = z.object({
name: z.string(),
version: z.string(),
});
export type BuildExternal = z.infer<typeof BuildExternal>;
export const BuildTarget = z.enum(["dev", "deploy"]);
export type BuildTarget = z.infer<typeof BuildTarget>;
export const BuildRuntime = z.enum(["node20", "bun"]);
export type BuildRuntime = z.infer<typeof BuildRuntime>;
export const BuildManifest = z.object({
target: BuildTarget,
runtime: BuildRuntime,
config: ConfigManifest,
files: z.array(TaskFile),
outputPath: z.string(),
workerEntryPath: z.string(),
workerForkPath: z.string(),
loaderPath: z.string().optional(),
configPath: z.string(),
externals: BuildExternal.array().optional(),
build: z.object({
env: z.record(z.string()).optional(),
commands: z.array(z.string()).optional(),
}),
deploy: z.object({
env: z.record(z.string()).optional(),
}),
});
export type BuildManifest = z.infer<typeof BuildManifest>;
export const IndexMessage = z.object({
type: z.literal("index"),
data: z.object({
build: BuildManifest,
}),
});
export type IndexMessage = z.infer<typeof IndexMessage>;
export const TaskManifest = z.object({
id: z.string(),
exportName: z.string(),
file: TaskFile,
});
export type TaskManifest = z.infer<typeof TaskManifest>;
export const ExecuteTaskMessage = z.object({
type: z.literal("execute-task"),
data: z.object({
task: TaskManifest,
payload: z.unknown(),
projectRef: z.string(),
configPath: z.string(),
}),
});
export type ExecuteTaskMessage = z.infer<typeof ExecuteTaskMessage>;
export const RunExecution = z.object({
task: TaskManifest,
payload: z.unknown(),
projectRef: z.string(),
configPath: z.string(),
entryPath: z.string(),
loaderPath: z.string().optional(),
env: z.record(z.string()),
cwd: z.string().optional(),
});
export type RunExecution = z.infer<typeof RunExecution>;
export const ParentToChildMessages = z.discriminatedUnion("type", [
IndexMessage,
ExecuteTaskMessage,
]);
export type ParentToChildMessages = z.infer<typeof ParentToChildMessages>;
export const WorkerManifest = z.object({
tasks: TaskManifest.array(),
});
export type WorkerManifest = z.infer<typeof WorkerManifest>;
export const WorkerManifestMessage = z.object({
type: z.literal("worker-manifest"),
data: z.object({
manifest: WorkerManifest,
}),
});
export type WorkerManifestMessage = z.infer<typeof WorkerManifestMessage>;
export const FailedTaskCompletion = z.object({
ok: z.literal(false),
error: z.object({
message: z.string(),
stack: z.string().optional(),
name: z.string().optional(),
}),
});
export type FailedTaskCompletion = z.infer<typeof FailedTaskCompletion>;
export const SuccessfulTaskCompletion = z.object({
ok: z.literal(true),
output: z.unknown(),
});
export type SuccessfulTaskCompletion = z.infer<typeof SuccessfulTaskCompletion>;
export const TaskCompletion = z.discriminatedUnion("ok", [
FailedTaskCompletion,
SuccessfulTaskCompletion,
]);
export type TaskCompletion = z.infer<typeof TaskCompletion>;
export const CompletedTask = z.object({
id: z.string(),
completion: TaskCompletion,
spans: z.array(z.any()),
});
export type CompletedTask = z.infer<typeof CompletedTask>;
export const CompletedTaskMessage = z.object({
type: z.literal("completed-task"),
data: CompletedTask,
});
export type CompletedTaskMessage = z.infer<typeof CompletedTaskMessage>;
export const ChildToParentMessages = z.discriminatedUnion("type", [
WorkerManifestMessage,
CompletedTaskMessage,
]);
export type ChildToParentMessages = z.infer<typeof ChildToParentMessages>;
export const TriggerTaskResult = z.discriminatedUnion("ok", [
z.object({
ok: z.literal(true),
output: z.unknown(),
}),
z.object({
ok: z.literal(false),
error: z.string(),
}),
]);
export type TriggerTaskResult = z.infer<typeof TriggerTaskResult>;
+13
View File
@@ -0,0 +1,13 @@
import { z } from "zod";
export const ConfigManifest = z.object({
projectRef: z.string(),
dirs: z.string().array(),
external: z.string().array().optional(),
});
export type ConfigManifest = z.infer<typeof ConfigManifest>;
export const BuildRuntime = z.enum(["node20", "bun"]);
export type BuildRuntime = z.infer<typeof BuildRuntime>;
+1
View File
@@ -8,3 +8,4 @@ export * from "./style.js";
export * from "./fetch.js";
export * from "./eventFilter.js";
export * from "./openTelemetry.js";
export * from "./config.js";
-1
View File
@@ -7,7 +7,6 @@ import {
import { Prettify } from "./utils.js";
export * from "./utils.js";
export * from "./config.js";
export type InitOutput = Record<string, any> | void | undefined;
@@ -1,5 +0,0 @@
import { dependencies } from "../../../package.json"
export function detectDependencyVersion(dependency: string): string | undefined {
return (dependencies as Record<string, string>)[dependency]
}
+4 -3
View File
@@ -14,7 +14,7 @@ import {
import { SemanticInternalAttributes } from "../semanticInternalAttributes.js";
import { taskContext } from "../task-context-api.js";
import { TriggerTracer } from "../tracer.js";
import { HandleErrorFunction, ProjectConfig, TaskMetadataWithFunctions } from "../types/index.js";
import { HandleErrorFunction, TaskMetadataWithFunctions } from "../types/index.js";
import {
conditionallyExportPacket,
conditionallyImportPacket,
@@ -26,13 +26,14 @@ import { calculateNextRetryDelay } from "../utils/retries.js";
import { accessoryAttributes } from "../utils/styleAttributes.js";
import { UsageMeasurement } from "../usage/types.js";
import { ApiError, RateLimitError } from "../apiClient/errors.js";
import { TriggerConfig } from "../index.js";
export type TaskExecutorOptions = {
tracingSDK: TracingSDK;
tracer: TriggerTracer;
consoleInterceptor: ConsoleInterceptor;
projectConfig: Config;
importedConfig: ProjectConfig | undefined;
importedConfig: TriggerConfig | undefined;
handleErrorFn: HandleErrorFunction | undefined;
};
@@ -41,7 +42,7 @@ export class TaskExecutor {
private _tracer: TriggerTracer;
private _consoleInterceptor: ConsoleInterceptor;
private _config: Config;
private _importedConfig: ProjectConfig | undefined;
private _importedConfig: TriggerConfig | undefined;
private _handleErrorFn: HandleErrorFunction | undefined;
constructor(
+1 -1
View File
@@ -15,7 +15,7 @@
"react-email": "^2.1.1",
"resend": "^3.2.0",
"tiny-invariant": "^1.2.0",
"zod": "3.22.3"
"zod": "3.23.8"
},
"devDependencies": {
"@trigger.dev/tsconfig": "workspace:*",
+1 -1
View File
@@ -48,7 +48,7 @@
"ulid": "^2.3.0",
"uuid": "^9.0.0",
"ws": "^8.11.0",
"zod": "3.22.3",
"zod": "3.23.8",
"msw": "^2.2.1"
},
"devDependencies": {
+8 -1
View File
@@ -1,8 +1,15 @@
import type { TriggerConfig } from "@trigger.dev/core/v3";
export type {
ProjectConfig as TriggerConfig,
HandleErrorArgs,
HandleErrorFunction,
ResolveEnvironmentVariablesFunction,
ResolveEnvironmentVariablesParams,
ResolveEnvironmentVariablesResult,
} from "@trigger.dev/core/v3";
export function defineConfig(config: TriggerConfig): TriggerConfig {
return config;
}
export type { TriggerConfig };
+638 -259
View File
File diff suppressed because it is too large Load Diff
+3 -6
View File
@@ -24,8 +24,7 @@
"@sindresorhus/slugify": "^2.2.1",
"@t3-oss/env-nextjs": "^0.10.1",
"@traceloop/instrumentation-openai": "^0.3.9",
"@trigger.dev/core": "workspace:^3.0.0-beta.0",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.0",
"@trigger.dev/sdk": "workspace:*",
"dotenv": "^16.4.5",
"execa": "^8.0.1",
"msw": "^2.2.1",
@@ -38,7 +37,7 @@
"stripe": "^12.14.0",
"typeorm": "^0.3.20",
"yt-dlp-wrap": "^2.3.12",
"zod": "3.22.3"
"zod": "3.23.8"
},
"devDependencies": {
"@opentelemetry/api": "^1.8.0",
@@ -56,14 +55,12 @@
"@opentelemetry/sdk-trace-base": "^1.22.0",
"@opentelemetry/sdk-trace-node": "^1.22.0",
"@opentelemetry/semantic-conventions": "^1.22.0",
"@trigger.dev/tsconfig": "workspace:*",
"@types/node": "20.4.2",
"@types/react": "^18.3.1",
"esbuild": "^0.19.11",
"trigger.dev": "workspace:*",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.0.1",
"typescript": "^5.3.0"
"typescript": "^5.5.4"
}
}
+5 -10
View File
@@ -1,7 +1,6 @@
import type { TriggerConfig, ResolveEnvironmentVariablesFunction } from "@trigger.dev/sdk/v3";
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
import { AppDataSource } from "@/trigger/orm";
import { InfisicalClient } from "@infisical/sdk";
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
import { defineConfig, type ResolveEnvironmentVariablesFunction } from "@trigger.dev/sdk/v3";
export { handleError } from "./src/handleError";
@@ -32,7 +31,7 @@ export const resolveEnvVars: ResolveEnvironmentVariablesFunction = async ({
};
};
export const config: TriggerConfig = {
export default defineConfig({
project: "yubjwjsfkxnylobaqvqz",
machine: "small-2x",
retries: {
@@ -53,13 +52,9 @@ export const config: TriggerConfig = {
logLevel: "info",
postInstall: "echo '========== config.postInstall'",
onStart: async (payload, { ctx }) => {
if (ctx.organization.id === "clsylhs0v0002dyx75xx4pod1") {
console.log("Initializing the app data source");
await AppDataSource.initialize();
}
console.log(`Task ${ctx.task.id} started ${ctx.run.id}`);
},
onFailure: async (payload, error, { ctx }) => {
console.log(`Task ${ctx.task.id} failed ${ctx.run.id}`);
},
};
});
+12 -14
View File
@@ -1,19 +1,17 @@
{
"extends": "@trigger.dev/tsconfig/node18.json",
"include": ["./src/**/*.ts", "trigger.config.ts", "src/trigger/email.tsx"],
"compilerOptions": {
"jsx": "react-jsx",
"baseUrl": ".",
"lib": ["DOM", "DOM.Iterable"],
"paths": {
"@/*": ["./src/*"],
"@trigger.dev/core/v3": ["../../packages/core/src/v3/index"],
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
"@trigger.dev/sdk/v3": ["../../packages/trigger-sdk/src/v3/index"],
"@trigger.dev/sdk/v3/*": ["../../packages/trigger-sdk/src/v3/*"]
},
"target": "esnext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"skipLibCheck": true,
"customConditions": ["triggerdotdev-source"],
"jsx": "preserve",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowJs": true
}
"lib": ["DOM", "DOM.Iterable"]
},
"include": ["./src/**/*.ts", "trigger.config.ts", "src/trigger/email.tsx"]
}