Rebuild fern client sdk to 1.06 (#4331)

This commit is contained in:
Marc Kelechava
2025-12-19 12:16:02 -08:00
committed by GitHub
parent 08ca5a0b45
commit 9788138861
50 changed files with 2169 additions and 909 deletions

View File

@@ -0,0 +1 @@
export * from "./file/exports.js";

View File

@@ -0,0 +1 @@
export type { Uploadable } from "./types.js";

View File

@@ -0,0 +1,217 @@
import type { Uploadable } from "./types.js";
export async function toBinaryUploadRequest(
file: Uploadable,
): Promise<{ body: Uploadable.FileLike; headers?: Record<string, string> }> {
const { data, filename, contentLength, contentType } = await getFileWithMetadata(file);
const request = {
body: data,
headers: {} as Record<string, string>,
};
if (filename) {
request.headers["Content-Disposition"] = `attachment; filename="${filename}"`;
}
if (contentType) {
request.headers["Content-Type"] = contentType;
}
if (contentLength != null) {
request.headers["Content-Length"] = contentLength.toString();
}
return request;
}
export async function toMultipartDataPart(
file: Uploadable,
): Promise<{ data: Uploadable.FileLike; filename?: string; contentType?: string }> {
const { data, filename, contentType } = await getFileWithMetadata(file, {
noSniffFileSize: true,
});
return {
data,
filename,
contentType,
};
}
async function getFileWithMetadata(
file: Uploadable,
{ noSniffFileSize }: { noSniffFileSize?: boolean } = {},
): Promise<Uploadable.WithMetadata> {
if (isFileLike(file)) {
return getFileWithMetadata(
{
data: file,
},
{ noSniffFileSize },
);
}
if ("path" in file) {
const fs = await import("fs");
if (!fs || !fs.createReadStream) {
throw new Error("File path uploads are not supported in this environment.");
}
const data = fs.createReadStream(file.path);
const contentLength =
file.contentLength ?? (noSniffFileSize === true ? undefined : await tryGetFileSizeFromPath(file.path));
const filename = file.filename ?? getNameFromPath(file.path);
return {
data,
filename,
contentType: file.contentType,
contentLength,
};
}
if ("data" in file) {
const data = file.data;
const contentLength =
file.contentLength ??
(await tryGetContentLengthFromFileLike(data, {
noSniffFileSize,
}));
const filename = file.filename ?? tryGetNameFromFileLike(data);
return {
data,
filename,
contentType: file.contentType ?? tryGetContentTypeFromFileLike(data),
contentLength,
};
}
throw new Error(`Invalid FileUpload of type ${typeof file}: ${JSON.stringify(file)}`);
}
function isFileLike(value: unknown): value is Uploadable.FileLike {
return (
isBuffer(value) ||
isArrayBufferView(value) ||
isArrayBuffer(value) ||
isUint8Array(value) ||
isBlob(value) ||
isFile(value) ||
isStreamLike(value) ||
isReadableStream(value)
);
}
async function tryGetFileSizeFromPath(path: string): Promise<number | undefined> {
try {
const fs = await import("fs");
if (!fs || !fs.promises || !fs.promises.stat) {
return undefined;
}
const fileStat = await fs.promises.stat(path);
return fileStat.size;
} catch (_fallbackError) {
return undefined;
}
}
function tryGetNameFromFileLike(data: Uploadable.FileLike): string | undefined {
if (isNamedValue(data)) {
return data.name;
}
if (isPathedValue(data)) {
return getNameFromPath(data.path.toString());
}
return undefined;
}
async function tryGetContentLengthFromFileLike(
data: Uploadable.FileLike,
{ noSniffFileSize }: { noSniffFileSize?: boolean } = {},
): Promise<number | undefined> {
if (isBuffer(data)) {
return data.length;
}
if (isArrayBufferView(data)) {
return data.byteLength;
}
if (isArrayBuffer(data)) {
return data.byteLength;
}
if (isBlob(data)) {
return data.size;
}
if (isFile(data)) {
return data.size;
}
if (noSniffFileSize === true) {
return undefined;
}
if (isPathedValue(data)) {
return await tryGetFileSizeFromPath(data.path.toString());
}
return undefined;
}
function tryGetContentTypeFromFileLike(data: Uploadable.FileLike): string | undefined {
if (isBlob(data)) {
return data.type;
}
if (isFile(data)) {
return data.type;
}
return undefined;
}
function getNameFromPath(path: string): string | undefined {
const lastForwardSlash = path.lastIndexOf("/");
const lastBackSlash = path.lastIndexOf("\\");
const lastSlashIndex = Math.max(lastForwardSlash, lastBackSlash);
return lastSlashIndex >= 0 ? path.substring(lastSlashIndex + 1) : path;
}
type NamedValue = {
name: string;
} & unknown;
type PathedValue = {
path: string | { toString(): string };
} & unknown;
type StreamLike = {
read?: () => unknown;
pipe?: (dest: unknown) => unknown;
} & unknown;
function isNamedValue(value: unknown): value is NamedValue {
return typeof value === "object" && value != null && "name" in value;
}
function isPathedValue(value: unknown): value is PathedValue {
return typeof value === "object" && value != null && "path" in value;
}
function isStreamLike(value: unknown): value is StreamLike {
return typeof value === "object" && value != null && ("read" in value || "pipe" in value);
}
function isReadableStream(value: unknown): value is ReadableStream {
return typeof value === "object" && value != null && "getReader" in value;
}
function isBuffer(value: unknown): value is Buffer {
return typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(value);
}
function isArrayBufferView(value: unknown): value is ArrayBufferView {
return typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value);
}
function isArrayBuffer(value: unknown): value is ArrayBuffer {
return typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer;
}
function isUint8Array(value: unknown): value is Uint8Array {
return typeof Uint8Array !== "undefined" && value instanceof Uint8Array;
}
function isBlob(value: unknown): value is Blob {
return typeof Blob !== "undefined" && value instanceof Blob;
}
function isFile(value: unknown): value is File {
return typeof File !== "undefined" && value instanceof File;
}

