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
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 */
|
/* eslint-disable */
|
||||||
export default {
|
export default {
|
||||||
displayName: 'demo-nextjs-app',
|
displayName: 'demo-nextjs-app-router',
|
||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
transform: {
|
transform: {
|
||||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
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
|
// @snippet:end
|
||||||
|
|
||||||
function Error(props) {
|
type ErrorProps = {
|
||||||
|
error: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Error(props: ErrorProps) {
|
||||||
if (!props.error) {
|
if (!props.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -64,8 +68,7 @@ export function Index() {
|
|||||||
setElapsedTime(0);
|
setElapsedTime(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnClick = async (e) => {
|
const generateImage = async () => {
|
||||||
e.preventDefault();
|
|
||||||
reset();
|
reset();
|
||||||
// @snippet:start("client.queue.subscribe")
|
// @snippet:start("client.queue.subscribe")
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -85,12 +88,12 @@ export function Index() {
|
|||||||
update.status === 'IN_PROGRESS' ||
|
update.status === 'IN_PROGRESS' ||
|
||||||
update.status === 'COMPLETED'
|
update.status === 'COMPLETED'
|
||||||
) {
|
) {
|
||||||
setLogs(update.logs.map((log) => log.message));
|
setLogs((update.logs || []).map((log) => log.message));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setResult(result);
|
setResult(result);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
setError(error);
|
setError(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -121,7 +124,10 @@ export function Index() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<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"
|
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}
|
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",
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
"sourceRoot": "apps/demo-nextjs-app",
|
"sourceRoot": "apps/demo-nextjs-page-router",
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"targets": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
@ -9,11 +9,11 @@
|
|||||||
"outputs": ["{options.outputPath}"],
|
"outputs": ["{options.outputPath}"],
|
||||||
"defaultConfiguration": "production",
|
"defaultConfiguration": "production",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/apps/demo-nextjs-app"
|
"outputPath": "dist/apps/demo-nextjs-page-router"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"outputPath": "apps/demo-nextjs-app"
|
"outputPath": "apps/demo-nextjs-page-router"
|
||||||
},
|
},
|
||||||
"production": {}
|
"production": {}
|
||||||
}
|
}
|
||||||
@ -22,16 +22,16 @@
|
|||||||
"executor": "@nx/next:server",
|
"executor": "@nx/next:server",
|
||||||
"defaultConfiguration": "development",
|
"defaultConfiguration": "development",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "demo-nextjs-app:build",
|
"buildTarget": "demo-nextjs-page-router:build",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "demo-nextjs-app:build:development",
|
"buildTarget": "demo-nextjs-page-router:build:development",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "demo-nextjs-app:build:production",
|
"buildTarget": "demo-nextjs-page-router:build:production",
|
||||||
"dev": false
|
"dev": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,14 +39,14 @@
|
|||||||
"export": {
|
"export": {
|
||||||
"executor": "@nx/next:export",
|
"executor": "@nx/next:export",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "demo-nextjs-app:build:production"
|
"buildTarget": "demo-nextjs-page-router:build:production"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "@nx/jest:jest",
|
"executor": "@nx/jest:jest",
|
||||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "apps/demo-nextjs-app/jest.config.ts",
|
"jestConfig": "apps/demo-nextjs-page-router/jest.config.ts",
|
||||||
"passWithNoTests": true
|
"passWithNoTests": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -54,7 +54,9 @@
|
|||||||
"executor": "@nx/linter:eslint",
|
"executor": "@nx/linter:eslint",
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
"options": {
|
"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",
|
"name": "@fal-ai/serverless-client",
|
||||||
"description": "The fal serverless JS/TS client",
|
"description": "The fal serverless JS/TS client",
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -41,10 +41,11 @@ export function buildUrl<Input>(
|
|||||||
const { host } = getConfig();
|
const { host } = getConfig();
|
||||||
const method = (options.method ?? 'post').toLowerCase();
|
const method = (options.method ?? 'post').toLowerCase();
|
||||||
const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/');
|
const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/');
|
||||||
|
const input = options.input;
|
||||||
const params =
|
const params =
|
||||||
method === 'get' ? new URLSearchParams(options.input ?? {}) : undefined;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
// TODO: change to params.size once it's officially supported
|
method === 'get' && input ? new URLSearchParams(input as any) : undefined;
|
||||||
const queryParams = params && params['size'] ? `?${params.toString()}` : '';
|
const queryParams = params ? `?${params.toString()}` : '';
|
||||||
const parts = id.split('/');
|
const parts = id.split('/');
|
||||||
|
|
||||||
// if a fal.ai url is passed, just use it
|
// if a fal.ai url is passed, just use it
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fal-ai/serverless-proxy",
|
"name": "@fal-ai/serverless-proxy",
|
||||||
"version": "0.3.5",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const handler: RequestHandler = async (request, response, next) => {
|
|||||||
getHeaders: () => request.headers,
|
getHeaders: () => request.headers,
|
||||||
getHeader: (name) => request.headers[name],
|
getHeader: (name) => request.headers[name],
|
||||||
sendHeader: (name, value) => response.setHeader(name, value),
|
sendHeader: (name, value) => response.setHeader(name, value),
|
||||||
getBody: () => JSON.stringify(request.body),
|
getBody: async () => JSON.stringify(request.body),
|
||||||
});
|
});
|
||||||
next();
|
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 =
|
const FAL_KEY_SECRET =
|
||||||
process.env.FAL_KEY_SECRET || process.env.NEXT_PUBLIC_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
|
* 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.
|
* request objects that are used by different frameworks, like Express and NextJS.
|
||||||
*/
|
*/
|
||||||
export interface ProxyBehavior {
|
export interface ProxyBehavior<ResponseType> {
|
||||||
id: string;
|
id: string;
|
||||||
method: string;
|
method: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
respondWith(status: number, data: string | any): void;
|
respondWith(status: number, data: string | any): ResponseType;
|
||||||
getHeaders(): Record<string, string | string[] | undefined>;
|
getHeaders(): Record<string, HeaderValue>;
|
||||||
getHeader(name: string): string | string[] | undefined;
|
getHeader(name: string): HeaderValue;
|
||||||
sendHeader(name: string, value: string): void;
|
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.
|
* @param request the header value.
|
||||||
* @returns the header value as `string` or `undefined` if the header is not set.
|
* @returns the header value as `string` or `undefined` if the header is not set.
|
||||||
*/
|
*/
|
||||||
function singleHeaderValue(
|
function singleHeaderValue(value: HeaderValue): string | undefined {
|
||||||
value: string | string[] | undefined
|
if (!value) {
|
||||||
): string | undefined {
|
|
||||||
if (value === undefined) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@ -51,6 +51,8 @@ function getFalKey(): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EXCLUDED_HEADERS = ['content-length'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request handler that proxies the request to the fal-serverless
|
* A request handler that proxies the request to the fal-serverless
|
||||||
* endpoint. This is useful so client-side calls to the fal-serverless endpoint
|
* 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.
|
* @param behavior the request proxy behavior.
|
||||||
* @returns Promise<any> the promise that will be resolved once the request is done.
|
* @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));
|
const targetUrl = singleHeaderValue(behavior.getHeader(TARGET_URL_HEADER));
|
||||||
if (!targetUrl) {
|
if (!targetUrl) {
|
||||||
behavior.respondWith(400, `Missing the ${TARGET_URL_HEADER} header`);
|
return behavior.respondWith(400, `Missing the ${TARGET_URL_HEADER} header`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (targetUrl.indexOf('fal.ai') === -1) {
|
if (targetUrl.indexOf('fal.ai') === -1) {
|
||||||
behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`);
|
return behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const falKey = getFalKey();
|
const falKey = getFalKey();
|
||||||
if (!falKey) {
|
if (!falKey) {
|
||||||
behavior.respondWith(401, 'Missing fal.ai credentials');
|
return behavior.respondWith(401, 'Missing fal.ai credentials');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass over headers prefixed with x-fal-*
|
// 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) => {
|
Object.keys(behavior.getHeaders()).forEach((key) => {
|
||||||
if (key.toLowerCase().startsWith('x-fal-')) {
|
if (key.toLowerCase().startsWith('x-fal-')) {
|
||||||
headers[key.toLowerCase()] = behavior.getHeader(key);
|
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, {
|
const res = await fetch(targetUrl, {
|
||||||
method: behavior.method,
|
method: behavior.method,
|
||||||
headers: {
|
headers: {
|
||||||
@ -94,22 +97,26 @@ export const handleRequest = async (behavior: ProxyBehavior) => {
|
|||||||
`Key ${falKey}`,
|
`Key ${falKey}`,
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
'content-type': '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:
|
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) => {
|
res.headers.forEach((value, key) => {
|
||||||
|
if (!EXCLUDED_HEADERS.includes(key.toLowerCase())) {
|
||||||
behavior.sendHeader(key, value);
|
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();
|
const data = await res.json();
|
||||||
behavior.respondWith(res.status, data);
|
return behavior.respondWith(res.status, data);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const data = await res.text();
|
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 { NextApiHandler } from 'next/types';
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
import { DEFAULT_PROXY_ROUTE, handleRequest } from './index';
|
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.
|
* 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 request the Next API request object.
|
||||||
* @param response the Next API response 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) => {
|
export const handler: NextApiHandler = async (request, response) => {
|
||||||
return handleRequest({
|
return handleRequest({
|
||||||
id: 'nextjs',
|
id: 'nextjs-page-router',
|
||||||
method: request.method,
|
method: request.method || 'POST',
|
||||||
respondWith: (status, data) =>
|
respondWith: (status, data) =>
|
||||||
typeof data === 'string'
|
typeof data === 'string'
|
||||||
? response.status(status).json({ detail: data })
|
? response.status(status).json({ detail: data })
|
||||||
@ -24,6 +26,46 @@ export const handler: NextApiHandler = async (request, response) => {
|
|||||||
getHeaders: () => request.headers,
|
getHeaders: () => request.headers,
|
||||||
getHeader: (name) => request.headers[name],
|
getHeader: (name) => request.headers[name],
|
||||||
sendHeader: (name, value) => response.setHeader(name, value),
|
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",
|
"module": "commonjs",
|
||||||
"outDir": "../../dist/out-tsc",
|
"outDir": "../../dist/out-tsc",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
36640
package-lock.json
generated
36640
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": "^1.18.1",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"next": "13.3.0",
|
"next": "13.4.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"regenerator-runtime": "0.13.7",
|
"regenerator-runtime": "0.13.7",
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"@nx/jest": "16.10.0",
|
"@nx/jest": "16.10.0",
|
||||||
"@nx/js": "16.10.0",
|
"@nx/js": "16.10.0",
|
||||||
"@nx/linter": "16.10.0",
|
"@nx/linter": "16.10.0",
|
||||||
"@nx/next": "16.10.0",
|
"@nx/next": "^16.10.0",
|
||||||
"@nx/node": "16.10.0",
|
"@nx/node": "16.10.0",
|
||||||
"@nx/react": "16.10.0",
|
"@nx/react": "16.10.0",
|
||||||
"@nx/web": "16.10.0",
|
"@nx/web": "16.10.0",
|
||||||
@ -56,15 +56,15 @@
|
|||||||
"@types/express": "4.17.13",
|
"@types/express": "4.17.13",
|
||||||
"@types/jest": "29.4.4",
|
"@types/jest": "29.4.4",
|
||||||
"@types/node": "18.14.2",
|
"@types/node": "18.14.2",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.24",
|
||||||
"@types/react-dom": "18.2.6",
|
"@types/react-dom": "18.2.9",
|
||||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||||
"@typescript-eslint/parser": "5.62.0",
|
"@typescript-eslint/parser": "5.62.0",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.13",
|
||||||
"babel-jest": "29.4.3",
|
"babel-jest": "29.4.3",
|
||||||
"cypress": "^11.0.0",
|
"cypress": "^11.0.0",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
"eslint-config-next": "^13.1.1",
|
"eslint-config-next": "13.4.1",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"eslint-plugin-cypress": "2.15.1",
|
"eslint-plugin-cypress": "2.15.1",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user