refactor: delete packages/ai/ — moved to @trigger.dev/sdk subpaths

All functionality now lives in:
- @trigger.dev/sdk/chat (frontend transport)
- @trigger.dev/sdk/ai (backend chatTask, pipeChat)

Co-authored-by: Eric Allam <eric@trigger.dev>
This commit is contained in:
Cursor Agent
2026-02-15 12:56:17 +00:00
committed by Eric Allam
parent ccc85e4f62
commit f0dd5e0907
11 changed files with 6 additions and 1762 deletions
-74
View File
@@ -1,74 +0,0 @@
{
"name": "@trigger.dev/ai",
"version": "4.3.3",
"description": "AI SDK integration for Trigger.dev - Custom ChatTransport for running AI chat as durable tasks",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/triggerdotdev/trigger.dev",
"directory": "packages/ai"
},
"type": "module",
"files": [
"dist"
],
"tshy": {
"selfLink": false,
"main": true,
"module": true,
"project": "./tsconfig.json",
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
]
},
"scripts": {
"clean": "rimraf dist .tshy .tshy-build .turbo",
"build": "tshy && pnpm run update-version",
"dev": "tshy --watch",
"typecheck": "tsc --noEmit",
"test": "vitest",
"update-version": "tsx ../../scripts/updateVersion.ts",
"check-exports": "attw --pack ."
},
"dependencies": {
"@trigger.dev/core": "workspace:4.3.3"
},
"peerDependencies": {
"ai": "^5.0.0 || ^6.0.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"ai": "^6.0.0",
"rimraf": "^3.0.2",
"tshy": "^3.0.2",
"tsx": "4.17.0",
"vitest": "^2.1.0"
},
"engines": {
"node": ">=18.20.0"
},
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"@triggerdotdev/source": "./src/index.ts",
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
}
},
"main": "./dist/commonjs/index.js",
"types": "./dist/commonjs/index.d.ts",
"module": "./dist/esm/index.js"
}
-132
View File
@@ -1,132 +0,0 @@
import { task as createTask } from "@trigger.dev/sdk";
import type { Task } from "@trigger.dev/core/v3";
import type { ChatTaskPayload } from "./types.js";
import { pipeChat } from "./pipeChat.js";
/**
* Options for defining a chat task.
*
* This is a simplified version of the standard task options with the payload
* pre-typed as `ChatTaskPayload`.
*/
export type ChatTaskOptions<TIdentifier extends string> = {
/** Unique identifier for the task */
id: TIdentifier;
/** Optional description of the task */
description?: string;
/** Retry configuration */
retry?: {
maxAttempts?: number;
factor?: number;
minTimeoutInMs?: number;
maxTimeoutInMs?: number;
randomize?: boolean;
};
/** Queue configuration */
queue?: {
name?: string;
concurrencyLimit?: number;
};
/** Machine preset for the task */
machine?: {
preset?: string;
};
/** Maximum duration in seconds */
maxDuration?: number;
/**
* The main run function for the chat task.
*
* Receives a `ChatTaskPayload` with the conversation messages, chat session ID,
* and trigger type.
*
* **Auto-piping:** If this function returns a value that has a `.toUIMessageStream()` method
* (like a `StreamTextResult` from `streamText()`), the stream will automatically be piped
* to the frontend via the chat realtime stream. If you need to pipe from deeper in your
* code, use `pipeChat()` instead and don't return the result.
*/
run: (payload: ChatTaskPayload) => Promise<unknown>;
};
/**
* An object that has a `toUIMessageStream()` method, like the result of `streamText()`.
*/
type UIMessageStreamable = {
toUIMessageStream: (...args: any[]) => AsyncIterable<unknown> | ReadableStream<unknown>;
};
function isUIMessageStreamable(value: unknown): value is UIMessageStreamable {
return (
typeof value === "object" &&
value !== null &&
"toUIMessageStream" in value &&
typeof (value as any).toUIMessageStream === "function"
);
}
/**
* Creates a Trigger.dev task pre-configured for AI SDK chat.
*
* This is a convenience wrapper around `task()` from `@trigger.dev/sdk` that:
* - **Pre-types the payload** as `ChatTaskPayload` — no manual typing needed
* - **Auto-pipes the stream** if the `run` function returns a `StreamTextResult`
*
* Requires `@trigger.dev/sdk` to be installed (it's a peer dependency).
*
* @example
* ```ts
* import { chatTask } from "@trigger.dev/ai";
* import { streamText, convertToModelMessages } from "ai";
* import { openai } from "@ai-sdk/openai";
*
* // Simple: return streamText result — auto-piped to the frontend
* export const myChatTask = chatTask({
* id: "my-chat-task",
* run: async ({ messages }) => {
* return streamText({
* model: openai("gpt-4o"),
* messages: convertToModelMessages(messages),
* });
* },
* });
* ```
*
* @example
* ```ts
* import { chatTask, pipeChat } from "@trigger.dev/ai";
*
* // Complex: use pipeChat() from deep inside your agent code
* export const myAgentTask = chatTask({
* id: "my-agent-task",
* run: async ({ messages }) => {
* await runComplexAgentLoop(messages);
* // pipeChat() called internally by the agent loop
* },
* });
* ```
*/
export function chatTask<TIdentifier extends string>(
options: ChatTaskOptions<TIdentifier>
): Task<TIdentifier, ChatTaskPayload, unknown> {
const { run: userRun, ...restOptions } = options;
return createTask<TIdentifier, ChatTaskPayload, unknown>({
...restOptions,
run: async (payload: ChatTaskPayload) => {
const result = await userRun(payload);
// If the run function returned a StreamTextResult or similar,
// automatically pipe it to the chat stream
if (isUIMessageStreamable(result)) {
await pipeChat(result);
}
return result;
},
});
}
-3
View File
@@ -1,3 +0,0 @@
export { TriggerChatTransport, createChatTransport } from "./transport.js";
export type { TriggerChatTransportOptions, ChatTaskPayload } from "./types.js";
export { VERSION } from "./version.js";
-137
View File
@@ -1,137 +0,0 @@
import { realtimeStreams } from "@trigger.dev/core/v3";
/**
* The default stream key used for chat transport communication.
*
* Both `TriggerChatTransport` (frontend) and `pipeChat` (backend) use this key
* by default to ensure they communicate over the same stream.
*/
export const CHAT_STREAM_KEY = "chat";
/**
* Options for `pipeChat`.
*/
export type PipeChatOptions = {
/**
* Override the stream key to pipe to.
* Must match the `streamKey` option on `TriggerChatTransport`.
*
* @default "chat"
*/
streamKey?: string;
/**
* An AbortSignal to cancel the stream.
*/
signal?: AbortSignal;
/**
* The target run ID to pipe the stream to.
* @default "self" (current run)
*/
target?: string;
};
/**
* An object that has a `toUIMessageStream()` method, like the result of `streamText()` from the AI SDK.
*/
type UIMessageStreamable = {
toUIMessageStream: (...args: any[]) => AsyncIterable<unknown> | ReadableStream<unknown>;
};
function isUIMessageStreamable(value: unknown): value is UIMessageStreamable {
return (
typeof value === "object" &&
value !== null &&
"toUIMessageStream" in value &&
typeof (value as any).toUIMessageStream === "function"
);
}
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
return (
typeof value === "object" &&
value !== null &&
Symbol.asyncIterator in value
);
}
function isReadableStream(value: unknown): value is ReadableStream<unknown> {
return (
typeof value === "object" &&
value !== null &&
typeof (value as any).getReader === "function"
);
}
/**
* Pipes a chat stream to the realtime stream, making it available to the
* `TriggerChatTransport` on the frontend.
*
* Accepts any of:
* - A `StreamTextResult` from the AI SDK (has `.toUIMessageStream()`)
* - An `AsyncIterable` of `UIMessageChunk`s
* - A `ReadableStream` of `UIMessageChunk`s
*
* This must be called from inside a Trigger.dev task's `run` function.
*
* @example
* ```ts
* import { task } from "@trigger.dev/sdk";
* import { pipeChat, type ChatTaskPayload } from "@trigger.dev/ai";
* import { streamText, convertToModelMessages } from "ai";
*
* export const myChatTask = task({
* id: "my-chat-task",
* run: async (payload: ChatTaskPayload) => {
* const result = streamText({
* model: openai("gpt-4o"),
* messages: convertToModelMessages(payload.messages),
* });
*
* await pipeChat(result);
* },
* });
* ```
*
* @example
* ```ts
* // Deep inside your agent library — pipeChat works from anywhere inside a task
* async function runAgentLoop(messages: CoreMessage[]) {
* const result = streamText({ model, messages });
* await pipeChat(result);
* }
* ```
*
* @param source - A StreamTextResult, AsyncIterable, or ReadableStream of UIMessageChunks
* @param options - Optional configuration
* @returns A promise that resolves when the stream has been fully piped
*/
export async function pipeChat(
source: UIMessageStreamable | AsyncIterable<unknown> | ReadableStream<unknown>,
options?: PipeChatOptions
): Promise<void> {
const streamKey = options?.streamKey ?? CHAT_STREAM_KEY;
// Resolve the source to an AsyncIterable or ReadableStream
let stream: AsyncIterable<unknown> | ReadableStream<unknown>;
if (isUIMessageStreamable(source)) {
stream = source.toUIMessageStream();
} else if (isAsyncIterable(source) || isReadableStream(source)) {
stream = source;
} else {
throw new Error(
"pipeChat: source must be a StreamTextResult (with .toUIMessageStream()), " +
"an AsyncIterable, or a ReadableStream"
);
}
// Pipe to the realtime stream
const instance = realtimeStreams.pipe(streamKey, stream, {
signal: options?.signal,
target: options?.target,
});
await instance.wait();
}
-842
View File
@@ -1,842 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { UIMessage, UIMessageChunk } from "ai";
import { TriggerChatTransport, createChatTransport } from "./transport.js";
// Helper: encode text as SSE format
function sseEncode(chunks: UIMessageChunk[]): string {
return chunks.map((chunk, i) => `id: ${i}\ndata: ${JSON.stringify(chunk)}\n\n`).join("");
}
// Helper: create a ReadableStream from SSE text
function createSSEStream(sseText: string): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(sseText));
controller.close();
},
});
}
// Helper: create test UIMessages
function createUserMessage(text: string): UIMessage {
return {
id: `msg-${Date.now()}`,
role: "user",
parts: [{ type: "text", text }],
};
}
function createAssistantMessage(text: string): UIMessage {
return {
id: `msg-${Date.now()}`,
role: "assistant",
parts: [{ type: "text", text }],
};
}
// Sample UIMessageChunks as the AI SDK would produce
const sampleChunks: UIMessageChunk[] = [
{ type: "text-start", id: "part-1" },
{ type: "text-delta", id: "part-1", delta: "Hello" },
{ type: "text-delta", id: "part-1", delta: " world" },
{ type: "text-delta", id: "part-1", delta: "!" },
{ type: "text-end", id: "part-1" },
];
describe("TriggerChatTransport", () => {
let originalFetch: typeof global.fetch;
beforeEach(() => {
originalFetch = global.fetch;
});
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
describe("constructor", () => {
it("should create transport with required options", () => {
const transport = new TriggerChatTransport({
taskId: "my-chat-task",
accessToken: "test-token",
});
expect(transport).toBeInstanceOf(TriggerChatTransport);
});
it("should accept optional configuration", () => {
const transport = new TriggerChatTransport({
taskId: "my-chat-task",
accessToken: "test-token",
baseURL: "https://custom.trigger.dev",
streamKey: "custom-stream",
headers: { "X-Custom": "value" },
});
expect(transport).toBeInstanceOf(TriggerChatTransport);
});
it("should accept a function for accessToken", () => {
let tokenCallCount = 0;
const transport = new TriggerChatTransport({
taskId: "my-chat-task",
accessToken: () => {
tokenCallCount++;
return `dynamic-token-${tokenCallCount}`;
},
});
expect(transport).toBeInstanceOf(TriggerChatTransport);
});
});
describe("sendMessages", () => {
it("should trigger the task and return a ReadableStream of UIMessageChunks", async () => {
const triggerRunId = "run_abc123";
const publicToken = "pub_token_xyz";
// Mock fetch to handle both the trigger request and the SSE stream request
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
const urlStr = typeof url === "string" ? url : url.toString();
// Handle the task trigger request
if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: triggerRunId }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": publicToken,
},
}
);
}
// Handle the SSE stream request
if (urlStr.includes("/realtime/v1/streams/")) {
const sseText = sseEncode(sampleChunks);
return new Response(createSSEStream(sseText), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
taskId: "my-chat-task",
accessToken: "test-token",
baseURL: "https://api.test.trigger.dev",
});
const messages: UIMessage[] = [createUserMessage("Hello!")];
const stream = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-1",
messageId: undefined,
messages,
abortSignal: undefined,
});
expect(stream).toBeInstanceOf(ReadableStream);
// Read all chunks from the stream
const reader = stream.getReader();
const receivedChunks: UIMessageChunk[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedChunks.push(value);
}
expect(receivedChunks).toHaveLength(sampleChunks.length);
expect(receivedChunks[0]).toEqual({ type: "text-start", id: "part-1" });
expect(receivedChunks[1]).toEqual({ type: "text-delta", id: "part-1", delta: "Hello" });
expect(receivedChunks[4]).toEqual({ type: "text-end", id: "part-1" });
});
it("should send the correct payload to the trigger API", async () => {
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_test" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "pub_token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
return new Response(createSSEStream(""), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
global.fetch = fetchSpy;
const transport = new TriggerChatTransport({
taskId: "my-chat-task",
accessToken: "test-token",
baseURL: "https://api.test.trigger.dev",
});
const messages: UIMessage[] = [createUserMessage("Hello!")];
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-123",
messageId: undefined,
messages,
abortSignal: undefined,
metadata: { custom: "data" },
});
// Verify the trigger fetch call
const triggerCall = fetchSpy.mock.calls.find((call: any[]) =>
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger")
);
expect(triggerCall).toBeDefined();
const triggerUrl = typeof triggerCall![0] === "string" ? triggerCall![0] : triggerCall![0].toString();
expect(triggerUrl).toContain("/api/v1/tasks/my-chat-task/trigger");
const triggerBody = JSON.parse(triggerCall![1]?.body as string);
const payload = JSON.parse(triggerBody.payload);
expect(payload.messages).toEqual(messages);
expect(payload.chatId).toBe("chat-123");
expect(payload.trigger).toBe("submit-message");
expect(payload.metadata).toEqual({ custom: "data" });
});
it("should use the correct stream URL with custom streamKey", async () => {
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_custom" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
return new Response(createSSEStream(""), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
global.fetch = fetchSpy;
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
streamKey: "my-custom-stream",
});
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-1",
messageId: undefined,
messages: [createUserMessage("test")],
abortSignal: undefined,
});
// Verify the stream URL uses the custom stream key
const streamCall = fetchSpy.mock.calls.find((call: any[]) =>
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/")
);
expect(streamCall).toBeDefined();
const streamUrl = typeof streamCall![0] === "string" ? streamCall![0] : streamCall![0].toString();
expect(streamUrl).toContain("/realtime/v1/streams/run_custom/my-custom-stream");
});
it("should include extra headers in stream requests", async () => {
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_hdrs" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
return new Response(createSSEStream(""), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
global.fetch = fetchSpy;
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
headers: { "X-Custom-Header": "custom-value" },
});
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-1",
messageId: undefined,
messages: [createUserMessage("test")],
abortSignal: undefined,
});
// Verify the stream request includes custom headers
const streamCall = fetchSpy.mock.calls.find((call: any[]) =>
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/")
);
expect(streamCall).toBeDefined();
const requestHeaders = streamCall![1]?.headers as Record<string, string>;
expect(requestHeaders["X-Custom-Header"]).toBe("custom-value");
});
});
describe("reconnectToStream", () => {
it("should return null when no session exists for chatId", async () => {
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
});
const result = await transport.reconnectToStream({
chatId: "nonexistent-chat",
});
expect(result).toBeNull();
});
it("should reconnect to an existing session", async () => {
const triggerRunId = "run_reconnect";
const publicToken = "pub_reconnect_token";
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: triggerRunId }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": publicToken,
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
const chunks: UIMessageChunk[] = [
{ type: "text-start", id: "part-1" },
{ type: "text-delta", id: "part-1", delta: "Reconnected!" },
{ type: "text-end", id: "part-1" },
];
return new Response(createSSEStream(sseEncode(chunks)), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
// First, send messages to establish a session
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-reconnect",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
// Now reconnect
const stream = await transport.reconnectToStream({
chatId: "chat-reconnect",
});
expect(stream).toBeInstanceOf(ReadableStream);
// Read the stream
const reader = stream!.getReader();
const receivedChunks: UIMessageChunk[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedChunks.push(value);
}
expect(receivedChunks.length).toBeGreaterThan(0);
});
});
describe("createChatTransport", () => {
it("should create a TriggerChatTransport instance", () => {
const transport = createChatTransport({
taskId: "my-task",
accessToken: "token",
});
expect(transport).toBeInstanceOf(TriggerChatTransport);
});
it("should pass options through to the transport", () => {
const transport = createChatTransport({
taskId: "custom-task",
accessToken: "custom-token",
baseURL: "https://custom.example.com",
streamKey: "custom-key",
headers: { "X-Test": "value" },
});
expect(transport).toBeInstanceOf(TriggerChatTransport);
});
});
describe("error handling", () => {
it("should propagate trigger API errors", async () => {
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ error: "Task not found" }),
{
status: 404,
headers: { "content-type": "application/json" },
}
);
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
taskId: "nonexistent-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
await expect(
transport.sendMessages({
trigger: "submit-message",
chatId: "chat-error",
messageId: undefined,
messages: [createUserMessage("test")],
abortSignal: undefined,
})
).rejects.toThrow();
});
});
describe("abort signal", () => {
it("should close the stream gracefully when aborted", async () => {
let streamResolve: (() => void) | undefined;
const streamWait = new Promise<void>((resolve) => {
streamResolve = resolve;
});
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_abort" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
// Create a slow stream that waits before sending data
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
const encoder = new TextEncoder();
controller.enqueue(
encoder.encode(`id: 0\ndata: ${JSON.stringify({ type: "text-start", id: "p1" })}\n\n`)
);
// Wait for the test to signal it's done
await streamWait;
controller.close();
},
});
return new Response(stream, {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const abortController = new AbortController();
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
const stream = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-abort",
messageId: undefined,
messages: [createUserMessage("test")],
abortSignal: abortController.signal,
});
// Read the first chunk
const reader = stream.getReader();
const first = await reader.read();
expect(first.done).toBe(false);
// Abort and clean up
abortController.abort();
streamResolve?.();
// The stream should close — reading should return done
const next = await reader.read();
expect(next.done).toBe(true);
});
});
describe("multiple sessions", () => {
it("should track multiple chat sessions independently", async () => {
let callCount = 0;
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
callCount++;
return new Response(
JSON.stringify({ id: `run_multi_${callCount}` }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": `token_${callCount}`,
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
return new Response(createSSEStream(""), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
// Start two independent chat sessions
await transport.sendMessages({
trigger: "submit-message",
chatId: "session-a",
messageId: undefined,
messages: [createUserMessage("Hello A")],
abortSignal: undefined,
});
await transport.sendMessages({
trigger: "submit-message",
chatId: "session-b",
messageId: undefined,
messages: [createUserMessage("Hello B")],
abortSignal: undefined,
});
// Both sessions should be independently reconnectable
const streamA = await transport.reconnectToStream({ chatId: "session-a" });
const streamB = await transport.reconnectToStream({ chatId: "session-b" });
const streamC = await transport.reconnectToStream({ chatId: "nonexistent" });
expect(streamA).toBeInstanceOf(ReadableStream);
expect(streamB).toBeInstanceOf(ReadableStream);
expect(streamC).toBeNull();
});
});
describe("dynamic accessToken", () => {
it("should call the accessToken function for each sendMessages call", async () => {
let tokenCallCount = 0;
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: `run_dyn_${tokenCallCount}` }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "stream-token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
const chunks: UIMessageChunk[] = [
{ type: "text-start", id: "p1" },
{ type: "text-end", id: "p1" },
];
return new Response(createSSEStream(sseEncode(chunks)), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: () => {
tokenCallCount++;
return `dynamic-token-${tokenCallCount}`;
},
baseURL: "https://api.test.trigger.dev",
});
// First call — the token function should be invoked
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-dyn-1",
messageId: undefined,
messages: [createUserMessage("first")],
abortSignal: undefined,
});
const firstCount = tokenCallCount;
expect(firstCount).toBeGreaterThanOrEqual(1);
// Second call — the token function should be invoked again
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-dyn-2",
messageId: undefined,
messages: [createUserMessage("second")],
abortSignal: undefined,
});
// Token function was called at least once more
expect(tokenCallCount).toBeGreaterThan(firstCount);
});
});
describe("body merging", () => {
it("should merge ChatRequestOptions.body into the task payload", async () => {
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_body" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
return new Response(createSSEStream(""), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
global.fetch = fetchSpy;
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-body",
messageId: undefined,
messages: [createUserMessage("test")],
abortSignal: undefined,
body: { systemPrompt: "You are helpful", temperature: 0.7 },
});
const triggerCall = fetchSpy.mock.calls.find((call: any[]) =>
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger")
);
const triggerBody = JSON.parse(triggerCall![1]?.body as string);
const payload = JSON.parse(triggerBody.payload);
// body properties should be merged into the payload
expect(payload.systemPrompt).toBe("You are helpful");
expect(payload.temperature).toBe(0.7);
// Standard fields should still be present
expect(payload.chatId).toBe("chat-body");
expect(payload.trigger).toBe("submit-message");
});
});
describe("message types", () => {
it("should handle regenerate-message trigger", async () => {
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr.includes("/trigger")) {
return new Response(
JSON.stringify({ id: "run_regen" }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "token",
},
}
);
}
if (urlStr.includes("/realtime/v1/streams/")) {
return new Response(createSSEStream(""), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
global.fetch = fetchSpy;
const transport = new TriggerChatTransport({
taskId: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
});
const messages: UIMessage[] = [
createUserMessage("Hello!"),
createAssistantMessage("Hi there!"),
];
await transport.sendMessages({
trigger: "regenerate-message",
chatId: "chat-regen",
messageId: "msg-to-regen",
messages,
abortSignal: undefined,
});
// Verify the payload includes the regenerate trigger type and messageId
const triggerCall = fetchSpy.mock.calls.find((call: any[]) =>
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger")
);
const triggerBody = JSON.parse(triggerCall![1]?.body as string);
const payload = JSON.parse(triggerBody.payload);
expect(payload.trigger).toBe("regenerate-message");
expect(payload.messageId).toBe("msg-to-regen");
});
});
});
-256
View File
@@ -1,256 +0,0 @@
import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
import {
ApiClient,
SSEStreamSubscription,
type SSEStreamPart,
} from "@trigger.dev/core/v3";
import type { TriggerChatTransportOptions, ChatSessionState } from "./types.js";
const DEFAULT_STREAM_KEY = "chat";
const DEFAULT_BASE_URL = "https://api.trigger.dev";
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
/**
* A custom AI SDK `ChatTransport` implementation that bridges the Vercel AI SDK's
* `useChat` hook with Trigger.dev's durable task execution and realtime streams.
*
* When `sendMessages` is called, the transport:
* 1. Triggers a Trigger.dev task with the chat messages as payload
* 2. Subscribes to the task's realtime stream to receive `UIMessageChunk` data
* 3. Returns a `ReadableStream<UIMessageChunk>` that the AI SDK processes natively
*
* The task receives a `ChatTaskPayload` containing the conversation messages,
* chat session ID, trigger type, and any custom metadata. Your task should use
* the AI SDK's `streamText` (or similar) to generate a response, then pipe
* the resulting `UIMessageStream` to the `"chat"` realtime stream key
* (or a custom key matching the `streamKey` option).
*
* @example
* ```tsx
* // Frontend — use with AI SDK's useChat hook
* import { useChat } from "@ai-sdk/react";
* import { TriggerChatTransport } from "@trigger.dev/ai";
*
* function Chat({ accessToken }: { accessToken: string }) {
* const { messages, sendMessage, status } = useChat({
* transport: new TriggerChatTransport({
* accessToken,
* taskId: "my-chat-task",
* }),
* });
*
* // ... render messages
* }
* ```
*
* @example
* ```ts
* // Backend — Trigger.dev task that handles chat
* import { task, streams } from "@trigger.dev/sdk";
* import { streamText, convertToModelMessages } from "ai";
* import type { ChatTaskPayload } from "@trigger.dev/ai";
*
* export const myChatTask = task({
* id: "my-chat-task",
* run: async (payload: ChatTaskPayload) => {
* const result = streamText({
* model: openai("gpt-4o"),
* messages: convertToModelMessages(payload.messages),
* });
*
* const { waitUntilComplete } = streams.pipe("chat", result.toUIMessageStream());
* await waitUntilComplete();
* },
* });
* ```
*/
export class TriggerChatTransport implements ChatTransport<UIMessage> {
private readonly taskId: string;
private readonly resolveAccessToken: () => string;
private readonly baseURL: string;
private readonly streamKey: string;
private readonly extraHeaders: Record<string, string>;
private readonly streamTimeoutSeconds: number;
/**
* Tracks active chat sessions for reconnection support.
* Maps chatId → session state (runId, publicAccessToken).
*/
private sessions: Map<string, ChatSessionState> = new Map();
constructor(options: TriggerChatTransportOptions) {
this.taskId = options.taskId;
this.resolveAccessToken =
typeof options.accessToken === "function"
? options.accessToken
: () => options.accessToken as string;
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
this.streamKey = options.streamKey ?? DEFAULT_STREAM_KEY;
this.extraHeaders = options.headers ?? {};
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
}
/**
* Sends messages to a Trigger.dev task and returns a streaming response.
*
* This method:
* 1. Triggers the configured task with the chat messages as payload
* 2. Subscribes to the task's realtime stream for UIMessageChunk events
* 3. Returns a ReadableStream that the AI SDK's useChat hook processes
*/
sendMessages = async (
options: {
trigger: "submit-message" | "regenerate-message";
chatId: string;
messageId: string | undefined;
messages: UIMessage[];
abortSignal: AbortSignal | undefined;
} & ChatRequestOptions
): Promise<ReadableStream<UIMessageChunk>> => {
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
// Build the payload for the task — this becomes the ChatTaskPayload
const payload = {
messages,
chatId,
trigger,
messageId,
metadata,
...(body ?? {}),
};
const currentToken = this.resolveAccessToken();
// Trigger the task — use the already-resolved token directly
const apiClient = new ApiClient(this.baseURL, currentToken);
const triggerResponse = await apiClient.triggerTask(this.taskId, {
payload: JSON.stringify(payload),
options: {
payloadType: "application/json",
},
});
const runId = triggerResponse.id;
const publicAccessToken =
"publicAccessToken" in triggerResponse
? (triggerResponse as { publicAccessToken?: string }).publicAccessToken
: undefined;
// Store session state for reconnection
this.sessions.set(chatId, {
runId,
publicAccessToken: publicAccessToken ?? currentToken,
});
// Subscribe to the realtime stream for this run
return this.subscribeToStream(runId, publicAccessToken ?? currentToken, abortSignal);
};
/**
* Reconnects to an existing streaming response for the specified chat session.
*
* Returns a ReadableStream if an active session exists, or null if no session is found.
*/
reconnectToStream = async (
options: {
chatId: string;
} & ChatRequestOptions
): Promise<ReadableStream<UIMessageChunk> | null> => {
const session = this.sessions.get(options.chatId);
if (!session) {
return null;
}
return this.subscribeToStream(session.runId, session.publicAccessToken, undefined);
};
/**
* Creates a ReadableStream<UIMessageChunk> by subscribing to the realtime SSE stream
* for a given run.
*/
private subscribeToStream(
runId: string,
accessToken: string,
abortSignal: AbortSignal | undefined
): ReadableStream<UIMessageChunk> {
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
...this.extraHeaders,
};
const subscription = new SSEStreamSubscription(
`${this.baseURL}/realtime/v1/streams/${runId}/${this.streamKey}`,
{
headers,
signal: abortSignal,
timeoutInSeconds: this.streamTimeoutSeconds,
}
);
return new ReadableStream<UIMessageChunk>({
start: async (controller) => {
try {
const sseStream = await subscription.subscribe();
const reader = sseStream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
if (abortSignal?.aborted) {
reader.cancel();
reader.releaseLock();
controller.close();
return;
}
// Each SSE part's chunk is a UIMessageChunk
controller.enqueue(value.chunk as UIMessageChunk);
}
} catch (readError) {
reader.releaseLock();
throw readError;
}
} catch (error) {
// Don't error the stream for abort errors — just close gracefully
if (error instanceof Error && error.name === "AbortError") {
controller.close();
return;
}
controller.error(error);
}
},
});
}
}
/**
* Creates a new `TriggerChatTransport` instance.
*
* This is a convenience factory function equivalent to `new TriggerChatTransport(options)`.
*
* @example
* ```tsx
* import { useChat } from "@ai-sdk/react";
* import { createChatTransport } from "@trigger.dev/ai";
*
* const transport = createChatTransport({
* taskId: "my-chat-task",
* accessToken: publicAccessToken,
* });
*
* function Chat() {
* const { messages, sendMessage } = useChat({ transport });
* // ...
* }
* ```
*/
export function createChatTransport(options: TriggerChatTransportOptions): TriggerChatTransport {
return new TriggerChatTransport(options);
}
-117
View File
@@ -1,117 +0,0 @@
import type { UIMessage } from "ai";
/**
* Options for creating a TriggerChatTransport.
*/
export type TriggerChatTransportOptions = {
/**
* The Trigger.dev task ID to trigger for chat completions.
* This task will receive the chat messages as its payload.
*/
task: string;
/**
* An access token for authenticating with the Trigger.dev API.
*
* This must be a token with permission to trigger the task. You can use:
* - A **trigger public token** created via `auth.createTriggerPublicToken(taskId)` (recommended for frontend use)
* - A **secret API key** (for server-side use only — never expose in the browser)
*
* The token returned from triggering the task (`publicAccessToken`) is automatically
* used for subscribing to the realtime stream.
*
* Can also be a function that returns a token string, useful for dynamic token refresh:
* ```ts
* accessToken: () => getLatestToken()
* ```
*/
accessToken: string | (() => string);
/**
* Base URL for the Trigger.dev API.
*
* @default "https://api.trigger.dev"
*/
baseURL?: string;
/**
* The stream key where the task pipes UIMessageChunk data.
* When using `chatTask()` or `pipeChat()`, this is handled automatically.
* Only set this if you're using a custom stream key.
*
* @default "chat"
*/
streamKey?: string;
/**
* Additional headers to include in API requests to Trigger.dev.
*/
headers?: Record<string, string>;
/**
* The number of seconds to wait for the realtime stream to produce data
* before timing out. If no data arrives within this period, the stream
* will be closed.
*
* @default 120
*/
streamTimeoutSeconds?: number;
};
/**
* The payload shape that the transport sends to the triggered task.
*
* When using `chatTask()`, the payload is automatically typed — you don't need
* to import this type. When using `task()` directly, use this type to annotate
* your payload:
*
* @example
* ```ts
* import { task } from "@trigger.dev/sdk";
* import { pipeChat, type ChatTaskPayload } from "@trigger.dev/ai";
*
* export const myChatTask = task({
* id: "my-chat-task",
* run: async (payload: ChatTaskPayload) => {
* const result = streamText({
* model: openai("gpt-4o"),
* messages: convertToModelMessages(payload.messages),
* });
* await pipeChat(result);
* },
* });
* ```
*/
export type ChatTaskPayload<TMessage extends UIMessage = UIMessage> = {
/** The array of UI messages representing the conversation history */
messages: TMessage[];
/** The unique identifier for the chat session */
chatId: string;
/**
* The type of message submission:
* - `"submit-message"`: A new user message was submitted
* - `"regenerate-message"`: The user wants to regenerate the last assistant response
*/
trigger: "submit-message" | "regenerate-message";
/**
* The ID of the message to regenerate (only present for `"regenerate-message"` trigger).
*/
messageId?: string;
/**
* Custom metadata attached to the chat request by the frontend.
*/
metadata?: unknown;
};
/**
* Internal state for tracking active chat sessions, used for stream reconnection.
* @internal
*/
export type ChatSessionState = {
runId: string;
publicAccessToken: string;
};
-1
View File
@@ -1 +0,0 @@
export const VERSION = "0.0.0";
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "../../.configs/tsconfig.base.json",
"compilerOptions": {
"isolatedDeclarations": false,
"composite": true,
"sourceMap": true,
"stripInternal": true
},
"include": ["./src/**/*.ts"]
}
-8
View File
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/**/*.test.ts"],
globals: true,
},
});
+6 -182
View File
@@ -1147,7 +1147,7 @@ importers:
version: 18.3.1
react-email:
specifier: ^2.1.1
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0)
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0)
resend:
specifier: ^3.2.0
version: 3.2.0
@@ -1435,31 +1435,6 @@ importers:
specifier: 8.6.6
version: 8.6.6
packages/ai:
dependencies:
'@trigger.dev/core':
specifier: workspace:4.3.3
version: link:../core
devDependencies:
'@arethetypeswrong/cli':
specifier: ^0.15.4
version: 0.15.4
ai:
specifier: ^6.0.0
version: 6.0.116(zod@3.25.76)
rimraf:
specifier: ^3.0.2
version: 3.0.2
tshy:
specifier: ^3.0.2
version: 3.0.2
tsx:
specifier: 4.17.0
version: 4.17.0
vitest:
specifier: ^2.1.0
version: 2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
packages/build:
dependencies:
'@prisma/config':
@@ -11167,23 +11142,9 @@ packages:
'@vitest/browser':
optional: true
'@vitest/expect@2.1.9':
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
'@vitest/expect@3.1.4':
resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==}
'@vitest/mocker@2.1.9':
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
peerDependencies:
msw: ^2.4.9
vite: ^6.4.2
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/mocker@3.1.4':
resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==}
peerDependencies:
@@ -11207,15 +11168,9 @@ packages:
'@vitest/runner@3.1.4':
resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==}
'@vitest/snapshot@2.1.9':
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
'@vitest/snapshot@3.1.4':
resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==}
'@vitest/spy@2.1.9':
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
'@vitest/spy@3.1.4':
resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==}
@@ -19753,11 +19708,6 @@ packages:
engines: {node: '>=v14.16.0'}
hasBin: true
vite-node@2.1.9:
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
vite-node@3.1.4:
resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -19834,31 +19784,6 @@ packages:
yaml:
optional: true
vitest@2.1.9:
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': 20.14.14
'@vitest/browser': 2.1.9
'@vitest/ui': 2.1.9
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vitest@3.1.4:
resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -31573,13 +31498,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/expect@2.1.9':
dependencies:
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.2.0
tinyrainbow: 1.2.0
'@vitest/expect@3.1.4':
dependencies:
'@vitest/spy': 3.1.4
@@ -31587,14 +31505,6 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@2.1.9(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
'@vitest/mocker@3.1.4(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 3.1.4
@@ -31621,22 +31531,12 @@ snapshots:
'@vitest/utils': 3.1.4
pathe: 2.0.3
'@vitest/snapshot@2.1.9':
dependencies:
'@vitest/pretty-format': 2.1.9
magic-string: 0.30.21
pathe: 1.1.2
'@vitest/snapshot@3.1.4':
dependencies:
'@vitest/pretty-format': 3.1.4
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@2.1.9':
dependencies:
tinyspy: 3.0.2
'@vitest/spy@3.1.4':
dependencies:
tinyspy: 3.0.2
@@ -39157,7 +39057,7 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0):
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0):
dependencies:
'@babel/parser': 7.24.1
'@radix-ui/colors': 1.0.1
@@ -39194,8 +39094,8 @@ snapshots:
react: 18.3.1
react-dom: 18.2.0(react@18.3.1)
shelljs: 0.8.5
socket.io: 4.7.3(bufferutil@4.0.9)
socket.io-client: 4.7.3(bufferutil@4.0.9)
socket.io: 4.7.3
socket.io-client: 4.7.3
sonner: 1.3.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1)
source-map-js: 1.0.2
stacktrace-parser: 0.1.10
@@ -40422,7 +40322,7 @@ snapshots:
- supports-color
- utf-8-validate
socket.io-client@4.7.3(bufferutil@4.0.9):
socket.io-client@4.7.3:
dependencies:
'@socket.io/component-emitter': 3.1.0
debug: 4.3.7(supports-color@10.0.0)
@@ -40451,7 +40351,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
socket.io@4.7.3(bufferutil@4.0.9):
socket.io@4.7.3:
dependencies:
accepts: 1.3.8
base64id: 2.0.0
@@ -42018,27 +41918,6 @@ snapshots:
- supports-color
- terser
vite-node@2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3):
dependencies:
cac: 6.7.14
debug: 4.4.3(supports-color@10.0.0)
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-node@3.1.4(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3):
dependencies:
cac: 6.7.14
@@ -42118,23 +41997,6 @@ snapshots:
tsx: 3.12.2
yaml: 2.8.3
vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.1
fdir: 6.4.4(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.10
rollup: 4.60.1
tinyglobby: 0.2.13
optionalDependencies:
'@types/node': 20.14.14
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.29.2
terser: 5.44.1
tsx: 4.17.0
yaml: 2.8.3
vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3):
dependencies:
esbuild: 0.25.1
@@ -42152,44 +42014,6 @@ snapshots:
tsx: 4.20.6
yaml: 2.8.3
vitest@2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3))
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.2.0
debug: 4.4.3(supports-color@10.0.0)
expect-type: 1.2.1
magic-string: 0.30.21
pathe: 1.1.2
std-env: 3.9.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 1.2.0
vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
vite-node: 2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.14.14
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3):
dependencies:
'@vitest/expect': 3.1.4