Skip to content

Commit 098932e

Browse files
authored
v3: edge runtime support (#1172)
* v3: remove node:stream and simplify env var upload API to better work with non-node runtimes * Remove file/Response envvars upload docs * Add changeset
1 parent 65f960e commit 098932e

File tree

11 files changed

+34
-414
lines changed

11 files changed

+34
-414
lines changed

.changeset/slow-kiwis-hide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
v3: vercel edge runtime support

docs/v3-openapi.yaml

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -811,17 +811,7 @@ paths:
811811
description: Whether to override existing variables or not
812812
default: false
813813
required: ["variables"]
814-
multipart/form-data:
815-
schema:
816-
type: object
817-
properties:
818-
variables:
819-
type: string
820-
format: binary
821-
override:
822-
type: boolean
823-
required:
824-
- variables
814+
825815
responses:
826816
"200":
827817
description: Environment variables imported successfully
@@ -864,57 +854,11 @@ paths:
864854
source: |-
865855
import { envvars } from "@trigger.dev/sdk/v3";
866856
867-
// Import variables from an array
868-
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
869-
variables: [
870-
{
871-
name: "SLACK_API_KEY",
872-
value: "slack_123456"
873-
}
874-
],
875-
override: false
876-
});
877-
- lang: typescript
878-
label: Import variables from a read stream
879-
source: |-
880-
import { envvars } from "@trigger.dev/sdk/v3";
881-
import { createReadStream } from "node:fs";
882-
883-
// Import variables in dotenv format from a file
884-
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
885-
variables: createReadStream(".env"),
886-
override: false
887-
});
888-
- lang: typescript
889-
label: Import variables from a response
890-
source: |-
891-
import { envvars } from "@trigger.dev/sdk/v3";
892-
893-
// Import variables in dotenv format from a response
894857
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
895-
variables: await fetch("https://example.com/.env"),
858+
variables: { SLACK_API_KEY: "slack_key_1234" },
896859
override: false
897860
});
898-
- lang: typescript
899-
label: Import variables from a Buffer
900-
source: |-
901-
import { envvars } from "@trigger.dev/sdk/v3";
902861
903-
// Import variables in dotenv format from a buffer
904-
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
905-
variables: Buffer.from("SLACK_API_KEY=slack_1234"),
906-
override: false
907-
});
908-
- lang: typescript
909-
label: Import variables from a File
910-
source: |-
911-
import { envvars } from "@trigger.dev/sdk/v3";
912-
913-
// Import variables in dotenv format from a file
914-
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
915-
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
916-
override: false
917-
});
918862
919863
"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
920864
parameters:

