Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: next proxy app router support #22

Merged
merged 13 commits into from
Oct 30, 2023
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ Thumbs.db

# Next.js
.next
*.local
*.local
40 changes: 40 additions & 0 deletions apps/demo-nextjs-app-router/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { route } from '@fal-ai/serverless-proxy/nextjs';

export const { GET, POST } = route;
18 changes: 18 additions & 0 deletions apps/demo-nextjs-app-router/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 170 additions & 0 deletions apps/demo-nextjs-app-router/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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;

Check warning on line 27 in apps/demo-nextjs-app-router/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
};

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) {

Check warning on line 98 in apps/demo-nextjs-app-router/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
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>
);
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions apps/demo-nextjs-app-router/next.config.js
Original file line number Diff line number Diff line change
@@ -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 changes: 68 additions & 0 deletions apps/demo-nextjs-app-router/project.json
Original file line number Diff line number Diff line change
@@ -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": []
}
Binary file added apps/demo-nextjs-app-router/public/favicon.ico
Binary file not shown.
18 changes: 18 additions & 0 deletions apps/demo-nextjs-app-router/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -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: [],
};
Loading
Loading