View File

@@ -0,0 +1,2 @@
export * from "./file.js";
export * from "./types.js";

View File

@@ -0,0 +1,81 @@
/**
* A file that can be uploaded. Can be a file-like object (stream, buffer, blob, etc.),
* a path to a file, or an object with a file-like object and metadata.
*/
export type Uploadable = Uploadable.FileLike | Uploadable.FromPath | Uploadable.WithMetadata;
export namespace Uploadable {
/**
* Various file-like objects that can be used to upload a file.
*/
export type FileLike =
| ArrayBuffer
| ArrayBufferLike
| ArrayBufferView
| Uint8Array
| import("buffer").Buffer
| import("buffer").Blob
| import("buffer").File
| import("stream").Readable
| import("stream/web").ReadableStream
| globalThis.Blob
| globalThis.File
| ReadableStream;
/**
* A file path with optional metadata, used for uploading a file from the file system.
*/
export type FromPath = {
/** The path to the file to upload */
path: string;
/**
* Optional override for the file name (defaults to basename of path).
* This is used to set the `Content-Disposition` header in upload requests.
*/
filename?: string;
/**
* Optional MIME type of the file (e.g., 'image/jpeg', 'text/plain').
* This is used to set the `Content-Type` header in upload requests.
*/
contentType?: string;
/**
* Optional file size in bytes.
* If not provided, the file size will be determined from the file system.
* The content length is used to set the `Content-Length` header in upload requests.
*/
contentLength?: number;
};
/**
* A file-like object with metadata, used for uploading files.
*/
export type WithMetadata = {
/** The file data */
data: FileLike;
/**
* Optional override for the file name (defaults to basename of path).
* This is used to set the `Content-Disposition` header in upload requests.
*/
filename?: string;
/**
* Optional MIME type of the file (e.g., 'image/jpeg', 'text/plain').
* This is used to set the `Content-Type` header in upload requests.
*
* If not provided, the content type may be determined from the data itself.
* * If the data is a `File`, `Blob`, or similar, the content type will be determined from the file itself, if the type is set.
* * Any other data type will not have a content type set, and the upload request will use `Content-Type: application/octet-stream` instead.
*/
contentType?: string;
/**
* Optional file size in bytes.
* The content length is used to set the `Content-Length` header in upload requests.
* If the content length is not provided and cannot be determined, the upload request will not include the `Content-Length` header, but will use `Transfer-Encoding: chunked` instead.
*
* If not provided, the file size will be determined depending on the data type.
* * If the data is of type `fs.ReadStream` (`createReadStream`), the size will be determined from the file system.
* * If the data is a `Buffer`, `ArrayBuffer`, `Uint8Array`, `Blob`, `File`, or similar, the size will be determined from the data itself.
* * If the data is a `Readable` or `ReadableStream`, the size will not be determined.
*/
contentLength?: number;
};
}

