From 178faaa7385342134d5f1e6515e1eeda637c34ef Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 18 Dec 2024 17:55:39 +0530 Subject: [PATCH 01/13] wip: Add `frameworkOption` "vite" --- .../create-toolpad-app/src/generateProject.ts | 198 ++++++++++++------ packages/create-toolpad-app/src/index.ts | 32 ++- .../src/templates/{ => nextjs}/auth/auth.ts | 2 +- .../src/templates/{ => nextjs}/auth/env.ts | 0 .../templates/{ => nextjs}/auth/envLocal.ts | 2 +- .../templates/{ => nextjs}/auth/middleware.ts | 0 .../{ => nextjs}/auth/nextjs-app/actions.ts | 2 +- .../auth/nextjs-app/signInPage.ts | 2 +- .../{ => nextjs}/auth/nextjs-pages/signIn.ts | 2 +- .../src/templates/{ => nextjs}/auth/prisma.ts | 0 .../src/templates/{ => nextjs}/auth/route.ts | 0 .../{ => nextjs}/auth/schemaPrisma.ts | 2 +- .../src/templates/{ => nextjs}/auth/utils.ts | 0 .../nextjs-app/dashboardLayout.ts | 0 .../{ => nextjs}/nextjs-app/rootLayout.ts | 0 .../{ => nextjs}/nextjs-pages/app.ts | 0 .../{ => nextjs}/nextjs-pages/document.ts | 0 packages/create-toolpad-app/src/types.ts | 3 + 18 files changed, 167 insertions(+), 78 deletions(-) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/auth.ts (99%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/env.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/envLocal.ts (96%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/middleware.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/nextjs-app/actions.ts (96%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/nextjs-app/signInPage.ts (97%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/nextjs-pages/signIn.ts (98%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/prisma.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/route.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/schemaPrisma.ts (97%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/auth/utils.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/nextjs-app/dashboardLayout.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/nextjs-app/rootLayout.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/nextjs-pages/app.ts (100%) rename packages/create-toolpad-app/src/templates/{ => nextjs}/nextjs-pages/document.ts (100%) diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts index 2fa766bdd23..16167a34a53 100644 --- a/packages/create-toolpad-app/src/generateProject.ts +++ b/packages/create-toolpad-app/src/generateProject.ts @@ -10,42 +10,45 @@ import ordersPage from './templates/ordersPage'; import packageJson from './templates/packageJson'; import indexPage from './templates/indexPage'; +// Vite specific files +import viteApp from './templates/vite/App'; +import viteConfig from './templates/vite/viteConfig'; +import viteMain from './templates/vite/main'; +import viteHtml from './templates/vite/html'; +import viteSignIn from './templates/vite/auth/signIn'; + // App router specific files -import rootLayout from './templates/nextjs-app/rootLayout'; -import dashboardLayout from './templates/nextjs-app/dashboardLayout'; +import rootLayout from './templates/nextjs/nextjs-app/rootLayout'; +import dashboardLayout from './templates/nextjs/nextjs-app/dashboardLayout'; // Pages router specific files -import app from './templates/nextjs-pages/app'; -import document from './templates/nextjs-pages/document'; +import app from './templates/nextjs/nextjs-pages/app'; +import document from './templates/nextjs/nextjs-pages/document'; // Auth specific files for all apps -import auth from './templates/auth/auth'; -import envLocal from './templates/auth/envLocal'; -import middleware from './templates/auth/middleware'; -import routeHandler from './templates/auth/route'; -import prisma from './templates/auth/prisma'; -import env from './templates/auth/env'; -import schemaPrisma from './templates/auth/schemaPrisma'; +import auth from './templates/nextjs/auth/auth'; +import envLocal from './templates/nextjs/auth/envLocal'; +import middleware from './templates/nextjs/auth/middleware'; +import routeHandler from './templates/nextjs/auth/route'; +import prisma from './templates/nextjs/auth/prisma'; +import env from './templates/nextjs/auth/env'; +import schemaPrisma from './templates/nextjs/auth/schemaPrisma'; // Auth files for app router -import signInPage from './templates/auth/nextjs-app/signInPage'; -import signInAction from './templates/auth/nextjs-app/actions'; +import signInPage from './templates/nextjs/auth/nextjs-app/signInPage'; +import signInAction from './templates/nextjs/auth/nextjs-app/actions'; // Auth files for pages router -import signInPagePagesRouter from './templates/auth/nextjs-pages/signIn'; +import signInPagePagesRouter from './templates/nextjs/auth/nextjs-pages/signIn'; import { GenerateProjectOptions } from './types'; export default function generateProject( options: GenerateProjectOptions, ): Map { - // Add app name to package.json - - // Default files, common to all apps - const files = new Map([ + // Common files regardless of framework + const commonFiles = new Map([ ['theme.ts', { content: theme }], - ['next-env.d.ts', { content: nextTypes }], - ['next.config.mjs', { content: nextConfig(options) }], ['.eslintrc.json', { content: eslintConfig }], ['tsconfig.json', { content: tsConfig }], ['README.md', { content: readme }], @@ -57,72 +60,135 @@ export default function generateProject( }, ], ]); - const indexPageContent = indexPage(options); - - switch (options.router) { - case 'nextjs-pages': { - const nextJsPagesRouterStarter = new Map([ - ['pages/index.tsx', { content: indexPageContent }], - ['pages/orders/index.tsx', { content: ordersPage(options) }], - ['pages/_document.tsx', { content: document }], - ['pages/_app.tsx', { content: app(options) }], + + switch (options.framework) { + case 'vite': { + const viteFiles = new Map([ + ['vite.config.ts', { content: viteConfig }], + ['src/main.tsx', { content: viteMain }], + ['src/App.tsx', { content: viteApp(options) }], + ['src/pages/index.tsx', { content: indexPage(options) }], + ['src/pages/orders.tsx', { content: ordersPage(options) }], + ['index.html', { content: viteHtml }], ]); + if (options.auth) { const authFiles = new Map([ - ['auth.ts', { content: auth(options) }], + ['src/auth.ts', { content: auth(options) }], ['.env.local', { content: envLocal(options) }], - ['middleware.ts', { content: middleware }], - // next-auth v5 does not provide an API route, so this file must be in the app router - // even if the rest of the app is using pages router - // https://authjs.dev/getting-started/installation#configure - ['app/api/auth/[...nextAuth]/route.ts', { content: routeHandler }], - ['pages/auth/signin.tsx', { content: signInPagePagesRouter(options) }], + ['src/pages/auth/signin.tsx', { content: viteSignIn(options) }], ]); + if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - // Prisma adapter support requires removal of middleware - authFiles.delete('middleware.ts'); const prismaFiles = new Map([ ['prisma.ts', { content: prisma }], ['.env', { content: env }], ['prisma/schema.prisma', { content: schemaPrisma(options) }], ]); - return new Map([...files, ...nextJsPagesRouterStarter, ...authFiles, ...prismaFiles]); + return new Map([...commonFiles, ...viteFiles, ...authFiles, ...prismaFiles]); } - return new Map([...files, ...nextJsPagesRouterStarter, ...authFiles]); + + return new Map([...commonFiles, ...viteFiles, ...authFiles]); } - return new Map([...files, ...nextJsPagesRouterStarter]); + + return new Map([...commonFiles, ...viteFiles]); } - case 'nextjs-app': + + case 'nextjs': default: { - const nextJsAppRouterStarter = new Map([ - ['app/(dashboard)/layout.tsx', { content: dashboardLayout }], - ['app/layout.tsx', { content: rootLayout(options) }], - ['app/(dashboard)/page.tsx', { content: indexPageContent }], - ['app/(dashboard)/orders/page.tsx', { content: ordersPage(options) }], + const nextCommonFiles = new Map([ + ['next-env.d.ts', { content: nextTypes }], + ['next.config.mjs', { content: nextConfig(options) }], ]); - if (options.auth) { - const authFiles = new Map([ - ['auth.ts', { content: auth(options) }], - ['.env.local', { content: envLocal(options) }], - ['middleware.ts', { content: middleware }], - ['app/api/auth/[...nextAuth]/route.ts', { content: routeHandler }], - ['app/auth/signin/page.tsx', { content: signInPage(options) }], - ['app/auth/signin/actions.ts', { content: signInAction(options) }], - ]); - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - // Prisma adapater support requires removal of middleware - authFiles.delete('middleware.ts'); - const prismaFiles = new Map([ - ['prisma.ts', { content: prisma }], - ['.env', { content: env }], - ['prisma/schema.prisma', { content: schemaPrisma(options) }], + + switch (options.router) { + case 'nextjs-pages': { + const nextJsPagesRouterStarter = new Map([ + ['pages/index.tsx', { content: indexPage(options) }], + ['pages/orders/index.tsx', { content: ordersPage(options) }], + ['pages/_document.tsx', { content: document }], + ['pages/_app.tsx', { content: app(options) }], ]); - return new Map([...files, ...nextJsAppRouterStarter, ...authFiles, ...prismaFiles]); + if (options.auth) { + const authFiles = new Map([ + ['auth.ts', { content: auth(options) }], + ['.env.local', { content: envLocal(options) }], + ['middleware.ts', { content: middleware }], + // next-auth v5 does not provide an API route, so this file must be in the app router + // even if the rest of the app is using pages router + // https://authjs.dev/getting-started/installation#configure + ['app/api/auth/[...nextAuth]/route.ts', { content: routeHandler }], + ['pages/auth/signin.tsx', { content: signInPagePagesRouter(options) }], + ]); + if (options.hasNodemailerProvider || options.hasPasskeyProvider) { + // Prisma adapter support requires removal of middleware + authFiles.delete('middleware.ts'); + const prismaFiles = new Map([ + ['prisma.ts', { content: prisma }], + ['.env', { content: env }], + ['prisma/schema.prisma', { content: schemaPrisma(options) }], + ]); + return new Map([ + ...commonFiles, + ...nextCommonFiles, + ...nextJsPagesRouterStarter, + ...authFiles, + ...prismaFiles, + ]); + } + return new Map([ + ...commonFiles, + ...nextCommonFiles, + ...nextJsPagesRouterStarter, + ...authFiles, + ]); + } + return new Map([...commonFiles, ...nextCommonFiles, ...nextJsPagesRouterStarter]); } + case 'nextjs-app': + default: { + const nextJsAppRouterStarter = new Map([ + ['app/(dashboard)/layout.tsx', { content: dashboardLayout }], + ['app/layout.tsx', { content: rootLayout(options) }], + ['app/(dashboard)/page.tsx', { content: indexPage(options) }], + ['app/(dashboard)/orders/page.tsx', { content: ordersPage(options) }], + ]); + if (options.auth) { + const authFiles = new Map([ + ['auth.ts', { content: auth(options) }], + ['.env.local', { content: envLocal(options) }], + ['middleware.ts', { content: middleware }], + ['app/api/auth/[...nextAuth]/route.ts', { content: routeHandler }], + ['app/auth/signin/page.tsx', { content: signInPage(options) }], + ['app/auth/signin/actions.ts', { content: signInAction(options) }], + ]); + if (options.hasNodemailerProvider || options.hasPasskeyProvider) { + // Prisma adapater support requires removal of middleware + authFiles.delete('middleware.ts'); + const prismaFiles = new Map([ + ['prisma.ts', { content: prisma }], + ['.env', { content: env }], + ['prisma/schema.prisma', { content: schemaPrisma(options) }], + ]); + return new Map([ + ...commonFiles, + ...nextCommonFiles, + ...nextJsAppRouterStarter, + ...authFiles, + ...prismaFiles, + ]); + } - return new Map([...files, ...nextJsAppRouterStarter, ...authFiles]); + return new Map([ + ...commonFiles, + ...nextCommonFiles, + ...nextJsAppRouterStarter, + ...authFiles, + ]); + } + return new Map([...commonFiles, ...nextCommonFiles, ...nextJsAppRouterStarter]); + } } - return new Map([...files, ...nextJsAppRouterStarter]); } } } diff --git a/packages/create-toolpad-app/src/index.ts b/packages/create-toolpad-app/src/index.ts index 632570eb7ba..27e1acd7a37 100644 --- a/packages/create-toolpad-app/src/index.ts +++ b/packages/create-toolpad-app/src/index.ts @@ -18,7 +18,12 @@ import generateStudioProject from './generateStudioProject'; import writeFiles from './writeFiles'; import { downloadAndExtractExample } from './examples'; import type { PackageJson } from './templates/packageType'; -import type { SupportedRouter, PackageManager, GenerateProjectOptions } from './types'; +import type { + SupportedFramework, + SupportedRouter, + PackageManager, + GenerateProjectOptions, +} from './types'; /** * Find package.json of the create-toolpad-app package @@ -294,14 +299,28 @@ const run = async () => { await scaffoldStudioProject(absolutePath, installFlag); } else { // Otherwise, create a new project with Toolpad Core - const routerOption: SupportedRouter = await select({ - message: 'Which router would you like to use?', - default: 'nextjs-app', + const frameworkOption: SupportedFramework = await select({ + message: 'Which framework would you like to use?', + default: 'nextjs', choices: [ - { name: 'Next.js App Router', value: 'nextjs-app' }, - { name: 'Next.js Pages Router', value: 'nextjs-pages' }, + { name: 'Next.js', value: 'nextjs' }, + { name: 'Vite', value: 'vite' }, ], }); + + let routerOption: SupportedRouter | undefined; + + if (frameworkOption === 'nextjs') { + routerOption = await select({ + message: 'Which router would you like to use?', + default: 'nextjs-app', + choices: [ + { name: 'Next.js App Router', value: 'nextjs-app' }, + { name: 'Next.js Pages Router', value: 'nextjs-pages' }, + ], + }); + } + const authFlag = await confirm({ message: 'Would you like to enable authentication?', default: true, @@ -345,6 +364,7 @@ const run = async () => { absolutePath, coreVersion: args.coreVersion, router: routerOption, + framework: frameworkOption, auth: authFlag, install: installFlag, authProviders: authProviderOptions, diff --git a/packages/create-toolpad-app/src/templates/auth/auth.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts similarity index 99% rename from packages/create-toolpad-app/src/templates/auth/auth.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts index 421365a9348..79f71489c87 100644 --- a/packages/create-toolpad-app/src/templates/auth/auth.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts @@ -1,7 +1,7 @@ import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; import { kebabToConstant, kebabToPascal } from '@toolpad/utils/strings'; import { requiresIssuer, requiresTenantId } from './utils'; -import { Template } from '../../types'; +import { Template } from '../../../types'; const CredentialsProviderTemplate = `Credentials({ credentials: { diff --git a/packages/create-toolpad-app/src/templates/auth/env.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/env.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/auth/env.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/env.ts diff --git a/packages/create-toolpad-app/src/templates/auth/envLocal.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts similarity index 96% rename from packages/create-toolpad-app/src/templates/auth/envLocal.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts index fec3ca8065e..befc0072fb1 100644 --- a/packages/create-toolpad-app/src/templates/auth/envLocal.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts @@ -1,6 +1,6 @@ import { kebabToConstant } from '@toolpad/utils/strings'; import { requiresIssuer, requiresTenantId } from './utils'; -import { Template } from '../../types'; +import { Template } from '../../../types'; const env: Template = (options) => { const { authProviders: providers } = options; diff --git a/packages/create-toolpad-app/src/templates/auth/middleware.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/middleware.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/auth/middleware.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/middleware.ts diff --git a/packages/create-toolpad-app/src/templates/auth/nextjs-app/actions.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts similarity index 96% rename from packages/create-toolpad-app/src/templates/auth/nextjs-app/actions.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts index 6d85ee1373f..6ed7e9b1e56 100644 --- a/packages/create-toolpad-app/src/templates/auth/nextjs-app/actions.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts @@ -1,4 +1,4 @@ -import { Template } from '../../../types'; +import { Template } from '../../../../types'; const actionsTemplate: Template = (options) => { const { hasCredentialsProvider, hasNodemailerProvider } = options; diff --git a/packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts similarity index 97% rename from packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts index a1d4b43d0c2..d30264a6fd2 100644 --- a/packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts @@ -1,4 +1,4 @@ -import { Template } from '../../../types'; +import { Template } from '../../../../types'; const signInPage: Template = (options) => { const { hasPasskeyProvider, hasNodemailerProvider } = options; diff --git a/packages/create-toolpad-app/src/templates/auth/nextjs-pages/signIn.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts similarity index 98% rename from packages/create-toolpad-app/src/templates/auth/nextjs-pages/signIn.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts index 716dfe99be3..8fe3c4aefdd 100644 --- a/packages/create-toolpad-app/src/templates/auth/nextjs-pages/signIn.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts @@ -1,4 +1,4 @@ -import { Template } from '../../../types'; +import { Template } from '../../../../types'; const signIn: Template = (options) => { const { hasCredentialsProvider, hasNodemailerProvider, hasPasskeyProvider } = options; diff --git a/packages/create-toolpad-app/src/templates/auth/prisma.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/prisma.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/auth/prisma.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/prisma.ts diff --git a/packages/create-toolpad-app/src/templates/auth/route.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/route.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/auth/route.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/route.ts diff --git a/packages/create-toolpad-app/src/templates/auth/schemaPrisma.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts similarity index 97% rename from packages/create-toolpad-app/src/templates/auth/schemaPrisma.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts index 609e74a527d..4fb2b066f13 100644 --- a/packages/create-toolpad-app/src/templates/auth/schemaPrisma.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts @@ -1,4 +1,4 @@ -import { Template } from '../../types'; +import { Template } from '../../../types'; const schemaPrisma: Template = (options) => ` diff --git a/packages/create-toolpad-app/src/templates/auth/utils.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/auth/utils.ts rename to packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts diff --git a/packages/create-toolpad-app/src/templates/nextjs-app/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/nextjs-app/dashboardLayout.ts rename to packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts diff --git a/packages/create-toolpad-app/src/templates/nextjs-app/rootLayout.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/nextjs-app/rootLayout.ts rename to packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts diff --git a/packages/create-toolpad-app/src/templates/nextjs-pages/app.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/nextjs-pages/app.ts rename to packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts diff --git a/packages/create-toolpad-app/src/templates/nextjs-pages/document.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/document.ts similarity index 100% rename from packages/create-toolpad-app/src/templates/nextjs-pages/document.ts rename to packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/document.ts diff --git a/packages/create-toolpad-app/src/types.ts b/packages/create-toolpad-app/src/types.ts index 0533f9aa433..9f49b9fde76 100644 --- a/packages/create-toolpad-app/src/types.ts +++ b/packages/create-toolpad-app/src/types.ts @@ -4,6 +4,8 @@ import { PackageJson } from './templates/packageType'; export type SupportedRouter = 'nextjs-app' | 'nextjs-pages'; export type PackageManager = 'npm' | 'pnpm' | 'yarn'; +export type SupportedFramework = 'nextjs' | 'vite'; + type ProjectType = 'core' | 'studio'; export interface GenerateProjectOptions { @@ -16,6 +18,7 @@ export interface GenerateProjectOptions { hasPasskeyProvider?: boolean; install?: boolean; router?: SupportedRouter; + framework?: SupportedFramework; coreVersion?: string; projectType?: ProjectType; packageManager?: PackageManager; From 3bd185dbcc1a1270c4796408accc5ca6e544ba92 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 18 Dec 2024 17:55:47 +0530 Subject: [PATCH 02/13] wip: Add vite files --- .../src/templates/vite/App.ts | 37 +++++++++++++++++++ .../src/templates/vite/html.ts | 12 ++++++ .../src/templates/vite/main.ts | 30 +++++++++++++++ .../src/templates/vite/viteConfig.ts | 12 ++++++ 4 files changed, 91 insertions(+) create mode 100644 packages/create-toolpad-app/src/templates/vite/App.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/html.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/main.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/viteConfig.ts diff --git a/packages/create-toolpad-app/src/templates/vite/App.ts b/packages/create-toolpad-app/src/templates/vite/App.ts new file mode 100644 index 00000000000..80c38041fcf --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/App.ts @@ -0,0 +1,37 @@ +import { GenerateProjectOptions } from '../../types'; + +export default function viteApp(options: GenerateProjectOptions) { + return `import * as React from 'react'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import { Outlet } from 'react-router-dom'; +import { AppProvider, type Navigation } from '@toolpad/core/react-router-dom'; + +const NAVIGATION: Navigation = [ + { + kind: 'header', + title: 'Main items', + }, + { + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, +]; + +const BRANDING = { + title: ${JSON.stringify(options.name || 'My Toolpad Core App')}, +}; + +export default function App() { + return ( + + + + ); +}`; +} diff --git a/packages/create-toolpad-app/src/templates/vite/html.ts b/packages/create-toolpad-app/src/templates/vite/html.ts new file mode 100644 index 00000000000..5ba589de985 --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/html.ts @@ -0,0 +1,12 @@ +export default ` + + + + + Toolpad Core App + + +
+ + +`; diff --git a/packages/create-toolpad-app/src/templates/vite/main.ts b/packages/create-toolpad-app/src/templates/vite/main.ts new file mode 100644 index 00000000000..4c56e8b94d0 --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/main.ts @@ -0,0 +1,30 @@ +export default `import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import App from './App'; +import Dashboard from './pages'; +import Orders from './pages/orders'; + +const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'orders', + element: , + }, + ], + }, +]); + +const root = createRoot(document.getElementById('root')!); +root.render( + + + , +);`; diff --git a/packages/create-toolpad-app/src/templates/vite/viteConfig.ts b/packages/create-toolpad-app/src/templates/vite/viteConfig.ts new file mode 100644 index 00000000000..4beb97313c7 --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/viteConfig.ts @@ -0,0 +1,12 @@ +export default `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': '/src', + }, + }, +});`; From becad6cc27a72f929ed2c3c56479a2a10c131c19 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 18 Dec 2024 21:56:02 +0530 Subject: [PATCH 03/13] feat: Add `frameworkOption` to CLI --- examples/core/firebase-vite/package.json | 8 -- .../create-toolpad-app/src/generateProject.ts | 30 ++--- packages/create-toolpad-app/src/index.ts | 58 +++++---- .../src/templates/indexPage.ts | 40 +++--- .../src/templates/packageJson.ts | 52 +++++--- .../src/templates/vite/App.ts | 74 ++++++++++- .../src/templates/vite/SessionContext.ts | 25 ++++ .../src/templates/vite/auth/env.ts | 8 ++ .../src/templates/vite/auth/firebase.ts | 116 ++++++++++++++++++ .../src/templates/vite/auth/firebaseConfig.ts | 34 +++++ .../src/templates/vite/auth/signin.ts | 101 +++++++++++++++ .../src/templates/vite/dashboardLayout.ts | 64 ++++++++++ .../src/templates/vite/main.ts | 50 +++++--- .../src/templates/vite/viteConfig.ts | 5 - pnpm-lock.yaml | 8 +- 15 files changed, 561 insertions(+), 112 deletions(-) create mode 100644 packages/create-toolpad-app/src/templates/vite/SessionContext.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/auth/env.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/auth/firebase.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/auth/signin.ts create mode 100644 packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts diff --git a/examples/core/firebase-vite/package.json b/examples/core/firebase-vite/package.json index 029bb677f2b..223e09388af 100644 --- a/examples/core/firebase-vite/package.json +++ b/examples/core/firebase-vite/package.json @@ -9,16 +9,8 @@ "dependencies": { "firebase": "^11", "@emotion/react": "^11", - "@fontsource-variable/inter": "^5.1.0", "@emotion/styled": "^11", "@mui/icons-material": "^6", - "@mui/x-charts": "^7", - "@mui/x-tree-view": "^7", - "@mui/x-data-grid": "^7", - "@mui/x-date-pickers": "^7", - "dayjs": "^1", - "clsx": "^2", - "@react-spring/web": "^9", "@mui/material": "^6", "@toolpad/core": "latest", "react": "^18", diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts index 16167a34a53..a04ccb93112 100644 --- a/packages/create-toolpad-app/src/generateProject.ts +++ b/packages/create-toolpad-app/src/generateProject.ts @@ -15,7 +15,15 @@ import viteApp from './templates/vite/App'; import viteConfig from './templates/vite/viteConfig'; import viteMain from './templates/vite/main'; import viteHtml from './templates/vite/html'; -import viteSignIn from './templates/vite/auth/signIn'; +import viteDashboardLayout from './templates/vite/dashboardLayout'; +// Vite Auth specific files +import viteSessionContext from './templates/vite/SessionContext'; +import viteSignIn from './templates/vite/auth/signin'; +import viteEnv from './templates/vite/auth/env'; +import viteFirebaseAuth from './templates/vite/auth/firebase'; +import viteFirebaseConfig from './templates/vite/auth/firebaseConfig'; + +// Nextjs specific files // App router specific files import rootLayout from './templates/nextjs/nextjs-app/rootLayout'; @@ -65,7 +73,8 @@ export default function generateProject( case 'vite': { const viteFiles = new Map([ ['vite.config.ts', { content: viteConfig }], - ['src/main.tsx', { content: viteMain }], + ['src/main.tsx', { content: viteMain(options) }], + ['src/layouts/dashboard.tsx', { content: viteDashboardLayout(options) }], ['src/App.tsx', { content: viteApp(options) }], ['src/pages/index.tsx', { content: indexPage(options) }], ['src/pages/orders.tsx', { content: ordersPage(options) }], @@ -74,20 +83,13 @@ export default function generateProject( if (options.auth) { const authFiles = new Map([ - ['src/auth.ts', { content: auth(options) }], - ['.env.local', { content: envLocal(options) }], - ['src/pages/auth/signin.tsx', { content: viteSignIn(options) }], + ['src/firebase/auth.ts', { content: viteFirebaseAuth(options) }], + ['src/firebase/firebaseConfig.ts', { content: viteFirebaseConfig(options) }], + ['src/SessionContext.ts', { content: viteSessionContext }], + ['.env', { content: viteEnv }], + ['src/pages/signin.tsx', { content: viteSignIn(options) }], ]); - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - const prismaFiles = new Map([ - ['prisma.ts', { content: prisma }], - ['.env', { content: env }], - ['prisma/schema.prisma', { content: schemaPrisma(options) }], - ]); - return new Map([...commonFiles, ...viteFiles, ...authFiles, ...prismaFiles]); - } - return new Map([...commonFiles, ...viteFiles, ...authFiles]); } diff --git a/packages/create-toolpad-app/src/index.ts b/packages/create-toolpad-app/src/index.ts index 27e1acd7a37..e7d1f578cde 100644 --- a/packages/create-toolpad-app/src/index.ts +++ b/packages/create-toolpad-app/src/index.ts @@ -330,35 +330,43 @@ const run = async () => { authProviderOptions = await checkbox({ message: 'Select authentication providers to enable:', required: true, - choices: [ - { name: 'Google', value: 'google' }, - { name: 'GitHub', value: 'github' }, - { name: 'Passkey', value: 'passkey' }, - { name: 'Magic Link', value: 'nodemailer' }, - { name: 'Credentials', value: 'credentials' }, - { name: 'GitLab', value: 'gitlab' }, - { name: 'Twitter', value: 'twitter' }, - { name: 'Facebook', value: 'facebook' }, - { name: 'Cognito', value: 'cognito' }, - { name: 'Microsoft Entra ID', value: 'microsoft-entra-id' }, - { name: 'Apple', value: 'apple' }, - { name: 'Instagram', value: 'instagram' }, - { name: 'TikTok', value: 'tiktok' }, - { name: 'LinkedIn', value: 'linkedin' }, - { name: 'Slack', value: 'slack' }, - { name: 'Spotify', value: 'spotify' }, - { name: 'Twitch', value: 'twitch' }, - { name: 'Discord', value: 'discord' }, - { name: 'Line', value: 'line' }, - { name: 'Auth0', value: 'auth0' }, - { name: 'Keycloak', value: 'keycloak' }, - { name: 'Okta', value: 'okta' }, - { name: 'FusionAuth', value: 'fusionauth' }, - ], + choices: + frameworkOption === 'nextjs' + ? [ + { name: 'Google', value: 'google' }, + { name: 'GitHub', value: 'github' }, + { name: 'Passkey', value: 'passkey' }, + { name: 'Magic Link', value: 'nodemailer' }, + { name: 'Credentials', value: 'credentials' }, + { name: 'GitLab', value: 'gitlab' }, + { name: 'Twitter', value: 'twitter' }, + { name: 'Facebook', value: 'facebook' }, + { name: 'Cognito', value: 'cognito' }, + { name: 'Microsoft Entra ID', value: 'microsoft-entra-id' }, + { name: 'Apple', value: 'apple' }, + { name: 'Instagram', value: 'instagram' }, + { name: 'TikTok', value: 'tiktok' }, + { name: 'LinkedIn', value: 'linkedin' }, + { name: 'Slack', value: 'slack' }, + { name: 'Spotify', value: 'spotify' }, + { name: 'Twitch', value: 'twitch' }, + { name: 'Discord', value: 'discord' }, + { name: 'Line', value: 'line' }, + { name: 'Auth0', value: 'auth0' }, + { name: 'Keycloak', value: 'keycloak' }, + { name: 'Okta', value: 'okta' }, + { name: 'FusionAuth', value: 'fusionauth' }, + ] + : [ + { name: 'Google', value: 'google' }, + { name: 'GitHub', value: 'github' }, + { name: 'Credentials', value: 'credentials' }, + ], }); hasNodemailerProvider = authProviderOptions?.includes('nodemailer'); hasPasskeyProvider = authProviderOptions?.includes('passkey'); } + const options = { name: path.basename(absolutePath), absolutePath, diff --git a/packages/create-toolpad-app/src/templates/indexPage.ts b/packages/create-toolpad-app/src/templates/indexPage.ts index 5283e0b0d93..5576feda920 100644 --- a/packages/create-toolpad-app/src/templates/indexPage.ts +++ b/packages/create-toolpad-app/src/templates/indexPage.ts @@ -9,30 +9,32 @@ import Typography from '@mui/material/Typography';`; let sessionHandling = ''; - let welcomeMessage = `Welcome to Toolpad Core!`; - - if (authEnabled) { - welcomeMessage = `Welcome to Toolpad, {session?.user?.name || ${options.hasNodemailerProvider || options.hasPasskeyProvider ? `session?.user?.email ||` : ''}'User'}!`; - if (routerType === 'nextjs-app') { - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - imports += `\nimport { redirect } from 'next/navigation';\nimport { headers } from 'next/headers';`; - } - imports += `\nimport { auth } from '../../auth';`; - sessionHandling = `const session = await auth();`; - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - sessionHandling += `\nconst currentUrl = (await headers()).get('referer') || process.env.AUTH_URL || 'http://localhost:3000'; + let welcomeMessage = `Welcome to Toolpad Core!`; + + if (options.framework === 'nextjs') { + if (authEnabled) { + welcomeMessage = `Welcome to Toolpad, {session?.user?.name || ${options.hasNodemailerProvider || options.hasPasskeyProvider ? `session?.user?.email ||` : ''}'User'}!`; + if (routerType === 'nextjs-app') { + if (options.hasNodemailerProvider || options.hasPasskeyProvider) { + imports += `\nimport { redirect } from 'next/navigation';\nimport { headers } from 'next/headers';`; + } + imports += `\nimport { auth } from '../../auth';`; + sessionHandling = `const session = await auth();`; + if (options.hasNodemailerProvider || options.hasPasskeyProvider) { + sessionHandling += `\nconst currentUrl = (await headers()).get('referer') || process.env.AUTH_URL || 'http://localhost:3000'; if (!session) { // Get the current URL to redirect to signIn with \`callbackUrl\` const redirectUrl = new URL('/auth/signin', currentUrl);\nredirectUrl.searchParams.set('callbackUrl', currentUrl);\nredirect(redirectUrl.toString()); } `; + } + } else if (routerType === 'nextjs-pages') { + imports += `\nimport { useSession } from 'next-auth/react';`; + sessionHandling = `const { data: session } = useSession();`; } - } else if (routerType === 'nextjs-pages') { - imports += `\nimport { useSession } from 'next-auth/react';`; - sessionHandling = `const { data: session } = useSession();`; + } else { + imports += `\nimport Link from 'next/link';`; + welcomeMessage = `Welcome to Toolpad Core!`; } - } else { - imports += `\nimport Link from 'next/link';`; - welcomeMessage = `Welcome to Toolpad Core!`; } const isAsync = authEnabled && routerType === 'nextjs-app' ? 'async ' : ''; @@ -44,8 +46,6 @@ import Typography from '@mui/material/Typography';`; return `${imports} - - export default ${isAsync}function HomePage() { ${sessionHandling} diff --git a/packages/create-toolpad-app/src/templates/packageJson.ts b/packages/create-toolpad-app/src/templates/packageJson.ts index a9c619e0208..959595c576f 100644 --- a/packages/create-toolpad-app/src/templates/packageJson.ts +++ b/packages/create-toolpad-app/src/templates/packageJson.ts @@ -5,6 +5,7 @@ const packageJson: PackageJsonTemplate = (options) => { name: appName, projectType, router: routerType, + framework, auth: authOption, coreVersion, hasNodemailerProvider, @@ -29,7 +30,6 @@ const packageJson: PackageJsonTemplate = (options) => { const dependencies: Record = { react: '^18', 'react-dom': '^18', - next: '^15', '@toolpad/core': coreVersion ?? 'latest', '@mui/material': '^6', '@mui/material-nextjs': '^6', @@ -38,13 +38,34 @@ const packageJson: PackageJsonTemplate = (options) => { '@emotion/styled': '^11', }; + const devDependencies: Record = { + typescript: '^5', + '@types/react': '^18', + '@types/react-dom': '^18', + eslint: '^8', + }; + + if (framework === 'nextjs') { + dependencies.next = '^15'; + devDependencies['eslint-config-next'] = '^15'; + devDependencies['@types/node'] = '^20'; + } else if (framework === 'vite') { + dependencies['react-router-dom'] = '^6'; + devDependencies['@vitejs/plugin-react'] = '^4.3.2'; + devDependencies.vite = '^5.4.8'; + } + if (routerType === 'nextjs-pages') { dependencies['@emotion/cache'] = '^11'; dependencies['@emotion/server'] = '^11'; } if (authOption) { - dependencies['next-auth'] = '5.0.0-beta.25'; + if (framework === 'nextjs') { + dependencies['next-auth'] = '5.0.0-beta.25'; + } else if (framework === 'vite') { + dependencies.firebase = '^11'; + } } if (hasNodemailerProvider || hasPasskeyProvider) { @@ -61,25 +82,22 @@ const packageJson: PackageJsonTemplate = (options) => { dependencies['@simplewebauthn/server'] = '^9'; } - const devDependencies: Record = { - typescript: '^5', - '@types/node': '^20', - '@types/react': '^18', - '@types/react-dom': '^18', - eslint: '^8', - 'eslint-config-next': '^15', - }; - if (hasNodemailerProvider || hasPasskeyProvider) { devDependencies.prisma = '^5'; } - const scripts = { - dev: 'next dev', - build: 'next build', - start: 'next start', - lint: 'next lint', - }; + const scripts = + framework === 'nextjs' + ? { + dev: 'next dev', + build: 'next build', + start: 'next start', + lint: 'next lint', + } + : { + dev: 'vite', + preview: 'vite preview', + }; return { name: appName, diff --git a/packages/create-toolpad-app/src/templates/vite/App.ts b/packages/create-toolpad-app/src/templates/vite/App.ts index 80c38041fcf..4d9d679817f 100644 --- a/packages/create-toolpad-app/src/templates/vite/App.ts +++ b/packages/create-toolpad-app/src/templates/vite/App.ts @@ -1,11 +1,18 @@ -import { GenerateProjectOptions } from '../../types'; +import { Template } from '../../types'; -export default function viteApp(options: GenerateProjectOptions) { +const appTemplate: Template = (options) => { return `import * as React from 'react'; import DashboardIcon from '@mui/icons-material/Dashboard'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Outlet } from 'react-router-dom'; -import { AppProvider, type Navigation } from '@toolpad/core/react-router-dom'; +${ + options.auth + ? `import { AppProvider } from '@toolpad/core/react-router-dom'; +import type { Navigation, Authentication } from '@toolpad/core/AppProvider'; +import { firebaseSignOut, onAuthStateChanged } from './firebase/auth'; +import SessionContext, { type Session } from './SessionContext';` + : `import { AppProvider, type Navigation } from '@toolpad/core/react-router-dom';` +} const NAVIGATION: Navigation = [ { @@ -26,12 +33,69 @@ const NAVIGATION: Navigation = [ const BRANDING = { title: ${JSON.stringify(options.name || 'My Toolpad Core App')}, }; +${ + options.auth + ? ` +const AUTHENTICATION: Authentication = { + signOut: firebaseSignOut, +};` + : '' +} export default function App() { + ${ + options.auth + ? `const [session, setSession] = React.useState(null); + const [loading, setLoading] = React.useState(true); + + const sessionContextValue = React.useMemo( + () => ({ + session, + setSession, + loading, + }), + [session, loading], + ); + + React.useEffect(() => { + const unsubscribe = onAuthStateChanged((user) => { + if (user) { + setSession({ + user: { + name: user.name || '', + email: user.email || '', + image: user.image || '', + }, + }); + } else { + setSession(null); + } + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + + return ( + + + + + + );` + : ` return ( - ); + );` + } }`; -} +}; + +export default appTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/SessionContext.ts b/packages/create-toolpad-app/src/templates/vite/SessionContext.ts new file mode 100644 index 00000000000..d60bc9e8464 --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/SessionContext.ts @@ -0,0 +1,25 @@ +export default `import * as React from 'react'; + +export interface Session { + user: { + name?: string; + email?: string; + image?: string; + }; +} + +interface SessionContextType { + session: Session | null; + setSession: (session: Session | null) => void; + loading: boolean; +} + +const SessionContext = React.createContext({ + session: null, + setSession: () => {}, + loading: true, +}); + +export default SessionContext; + +export const useSession = () => React.useContext(SessionContext);`; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/env.ts b/packages/create-toolpad-app/src/templates/vite/auth/env.ts new file mode 100644 index 00000000000..2f72af4cc1a --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/auth/env.ts @@ -0,0 +1,8 @@ +const envTemplate = `VITE_FIREBASE_API_KEY="" +VITE_FIREBASE_AUTH_DOMAIN="" +VITE_FIREBASE_PROJECT_ID="" +VITE_FIREBASE_STORAGE_BUCKET="" +VITE_FIREBASE_MESSAGE_SENDER_ID="" +VITE_FIREBASE_APP_ID=""`; + +export default envTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/firebase.ts b/packages/create-toolpad-app/src/templates/vite/auth/firebase.ts new file mode 100644 index 00000000000..0d7a556a625 --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/auth/firebase.ts @@ -0,0 +1,116 @@ +import { Template } from '../../../types'; + +const firebaseAuthTemplate: Template = (options) => { + const { hasCredentialsProvider = true } = options; + + const hasGoogleProvider = true; + const hasGithubProvider = true; + + return `import { + ${[ + ...(hasGoogleProvider ? ['GoogleAuthProvider,'] : []), + ...(hasGithubProvider ? ['GithubAuthProvider,'] : []), + 'setPersistence,', + 'browserSessionPersistence,', + ...(hasCredentialsProvider ? ['signInWithEmailAndPassword,'] : []), + 'signInWithPopup,', + 'signOut,', + ].join('\n ')} +} from 'firebase/auth'; +import { firebaseAuth } from './firebaseConfig'; + +${hasGoogleProvider ? 'const googleProvider = new GoogleAuthProvider();' : ''} +${hasGithubProvider ? 'const githubProvider = new GithubAuthProvider();' : ''} + +${ + hasGoogleProvider + ? `// Sign in with Google functionality +export const signInWithGoogle = async () => { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const result = await signInWithPopup(firebaseAuth, googleProvider); + return { + success: true, + user: result.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +};` + : '' +} + +${ + hasGithubProvider + ? `// Sign in with GitHub functionality +export const signInWithGithub = async () => { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const result = await signInWithPopup(firebaseAuth, githubProvider); + return { + success: true, + user: result.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +};` + : '' +} + +${ + hasCredentialsProvider + ? `// Sign in with email and password +export async function signInWithCredentials(email: string, password: string) { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const userCredential = await signInWithEmailAndPassword(firebaseAuth, email, password); + return { + success: true, + user: userCredential.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message || 'Failed to sign in with email/password', + }; + } +}` + : '' +} + +// Sign out functionality +export const firebaseSignOut = async () => { + try { + await signOut(firebaseAuth); + return { success: true }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } +}; + +// Auth state observer +export const onAuthStateChanged = (callback: (user: any) => void) => { + return firebaseAuth.onAuthStateChanged(callback); +};`; +}; + +export default firebaseAuthTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts b/packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts new file mode 100644 index 00000000000..905cea709dd --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts @@ -0,0 +1,34 @@ +import { Template } from '../../../types'; + +const firebaseConfigTemplate: Template = () => `import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; + +const requiredEnvVars = { + VITE_FIREBASE_API_KEY: import.meta.env.VITE_FIREBASE_API_KEY, + VITE_FIREBASE_AUTH_DOMAIN: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + VITE_FIREBASE_PROJECT_ID: import.meta.env.VITE_FIREBASE_PROJECT_ID, + VITE_FIREBASE_STORAGE_BUCKET: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, + VITE_FIREBASE_MESSAGE_SENDER_ID: import.meta.env.VITE_FIREBASE_MESSAGE_SENDER_ID, + VITE_FIREBASE_APP_ID: import.meta.env.VITE_FIREBASE_APP_ID, +}; + +// Check for missing environment variables +Object.entries(requiredEnvVars).forEach(([key, value]) => { + if (!value) { + console.warn(\`Missing required environment variable: \${key}\`); + } +}); + +const app = initializeApp({ + apiKey: requiredEnvVars.VITE_FIREBASE_API_KEY, + authDomain: requiredEnvVars.VITE_FIREBASE_AUTH_DOMAIN, + projectId: requiredEnvVars.VITE_FIREBASE_PROJECT_ID, + storageBucket: requiredEnvVars.VITE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: requiredEnvVars.VITE_FIREBASE_MESSAGE_SENDER_ID, + appId: requiredEnvVars.VITE_FIREBASE_APP_ID, +}); + +export const firebaseAuth = getAuth(app); +export default app;`; + +export default firebaseConfigTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts new file mode 100644 index 00000000000..03090c6b8ac --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts @@ -0,0 +1,101 @@ +import { Template } from '../../../types'; + +const signinTemplate: Template = (options) => { + const { hasCredentialsProvider = true } = options; + + const hasGoogleProvider = options.authProviders?.includes('google'); + const hasGithubProvider = options.authProviders?.includes('github'); + + const providers = [ + ...(hasGoogleProvider ? [`{ id: 'google', name: 'Google' }`] : []), + ...(hasGithubProvider ? [`{ id: 'github', name: 'GitHub' }`] : []), + ...(hasCredentialsProvider ? [`{ id: 'credentials', name: 'Credentials' }`] : []), + ]; + + return `'use client'; +import * as React from 'react'; +import Alert from '@mui/material/Alert'; +import LinearProgress from '@mui/material/LinearProgress'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { useSession, type Session } from '../SessionContext'; +import { ${[ + hasGoogleProvider && 'signInWithGoogle', + hasGithubProvider && 'signInWithGithub', + hasCredentialsProvider && 'signInWithCredentials', + ] + .filter(Boolean) + .join(', ')} } from '../firebase/auth'; + + +export default function SignIn() { + const { session, setSession, loading } = useSession(); + const navigate = useNavigate(); + + if (loading) { + return ; + } + + if (session) { + return ; + } + + return ( + { + let result; + try { + ${ + hasGoogleProvider + ? `if (provider.id === 'google') { + result = await signInWithGoogle(); + }` + : '' + } + ${ + hasGithubProvider + ? `if (provider.id === 'github') { + result = await signInWithGithub(); + }` + : '' + } + ${ + hasCredentialsProvider + ? `if (provider.id === 'credentials') { + const email = formData?.get('email') as string; + const password = formData?.get('password') as string; + + if (!email || !password) { + return { error: 'Email and password are required' }; + } + + result = await signInWithCredentials(email, password); + }` + : '' + } + + if (result?.success && result?.user) { + const userSession: Session = { + user: { + name: result.user.displayName || '', + email: result.user.email || '', + image: result.user.photoURL || '', + }, + }; + setSession(userSession); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + return { error: result?.error || 'Failed to sign in' }; + } catch (error) { + return { error: error instanceof Error ? error.message : 'An error occurred' }; + } + }} + + /> + ); +}`; +}; + +export default signinTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts new file mode 100644 index 00000000000..74f6ccfeb2e --- /dev/null +++ b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts @@ -0,0 +1,64 @@ +import { Template } from '../../types'; + +const dashboardTemplate: Template = (options) => { + const { auth } = options; + + return `import * as React from 'react'; +${ + auth + ? `import LinearProgress from '@mui/material/LinearProgress'; +import { Outlet, Navigate, useLocation } from 'react-router-dom';` + : `import { Outlet } from 'react-router-dom';` +} +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { PageContainer } from '@toolpad/core/PageContainer'; +${ + auth + ? `import { Account } from '@toolpad/core/Account'; + +import { useSession } from '../SessionContext'; + +function CustomAccount() { + return ( + + ); +}` + : '' +} + +export default function Layout() { + ${ + auth + ? `const { session, loading } = useSession(); + const location = useLocation(); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!session) { + const redirectTo = \`/sign-in?callbackUrl=\${encodeURIComponent(location.pathname)}\`; + return ; + }` + : '' + } + + return ( + + + + + + ); +}`; +}; + +export default dashboardTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/main.ts b/packages/create-toolpad-app/src/templates/vite/main.ts index 4c56e8b94d0..730825af52a 100644 --- a/packages/create-toolpad-app/src/templates/vite/main.ts +++ b/packages/create-toolpad-app/src/templates/vite/main.ts @@ -1,30 +1,52 @@ -export default `import * as React from 'react'; -import { createRoot } from 'react-dom/client'; +import { Template } from '../../types'; + +const mainTemplate: Template = (options) => { + const { auth } = options; + + return `import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import App from './App'; -import Dashboard from './pages'; -import Orders from './pages/orders'; +import Layout from './layouts/dashboard'; +import DashboardPage from './pages'; +import OrdersPage from './pages/orders'; +${auth ? `import SignInPage from './pages/signin';` : ''} const router = createBrowserRouter([ { - path: '/', - element: , + Component: App, children: [ { - index: true, - element: , - }, + path: '/', + Component: Layout, + children: [ + { + path: '', + Component: DashboardPage, + }, + { + path: 'orders', + Component: OrdersPage, + }, + ], + },${ + auth + ? ` { - path: 'orders', - element: , - }, + path: '/sign-in', + Component: SignInPage, + },` + : '' + } ], }, ]); -const root = createRoot(document.getElementById('root')!); -root.render( +ReactDOM.createRoot(document.getElementById('root')!).render( , );`; +}; + +export default mainTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/viteConfig.ts b/packages/create-toolpad-app/src/templates/vite/viteConfig.ts index 4beb97313c7..83bdbb57893 100644 --- a/packages/create-toolpad-app/src/templates/vite/viteConfig.ts +++ b/packages/create-toolpad-app/src/templates/vite/viteConfig.ts @@ -4,9 +4,4 @@ import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - resolve: { - alias: { - '@': '/src', - }, - }, });`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb7dd3f867a..a1d553d90d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.37.2(eslint@8.57.1) eslint-plugin-react-compiler: specifier: latest - version: 19.0.0-beta-37ed2a7-20241206(eslint@8.57.1) + version: 19.0.0-beta-201e55d-20241215(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 5.0.0 version: 5.0.0(eslint@8.57.1) @@ -6067,8 +6067,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206: - resolution: {integrity: sha512-5Pex1fUCJwLwwqEJe6NkgTn45kUjjj9TZP6IrW4IcpWM/YaEe+QvcOeF60huDjBq0kz1svGeW2nw8WdY+qszAw==} + eslint-plugin-react-compiler@19.0.0-beta-201e55d-20241215: + resolution: {integrity: sha512-KTOMW6Z4Eg2r5BPT/BncbwPswfuVH9KPPDVszOTQFXGNlxhExL8IZoSE3blYxk4e0wFDbCcAwe4UlTauQ8lIww==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15927,7 +15927,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-201e55d-20241215(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2 From 18d820c38641274019dc5714c8e9fa41f43b0ca8 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Thu, 19 Dec 2024 12:31:43 +0530 Subject: [PATCH 04/13] fix: Types and tests --- .../src/templates/gitignore.ts | 16 +++ .../templates/nextjs/nextjs-app/rootLayout.ts | 2 +- .../src/templates/nextjs/nextjs-pages/app.ts | 2 +- .../src/templates/vite/App.ts | 1 + .../create-toolpad-app/tests/index.spec.ts | 135 ++++++++++++++++++ 5 files changed, 154 insertions(+), 2 deletions(-) diff --git a/packages/create-toolpad-app/src/templates/gitignore.ts b/packages/create-toolpad-app/src/templates/gitignore.ts index 82518c80706..8ccd0b95cf7 100644 --- a/packages/create-toolpad-app/src/templates/gitignore.ts +++ b/packages/create-toolpad-app/src/templates/gitignore.ts @@ -91,6 +91,9 @@ const gitignore = ` # Nuxt.js build / generate output .nuxt dist + + # Vite build output + dist-ssr # Gatsby files .cache/ @@ -119,6 +122,19 @@ const gitignore = ` # TernJS port file .tern-port + + + # Editor directories and files + .vscode/* + !.vscode/extensions.json + .idea + .DS_Store + *.suo + *.ntvs* + *.njsproj + *.sln + *.sw? + # Stores VS Code versions used for testing VS Code extensions .vscode-test diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts index 2ada7023096..ce6205f10de 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts @@ -1,4 +1,4 @@ -import { Template } from '../../types'; +import { Template } from '../../../types'; const rootLayout: Template = (options) => { const authEnabled = options.auth; diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts index 4f4f24b9092..2a0a1fbaba7 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts @@ -1,4 +1,4 @@ -import type { Template } from '../../types'; +import type { Template } from '../../../types'; const app: Template = (options) => { const authEnabled = options.auth; diff --git a/packages/create-toolpad-app/src/templates/vite/App.ts b/packages/create-toolpad-app/src/templates/vite/App.ts index 4d9d679817f..a2c829bf5a5 100644 --- a/packages/create-toolpad-app/src/templates/vite/App.ts +++ b/packages/create-toolpad-app/src/templates/vite/App.ts @@ -37,6 +37,7 @@ ${ options.auth ? ` const AUTHENTICATION: Authentication = { + signIn: () => {}, signOut: firebaseSignOut, };` : '' diff --git a/packages/create-toolpad-app/tests/index.spec.ts b/packages/create-toolpad-app/tests/index.spec.ts index 4f313ae7ea2..8fb12c5e802 100644 --- a/packages/create-toolpad-app/tests/index.spec.ts +++ b/packages/create-toolpad-app/tests/index.spec.ts @@ -124,6 +124,10 @@ test( }); cp.stdout?.pipe(process.stdout); cp.stderr?.pipe(process.stderr); + + // Wait for the framework prompt and select Next.js (default) + await waitForPromptAndRespond(cp, /Which framework/, '\n'); + // Wait for the router prompt and select the App Router await waitForPromptAndRespond(cp, /Which router/, '\n'); @@ -185,6 +189,9 @@ test( cp.stdout?.pipe(process.stdout); cp.stderr?.pipe(process.stderr); + // Wait for the framework prompt and select Next.js (default) + await waitForPromptAndRespond(cp, /Which framework/, '\n'); + // Wait for the router prompt and select the App Router await waitForPromptAndRespond(cp, /Which router/, '\n'); @@ -241,6 +248,134 @@ test( TEST_TIMEOUT, ); +test( + 'create-toolpad-app can bootstrap a Toolpad Core app with Vite without authentication', + async () => { + testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-vite-')); + cp = execa(cliPath, [testDir, '--coreVersion', 'latest'], { + cwd: currentDirectory, + }); + cp.stdout?.pipe(process.stdout); + cp.stderr?.pipe(process.stderr); + + // Wait for the framework prompt and select Vite (down arrow + enter) + await waitForPromptAndRespond(cp, /Which framework/, '\u001b[B'); + + // Wait for the authentication prompt and select 'No' + await waitForPromptAndRespond(cp, /enable authentication/, 'n'); + + const result = await cp; + expect(result.stdout).toMatch('Run the following to get started'); + const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { + encoding: 'utf-8', + }); + const packageJson = JSON.parse(packageJsonContent); + expect(packageJson).toEqual( + expect.objectContaining({ + dependencies: expect.objectContaining({ + '@toolpad/core': expect.any(String), + }), + scripts: expect.objectContaining({ + dev: 'vite', + preview: 'vite preview', + }), + }), + ); + + // check that file exists or not in the directory + const gitignore = await fs.readFile(path.resolve(testDir, './.gitignore'), { + encoding: 'utf-8', + }); + + expect(gitignore.length).toBeGreaterThan(0); + + toolpadProcess = execa('pnpm', ['dev'], { + cwd: testDir, + env: { + FORCE_COLOR: '0', + BROWSER: 'none', + }, + }); + toolpadProcess.stdout?.pipe(process.stdout); + toolpadProcess.stderr?.pipe(process.stderr); + + // Add console.log to see what output we're getting + toolpadProcess.stdout?.on('data', (data) => { + console.log('Vite output:', data.toString()); + }); + + // Modify the regex to be more lenient + const match = await waitForMatch(toolpadProcess.stdout!, /ready in/); + + expect(match).toBeTruthy(); + }, + TEST_TIMEOUT, +); + +test( + 'create-toolpad-app can bootstrap a Toolpad Core app with Vite with authentication', + async () => { + testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-vite-auth-')); + cp = execa(cliPath, [testDir, '--coreVersion', 'latest'], { + cwd: currentDirectory, + }); + cp.stdout?.pipe(process.stdout); + cp.stderr?.pipe(process.stderr); + + // Wait for the framework prompt and select Vite (down arrow + enter) + await waitForPromptAndRespond(cp, /Which framework/, '\u001b[B\n'); + + // Wait for the authentication prompt and select 'Yes' + await waitForPromptAndRespond(cp, /enable authentication/, 'y'); + + // Wait for the auth providers prompt and select all (press 'a' then Enter) + await waitForPromptAndRespond(cp, /Select authentication providers/, 'a\n'); + + const result = await cp; + expect(result.stdout).toMatch('Run the following to get started'); + + const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { + encoding: 'utf-8', + }); + const packageJson = JSON.parse(packageJsonContent); + expect(packageJson).toEqual( + expect.objectContaining({ + dependencies: expect.objectContaining({ + '@toolpad/core': expect.any(String), + firebase: expect.any(String), + }), + scripts: expect.objectContaining({ + dev: 'vite', + preview: 'vite preview', + }), + }), + ); + + // Check if auth.ts file is created + const authFileExists = await fs + .access(path.resolve(testDir, './src/firebase/auth.ts')) + .then(() => true) + .catch(() => false); + + expect(authFileExists).toBe(true); + + toolpadProcess = execa('pnpm', ['dev'], { + cwd: testDir, + env: { + FORCE_COLOR: '0', + BROWSER: 'none', + }, + }); + toolpadProcess.stdout?.pipe(process.stdout); + toolpadProcess.stderr?.pipe(process.stderr); + + const match = await waitForMatch(toolpadProcess.stdout!, /ready in/); + + expect(match).toBeTruthy(); + }, + TEST_TIMEOUT, +); + afterEach(async () => { if (toolpadProcess) { await terminate(toolpadProcess.pid!); From 38938362c70caddb9a21f01bf965a776e4c239a0 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 20 Dec 2024 11:11:38 +0530 Subject: [PATCH 05/13] fix: Add to docs --- .../components/sign-in-page/sign-in-page.md | 25 +- .../toolpad/core/integrations/react-router.md | 306 ++++++++++++++---- 2 files changed, 270 insertions(+), 61 deletions(-) diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index cddc44d4daf..22a4190d3a1 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -211,8 +211,31 @@ If you're using the default [Next.js example](https://github.com/mui/toolpad/tre If you're not on the Next Auth v5 version yet, see the [example with Next Auth v4](https://github.com/mui/toolpad/tree/master/examples/core/auth-nextjs-pages-nextauth-4/) to get started. ::: +## Firebase + +### Vite with React Router + +### Setting up + +You need to create a `.env` file in the root of your project directory with the following environment variables: + +```bash +VITE_FIREBASE_API_KEY= +VITE_FIREBASE_AUTH_DOMAIN= +VITE_FIREBASE_PROJECT_ID= +VITE_FIREBASE_STORAGE_BUCKET= +VITE_FIREBASE_MESSAGE_SENDER_ID= +VITE_FIREBASE_APP_ID= +``` + +You can find these in the Firebase project settings. + :::info -If you're using Vite with React Router, check out the [example with Vite and React Router](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/) that showcases how to use `SignInPage` along with any external authentication library of your choice. +Head to the [integration docs](https://mui.com/toolpad/core/integrations/react-router) for code examples on integrating Firebase authentication with Vite and React Router. +::: + +:::success +If you're using [`create-toolpad-app`](/toolpad/core/introduction/installation/), or the default [Vite with Firebase authentication example](https://github.com/mui/toolpad/tree/master/examples/core/firebase-vite/), the integration is already set up for you. ::: ## Customization diff --git a/docs/data/toolpad/core/integrations/react-router.md b/docs/data/toolpad/core/integrations/react-router.md index e0fd48b0d82..f3e9f0d2aa5 100644 --- a/docs/data/toolpad/core/integrations/react-router.md +++ b/docs/data/toolpad/core/integrations/react-router.md @@ -176,30 +176,157 @@ For a full working example, see the [Toolpad Core Vite app with React Router exa ## (Optional) Set up authentication -You can use the `SignInPage` component to add authentication along with an external authentication provider of your choice. The following code demonstrates the code required to set up authentication with a mock provider. +You can use the `SignInPage` component to add authentication along with an external authentication provider of your choice. The following code demonstrates the code required to set up authentication with Firebase. ### Define a `SessionContext` to act as the mock authentication provider ```tsx title="src/SessionContext.ts" import * as React from 'react'; -import type { Session } from '@toolpad/core'; -export interface SessionContextValue { +export interface Session { + user: { + name?: string; + email?: string; + image?: string; + }; +} + +interface SessionContextType { session: Session | null; - setSession: (session: Session | null) => void; + setSession: (session: Session) => void; + loading: boolean; } -export const SessionContext = React.createContext({ - session: {}, +const SessionContext = React.createContext({ + session: null, setSession: () => {}, + loading: true, +}); + +export default SessionContext; + +export const useSession = () => React.useContext(SessionContext); +``` + +### Add Firebase authentication + +```tsx title="src/firebase/firebaseConfig.ts" +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; + +const app = initializeApp({ + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGE_SENDER_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID, }); -export function useSession() { - return React.useContext(SessionContext); +export const firebaseAuth = getAuth(app); +export default app; +``` + +```tsx title="src/firebase/auth.ts" +import { + GoogleAuthProvider, + GithubAuthProvider, + signInWithPopup, + setPersistence, + browserSessionPersistence, + signInWithEmailAndPassword, + signOut, +} from 'firebase/auth'; +import { firebaseAuth } from './firebaseConfig'; + +const googleProvider = new GoogleAuthProvider(); +const githubProvider = new GithubAuthProvider(); + +// Sign in with Google functionality +export const signInWithGoogle = async () => { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const result = await signInWithPopup(firebaseAuth, googleProvider); + return { + success: true, + user: result.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +}; + +// Sign in with GitHub functionality +export const signInWithGithub = async () => { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const result = await signInWithPopup(firebaseAuth, githubProvider); + return { + success: true, + user: result.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +}; + +// Sign in with email and password + +export async function signInWithCredentials(email: string, password: string) { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const userCredential = await signInWithEmailAndPassword( + firebaseAuth, + email, + password, + ); + return { + success: true, + user: userCredential.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message || 'Failed to sign in with email/password', + }; + } } + +// Sign out functionality +export const firebaseSignOut = async () => { + try { + await signOut(firebaseAuth); + return { success: true }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } +}; + +// Auth state observer +export const onAuthStateChanged = (callback: (user: any) => void) => { + return firebaseAuth.onAuthStateChanged(callback); +}; ``` -### Add the mock authentication and session data to the `AppProvider` +### Add authentication and session data to the `AppProvider` ```tsx title="src/App.tsx" import * as React from 'react'; @@ -208,7 +335,12 @@ import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { AppProvider } from '@toolpad/core/react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom'; import type { Navigation, Session } from '@toolpad/core'; -import { SessionContext } from './SessionContext'; +import { + firebaseSignOut, + signInWithGoogle, + onAuthStateChanged, +} from './firebase/auth'; +import SessionContext, { type Session } from './SessionContext'; const NAVIGATION: Navigation = [ { @@ -230,35 +362,55 @@ const BRANDING = { title: 'My Toolpad Core App', }; +const AUTHENTICATION: Authentication = { + signIn: signInWithGoogle, + signOut: firebaseSignOut, +}; + export default function App() { const [session, setSession] = React.useState(null); - const navigate = useNavigate(); - - const signIn = React.useCallback(() => { - navigate('/sign-in'); - }, [navigate]); - - const signOut = React.useCallback(() => { - setSession(null); - navigate('/sign-in'); - }, [navigate]); + const [loading, setLoading] = React.useState(true); const sessionContextValue = React.useMemo( - () => ({ session, setSession }), - [session, setSession], + () => ({ + session, + setSession, + loading, + }), + [session, loading], ); + React.useEffect(() => { + // Returns an `unsubscribe` function to be called during teardown + const unsubscribe = onAuthStateChanged((user: User | null) => { + if (user) { + setSession({ + user: { + name: user.displayName || '', + email: user.email || '', + image: user.photoURL || '', + }, + }); + } else { + setSession(null); + } + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + return ( - - + + - - + + ); } ``` @@ -267,15 +419,24 @@ export default function App() { ```tsx title="src/layouts/dashboard.tsx" import * as React from 'react'; +import LinearProgress from '@mui/material/LinearProgress'; import { Outlet, Navigate, useLocation } from 'react-router-dom'; import { DashboardLayout } from '@toolpad/core/DashboardLayout'; import { PageContainer } from '@toolpad/core/PageContainer'; import { useSession } from '../SessionContext'; export default function Layout() { - const { session } = useSession(); + const { session, loading } = useSession(); const location = useLocation(); + if (loading) { + return ( +
+ +
+ ); + } + if (!session) { // Add the `callbackUrl` search parameter const redirectTo = `/sign-in?callbackUrl=${encodeURIComponent(location.pathname)}`; @@ -301,48 +462,73 @@ You can protect any page or groups of pages through this mechanism. 'use client'; import * as React from 'react'; import { SignInPage } from '@toolpad/core/SignInPage'; -import type { Session } from '@toolpad/core/AppProvider'; -import { useNavigate } from 'react-router-dom'; -import { useSession } from '../SessionContext'; - -const fakeAsyncGetSession = async (formData: any): Promise => { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (formData.get('password') === 'password') { - resolve({ - user: { - name: 'Bharat Kashyap', - email: formData.get('email') || '', - image: 'https://avatars.githubusercontent.com/u/19550456', - }, - }); - } - reject(new Error('Incorrect credentials.')); - }, 1000); - }); -}; +import LinearProgress from '@mui/material/LinearProgress'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { useSession, type Session } from '../SessionContext'; +import { + signInWithGoogle, + signInWithGithub, + signInWithCredentials, +} from '../firebase/auth'; export default function SignIn() { - const { setSession } = useSession(); + const { session, setSession, loading } = useSession(); const navigate = useNavigate(); + + if (loading) { + return ; + } + + if (session) { + return ; + } + return ( { - // Demo session + let result; try { - const session = await fakeAsyncGetSession(formData); - if (session) { - setSession(session); + if (provider.id === 'google') { + result = await signInWithGoogle(); + } + if (provider.id === 'github') { + result = await signInWithGithub(); + } + if (provider.id === 'credentials') { + const email = formData?.get('email') as string; + const password = formData?.get('password') as string; + + if (!email || !password) { + return { error: 'Email and password are required' }; + } + + result = await signInWithCredentials(email, password); + } + + if (result?.success && result?.user) { + // Convert Firebase user to Session format + const userSession: Session = { + user: { + name: result.user.displayName || '', + email: result.user.email || '', + image: result.user.photoURL || '', + }, + }; + setSession(userSession); navigate(callbackUrl || '/', { replace: true }); return {}; } + return { error: result?.error || 'Failed to sign in' }; } catch (error) { return { error: error instanceof Error ? error.message : 'An error occurred', }; } - return {}; }} /> ); @@ -395,5 +581,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ``` :::info -For a full working example, see the [Toolpad Core Vite app with React Router and authentication example](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/) +For a full working example, see the [Toolpad Core Vite app with React Router and Firebase authentication example](https://github.com/mui/toolpad/tree/master/examples/core/firebase-vite/) ::: From 0a62c46d8682367c832a1d3ecd0b5c13d3ad045b Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 20 Dec 2024 11:12:14 +0530 Subject: [PATCH 06/13] fix: Missing trailing slash --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index 22a4190d3a1..5dfe1948a9d 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -231,7 +231,7 @@ VITE_FIREBASE_APP_ID= You can find these in the Firebase project settings. :::info -Head to the [integration docs](https://mui.com/toolpad/core/integrations/react-router) for code examples on integrating Firebase authentication with Vite and React Router. +Head to the [integration docs](https://mui.com/toolpad/core/integrations/react-router/) for code examples on integrating Firebase authentication with Vite and React Router. ::: :::success From ca0685fb3ef2bec420b6a4696e3dc454e8611a6e Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 20 Dec 2024 11:30:13 +0530 Subject: [PATCH 07/13] fix: docs cleanup --- .../components/sign-in-page/sign-in-page.md | 8 ++--- examples/core/auth-vite/README.md | 33 ++++++++++++++++--- examples/core/firebase-vite/README.md | 33 ++++++++++++++++--- examples/core/vite/README.md | 8 +++++ 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index 5dfe1948a9d..7e240613cda 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -211,11 +211,11 @@ If you're using the default [Next.js example](https://github.com/mui/toolpad/tre If you're not on the Next Auth v5 version yet, see the [example with Next Auth v4](https://github.com/mui/toolpad/tree/master/examples/core/auth-nextjs-pages-nextauth-4/) to get started. ::: -## Firebase +### Firebase -### Vite with React Router +#### Vite with React Router -### Setting up +##### Setting up You need to create a `.env` file in the root of your project directory with the following environment variables: @@ -235,7 +235,7 @@ Head to the [integration docs](https://mui.com/toolpad/core/integrations/react-r ::: :::success -If you're using [`create-toolpad-app`](/toolpad/core/introduction/installation/), or the default [Vite with Firebase authentication example](https://github.com/mui/toolpad/tree/master/examples/core/firebase-vite/), the integration is already set up for you. +If you're using [create-toolpad-app](/toolpad/core/introduction/installation/), or the default [Vite with Firebase authentication example](https://github.com/mui/toolpad/tree/master/examples/core/firebase-vite/), the integration is already set up for you. ::: ## Customization diff --git a/examples/core/auth-vite/README.md b/examples/core/auth-vite/README.md index 77643b462dd..ff224458254 100644 --- a/examples/core/auth-vite/README.md +++ b/examples/core/auth-vite/README.md @@ -1,8 +1,31 @@ -# React + TypeScript + Vite +# Toolpad Core - Vite & React Router -This template provides a minimal setup to get React working in Vite with HMR. +This example provides a minimal setup to get Toolpad Core working in Vite with HMR, as well as routing with React Router and a mock authentication setup. -Currently, two official plugins are available: +## Getting Started -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:5173](http://localhost:5173) with your browser to see the result. + +## Clone using `create-toolpad-app` + +To copy this example and customize it for your needs, run + +```bash +npx create-toolpad-app@latest --example auth-vite +``` + +## The source + +[Check out the source code](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/) diff --git a/examples/core/firebase-vite/README.md b/examples/core/firebase-vite/README.md index 72355db4e34..c01e46b551e 100644 --- a/examples/core/firebase-vite/README.md +++ b/examples/core/firebase-vite/README.md @@ -1,8 +1,31 @@ -# React + TypeScript + Vite +# Toolpad Core - Vite & React Router & Firebase -This template provides a minimal setup to get React working in Vite with HMR, and authentication with Firebase. +This example provides a minimal setup to get Toolpad Core working in Vite with HMR, as well as routing with React Router and authentication with Firebase. -Currently, two official plugins are available: +## Getting Started -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:5173](http://localhost:5173) with your browser to see the result. + +## Clone using `create-toolpad-app` + +To copy this example and customize it for your needs, run + +```bash +npx create-toolpad-app@latest --example firebase-vite +``` + +## The source + +[Check out the source code](https://github.com/mui/toolpad/tree/master/examples/core/vite/) diff --git a/examples/core/vite/README.md b/examples/core/vite/README.md index 5f2dc7485c8..3c6ed87c111 100644 --- a/examples/core/vite/README.md +++ b/examples/core/vite/README.md @@ -18,6 +18,14 @@ bun dev Open [http://localhost:5173](http://localhost:5173) with your browser to see the result. +## Clone using `create-toolpad-app` + +To copy this example and customize it for your needs, run + +```bash +npx create-toolpad-app@latest --example vite +``` + ## The source [Check out the source code](https://github.com/mui/toolpad/tree/master/examples/core/vite/) From a909c3b6b78928261ccb60daa1e39f682bdd7f7c Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Tue, 24 Dec 2024 14:55:15 +0530 Subject: [PATCH 08/13] chore: Trigger preview From a49a2ee7fe9d8ce370964985e0237f53ca6e332b Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Mon, 30 Dec 2024 14:18:28 +0530 Subject: [PATCH 09/13] fix: CI --- pnpm-lock.yaml | 118 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 21 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c30df23cd5..30689872801 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1356,7 +1356,7 @@ importers: version: 18.3.1(react@18.3.1) recharts: specifier: alpha - version: 2.13.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.0.0-alpha.0(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@5.0.1) packages: @@ -3932,6 +3932,17 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@reduxjs/toolkit@2.5.0': + resolution: {integrity: sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@remix-run/router@1.19.2': resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==} engines: {node: '>=14.0.0'} @@ -4468,6 +4479,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -6927,6 +6941,9 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -9125,6 +9142,18 @@ packages: react-is@19.0.0: resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -9258,15 +9287,13 @@ packages: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} - recharts-scale@0.4.5: - resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - - recharts@2.13.0-alpha.5: - resolution: {integrity: sha512-mm8ORfDusDhyWlrY/2NntUAsNeYukteplvRqKGkBEmqNPwqYq9GoEzaVsVDYj8bjGSKJynWGhjEO1NFcntl29g==} - engines: {node: '>=14'} + recharts@3.0.0-alpha.0: + resolution: {integrity: sha512-phxJucbwKseB0fwvu2JDhFsXValIcTe5PdzJEOZH2e+mL6zAAWMMZjp4MN8yrHMsfSiK2T63xQLp5HL6HQC3pA==} + engines: {node: '>=18'} peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 recursive-readdir@2.2.3: resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} @@ -9280,6 +9307,14 @@ packages: resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} engines: {node: '>=12'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -10353,6 +10388,11 @@ packages: urlpattern-polyfill@8.0.2: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -10401,8 +10441,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + victory-vendor@37.3.5: + resolution: {integrity: sha512-+K2VBMmB7peKG3Gjp79XjgsbfsYgD0eZRSmKz7p5a4V0NhYq43eM/b0gpSLq+Dhwag96QaWsU75/6bFVBjVE7A==} vite-node@2.1.7: resolution: {integrity: sha512-b/5MxSWd0ftWt1B1LHfzCw0ASzaxHztUwP0rcsBhkDSGy9ZDEDieSIjFG3I78nI9dUN0eSeD6LtuKPZGjwwpZQ==} @@ -13580,6 +13620,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@reduxjs/toolkit@2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 18.3.1 + react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) + '@remix-run/router@1.19.2': {} '@rollup/rollup-android-arm-eabi@4.24.3': @@ -14170,6 +14220,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/webidl-conversions@7.0.3': {} '@types/webpack-dev-server@3.11.6': @@ -17313,6 +17365,8 @@ snapshots: immediate@3.0.6: {} + immer@10.1.1: {} + immer@9.0.21: {} import-fresh@3.3.0: @@ -19717,6 +19771,15 @@ snapshots: react-is@19.0.0: {} + react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + redux: 5.0.1 + react-refresh@0.14.2: {} react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -19884,22 +19947,25 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 - recharts-scale@0.4.5: - dependencies: - decimal.js-light: 2.5.1 - - recharts@2.13.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + recharts@3.0.0-alpha.0(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@5.0.1): dependencies: + '@reduxjs/toolkit': 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1) clsx: 2.1.1 - eventemitter3: 4.0.7 + decimal.js-light: 2.5.1 + eventemitter3: 5.0.1 + immer: 10.1.1 lodash: 4.17.21 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-is: 18.3.1 + react-is: 19.0.0 + react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - recharts-scale: 0.4.5 tiny-invariant: 1.3.3 - victory-vendor: 36.9.2 + use-sync-external-store: 1.4.0(react@18.3.1) + victory-vendor: 37.3.5 + transitivePeerDependencies: + - '@types/react' + - redux recursive-readdir@2.2.3: dependencies: @@ -19915,6 +19981,12 @@ snapshots: indent-string: 5.0.0 strip-indent: 4.0.0 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -21105,6 +21177,10 @@ snapshots: urlpattern-polyfill@8.0.2: {} + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -21146,7 +21222,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - victory-vendor@36.9.2: + victory-vendor@37.3.5: dependencies: '@types/d3-array': 3.2.1 '@types/d3-ease': 3.0.2 From 20478852b1ccb957207982ad2903aaab0ff427ed Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Tue, 7 Jan 2025 15:18:02 +0530 Subject: [PATCH 10/13] fix: More testing --- packages/create-toolpad-app/src/generateProject.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts index a04ccb93112..0ea3b8e7641 100644 --- a/packages/create-toolpad-app/src/generateProject.ts +++ b/packages/create-toolpad-app/src/generateProject.ts @@ -1,9 +1,6 @@ // Common files for all apps import theme from './templates/theme'; import eslintConfig from './templates/eslintConfig'; -import nextConfig from './templates/nextConfig'; -import nextTypes from './templates/nextTypes'; -import tsConfig from './templates/tsConfig'; import readme from './templates/readme'; import gitignore from './templates/gitignore'; import ordersPage from './templates/ordersPage'; @@ -24,7 +21,9 @@ import viteFirebaseAuth from './templates/vite/auth/firebase'; import viteFirebaseConfig from './templates/vite/auth/firebaseConfig'; // Nextjs specific files - +import tsConfig from './templates/tsConfig'; +import nextConfig from './templates/nextConfig'; +import nextTypes from './templates/nextTypes'; // App router specific files import rootLayout from './templates/nextjs/nextjs-app/rootLayout'; import dashboardLayout from './templates/nextjs/nextjs-app/dashboardLayout'; @@ -58,7 +57,6 @@ export default function generateProject( const commonFiles = new Map([ ['theme.ts', { content: theme }], ['.eslintrc.json', { content: eslintConfig }], - ['tsconfig.json', { content: tsConfig }], ['README.md', { content: readme }], ['.gitignore', { content: gitignore }], [ @@ -72,7 +70,7 @@ export default function generateProject( switch (options.framework) { case 'vite': { const viteFiles = new Map([ - ['vite.config.ts', { content: viteConfig }], + ['vite.config.mts', { content: viteConfig }], ['src/main.tsx', { content: viteMain(options) }], ['src/layouts/dashboard.tsx', { content: viteDashboardLayout(options) }], ['src/App.tsx', { content: viteApp(options) }], @@ -99,6 +97,7 @@ export default function generateProject( case 'nextjs': default: { const nextCommonFiles = new Map([ + ['tsconfig.json', { content: tsConfig }], ['next-env.d.ts', { content: nextTypes }], ['next.config.mjs', { content: nextConfig(options) }], ]); From 85376a9359bcf02fee005587f593ea530207e7c5 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Tue, 7 Jan 2025 19:28:31 +0530 Subject: [PATCH 11/13] wip: Remove `react-router-dom` --- packages/create-toolpad-app/src/templates/vite/App.ts | 6 +++--- playground/vite/src/App.tsx | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/create-toolpad-app/src/templates/vite/App.ts b/packages/create-toolpad-app/src/templates/vite/App.ts index a2c829bf5a5..a1ecd993f5a 100644 --- a/packages/create-toolpad-app/src/templates/vite/App.ts +++ b/packages/create-toolpad-app/src/templates/vite/App.ts @@ -4,14 +4,14 @@ const appTemplate: Template = (options) => { return `import * as React from 'react'; import DashboardIcon from '@mui/icons-material/Dashboard'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import { Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router'; ${ options.auth - ? `import { AppProvider } from '@toolpad/core/react-router-dom'; + ? `import { AppProvider } from '@toolpad/core/react-router'; import type { Navigation, Authentication } from '@toolpad/core/AppProvider'; import { firebaseSignOut, onAuthStateChanged } from './firebase/auth'; import SessionContext, { type Session } from './SessionContext';` - : `import { AppProvider, type Navigation } from '@toolpad/core/react-router-dom';` + : `import { AppProvider, type Navigation } from '@toolpad/core/react-router';` } const NAVIGATION: Navigation = [ diff --git a/playground/vite/src/App.tsx b/playground/vite/src/App.tsx index ea7cb04796c..cef8b4bbbad 100644 --- a/playground/vite/src/App.tsx +++ b/playground/vite/src/App.tsx @@ -3,7 +3,6 @@ import DashboardIcon from '@mui/icons-material/Dashboard'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Outlet } from 'react-router'; import { ReactRouterAppProvider } from '@toolpad/core/react-router'; -import type { Navigation } from '@toolpad/core/AppProvider'; const NAVIGATION: Navigation = [ { From eb0c16edb6f702c0da2af8a40e9754f9de60d163 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 8 Jan 2025 14:34:11 +0530 Subject: [PATCH 12/13] fix: Change imports for framework specific providers --- .../create-toolpad-app/src/templates/packageJson.ts | 2 +- .../create-toolpad-app/src/templates/vite/App.ts | 12 ++++++------ .../src/templates/vite/auth/signin.ts | 2 +- .../src/templates/vite/dashboardLayout.ts | 4 ++-- .../create-toolpad-app/src/templates/vite/main.ts | 2 +- playground/vite/src/App.tsx | 1 + 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/create-toolpad-app/src/templates/packageJson.ts b/packages/create-toolpad-app/src/templates/packageJson.ts index 959595c576f..c952d572e2a 100644 --- a/packages/create-toolpad-app/src/templates/packageJson.ts +++ b/packages/create-toolpad-app/src/templates/packageJson.ts @@ -50,7 +50,7 @@ const packageJson: PackageJsonTemplate = (options) => { devDependencies['eslint-config-next'] = '^15'; devDependencies['@types/node'] = '^20'; } else if (framework === 'vite') { - dependencies['react-router-dom'] = '^6'; + dependencies['react-router'] = '^7'; devDependencies['@vitejs/plugin-react'] = '^4.3.2'; devDependencies.vite = '^5.4.8'; } diff --git a/packages/create-toolpad-app/src/templates/vite/App.ts b/packages/create-toolpad-app/src/templates/vite/App.ts index a1ecd993f5a..7afe06e45d1 100644 --- a/packages/create-toolpad-app/src/templates/vite/App.ts +++ b/packages/create-toolpad-app/src/templates/vite/App.ts @@ -7,11 +7,11 @@ import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Outlet } from 'react-router'; ${ options.auth - ? `import { AppProvider } from '@toolpad/core/react-router'; + ? `import { ReactRouterAppProvider } from '@toolpad/core/react-router'; import type { Navigation, Authentication } from '@toolpad/core/AppProvider'; import { firebaseSignOut, onAuthStateChanged } from './firebase/auth'; import SessionContext, { type Session } from './SessionContext';` - : `import { AppProvider, type Navigation } from '@toolpad/core/react-router';` + : `import { ReactRouterAppProvider } from '@toolpad/core/react-router';\nimport type { Navigation } from '@toolpad/core/AppProvider';` } const NAVIGATION: Navigation = [ @@ -78,7 +78,7 @@ export default function App() { }, []); return ( - - + );` : ` return ( - + - + );` } }`; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts index 03090c6b8ac..bbb0d580cf3 100644 --- a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts +++ b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts @@ -17,7 +17,7 @@ import * as React from 'react'; import Alert from '@mui/material/Alert'; import LinearProgress from '@mui/material/LinearProgress'; import { SignInPage } from '@toolpad/core/SignInPage'; -import { Navigate, useNavigate } from 'react-router-dom'; +import { Navigate, useNavigate } from 'react-router'; import { useSession, type Session } from '../SessionContext'; import { ${[ hasGoogleProvider && 'signInWithGoogle', diff --git a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts index 74f6ccfeb2e..d78e250a7c3 100644 --- a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts +++ b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts @@ -7,8 +7,8 @@ const dashboardTemplate: Template = (options) => { ${ auth ? `import LinearProgress from '@mui/material/LinearProgress'; -import { Outlet, Navigate, useLocation } from 'react-router-dom';` - : `import { Outlet } from 'react-router-dom';` +import { Outlet, Navigate, useLocation } from 'react-router';` + : `import { Outlet } from 'react-router';` } import { DashboardLayout } from '@toolpad/core/DashboardLayout'; import { PageContainer } from '@toolpad/core/PageContainer'; diff --git a/packages/create-toolpad-app/src/templates/vite/main.ts b/packages/create-toolpad-app/src/templates/vite/main.ts index 730825af52a..a7e9ce9728b 100644 --- a/packages/create-toolpad-app/src/templates/vite/main.ts +++ b/packages/create-toolpad-app/src/templates/vite/main.ts @@ -5,7 +5,7 @@ const mainTemplate: Template = (options) => { return `import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { createBrowserRouter, RouterProvider } from 'react-router'; import App from './App'; import Layout from './layouts/dashboard'; import DashboardPage from './pages'; diff --git a/playground/vite/src/App.tsx b/playground/vite/src/App.tsx index cef8b4bbbad..ea7cb04796c 100644 --- a/playground/vite/src/App.tsx +++ b/playground/vite/src/App.tsx @@ -3,6 +3,7 @@ import DashboardIcon from '@mui/icons-material/Dashboard'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Outlet } from 'react-router'; import { ReactRouterAppProvider } from '@toolpad/core/react-router'; +import type { Navigation } from '@toolpad/core/AppProvider'; const NAVIGATION: Navigation = [ { From bd2699dbd0835e8c08447735ba7131560ebff7f2 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Thu, 9 Jan 2025 19:36:13 +0530 Subject: [PATCH 13/13] fix: Add Firebase docs link --- .../toolpad/core/components/sign-in-page/sign-in-page.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index 44d3429b16b..9abf4d7bef6 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -228,14 +228,12 @@ VITE_FIREBASE_MESSAGE_SENDER_ID= VITE_FIREBASE_APP_ID= ``` -You can find these in the Firebase project settings. - :::info -Head to the [integration docs](https://mui.com/toolpad/core/integrations/react-router/) for code examples on integrating Firebase authentication with Vite and React Router. +Head to the [Firebase docs](https://firebase.google.com/docs/auth/) for details around configuring authentication with Firebase. You can also find code examples in the [integration docs for Vite & React Router](https://mui.com/toolpad/core/integrations/react-router/). ::: :::success -If you're using [create-toolpad-app](/toolpad/core/introduction/installation/), or the default [Vite with Firebase authentication example](https://github.com/mui/toolpad/tree/master/examples/core/firebase-vite/), the integration is already set up for you. +If you're using [create-toolpad-app](/toolpad/core/introduction/installation/), or the default [Vite with Firebase authentication example](https://github.com/mui/toolpad/tree/master/examples/core/firebase-vite/), the integration code is already provided. ::: ## Customization