feat: next proxy app router support (#22)
* feat: add nextjs app router support * chore: rename nextjs page router demo * feat: support nextjs app router with example * chore(proxy): bump to 0.4.0 * chore: update package-lock.json * chore: remove formidable * chore: update package-lock.json * fix: page export * fix: type issues * fix: remove comment * fix: eslint any warning * chore: client version bump 0.4.2
This commit is contained in:
parent
759d58a306
commit
3a98fd6d0f
2
.gitignore
vendored
2
.gitignore
vendored
@ -40,4 +40,4 @@ Thumbs.db
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
*.local
|
||||
*.local
|
||||
40
apps/demo-nextjs-app-router/.eslintrc.json
Normal file
40
apps/demo-nextjs-app-router/.eslintrc.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@nx/react-typescript",
|
||||
"next",
|
||||
"next/core-web-vitals",
|
||||
"../../.eslintrc.json"
|
||||
],
|
||||
"ignorePatterns": ["!**/*", ".next/**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.*"],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": [
|
||||
"error",
|
||||
"apps/demo-nextjs-app-router/pages"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3
apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts
Normal file
3
apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { route } from '@fal-ai/serverless-proxy/nextjs';
|
||||
|
||||
export const { GET, POST } = route;
|
||||
18
apps/demo-nextjs-app-router/app/layout.tsx
Normal file
18
apps/demo-nextjs-app-router/app/layout.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import './global.css';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Welcome to demo-nextjs-app-router',
|
||||
description: 'Generated by create-nx-workspace',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
170
apps/demo-nextjs-app-router/app/page.tsx
Normal file
170
apps/demo-nextjs-app-router/app/page.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
'use client';
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
requestMiddleware: fal.withProxy({
|
||||
targetUrl: '/api/fal/proxy', // the built-int nextjs proxy
|
||||
// targetUrl: 'http://localhost:3333/api/_fal/proxy', // or your own external proxy
|
||||
}),
|
||||
});
|
||||
// @snippet:end
|
||||
|
||||
// @snippet:start(client.result.type)
|
||||
type Image = {
|
||||
url: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
};
|
||||
type Result = {
|
||||
images: Image[];
|
||||
};
|
||||
// @snippet:end
|
||||
|
||||
type ErrorProps = {
|
||||
error: any;
|
||||
};
|
||||
|
||||
function Error(props: ErrorProps) {
|
||||
if (!props.error) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
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.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT =
|
||||
'a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd';
|
||||
|
||||
export default function Home() {
|
||||
// @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 generateImage = async () => {
|
||||
reset();
|
||||
// @snippet:start("client.queue.subscribe")
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.subscribe('110602490-lora', {
|
||||
input: {
|
||||
prompt,
|
||||
model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||
image_size: 'square_hd',
|
||||
},
|
||||
pollInterval: 5000, // Default is 1000 (every 1s)
|
||||
logs: true,
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
setResult(result);
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setElapsedTime(Date.now() - start);
|
||||
}
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
Hello <code className="font-light text-pink-600">fal</code>
|
||||
</h1>
|
||||
<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={(e) => {
|
||||
e.preventDefault();
|
||||
generateImage();
|
||||
}}
|
||||
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}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Image'}
|
||||
</button>
|
||||
|
||||
<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/70 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/70 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,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'demo-nextjs-app',
|
||||
displayName: 'demo-nextjs-app-router',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/demo-nextjs-app',
|
||||
coverageDirectory: '../../coverage/apps/demo-nextjs-app-router',
|
||||
};
|
||||
22
apps/demo-nextjs-app-router/next.config.js
Normal file
22
apps/demo-nextjs-app-router/next.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
//@ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { composePlugins, withNx } = require('@nx/next');
|
||||
|
||||
/**
|
||||
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
|
||||
**/
|
||||
const nextConfig = {
|
||||
nx: {
|
||||
// Set this to true if you would like to use SVGR
|
||||
// See: https://github.com/gregberge/svgr
|
||||
svgr: false,
|
||||
},
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
// Add more Next.js plugins to this list if needed.
|
||||
withNx,
|
||||
];
|
||||
|
||||
module.exports = composePlugins(...plugins)(nextConfig);
|
||||
68
apps/demo-nextjs-app-router/project.json
Normal file
68
apps/demo-nextjs-app-router/project.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "demo-nextjs-app-router",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/demo-nextjs-app-router",
|
||||
"projectType": "application",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/next:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/demo-nextjs-app-router"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"outputPath": "apps/demo-nextjs-app-router"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/next:server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "demo-nextjs-app-router:build",
|
||||
"dev": true
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "demo-nextjs-app-router:build:development",
|
||||
"dev": true
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "demo-nextjs-app-router:build:production",
|
||||
"dev": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"executor": "@nx/next:export",
|
||||
"options": {
|
||||
"buildTarget": "demo-nextjs-app-router:build:production"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/demo-nextjs-app-router/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/demo-nextjs-app-router/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
BIN
apps/demo-nextjs-app-router/public/favicon.ico
Normal file
BIN
apps/demo-nextjs-app-router/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
18
apps/demo-nextjs-app-router/tailwind.config.js
Normal file
18
apps/demo-nextjs-app-router/tailwind.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||
const { join } = require('path');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
join(
|
||||
__dirname,
|
||||
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
||||
),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
36
apps/demo-nextjs-app-router/tsconfig.json
Normal file
36
apps/demo-nextjs-app-router/tsconfig.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"../../apps/demo-nextjs-app-router/.next/types/**/*.ts",
|
||||
"../../dist/apps/demo-nextjs-app-router/.next/types/**/*.ts",
|
||||
"next-env.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jest.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
21
apps/demo-nextjs-app-router/tsconfig.spec.json
Normal file
21
apps/demo-nextjs-app-router/tsconfig.spec.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"],
|
||||
"jsx": "react"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
6
apps/demo-nextjs-page-router/index.d.ts
vendored
Normal file
6
apps/demo-nextjs-page-router/index.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export const ReactComponent: any;
|
||||
export default content;
|
||||
}
|
||||
11
apps/demo-nextjs-page-router/jest.config.ts
Normal file
11
apps/demo-nextjs-page-router/jest.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'demo-nextjs-page-router',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/demo-nextjs-page-router',
|
||||
};
|
||||
5
apps/demo-nextjs-page-router/next-env.d.ts
vendored
Normal file
5
apps/demo-nextjs-page-router/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
2
apps/demo-nextjs-page-router/pages/index.module.css
Normal file
2
apps/demo-nextjs-page-router/pages/index.module.css
Normal file
@ -0,0 +1,2 @@
|
||||
.page {
|
||||
}
|
||||
@ -21,7 +21,11 @@ type Result = {
|
||||
};
|
||||
// @snippet:end
|
||||
|
||||
function Error(props) {
|
||||
type ErrorProps = {
|
||||
error: any;
|
||||
};
|
||||
|
||||
function Error(props: ErrorProps) {
|
||||
if (!props.error) {
|
||||
return null;
|
||||
}
|
||||
@ -64,8 +68,7 @@ export function Index() {
|
||||
setElapsedTime(0);
|
||||
};
|
||||
|
||||
const handleOnClick = async (e) => {
|
||||
e.preventDefault();
|
||||
const generateImage = async () => {
|
||||
reset();
|
||||
// @snippet:start("client.queue.subscribe")
|
||||
setLoading(true);
|
||||
@ -85,12 +88,12 @@ export function Index() {
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
) {
|
||||
setLogs(update.logs.map((log) => log.message));
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
setResult(result);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -121,7 +124,10 @@ export function Index() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleOnClick}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
generateImage();
|
||||
}}
|
||||
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}
|
||||
>
|
||||
400
apps/demo-nextjs-page-router/pages/styles.css
Normal file
400
apps/demo-nextjs-page-router/pages/styles.css
Normal file
@ -0,0 +1,400 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
|
||||
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
shape-rendering: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
pre {
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(229, 231, 235, 1);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
overflow: scroll;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 768px;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
width: 100%;
|
||||
}
|
||||
#welcome {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
#welcome h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
#welcome span {
|
||||
display: block;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 2.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#hero {
|
||||
align-items: center;
|
||||
background-color: hsla(214, 62%, 21%, 1);
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#hero .text-container {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
#hero .text-container h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
#hero .text-container h2 svg {
|
||||
color: hsla(162, 47%, 50%, 1);
|
||||
height: 2rem;
|
||||
left: -0.25rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
#hero .text-container h2 span {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
#hero .text-container a {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
#hero .logo-container svg {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
width: 66.666667%;
|
||||
}
|
||||
#middle-content {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
gap: 4rem;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#learning-materials {
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#learning-materials h2 {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.list-item-link {
|
||||
align-items: center;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-link svg:first-child {
|
||||
margin-right: 1rem;
|
||||
height: 1.5rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.list-item-link > span {
|
||||
flex-grow: 1;
|
||||
font-weight: 400;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link > span > span {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
line-height: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link svg:last-child {
|
||||
height: 1rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1rem;
|
||||
}
|
||||
.list-item-link:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: hsla(162, 47%, 50%, 1);
|
||||
}
|
||||
.list-item-link:hover > span {
|
||||
}
|
||||
.list-item-link:hover > span > span {
|
||||
color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
.list-item-link:hover svg:last-child {
|
||||
transform: translateX(0.25rem);
|
||||
}
|
||||
#other-links {
|
||||
}
|
||||
.button-pill {
|
||||
padding: 1.5rem 2rem;
|
||||
transition-duration: 300ms;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.button-pill svg {
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
.button-pill > span {
|
||||
letter-spacing: -0.025em;
|
||||
font-weight: 400;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.button-pill span span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.button-pill:hover svg,
|
||||
.button-pill:hover {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
#nx-console:hover {
|
||||
background-color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console svg {
|
||||
color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-repo:hover {
|
||||
background-color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-repo svg {
|
||||
color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-cloud {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 2rem;
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#nx-cloud > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#nx-cloud > div svg {
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
#nx-cloud > div h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#nx-cloud > div h2 span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
#nx-cloud p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud pre {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#nx-cloud a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#commands {
|
||||
padding: 2.5rem 2rem;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#commands h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#commands p {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
details pre > span {
|
||||
color: rgba(181, 181, 181, 1);
|
||||
display: block;
|
||||
}
|
||||
summary {
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
summary:hover {
|
||||
background-color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
summary svg {
|
||||
height: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
#love {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 3.5rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
#love svg {
|
||||
color: rgba(252, 165, 165, 1);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: inline;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
#hero {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: flex;
|
||||
}
|
||||
#middle-content {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
15
apps/demo-nextjs-page-router/postcss.config.js
Normal file
15
apps/demo-nextjs-page-router/postcss.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
const { join } = require('path');
|
||||
|
||||
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
|
||||
// option from your application's configuration (i.e. project.json).
|
||||
//
|
||||
// See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "demo-nextjs-app",
|
||||
"name": "demo-nextjs-page-router",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/demo-nextjs-app",
|
||||
"sourceRoot": "apps/demo-nextjs-page-router",
|
||||
"projectType": "application",
|
||||
"targets": {
|
||||
"build": {
|
||||
@ -9,11 +9,11 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/demo-nextjs-app"
|
||||
"outputPath": "dist/apps/demo-nextjs-page-router"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"outputPath": "apps/demo-nextjs-app"
|
||||
"outputPath": "apps/demo-nextjs-page-router"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
@ -22,16 +22,16 @@
|
||||
"executor": "@nx/next:server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "demo-nextjs-app:build",
|
||||
"buildTarget": "demo-nextjs-page-router:build",
|
||||
"dev": true
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "demo-nextjs-app:build:development",
|
||||
"buildTarget": "demo-nextjs-page-router:build:development",
|
||||
"dev": true
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "demo-nextjs-app:build:production",
|
||||
"buildTarget": "demo-nextjs-page-router:build:production",
|
||||
"dev": false
|
||||
}
|
||||
}
|
||||
@ -39,14 +39,14 @@
|
||||
"export": {
|
||||
"executor": "@nx/next:export",
|
||||
"options": {
|
||||
"buildTarget": "demo-nextjs-app:build:production"
|
||||
"buildTarget": "demo-nextjs-page-router:build:production"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/demo-nextjs-app/jest.config.ts",
|
||||
"jestConfig": "apps/demo-nextjs-page-router/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
@ -54,7 +54,9 @@
|
||||
"executor": "@nx/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/demo-nextjs-app/**/*.{ts,tsx,js,jsx}"]
|
||||
"lintFilePatterns": [
|
||||
"apps/demo-nextjs-page-router/**/*.{ts,tsx,js,jsx}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
0
apps/demo-nextjs-page-router/public/.gitkeep
Normal file
0
apps/demo-nextjs-page-router/public/.gitkeep
Normal file
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@fal-ai/serverless-client",
|
||||
"description": "The fal serverless JS/TS client",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -41,10 +41,11 @@ export function buildUrl<Input>(
|
||||
const { host } = getConfig();
|
||||
const method = (options.method ?? 'post').toLowerCase();
|
||||
const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/');
|
||||
const input = options.input;
|
||||
const params =
|
||||
method === 'get' ? new URLSearchParams(options.input ?? {}) : undefined;
|
||||
// TODO: change to params.size once it's officially supported
|
||||
const queryParams = params && params['size'] ? `?${params.toString()}` : '';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
method === 'get' && input ? new URLSearchParams(input as any) : undefined;
|
||||
const queryParams = params ? `?${params.toString()}` : '';
|
||||
const parts = id.split('/');
|
||||
|
||||
// if a fal.ai url is passed, just use it
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fal-ai/serverless-proxy",
|
||||
"version": "0.3.5",
|
||||
"version": "0.4.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -24,7 +24,7 @@ export const handler: RequestHandler = async (request, response, next) => {
|
||||
getHeaders: () => request.headers,
|
||||
getHeader: (name) => request.headers[name],
|
||||
sendHeader: (name, value) => response.setHeader(name, value),
|
||||
getBody: () => JSON.stringify(request.body),
|
||||
getBody: async () => JSON.stringify(request.body),
|
||||
});
|
||||
next();
|
||||
};
|
||||
|
||||
@ -7,19 +7,21 @@ 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;
|
||||
|
||||
export type HeaderValue = string | string[] | undefined | null;
|
||||
|
||||
/**
|
||||
* The proxy behavior that is passed to the proxy handler. This is a subset of
|
||||
* request objects that are used by different frameworks, like Express and NextJS.
|
||||
*/
|
||||
export interface ProxyBehavior {
|
||||
export interface ProxyBehavior<ResponseType> {
|
||||
id: string;
|
||||
method: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
respondWith(status: number, data: string | any): void;
|
||||
getHeaders(): Record<string, string | string[] | undefined>;
|
||||
getHeader(name: string): string | string[] | undefined;
|
||||
respondWith(status: number, data: string | any): ResponseType;
|
||||
getHeaders(): Record<string, HeaderValue>;
|
||||
getHeader(name: string): HeaderValue;
|
||||
sendHeader(name: string, value: string): void;
|
||||
getBody(): string | undefined;
|
||||
getBody(): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,10 +31,8 @@ export interface ProxyBehavior {
|
||||
* @param request the header value.
|
||||
* @returns the header value as `string` or `undefined` if the header is not set.
|
||||
*/
|
||||
function singleHeaderValue(
|
||||
value: string | string[] | undefined
|
||||
): string | undefined {
|
||||
if (value === undefined) {
|
||||
function singleHeaderValue(value: HeaderValue): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
@ -51,6 +51,8 @@ function getFalKey(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const EXCLUDED_HEADERS = ['content-length'];
|
||||
|
||||
/**
|
||||
* A request handler that proxies the request to the fal-serverless
|
||||
* endpoint. This is useful so client-side calls to the fal-serverless endpoint
|
||||
@ -60,31 +62,32 @@ function getFalKey(): string | undefined {
|
||||
* @param behavior the request proxy behavior.
|
||||
* @returns Promise<any> the promise that will be resolved once the request is done.
|
||||
*/
|
||||
export const handleRequest = async (behavior: ProxyBehavior) => {
|
||||
export async function handleRequest<ResponseType>(
|
||||
behavior: ProxyBehavior<ResponseType>
|
||||
) {
|
||||
const targetUrl = singleHeaderValue(behavior.getHeader(TARGET_URL_HEADER));
|
||||
if (!targetUrl) {
|
||||
behavior.respondWith(400, `Missing the ${TARGET_URL_HEADER} header`);
|
||||
return;
|
||||
return behavior.respondWith(400, `Missing the ${TARGET_URL_HEADER} header`);
|
||||
}
|
||||
if (targetUrl.indexOf('fal.ai') === -1) {
|
||||
behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`);
|
||||
return;
|
||||
return behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`);
|
||||
}
|
||||
|
||||
const falKey = getFalKey();
|
||||
if (!falKey) {
|
||||
behavior.respondWith(401, 'Missing fal.ai credentials');
|
||||
return;
|
||||
return behavior.respondWith(401, 'Missing fal.ai credentials');
|
||||
}
|
||||
|
||||
// pass over headers prefixed with x-fal-*
|
||||
const headers: Record<string, string | string[] | undefined> = {};
|
||||
const headers: Record<string, HeaderValue> = {};
|
||||
Object.keys(behavior.getHeaders()).forEach((key) => {
|
||||
if (key.toLowerCase().startsWith('x-fal-')) {
|
||||
headers[key.toLowerCase()] = behavior.getHeader(key);
|
||||
}
|
||||
});
|
||||
|
||||
const proxyUserAgent = `@fal-ai/serverless-proxy/${behavior.id}`;
|
||||
const userAgent = singleHeaderValue(behavior.getHeader('user-agent'));
|
||||
const res = await fetch(targetUrl, {
|
||||
method: behavior.method,
|
||||
headers: {
|
||||
@ -94,22 +97,26 @@ export const handleRequest = async (behavior: ProxyBehavior) => {
|
||||
`Key ${falKey}`,
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'x-fal-client-proxy': `@fal-ai/serverless-proxy/${behavior.id}`,
|
||||
},
|
||||
'user-agent': userAgent,
|
||||
'x-fal-client-proxy': proxyUserAgent,
|
||||
} as HeadersInit,
|
||||
body:
|
||||
behavior.method?.toUpperCase() === 'GET' ? undefined : behavior.getBody(),
|
||||
behavior.method?.toUpperCase() === 'GET'
|
||||
? undefined
|
||||
: await behavior.getBody(),
|
||||
});
|
||||
|
||||
// copy headers from res to response
|
||||
// copy headers from fal to the proxied response
|
||||
res.headers.forEach((value, key) => {
|
||||
behavior.sendHeader(key, value);
|
||||
if (!EXCLUDED_HEADERS.includes(key.toLowerCase())) {
|
||||
behavior.sendHeader(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
if (res.headers.get('content-type').includes('application/json')) {
|
||||
if (res.headers.get('content-type')?.includes('application/json')) {
|
||||
const data = await res.json();
|
||||
behavior.respondWith(res.status, data);
|
||||
return;
|
||||
return behavior.respondWith(res.status, data);
|
||||
}
|
||||
const data = await res.text();
|
||||
behavior.respondWith(res.status, data);
|
||||
};
|
||||
return behavior.respondWith(res.status, data);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { NextApiHandler } from 'next/types';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { DEFAULT_PROXY_ROUTE, handleRequest } from './index';
|
||||
|
||||
/**
|
||||
@ -8,6 +9,7 @@ export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE;
|
||||
|
||||
/**
|
||||
* The Next API route handler for the fal.ai client proxy.
|
||||
* Use it with the /pages router in Next.js.
|
||||
*
|
||||
* @param request the Next API request object.
|
||||
* @param response the Next API response object.
|
||||
@ -15,8 +17,8 @@ export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE;
|
||||
*/
|
||||
export const handler: NextApiHandler = async (request, response) => {
|
||||
return handleRequest({
|
||||
id: 'nextjs',
|
||||
method: request.method,
|
||||
id: 'nextjs-page-router',
|
||||
method: request.method || 'POST',
|
||||
respondWith: (status, data) =>
|
||||
typeof data === 'string'
|
||||
? response.status(status).json({ detail: data })
|
||||
@ -24,6 +26,46 @@ export const handler: NextApiHandler = async (request, response) => {
|
||||
getHeaders: () => request.headers,
|
||||
getHeader: (name) => request.headers[name],
|
||||
sendHeader: (name, value) => response.setHeader(name, value),
|
||||
getBody: () => JSON.stringify(request.body),
|
||||
getBody: async () => JSON.stringify(request.body),
|
||||
});
|
||||
};
|
||||
|
||||
function fromHeaders(headers: Headers): Record<string, string | string[]> {
|
||||
// TODO once Header.entries() is available, use that instead
|
||||
// Object.fromEntries(headers.entries());
|
||||
const result: Record<string, string | string[]> = {};
|
||||
headers.forEach((value, key) => {
|
||||
result[key] = value;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Next API route handler for the fal.ai client proxy on App Router apps.
|
||||
*
|
||||
* @param request the Next API request object.
|
||||
* @returns a promise that resolves when the request is handled.
|
||||
*/
|
||||
async function routeHandler(request: NextRequest) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const responseHeaders: Record<string, any> = {};
|
||||
return await handleRequest({
|
||||
id: 'nextjs-app-router',
|
||||
method: request.method,
|
||||
respondWith: (status, data) =>
|
||||
NextResponse.json(typeof data === 'string' ? { detail: data } : data, {
|
||||
status,
|
||||
headers: responseHeaders,
|
||||
}),
|
||||
getHeaders: () => fromHeaders(request.headers),
|
||||
getHeader: (name) => request.headers.get(name),
|
||||
sendHeader: (name, value) => (responseHeaders[name] = value),
|
||||
getBody: async () => request.text(),
|
||||
});
|
||||
}
|
||||
|
||||
export const route = {
|
||||
handler: routeHandler,
|
||||
GET: routeHandler,
|
||||
POST: routeHandler,
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"module": "commonjs",
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
|
||||
2
nx.json
2
nx.json
@ -50,5 +50,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "demo-nextjs-app"
|
||||
"defaultProject": "demo-nextjs-page-router"
|
||||
}
|
||||
|
||||
36652
package-lock.json
generated
36652
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -25,7 +25,7 @@
|
||||
"http-proxy": "^1.18.1",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"js-base64": "^3.7.5",
|
||||
"next": "13.3.0",
|
||||
"next": "13.4.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"regenerator-runtime": "0.13.7",
|
||||
@ -42,7 +42,7 @@
|
||||
"@nx/jest": "16.10.0",
|
||||
"@nx/js": "16.10.0",
|
||||
"@nx/linter": "16.10.0",
|
||||
"@nx/next": "16.10.0",
|
||||
"@nx/next": "^16.10.0",
|
||||
"@nx/node": "16.10.0",
|
||||
"@nx/react": "16.10.0",
|
||||
"@nx/web": "16.10.0",
|
||||
@ -56,15 +56,15 @@
|
||||
"@types/express": "4.17.13",
|
||||
"@types/jest": "29.4.4",
|
||||
"@types/node": "18.14.2",
|
||||
"@types/react": "18.2.14",
|
||||
"@types/react-dom": "18.2.6",
|
||||
"@types/react": "18.2.24",
|
||||
"@types/react-dom": "18.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"autoprefixer": "10.4.13",
|
||||
"babel-jest": "29.4.3",
|
||||
"cypress": "^11.0.0",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "^13.1.1",
|
||||
"eslint-config-next": "13.4.1",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-cypress": "2.15.1",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user