feat: client proxy + nextjs support (#9)
* feat(client): add function alias support * feat(client): auto config from env vars * fix(client): username in aliased url * feat: nextjs proxy handler * fix: update client tests * feat(client): built-in queue support * feat: client + nextjs e2e demo * chore: update browserslist * chore: update version info * chore: update readme badges * fix: ignore lint error on require
This commit is contained in:
parent
3b11d468a8
commit
9a0497da30
@ -2,3 +2,4 @@
|
||||
|
||||
/dist
|
||||
/coverage
|
||||
package-lock.json
|
||||
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"python.formatting.provider": "none"
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
# The fal-serverless JS Client
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## About the project
|
||||
|
||||
|
||||
3
apps/demo-app/pages/api/_fal/proxy.ts
Normal file
3
apps/demo-app/pages/api/_fal/proxy.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// @snippet:start(client.proxy.nextjs)
|
||||
export { config, handler as default } from '@fal-ai/serverless-nextjs';
|
||||
// @snippet:end
|
||||
@ -1,21 +0,0 @@
|
||||
import {
|
||||
generateImage,
|
||||
GenerateImageInput,
|
||||
} from '../../services/generateImage';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
// not really type-safe, force cast because I can =P
|
||||
const prompt = req.body as GenerateImageInput;
|
||||
try {
|
||||
const imageUrl = await generateImage(prompt);
|
||||
res.status(200).json({ imageUrl });
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to update image', causedBy: error });
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Head from 'next/head';
|
||||
|
||||
import { generateImage } from '../services/generateImage';
|
||||
|
||||
const IMG_PLACEHOLDER = '/placeholder@2x.jpg';
|
||||
|
||||
export default function Diffusion() {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [imageUrl, setImageUrl] = useState(IMG_PLACEHOLDER);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setPrompt(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
// TODO replace this with direct serverless call once cors is solved
|
||||
// const response = await fetch('/api/generateImage', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify({ prompt }),
|
||||
// });
|
||||
// const data = await response.json();
|
||||
// setImageUrl(data.imageUrl);
|
||||
|
||||
const result = await generateImage({ prompt });
|
||||
setImageUrl(result);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 dark:text-white bg-white text-black">
|
||||
<Head>
|
||||
<title>fal-serverless diffusion</title>
|
||||
</Head>
|
||||
|
||||
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 py-10">
|
||||
<h1 className="text-4xl font-semibold mb-10">
|
||||
fal-serverless diffusion
|
||||
</h1>
|
||||
<h3 className="text-2xl">Enter a prompt to generate the image</h3>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col mt-8 w-full">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-lg rounded focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="e.g. cute panda in the style of ghibli studio"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold text-xl py-4 px-8 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
>
|
||||
Generate Image
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-10">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt="Generated Image"
|
||||
width={1024}
|
||||
height={1024}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,21 +1,23 @@
|
||||
import { getJoke } from '../services/getJoke';
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { withNextProxy } from '@fal-ai/serverless-nextjs';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
try {
|
||||
const result = await getJoke();
|
||||
return {
|
||||
props: {
|
||||
...result,
|
||||
},
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
requestMiddleware: withNextProxy(),
|
||||
});
|
||||
// @snippet:end
|
||||
|
||||
// @snippet:start(client.result.type)
|
||||
type Image = {
|
||||
url: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
props: {
|
||||
error: error.message,
|
||||
},
|
||||
type Result = {
|
||||
images: Image[];
|
||||
};
|
||||
}
|
||||
}
|
||||
// @snippet:end
|
||||
|
||||
function Error(props) {
|
||||
if (!props.error) {
|
||||
@ -26,43 +28,126 @@ function Error(props) {
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error}
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Index(props) {
|
||||
const handleClick = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const joke = await getJoke();
|
||||
console.log(joke);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const DEFAULT_PROMPT = "a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd";
|
||||
|
||||
export function Index() {
|
||||
// @snippet:start(client.ui.state)
|
||||
// Input state
|
||||
const [prompt, setPrompt] = useState<string>(DEFAULT_PROMPT);
|
||||
// Result state
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [result, setResult] = useState<Result | null>(null);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [elapsedTime, setElapsedTime] = useState<number>(0);
|
||||
// @snippet:end
|
||||
const image = useMemo(() => {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
return result.images[0];
|
||||
}, [result]);
|
||||
|
||||
const reset = () => {
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setLogs([]);
|
||||
setElapsedTime(0);
|
||||
};
|
||||
|
||||
const handleOnClick = async (e) => {
|
||||
e.preventDefault();
|
||||
reset();
|
||||
// @snippet:start(client.queue.subscribe)
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.queue.subscribe('110602490-lora', {
|
||||
input: {
|
||||
prompt,
|
||||
model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||
image_size: 'square_hd',
|
||||
},
|
||||
onQueueUpdate(status) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (status.status === 'IN_PROGRESS') {
|
||||
setLogs(status.logs.map((log) => log.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
setResult(result);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setElapsedTime(Date.now() - start);
|
||||
}
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 dark:text-white bg-white text-black">
|
||||
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 py-10">
|
||||
<div className="min-h-screen dark:bg-gray-800 dark:text-gray-50 bg-gray-100 text-gray-800">
|
||||
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
Hello <code>fal-serverless</code>
|
||||
Hello <code className="font-light text-pink-600">fal</code>
|
||||
</h1>
|
||||
<p className="text-lg mb-10">
|
||||
This page can access <strong>fal-serverless</strong> functions when
|
||||
it's rendering.
|
||||
</p>
|
||||
<Error error={props.error} />
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
Prompt
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-lg p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Imagine..."
|
||||
value={prompt}
|
||||
autoComplete="off"
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onBlur={(e) => setPrompt(e.target.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold text-xl py-4 px-8 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={handleOnClick}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
disabled={loading}
|
||||
>
|
||||
Get Joke
|
||||
{loading ? 'Generating...' : 'Generate Image'}
|
||||
</button>
|
||||
|
||||
<p className="mt-10">
|
||||
Here's a joke: <strong>{props.joke}</strong>
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="mx-auto">
|
||||
{image && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={image.url} alt="" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/80 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/80 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
|
||||
export type GenerateImageInput = {
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
type ImageType = 'gif' | 'png' | 'jpg' | 'jpeg';
|
||||
type ImageDataUri = `data:image/${ImageType};base64,${string}`;
|
||||
|
||||
fal.config({
|
||||
credentials: {
|
||||
userId: '',
|
||||
keyId: '',
|
||||
keySecret: '',
|
||||
},
|
||||
});
|
||||
|
||||
export async function generateImage(
|
||||
input: GenerateImageInput
|
||||
): Promise<ImageDataUri> {
|
||||
const result = await fal.run('a51c0ca0-9011-4ff0-8dc1-2ac0b42a9fd0', {
|
||||
path: '/generate',
|
||||
input,
|
||||
});
|
||||
const data = result['raw_data'];
|
||||
return `data:image/jpg;base64,${data}`;
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
|
||||
fal.config({
|
||||
host: 'gateway.alpha.fal.ai',
|
||||
credentials: {
|
||||
userId: process.env.FAL_USER_ID || '',
|
||||
keyId: process.env.FAL_KEY_ID || '',
|
||||
keySecret: process.env.FAL_KEY_SECRET || '',
|
||||
},
|
||||
});
|
||||
|
||||
export type GetJokeInput = {
|
||||
language?: string;
|
||||
};
|
||||
|
||||
export function getJoke(input?: GetJokeInput): Promise<{ joke: string }> {
|
||||
return fal.run('fastapi_get_joke', { input });
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import Index from '../pages/index';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@fal-ai/serverless-client",
|
||||
"description": "The fal serverless JS/TS client",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -7,11 +7,11 @@ describe('The config test suite', () => {
|
||||
credentials: {
|
||||
keyId: 'key-id',
|
||||
keySecret: 'key-secret',
|
||||
userId: 'user-id',
|
||||
},
|
||||
};
|
||||
config(newConfig);
|
||||
const currentConfig = getConfig();
|
||||
expect(currentConfig).toEqual(newConfig);
|
||||
expect(currentConfig.host).toBe(newConfig.host);
|
||||
expect(currentConfig.credentials).toEqual(newConfig.credentials);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,21 +1,80 @@
|
||||
import type { RequestMiddleware } from './middleware';
|
||||
import type { ResponseHandler } from './response';
|
||||
import { defaultResponseHandler } from './response';
|
||||
|
||||
export type Credentials = {
|
||||
keyId: string;
|
||||
keySecret: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type CredentialsResolver = () => Credentials;
|
||||
|
||||
export type Config = {
|
||||
credentials: Credentials;
|
||||
credentials?: Credentials | CredentialsResolver;
|
||||
host?: string;
|
||||
requestMiddleware?: RequestMiddleware;
|
||||
responseHandler?: ResponseHandler<any>;
|
||||
};
|
||||
|
||||
export type RequiredConfig = Required<Config>;
|
||||
|
||||
const DEFAULT_CONFIG: Partial<Config> = {
|
||||
host: 'gateway.shark.fal.ai',
|
||||
/**
|
||||
* Checks if the required FAL environment variables are set.
|
||||
*
|
||||
* @returns `true` if the required environment variables are set,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
function hasEnvVariables(): boolean {
|
||||
return (
|
||||
process &&
|
||||
process.env &&
|
||||
typeof process.env.FAL_KEY_ID !== 'undefined' &&
|
||||
typeof process.env.FAL_KEY_SECRET !== 'undefined'
|
||||
);
|
||||
}
|
||||
|
||||
export const credentialsFromEnv: CredentialsResolver = () => {
|
||||
if (!hasEnvVariables()) {
|
||||
return {
|
||||
keyId: '',
|
||||
keySecret: '',
|
||||
};
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
console.warn(
|
||||
"The fal credentials are exposed in the browser's environment. " +
|
||||
"That's not recommended for production use cases."
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
keyId: process.env.FAL_KEY_ID || '',
|
||||
keySecret: process.env.FAL_KEY_SECRET || '',
|
||||
};
|
||||
};
|
||||
|
||||
let configuration: RequiredConfig | undefined = undefined;
|
||||
/**
|
||||
* Get the default host for the fal-serverless gateway endpoint.
|
||||
* @private
|
||||
* @returns the default host. Depending on the platform it can default to
|
||||
* the environment variable `FAL_HOST`.
|
||||
*/
|
||||
function getDefaultHost(): string {
|
||||
const host = 'gateway.alpha.fal.ai';
|
||||
if (process && process.env) {
|
||||
return process.env.FAL_HOST || host;
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: Partial<Config> = {
|
||||
host: getDefaultHost(),
|
||||
credentials: credentialsFromEnv,
|
||||
requestMiddleware: (request) => Promise.resolve(request),
|
||||
responseHandler: defaultResponseHandler,
|
||||
};
|
||||
|
||||
let configuration: RequiredConfig;
|
||||
|
||||
/**
|
||||
* Configures the fal serverless client.
|
||||
@ -32,8 +91,8 @@ export function config(config: Config) {
|
||||
* @returns the current client configuration.
|
||||
*/
|
||||
export function getConfig(): RequiredConfig {
|
||||
if (typeof configuration === 'undefined') {
|
||||
throw new Error('You must configure fal-serverless first.');
|
||||
if (!configuration) {
|
||||
console.info('Using default configuration for the fal client');
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import { buildUrl } from './function';
|
||||
config({
|
||||
host: 'gateway.alpha.fal.ai',
|
||||
credentials: {
|
||||
userId: 'github|123456',
|
||||
keyId: 'a91ff3ca-71bc-4c8c-b400-859f6cbe804d',
|
||||
keySecret: '0123456789abcdfeghijklmnopqrstuv',
|
||||
},
|
||||
@ -13,15 +12,14 @@ config({
|
||||
|
||||
describe('The function test suite', () => {
|
||||
it('should build the URL with a function UUIDv4', () => {
|
||||
const { credentials } = getConfig();
|
||||
const id = randomUUID();
|
||||
const url = buildUrl(id);
|
||||
expect(url).toMatch(`trigger/${credentials.userId}/${id}`);
|
||||
const url = buildUrl(`12345/${id}`);
|
||||
expect(url).toMatch(`trigger/12345/${id}`);
|
||||
});
|
||||
|
||||
it('should build the URL with a function alias', () => {
|
||||
const { host } = getConfig();
|
||||
const alias = 'some-alias';
|
||||
const alias = '12345-some-alias';
|
||||
const url = buildUrl(alias);
|
||||
expect(url).toMatch(`${alias}.${host}`);
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import fetch from 'cross-fetch';
|
||||
import { getConfig } from './config';
|
||||
import { getUserAgent, isBrowser } from './runtime';
|
||||
import { isUUIDv4 } from './utils';
|
||||
import { EnqueueResult, QueueStatus } from './types';
|
||||
import { isUUIDv4, isValidUrl } from './utils';
|
||||
|
||||
/**
|
||||
* The function input and other configuration when running
|
||||
@ -22,7 +22,7 @@ type RunOptions<Input> = {
|
||||
/**
|
||||
* The HTTP method, defaults to `post`;
|
||||
*/
|
||||
readonly method?: 'get' | 'post' | 'put' | 'delete';
|
||||
readonly method?: 'get' | 'post' | 'put' | 'delete' | string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -38,150 +38,143 @@ export function buildUrl<Input>(
|
||||
id: string,
|
||||
options: RunOptions<Input> = {}
|
||||
): string {
|
||||
const { credentials, host } = getConfig();
|
||||
const { host } = getConfig();
|
||||
const method = (options.method ?? 'post').toLowerCase();
|
||||
const path = options.path ?? '';
|
||||
const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/');
|
||||
const params =
|
||||
method === 'get' ? new URLSearchParams(options.input ?? {}) : undefined;
|
||||
let queryParams = '';
|
||||
if (params) {
|
||||
queryParams = `?${params.toString()}`;
|
||||
// TODO: change to params.size once it's officially supported
|
||||
const queryParams = params && params['size'] ? `?${params.toString()}` : '';
|
||||
const parts = id.split('/');
|
||||
|
||||
// if a fal.ai url is passed, just use it
|
||||
if (isValidUrl(id)) {
|
||||
const url = id.endsWith('/') ? id : `${id}/`;
|
||||
return `${url}${path}${queryParams}`;
|
||||
}
|
||||
if (isUUIDv4(id)) {
|
||||
return `https://${host}/trigger/${credentials.userId}/${id}/${path}${queryParams}`;
|
||||
|
||||
if (parts.length === 2 && isUUIDv4(parts[1])) {
|
||||
return `https://${host}/trigger/${id}/${path}${queryParams}`;
|
||||
}
|
||||
const userId = credentials.userId.replace(/github\|/g, '');
|
||||
return `https://${userId}-${id}.${host}/${path}${queryParams}`;
|
||||
return `https://${id}.${host}/${path}${queryParams}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a fal serverless function identified by its `id`.
|
||||
* TODO: expand documentation and provide examples
|
||||
*
|
||||
* @param id the registered function id
|
||||
* @param id the registered function revision id or alias.
|
||||
* @returns the remote function output
|
||||
*/
|
||||
export async function run<Input, Output>(
|
||||
id: string,
|
||||
options: RunOptions<Input> = {}
|
||||
): Promise<Output> {
|
||||
const { credentials } = getConfig();
|
||||
const { credentials, requestMiddleware, responseHandler } = getConfig();
|
||||
const method = (options.method ?? 'post').toLowerCase();
|
||||
const userAgent = isBrowser ? {} : { 'User-Agent': getUserAgent() };
|
||||
const response = await fetch(buildUrl(id, options), {
|
||||
method,
|
||||
headers: {
|
||||
'X-Fal-Key-Id': credentials.keyId,
|
||||
'X-Fal-Key-Secret': credentials.keySecret,
|
||||
const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() };
|
||||
const { keyId, keySecret } =
|
||||
typeof credentials === 'function' ? credentials() : credentials;
|
||||
|
||||
const { url, headers } = await requestMiddleware({
|
||||
url: buildUrl(id, options),
|
||||
});
|
||||
const authHeader =
|
||||
keyId && keySecret ? { Authorization: `Key ${keyId}:${keySecret}` } : {};
|
||||
const requestHeaders = {
|
||||
...authHeader,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...userAgent,
|
||||
},
|
||||
mode: 'cors',
|
||||
...(headers ?? {}),
|
||||
} as HeadersInit;
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
mode: 'same-origin',
|
||||
credentials: 'same-origin',
|
||||
body:
|
||||
method !== 'get' && options.input
|
||||
? JSON.stringify(options.input)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const { status, statusText } = response;
|
||||
if (status < 200 || status >= 300) {
|
||||
// TODO better error type so handlers can differentiate
|
||||
throw new Error(statusText);
|
||||
return await responseHandler(response);
|
||||
}
|
||||
|
||||
// TODO move this elsewhere so it can be reused by websocket impl too
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return response.json();
|
||||
}
|
||||
if (contentType?.includes('text/html')) {
|
||||
return response.text() as Promise<Output>;
|
||||
}
|
||||
if (contentType?.includes('application/octet-stream')) {
|
||||
return response.arrayBuffer() as Promise<Output>;
|
||||
}
|
||||
// TODO convert to either number or bool automatically
|
||||
return response.text() as Promise<Output>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event contract for progress updates from the server function.
|
||||
*/
|
||||
export interface ProgressEvent<T> {
|
||||
readonly progress: number;
|
||||
readonly partialData: T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a result of a remote fal serverless function call.
|
||||
*
|
||||
* The contract allows developers to not only get the function
|
||||
* result, but also track its progress and logs through event
|
||||
* listeners.
|
||||
*
|
||||
* This flexibility enables developers to define complex / long-running functions
|
||||
* but also simple ones with an unified developer experience.
|
||||
*/
|
||||
export interface FunctionExecution<T> {
|
||||
/**
|
||||
* Listens to `progress` events.
|
||||
*
|
||||
* @param event of type `progress`
|
||||
* @param handler the callback to handle in-progress data.
|
||||
*/
|
||||
on(event: 'progress', handler: (info: ProgressEvent<T>) => void): void;
|
||||
|
||||
/**
|
||||
* Listens to `cancel` events.
|
||||
*
|
||||
* @param event of type `cancel`.
|
||||
* @param handler the callback to handle the cancellation signal.
|
||||
*/
|
||||
on(event: 'cancel', handler: () => void): void;
|
||||
|
||||
/**
|
||||
* Listens to logging events.
|
||||
*
|
||||
* @param event of type `log`
|
||||
* @param handler the callback to handle the forwarded `log`
|
||||
*/
|
||||
on(event: 'log', handler: (log: string) => void): void;
|
||||
|
||||
/**
|
||||
* Signals to the server that the execution should be cancelled.
|
||||
* Once the server cancels the execution sucessfully, the `cancel`
|
||||
* event will be fired.
|
||||
*
|
||||
* @see #on(event:)
|
||||
*/
|
||||
cancel(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Async function that represents the final result of the function call.
|
||||
*
|
||||
* ```ts
|
||||
* const image = await execution.result();
|
||||
* ```
|
||||
*
|
||||
* @returns Promise<T> the final result.
|
||||
* @throws in case the backing remote function raises an error.
|
||||
*/
|
||||
result(): Promise<T>;
|
||||
}
|
||||
|
||||
type ListenOptions<Input> = {
|
||||
readonly input?: Input;
|
||||
type QueueSubscribeOptions = {
|
||||
pollInterval?: number;
|
||||
onEnqueue?: (requestId: string) => void;
|
||||
onQueueUpdate?: (status: QueueStatus) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: document me
|
||||
*
|
||||
* @param id
|
||||
* @param options
|
||||
*/
|
||||
export function listen<Input, Output>(
|
||||
interface Queue {
|
||||
submit<Input>(id: string, options: RunOptions<Input>): Promise<EnqueueResult>;
|
||||
status(id: string, requestId: string): Promise<QueueStatus>;
|
||||
result<Output>(id: string, requestId: string): Promise<Output>;
|
||||
subscribe<Input, Output>(
|
||||
id: string,
|
||||
options: ListenOptions<Input>
|
||||
): FunctionExecution<Output> {
|
||||
throw 'TODO: implement me!';
|
||||
options: RunOptions<Input> & QueueSubscribeOptions
|
||||
): Promise<Output>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The fal run queue module. It allows to submit a function to the queue and get its result
|
||||
* on a separate call. This is useful for long running functions that can be executed
|
||||
* asynchronously and not .
|
||||
*/
|
||||
export const queue: Queue = {
|
||||
async submit<Input>(
|
||||
id: string,
|
||||
options: RunOptions<Input>
|
||||
): Promise<EnqueueResult> {
|
||||
return run(id, { ...options, method: 'post', path: '/fal/queue/submit/' });
|
||||
},
|
||||
async status(id: string, requestId: string): Promise<QueueStatus> {
|
||||
return run(id, {
|
||||
method: 'get',
|
||||
path: `/fal/queue/requests/${requestId}/status`,
|
||||
});
|
||||
},
|
||||
async result<Output>(id: string, requestId: string): Promise<Output> {
|
||||
return run(id, {
|
||||
method: 'get',
|
||||
path: `/fal/queue/requests/${requestId}/response`,
|
||||
});
|
||||
},
|
||||
async subscribe<Input, Output>(
|
||||
id: string,
|
||||
options: RunOptions<Input> & QueueSubscribeOptions = {}
|
||||
): Promise<Output> {
|
||||
const { request_id: requestId } = await queue.submit(id, options);
|
||||
if (options.onEnqueue) {
|
||||
options.onEnqueue(requestId);
|
||||
}
|
||||
return new Promise<Output>((resolve, reject) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
const pollInterval = options.pollInterval ?? 1000;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const requestStatus = await queue.status(id, requestId);
|
||||
if (options.onQueueUpdate) {
|
||||
options.onQueueUpdate(requestStatus);
|
||||
}
|
||||
if (requestStatus.status === 'COMPLETED') {
|
||||
clearTimeout(timeoutId);
|
||||
try {
|
||||
const result = await queue.result<Output>(id, requestId);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
timeoutId = setTimeout(poll, pollInterval);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
timeoutId = setTimeout(poll, pollInterval);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
export { config } from './config';
|
||||
export { config, getConfig } from './config';
|
||||
export type { Credentials } from './config';
|
||||
export { run } from './function';
|
||||
export type { FunctionExecution, ProgressEvent } from './function';
|
||||
export { queue, run } from './function';
|
||||
export { withMiddleware } from './middleware';
|
||||
export type { RequestMiddleware } from './middleware';
|
||||
export type { ResponseHandler } from './response';
|
||||
export type { QueueStatus } from './types';
|
||||
|
||||
34
libs/client/src/middleware.ts
Normal file
34
libs/client/src/middleware.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* A request configuration object.
|
||||
*
|
||||
* **Note:** This is a simplified version of the `RequestConfig` type from the
|
||||
* `fetch` API. It contains only the properties that are relevant for the
|
||||
* `fal-serverless` client. It also works around the fact that the `fetch` API
|
||||
* `Request` does not support mutability, its clone method has critical limitations
|
||||
* to our use case.
|
||||
*/
|
||||
export type RequestConfig = {
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
};
|
||||
|
||||
export type RequestMiddleware = (
|
||||
request: RequestConfig
|
||||
) => Promise<RequestConfig>;
|
||||
|
||||
/**
|
||||
* Setup a execution chain of middleware functions.
|
||||
*
|
||||
* @param middlewares one or more middleware functions.
|
||||
* @returns a middleware function that executes the given middlewares in order.
|
||||
*/
|
||||
export function withMiddleware(
|
||||
...middlewares: RequestMiddleware[]
|
||||
): RequestMiddleware {
|
||||
return (config) =>
|
||||
middlewares.reduce(
|
||||
(configPromise, middleware) =>
|
||||
configPromise.then((req) => middleware(req)),
|
||||
Promise.resolve(config)
|
||||
);
|
||||
}
|
||||
46
libs/client/src/response.ts
Normal file
46
libs/client/src/response.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export type ResponseHandler<Output> = (response: Response) => Promise<Output>;
|
||||
|
||||
type ApiErrorArgs = {
|
||||
message: string;
|
||||
status: number;
|
||||
body?: any;
|
||||
};
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly status: number;
|
||||
public readonly body?: any;
|
||||
constructor({ message, status, body }: ApiErrorArgs) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
export async function defaultResponseHandler<Output>(
|
||||
response: Response
|
||||
): Promise<Output> {
|
||||
const { status, statusText } = response;
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (!response.ok) {
|
||||
if (contentType?.includes('application/json')) {
|
||||
const body = await response.json();
|
||||
throw new ApiError({
|
||||
message: body.message || statusText,
|
||||
status,
|
||||
body,
|
||||
});
|
||||
}
|
||||
throw new Error(`HTTP ${status}: ${statusText}`);
|
||||
}
|
||||
if (contentType?.includes('application/json')) {
|
||||
return response.json() as Promise<Output>;
|
||||
}
|
||||
if (contentType?.includes('text/html')) {
|
||||
return response.text() as Promise<Output>;
|
||||
}
|
||||
if (contentType?.includes('application/octet-stream')) {
|
||||
return response.arrayBuffer() as Promise<Output>;
|
||||
}
|
||||
// TODO convert to either number or bool automatically
|
||||
return response.text() as Promise<Output>;
|
||||
}
|
||||
35
libs/client/src/types.ts
Normal file
35
libs/client/src/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export type Result<T> = {
|
||||
result: T;
|
||||
};
|
||||
|
||||
export type EnqueueResult = {
|
||||
request_id: string;
|
||||
};
|
||||
|
||||
// export type QueueStatus = {
|
||||
// status: "IN_PROGRESS" | "COMPLETED";
|
||||
// queue: number;
|
||||
// };
|
||||
|
||||
export type RequestLog = {
|
||||
message: string;
|
||||
level: 'STDERR' | 'STDOUT' | 'ERROR' | 'INFO' | 'WARN' | 'DEBUG';
|
||||
source: 'USER';
|
||||
timestamp: string; // Using string to represent date-time format, but you could also use 'Date' type if you're going to construct Date objects.
|
||||
};
|
||||
|
||||
export type QueueStatus =
|
||||
| {
|
||||
status: 'IN_PROGRESS' | 'COMPLETED';
|
||||
response_url: string;
|
||||
logs: RequestLog[];
|
||||
}
|
||||
| {
|
||||
status: 'IN_QUEUE';
|
||||
queue_position: number;
|
||||
response_url: string;
|
||||
};
|
||||
|
||||
export function isQueueStatus(obj: any): obj is QueueStatus {
|
||||
return obj && obj.status && obj.response_url;
|
||||
}
|
||||
@ -6,3 +6,12 @@ export function isUUIDv4(id: string): boolean {
|
||||
['8', '9', 'a', 'b'].includes(id[19])
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidUrl(url: string) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.hostname.endsWith('fal.ai');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
3
libs/nextjs/.babelrc
Normal file
3
libs/nextjs/.babelrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": [["@nrwl/js/babel", { "useBuiltIns": "usage" }]]
|
||||
}
|
||||
18
libs/nextjs/.eslintrc.json
Normal file
18
libs/nextjs/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
libs/nextjs/README.md
Normal file
11
libs/nextjs/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# nextjs
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test nextjs` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
|
||||
## Running lint
|
||||
|
||||
Run `nx lint nextjs` to execute the lint via [ESLint](https://eslint.org/).
|
||||
16
libs/nextjs/jest.config.ts
Normal file
16
libs/nextjs/jest.config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'nextjs',
|
||||
preset: '../../jest.preset.js',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
},
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'ts-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/nextjs',
|
||||
};
|
||||
32
libs/nextjs/package.json
Normal file
32
libs/nextjs/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@fal-ai/serverless-nextjs",
|
||||
"description": "The fal-serverless Next.js integration",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fal-ai/serverless-js.git",
|
||||
"directory": "libs/nextjs"
|
||||
},
|
||||
"keywords": [
|
||||
"fal",
|
||||
"serverless",
|
||||
"client",
|
||||
"next",
|
||||
"nextjs",
|
||||
"proxy"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"next": "^13.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
41
libs/nextjs/project.json
Normal file
41
libs/nextjs/project.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "nextjs",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/nextjs/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/nextjs",
|
||||
"tsConfig": "libs/nextjs/tsconfig.lib.json",
|
||||
"packageJson": "libs/nextjs/package.json",
|
||||
"main": "libs/nextjs/src/index.ts",
|
||||
"assets": ["libs/nextjs/*.md"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/nextjs/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/nextjs/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
29
libs/nextjs/src/config.ts
Normal file
29
libs/nextjs/src/config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { RequestMiddleware } from '@fal-ai/serverless-client';
|
||||
|
||||
export type NextProxyConfig = {
|
||||
targetUrl: string;
|
||||
};
|
||||
|
||||
const defaultConfig: NextProxyConfig = {
|
||||
targetUrl: '/api/_fal/proxy',
|
||||
};
|
||||
|
||||
export const TARGET_URL_HEADER = 'x-fal-target-url';
|
||||
|
||||
export function withNextProxy(
|
||||
config: NextProxyConfig = defaultConfig
|
||||
): RequestMiddleware {
|
||||
// when running on the server, we don't need to proxy the request
|
||||
if (typeof window === 'undefined') {
|
||||
return (requestConfig) => Promise.resolve(requestConfig);
|
||||
}
|
||||
return (requestConfig) =>
|
||||
Promise.resolve({
|
||||
...requestConfig,
|
||||
url: config.targetUrl,
|
||||
headers: {
|
||||
[TARGET_URL_HEADER]: requestConfig.url,
|
||||
...(requestConfig.headers || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
93
libs/nextjs/src/handler.ts
Normal file
93
libs/nextjs/src/handler.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import type { NextApiHandler, NextApiRequest, PageConfig } from 'next';
|
||||
import { TARGET_URL_HEADER } from './config';
|
||||
|
||||
const FAL_KEY_ID = process.env.FAL_KEY_ID || process.env.NEXT_PUBLIC_FAL_KEY_ID;
|
||||
const FAL_KEY_SECRET =
|
||||
process.env.FAL_KEY_SECRET || process.env.NEXT_PUBLIC_FAL_KEY_SECRET;
|
||||
|
||||
/**
|
||||
* Utility to get a header value as `string` from a Headers object.
|
||||
*
|
||||
* @private
|
||||
* @param request the Next request object.
|
||||
* @param key the header key.
|
||||
* @returns the header value as `string` or `undefined` if the header is not set.
|
||||
*/
|
||||
function getHeader(request: NextApiRequest, key: string): string | undefined {
|
||||
const headerValue = request.headers[key.toLowerCase()];
|
||||
if (Array.isArray(headerValue)) {
|
||||
return headerValue[0];
|
||||
}
|
||||
return headerValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up headers that should not be forwarded to the proxy.
|
||||
* @param request the Next request object.
|
||||
*/
|
||||
function cleanUpHeaders(request: NextApiRequest) {
|
||||
delete request.headers['origin'];
|
||||
delete request.headers['referer'];
|
||||
// delete request.headers['transfer-encoding'];
|
||||
delete request.headers[TARGET_URL_HEADER];
|
||||
}
|
||||
|
||||
/**
|
||||
* A Next request handler that proxies the request to the fal-serverless
|
||||
* endpoint. This is useful so client-side calls to the fal-serverless endpoint
|
||||
* can be made without CORS issues and the correct credentials can be added
|
||||
* effortlessly.
|
||||
*
|
||||
* @param request the Next request object.
|
||||
* @param response the Next response object.
|
||||
* @returns Promise<any> the promise that will be resolved once the request is done.
|
||||
*/
|
||||
export const handler: NextApiHandler = async (request, response) => {
|
||||
const targetUrl = getHeader(request, TARGET_URL_HEADER);
|
||||
if (!targetUrl) {
|
||||
response.status(400).send(`Missing the ${TARGET_URL_HEADER} header`);
|
||||
return;
|
||||
}
|
||||
if (targetUrl.indexOf('fal.ai') === -1) {
|
||||
response.status(412).send(`Invalid ${TARGET_URL_HEADER} header`);
|
||||
return;
|
||||
}
|
||||
|
||||
cleanUpHeaders(request);
|
||||
|
||||
const authHeader =
|
||||
FAL_KEY_ID && FAL_KEY_SECRET
|
||||
? { authorization: `Key ${FAL_KEY_ID}:${FAL_KEY_SECRET}` }
|
||||
: {};
|
||||
|
||||
const res = await fetch(targetUrl, {
|
||||
method: request.method,
|
||||
headers: {
|
||||
...authHeader,
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'x-fal-client-proxy': '@fal-ai/serverless-nextjs',
|
||||
},
|
||||
body:
|
||||
request.method?.toUpperCase() === 'GET'
|
||||
? undefined
|
||||
: JSON.stringify(request.body),
|
||||
});
|
||||
|
||||
// copy headers from res to response
|
||||
res.headers.forEach((value, key) => {
|
||||
response.setHeader(key, value);
|
||||
});
|
||||
|
||||
if (res.headers.get('content-type') === 'application/json') {
|
||||
const data = await res.json();
|
||||
response.status(res.status).json(data);
|
||||
return;
|
||||
}
|
||||
const data = await res.text();
|
||||
response.status(res.status).send(data);
|
||||
};
|
||||
|
||||
export const config: PageConfig = {
|
||||
api: {},
|
||||
};
|
||||
2
libs/nextjs/src/index.ts
Normal file
2
libs/nextjs/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './config';
|
||||
export * from './handler';
|
||||
13
libs/nextjs/tsconfig.json
Normal file
13
libs/nextjs/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
libs/nextjs/tsconfig.lib.json
Normal file
11
libs/nextjs/tsconfig.lib.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
20
libs/nextjs/tsconfig.spec.json
Normal file
20
libs/nextjs/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
1369
package-lock.json
generated
1369
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,10 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"core-js": "^3.6.5",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"encoding": "^0.1.13",
|
||||
"fast-glob": "^3.2.12",
|
||||
"http-proxy": "^1.18.1",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"js-base64": "^3.7.5",
|
||||
"next": "13.1.1",
|
||||
"react": "18.2.0",
|
||||
@ -43,7 +46,7 @@
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@theunderscorer/nx-semantic-release": "^2.2.1",
|
||||
"@types/jest": "28.1.1",
|
||||
"@types/node": "16.11.7",
|
||||
"@types/node": "^18.17.14",
|
||||
"@types/react": "18.0.20",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
@ -68,7 +71,7 @@
|
||||
"postcss": "8.4.19",
|
||||
"prettier": "^2.6.2",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"tailwindcss": "3.2.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"ts-jest": "28.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@fal-ai/serverless-client": ["libs/client/src/index.ts"],
|
||||
"@fal-ai/serverless-codegen": ["libs/codegen/src/index.ts"]
|
||||
"@fal-ai/serverless-codegen": ["libs/codegen/src/index.ts"],
|
||||
"@fal-ai/serverless-nextjs": ["libs/nextjs/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user