View File

@@ -0,0 +1,140 @@
import { toMultipartDataPart, type Uploadable } from "../../core/file/index.js";
import { toJson } from "../../core/json.js";
import { RUNTIME } from "../runtime/index.js";
interface FormDataRequest<Body> {
body: Body;
headers: Record<string, string>;
duplex?: "half";
}
export async function newFormData(): Promise<FormDataWrapper> {
return new FormDataWrapper();
}
export class FormDataWrapper {
private fd: FormData = new FormData();
public async setup(): Promise<void> {
// noop
}
public append(key: string, value: unknown): void {
this.fd.append(key, String(value));
}
public async appendFile(key: string, value: Uploadable): Promise<void> {
const { data, filename, contentType } = await toMultipartDataPart(value);
const blob = await convertToBlob(data, contentType);
if (filename) {
this.fd.append(key, blob, filename);
} else {
this.fd.append(key, blob);
}
}
public getRequest(): FormDataRequest<FormData> {
return {
body: this.fd,
headers: {},
duplex: "half" as const,
};
}
}
type StreamLike = {
read?: () => unknown;
pipe?: (dest: unknown) => unknown;
} & unknown;
function isStreamLike(value: unknown): value is StreamLike {
return typeof value === "object" && value != null && ("read" in value || "pipe" in value);
}
function isReadableStream(value: unknown): value is ReadableStream {
return typeof value === "object" && value != null && "getReader" in value;
}
function isBuffer(value: unknown): value is Buffer {
return typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(value);
}
function isArrayBufferView(value: unknown): value is ArrayBufferView {
return ArrayBuffer.isView(value);
}
async function streamToBuffer(stream: unknown): Promise<Buffer> {
if (RUNTIME.type === "node") {
const { Readable } = await import("stream");
if (stream instanceof Readable) {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
}
if (isReadableStream(stream)) {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return Buffer.from(result);
}
throw new Error(
`Unsupported stream type: ${typeof stream}. Expected Node.js Readable stream or Web ReadableStream.`,
);
}
async function convertToBlob(value: unknown, contentType?: string): Promise<Blob> {
if (isStreamLike(value) || isReadableStream(value)) {
const buffer = await streamToBuffer(value);
return new Blob([buffer], { type: contentType });
}
if (value instanceof Blob) {
return value;
}
if (isBuffer(value)) {
return new Blob([value], { type: contentType });
}
if (value instanceof ArrayBuffer) {
return new Blob([value], { type: contentType });
}
if (isArrayBufferView(value)) {
return new Blob([value], { type: contentType });
}
if (typeof value === "string") {
return new Blob([value], { type: contentType });
}
if (typeof value === "object" && value !== null) {
return new Blob([toJson(value)], { type: contentType ?? "application/json" });
}
return new Blob([String(value)], { type: contentType });
}

View File

@@ -0,0 +1,12 @@
import { toQueryString } from "../url/qs.js";
export function encodeAsFormParameter(value: unknown): Record<string, string> {
const stringified = toQueryString(value, { encode: false });
const keyValuePairs = stringified.split("&").map((pair) => {
const [key, value] = pair.split("=");
return [key, value] as const;
});
return Object.fromEntries(keyValuePairs);
}

View File

@@ -0,0 +1,2 @@
export { encodeAsFormParameter } from "./encodeAsFormParameter.js";
export * from "./FormDataWrapper.js";

View File

@@ -1,3 +1,5 @@
export * from "./fetcher/index.js";
export * as file from "./file/index.js";
export * from "./form-data-utils/index.js";
export * from "./runtime/index.js";
export * as url from "./url/index.js";