packages/cli-v3/src/utilities/configFiles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export async function readConfig(
178178
write: true,
179179
format: "cjs",
180180
platform: "node",
181-
target: ["es2018", "node18"],
181+
target: ["es2020", "node18"],
182182
outfile: builtConfigFilePath,
183183
logLevel: "silent",
184184
plugins: [

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@
142142
"@opentelemetry/sdk-trace-base": "^1.22.0",
143143
"@opentelemetry/sdk-trace-node": "^1.22.0",
144144
"@opentelemetry/semantic-conventions": "^1.22.0",
145-
"form-data-encoder": "^4.0.2",
146145
"humanize-duration": "^3.27.3",
147146
"socket.io-client": "4.7.4",
148147
"superjson": "^2.2.1",
@@ -157,6 +156,7 @@
157156
"@types/humanize-duration": "^3.27.1",
158157
"@types/jest": "^29.5.3",
159158
"@types/node": "20.12.7",
159+
"@types/readable-stream": "^4.0.14",
160160
"jest": "^29.6.2",
161161
"rimraf": "^3.0.2",
162162
"socket.io": "4.7.4",

packages/core/src/v3/apiClient/core.ts

Lines changed: 1 addition & 263 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { fromZodError } from "zod-validation-error";
33
import { ApiConnectionError, ApiError } from "./errors";
44
import { RetryOptions } from "../schemas";
55
import { calculateNextRetryDelay } from "../utils/retries";
6-
import { FormDataEncoder } from "form-data-encoder";
7-
import { Readable } from "node:stream";
6+
87
import {
98
CursorPage,
109
CursorPageParams,
@@ -114,59 +113,6 @@ export function zodfetchOffsetLimitPage<TItemSchema extends z.ZodTypeAny>(
114113
return new OffsetLimitPagePromise(fetchResult, schema, url, params, requestInit, options);
115114
}
116115

117-
export function zodupload<
118-
TResponseBodySchema extends z.ZodTypeAny,
119-
TBody = Record<string, unknown>,
120-
>(
121-
schema: TResponseBodySchema,
122-
url: string,
123-
body: TBody,
124-
requestInit?: RequestInit,
125-
options?: ZodFetchOptions
126-
): ApiPromise<z.output<TResponseBodySchema>> {
127-
const finalRequestInit = createMultipartFormRequestInit(body, requestInit);
128-
129-
return new ApiPromise(_doZodFetch(schema, url, finalRequestInit, options));
130-
}
131-
132-
async function createMultipartFormRequestInit<TBody = Record<string, unknown>>(
133-
body: TBody,
134-
requestInit?: RequestInit
135-
): Promise<RequestInit> {
136-
const form = await createForm(body);
137-
const encoder = new FormDataEncoder(form);
138-
139-
const finalHeaders: Record<string, string> = {};
140-
141-
for (const [key, value] of Object.entries(requestInit?.headers || {})) {
142-
finalHeaders[key] = value as string;
143-
}
144-
145-
for (const [key, value] of Object.entries(encoder.headers)) {
146-
finalHeaders[key] = value;
147-
}
148-
149-
finalHeaders["Content-Length"] = String(encoder.contentLength);
150-
151-
const finalRequestInit: RequestInit = {
152-
...requestInit,
153-
headers: finalHeaders,
154-
body: Readable.from(encoder) as any,
155-
// @ts-expect-error
156-
duplex: "half",
157-
};
158-
159-
return finalRequestInit;
160-
}
161-
162-
const createForm = async <T = Record<string, unknown>>(body: T | undefined): Promise<FormData> => {
163-
const form = new FormData();
164-
await Promise.all(
165-
Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))
166-
);
167-
return form;
168-
};
169-
170116
type ZodFetchResult<T> = {
171117
data: T;
172118
response: Response;
@@ -324,214 +270,6 @@ function requestInitWithCache(requestInit?: RequestInit): RequestInit {
324270
}
325271
}
326272

327-
const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
328-
if (value === undefined) return;
329-
if (value == null) {
330-
throw new TypeError(
331-
`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`
332-
);
333-
}
334-
335-
// TODO: make nested formats configurable
336-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
337-
form.append(key, String(value));
338-
} else if (
339-
isUploadable(value) ||
340-
isBlobLike(value) ||
341-
value instanceof Buffer ||
342-
value instanceof ArrayBuffer
343-
) {
344-
const file = await toFile(value);
345-
form.append(key, file as File);
346-
} else if (Array.isArray(value)) {
347-
await Promise.all(value.map((entry) => addFormValue(form, key + "[]", entry)));
348-
} else if (typeof value === "object") {
349-
await Promise.all(
350-
Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop))
351-
);
352-
} else {
353-
throw new TypeError(
354-
`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`
355-
);
356-
}
357-
};
358-
359-
export type ToFileInput = Uploadable | Exclude<BlobLikePart, string> | AsyncIterable<BlobLikePart>;
360-
361-
/**
362-
* Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
363-
* @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
364-
* @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
365-
* @param {Object=} options additional properties
366-
* @param {string=} options.type the MIME type of the content
367-
* @param {number=} options.lastModified the last modified timestamp
368-
* @returns a {@link File} with the given properties
369-
*/
370-
export async function toFile(
371-
value: ToFileInput | PromiseLike<ToFileInput>,
372-
name?: string | null | undefined,
373-
options?: FilePropertyBag | undefined
374-
): Promise<FileLike> {
375-
// If it's a promise, resolve it.
376-
value = await value;
377-
378-
// Use the file's options if there isn't one provided
379-
options ??= isFileLike(value) ? { lastModified: value.lastModified, type: value.type } : {};
380-
381-
if (isResponseLike(value)) {
382-
const blob = await value.blob();
383-
name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? "unknown_file";
384-
385-
return new File([blob as any], name, options);
386-
}
387-
388-
const bits = await getBytes(value);
389-
390-
name ||= getName(value) ?? "unknown_file";
391-
392-
if (!options.type) {
393-
const type = (bits[0] as any)?.type;
394-
if (typeof type === "string") {
395-
options = { ...options, type };
396-
}
397-
}
398-
399-
return new File(bits, name, options);
400-
}
401-
402-
function getName(value: any): string | undefined {
403-
return (
404-
getStringFromMaybeBuffer(value.name) ||
405-
getStringFromMaybeBuffer(value.filename) ||
406-
// For fs.ReadStream
407-
getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop()
408-
);
409-
}
410-
411-
const getStringFromMaybeBuffer = (x: string | Buffer | unknown): string | undefined => {
412-
if (typeof x === "string") return x;
413-
if (typeof Buffer !== "undefined" && x instanceof Buffer) return String(x);
414-
return undefined;
415-
};
416-
417-
async function getBytes(value: ToFileInput): Promise<Array<BlobPart>> {
418-
let parts: Array<BlobPart> = [];
419-
if (
420-
typeof value === "string" ||
421-
ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
422-
value instanceof ArrayBuffer
423-
) {
424-
parts.push(value);
425-
} else if (isBlobLike(value)) {
426-
parts.push(await value.arrayBuffer());
427-
} else if (
428-
isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
429-
) {
430-
for await (const chunk of value) {
431-
parts.push(chunk as BlobPart); // TODO, consider validating?
432-
}
433-
} else {
434-
throw new Error(
435-
`Unexpected data type: ${typeof value}; constructor: ${value?.constructor
436-
?.name}; props: ${propsForError(value)}`
437-
);
438-
}
439-
440-
return parts;
441-
}
442-
443-
function propsForError(value: any): string {
444-
const props = Object.getOwnPropertyNames(value);
445-
return `[${props.map((p) => `"${p}"`).join(", ")}]`;
446-
}
447-
448-
const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unknown> =>
449-
value != null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
450-
451-
/**
452-
* Intended to match web.Blob, node.Blob, node-fetch.Blob, etc.
453-
*/
454-
export interface BlobLike {
455-
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
456-
readonly size: number;
457-
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
458-
readonly type: string;
459-
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
460-
text(): Promise<string>;
461-
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
462-
slice(start?: number, end?: number): BlobLike;
463-
// unfortunately @types/node-fetch@^2.6.4 doesn't type the arrayBuffer method
464-
}
465-
466-
/**
467-
* Intended to match web.File, node.File, node-fetch.File, etc.
468-
*/
469-
export interface FileLike extends BlobLike {
470-
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
471-
readonly lastModified: number;
472-
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
473-
readonly name: string;
474-
}
475-
476-
/**
477-
* Intended to match web.Response, node.Response, node-fetch.Response, etc.
478-
*/
479-
export interface ResponseLike {
480-
url: string;
481-
blob(): Promise<BlobLike>;
482-
}
483-
484-
export type Uploadable = FileLike | ResponseLike | Readable;
485-
486-
export const isResponseLike = (value: any): value is ResponseLike =>
487-
value != null &&
488-
typeof value === "object" &&
489-
typeof value.url === "string" &&
490-
typeof value.blob === "function";
491-
492-
export const isFileLike = (value: any): value is FileLike =>
493-
value != null &&
494-
typeof value === "object" &&
495-
typeof value.name === "string" &&
496-
typeof value.lastModified === "number" &&
497-
isBlobLike(value);
498-
499-
/**
500-
* The BlobLike type omits arrayBuffer() because @types/node-fetch@^2.6.4 lacks it; but this check
501-
* adds the arrayBuffer() method type because it is available and used at runtime
502-
*/
503-
export const isBlobLike = (
504-
value: any
505-
): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
506-
value != null &&
507-
typeof value === "object" &&
508-
typeof value.size === "number" &&
509-
typeof value.type === "string" &&
510-
typeof value.text === "function" &&
511-
typeof value.slice === "function" &&
512-
typeof value.arrayBuffer === "function";
513-
514-
export const isFsReadStream = (value: any): value is Readable => value instanceof Readable;
515-
516-
export const isUploadable = (value: any): value is Uploadable => {
517-
return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
518-
};
519-
520-
export type BlobLikePart =
521-
| string
522-
| ArrayBuffer
523-
| ArrayBufferView
524-
| BlobLike
525-
| Uint8Array
526-
| DataView;
527-
528-
export const isRecordLike = (value: any): value is Record<string, string> =>
529-
value != null &&
530-
typeof value === "object" &&
531-
!Array.isArray(value) &&
532-
Object.keys(value).length > 0 &&
533-
Object.keys(value).every((key) => typeof key === "string" && typeof value[key] === "string");
534-
535273
/**
536274
* A subclass of `Promise` providing additional helper methods
537275
* for interacting with the SDK.

0 commit comments

Comments
 (0)