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

Next.js integration: run Toolpad apps as React server components #3125

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions examples/custom-server-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from 'react';
import { ToolpadApp } from '@mui/toolpad/next';

export default function ToolpadPage() {
return <ToolpadApp base="/my-next-app/my-toolpad" dir="./toolpad" />;
}
1 change: 1 addition & 0 deletions examples/custom-server-nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
8 changes: 8 additions & 0 deletions examples/custom-server-nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
/** @type {import('next').NextConfig} */
export default {
webpack: (config) => {
config.externals.push({
fsevents: 'commonjs fsevents',
chokidar: 'commonjs chokidar',
});
config.resolve.alias['@mui/icons-material'] = ['@mui/icons-material/esm'];
return config;
},
basePath: '/my-next-app',
};
3 changes: 2 additions & 1 deletion examples/custom-server-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
},
"dependencies": {
"@mui/toolpad": "0.1.46",
"chokidar": "3.5.3",
"express": "4.18.2",
"next": "14.0.2",
"next": "14.1.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-server-nextjs/toolpad/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
apiVersion: v1
kind: application
spec: {}
14 changes: 13 additions & 1 deletion examples/custom-server-nextjs/toolpad/pages/page/page.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.40/docs/schemas/v1/definitions.json#properties/Page
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.46/docs/schemas/v1/definitions.json#properties/Page

apiVersion: v1
kind: page
Expand All @@ -16,3 +16,15 @@ spec:
mode: link
value: To the Next.js app
href: /my-next-app
- component: Text
name: text1
props:
mode: null
value:
$$jsExpression: query.data
queries:
- name: query
mode: query
query:
function: functions2.ts#default
kind: local
7 changes: 7 additions & 0 deletions examples/custom-server-nextjs/toolpad/resources/functions2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Toolpad handlers file.
*/

export default async function handler(message: string) {
return `Hello ${message}`;
}
10 changes: 8 additions & 2 deletions examples/custom-server-nextjs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
18 changes: 18 additions & 0 deletions packages/toolpad-app/src/exports/next-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import * as React from 'react';
import ToolpadApp, { ToolpadAppProps } from '../runtime/ToolpadApp';
import { AppHostContext, AppHost } from '../runtime/AppHostContext';

const appContext: AppHost = {
isPreview: false,
isCustomServer: true,
};

export default function ToolpadAppClient(props: ToolpadAppProps) {
return (
<AppHostContext.Provider value={appContext}>
<ToolpadApp {...props} />
</AppHostContext.Provider>
);
}
18 changes: 18 additions & 0 deletions packages/toolpad-app/src/exports/next.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use server';

import * as React from 'react';
import ToolpadAppClient from './next-client';
import { loadDom } from '../server/localMode';
import createRuntimeState from '../runtime/createRuntimeState';

export interface ToolpadAppServerProps {
base: string;
dir: string;
}

export async function ToolpadApp({ base, dir = './toolpad' }: ToolpadAppServerProps) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the app decide which page to run? If it's the URL path concerning base, is it possible to abstract the URL from the app and modify the props as follows?

export interface ToolpadAppServerProps {
  dir: string;
  page: string;
}

It could help us dynamically render any page without worrying about the paths, giving user more control IMO. WDYT?

Copy link
Member Author

@Janpot Janpot Feb 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that makes sense. This is somewhat on hold because it depends on a bunch of low-level work that needs to happen first. We'll also have to abstract Toolpad routing to be able to plug in the Next.js router.

Another option I'm thinking of is to expose another component that renders a single page instead of the whole app.

Copy link

@buremba buremba Feb 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option I'm thinking of is to expose another component that renders a single page instead of the whole app.

Yes, that would be even more useful for my use case where I have a backend to pull the MDX file and I can transform it on the fly render the page without even shell. If we can pass components and resources as well, the applications can utilize Toolpad's application model to render the generated pages in our app.

const dom = await loadDom(dir);
const initialState = createRuntimeState({ dom });

return <ToolpadAppClient basename={base} state={initialState} />;
}
2 changes: 2 additions & 0 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import * as React from 'react';
import {
Stack,
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default defineConfig((options) => [
tsconfig: './tsconfig.runtime.json',
sourcemap: true,
esbuildPlugins: [cleanFolderOnFailure(path.resolve(__dirname, 'dist/runtime'))],
external: [/.*\.(svg|png|jpe?g|gif|ico|webp)$/],
external: [/.*\.(svg|png|jpe?g|gif|ico|webp)$/, './next-client'],
async onSuccess() {
// eslint-disable-next-line no-console
console.log('runtime: build successful');
Expand Down
Loading
Loading