From c93afe3859959c76baf442e17590b1a66ee2a537 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 27 Mar 2024 00:07:06 +0900 Subject: [PATCH] [cli] Introduce `--core` (#3304) --- .../public/gitignoreTemplate | 55 ++- .../create-toolpad-app/src/generateProject.ts | 409 ++++++++++++++++++ packages/create-toolpad-app/src/index.ts | 58 ++- packages/create-toolpad-app/src/writeFiles.ts | 22 + 4 files changed, 524 insertions(+), 20 deletions(-) create mode 100644 packages/create-toolpad-app/src/generateProject.ts create mode 100644 packages/create-toolpad-app/src/writeFiles.ts diff --git a/packages/create-toolpad-app/public/gitignoreTemplate b/packages/create-toolpad-app/public/gitignoreTemplate index a56149af866..c8142b9775a 100644 --- a/packages/create-toolpad-app/public/gitignoreTemplate +++ b/packages/create-toolpad-app/public/gitignoreTemplate @@ -1,78 +1,99 @@ # Logs + logs -*.log -npm-debug.log* +_.log +npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data + pids -*.pid -*.seed -*.pid.lock +_.pid +_.seed +\*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover + lib-cov # Coverage directory used by tools like istanbul + coverage -*.lcov +\*.lcov # nyc test coverage + .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + .grunt # Bower dependency directory (https://bower.io/) + bower_components # node-waf configuration + .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) + build/Release # Dependency directories + node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) + web_modules/ # TypeScript cache -*.tsbuildinfo + +\*.tsbuildinfo # Optional npm cache directory + .npm # Optional eslint cache + .eslintcache # Optional stylelint cache + .stylelintcache # Microbundle cache + .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history + .node_repl_history # Output of 'npm pack' -*.tgz + +\*.tgz # Yarn Integrity file + .yarn-integrity # dotenv environment variable files + .env .env.development.local .env.test.local @@ -80,49 +101,65 @@ web_modules/ .env.local # parcel-bundler cache (https://parceljs.org/) + .cache .parcel-cache # Next.js build output + .next out # Nuxt.js build / generate output + .nuxt dist # Gatsby files + .cache/ + # Comment in the public line in if your project uses Gatsby and not Next.js + # https://nextjs.org/blog/next-9-1#public-directory-support + # public # vuepress build output + .vuepress/dist # vuepress v2.x temp and cache directory + .temp .cache # Docusaurus cache and generated files + .docusaurus # Serverless directories + .serverless/ # FuseBox cache + .fusebox/ # DynamoDB Local files + .dynamodb/ # TernJS port file + .tern-port # Stores VSCode versions used for testing VSCode extensions + .vscode-test # yarn v2 + .yarn/cache .yarn/unplugged .yarn/build-state.yml diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts new file mode 100644 index 00000000000..3355f0debd9 --- /dev/null +++ b/packages/create-toolpad-app/src/generateProject.ts @@ -0,0 +1,409 @@ +import path from 'path'; +import { PackageJson } from './packageType'; + +interface GenerateProjectOptions { + name: string; +} + +export default function generateProject( + options: GenerateProjectOptions, +): Map { + const rootLayoutContent = ` + import { AppProvider } from '@toolpad/core'; + import theme from '../theme'; + + export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); + } + `; + + const dashboardLayoutContent = ` + import { + AppBar, + Badge, + Box, + Container, + Divider, + Drawer, + IconButton, + List, + ListItemButton, + ListItemIcon, + Toolbar, + } from "@mui/material"; + + import HomeIcon from "@mui/icons-material/Home"; + import SettingsIcon from "@mui/icons-material/Settings"; + import NotificationsIcon from "@mui/icons-material/Notifications"; + + export default function Layout({ + children, + }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {children} + + + ); + } + `; + + const rootPageContent = ` + import Link from "next/link"; + import { Button, Container, Typography, Box } from "@mui/material"; + + export default function Home() { + return ( + + + + Welcome to{" "} + + Toolpad Core! + + + + + Get started by editing (dashboard)/page/page.tsx + + + + + + + + + + ); + } + `; + + const dashboardPageContent = ` + import { Typography, Container } from "@mui/material"; + export default function Home() { + return ( +
+ + + Dashboard content! + + +
+ ); + } + `; + + const themeContent = ` + "use client" + import { Roboto } from "next/font/google"; + import { createTheme } from "@mui/material/styles"; + + const roboto = Roboto({ + weight: ["300", "400", "500", "700"], + subsets: ["latin"], + display: "swap", + }); + + const theme = createTheme({ + typography: { + fontFamily: roboto.style.fontFamily, + }, + components: { + MuiAppBar: { + styleOverrides: { + root: { + boxShadow: "none", + }, + }, + }, + MuiList: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: "28px", + }, + }, + }, + }, + }); + + export default theme; + `; + + const eslintConfigContent = `{ + "extends": "next/core-web-vitals" + } + `; + + const nextTypesContent = `/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. + `; + + const nextConfigContent = ` + /** @type {import('next').NextConfig} */ + const nextConfig = {}; + export default nextConfig; + `; + + const tsConfigContent = `{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] + } + `; + + const packageJson: PackageJson = { + name: path.basename(options.name), + version: '0.1.0', + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + lint: 'next lint', + }, + dependencies: { + react: '^18', + 'react-dom': '^18', + next: '^14', + '@toolpad/core': 'latest', + '@mui/material': '^5', + '@mui/icons-material': '^5', + '@emotion/react': '^11', + '@emotion/styled': '^11', + '@emotion/cache': '^11', + }, + devDependencies: { + typescript: '^5', + '@types/node': '^20', + '@types/react': '^18', + '@types/react-dom': '^18', + eslint: '^8', + 'eslint-config-next': '^14', + }, + }; + + const gitignoreTemplate = ` + # Logs + logs + *.log + npm-debug.log* + yarn-debug.log* + yarn-error.log* + lerna-debug.log* + .pnpm-debug.log* + + # Diagnostic reports (https://nodejs.org/api/report.html) + report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + + # Runtime data + pids + *.pid + *.seed + *.pid.lock + + # Directory for instrumented libs generated by jscoverage/JSCover + lib-cov + + # Coverage directory used by tools like istanbul + coverage + *.lcov + + # nyc test coverage + .nyc_output + + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + .grunt + + # Bower dependency directory (https://bower.io/) + bower_components + + # node-waf configuration + .lock-wscript + + # Compiled binary addons (https://nodejs.org/api/addons.html) + build/Release + + # Dependency directories + node_modules/ + jspm_packages/ + + # Snowpack dependency directory (https://snowpack.dev/) + web_modules/ + + # TypeScript cache + *.tsbuildinfo + + # Optional npm cache directory + .npm + + # Optional eslint cache + .eslintcache + + # Optional stylelint cache + .stylelintcache + + # Microbundle cache + .rpt2_cache/ + .rts2_cache_cjs/ + .rts2_cache_es/ + .rts2_cache_umd/ + + # Optional REPL history + .node_repl_history + + # Output of 'npm pack' + *.tgz + + # Yarn Integrity file + .yarn-integrity + + # dotenv environment variable files + .env + .env.development.local + .env.test.local + .env.production.local + .env.local + + # parcel-bundler cache (https://parceljs.org/) + .cache + .parcel-cache + + # Next.js build output + .next + out + + # Nuxt.js build / generate output + .nuxt + dist + + # Gatsby files + .cache/ + # Comment in the public line in if your project uses Gatsby and not Next.js + # https://nextjs.org/blog/next-9-1#public-directory-support + # public + + # vuepress build output + .vuepress/dist + + # vuepress v2.x temp and cache directory + .temp + .cache + + # Docusaurus cache and generated files + .docusaurus + + # Serverless directories + .serverless/ + + # FuseBox cache + .fusebox/ + + # DynamoDB Local files + .dynamodb/ + + # TernJS port file + .tern-port + + # Stores VSCode versions used for testing VSCode extensions + .vscode-test + + # yarn v2 + .yarn/cache + .yarn/unplugged + .yarn/build-state.yml + .yarn/install-state.gz + `; + + // Define all files to be created up front + return new Map([ + ['app/api/auth/[...nextAuth]/route.ts', { content: '' }], + ['app/auth/[...path]/page.tsx', { content: '' }], + ['app/(dashboard)/page/page.tsx', { content: dashboardPageContent }], + ['app/(dashboard)/layout.tsx', { content: dashboardLayoutContent }], + ['app/layout.tsx', { content: rootLayoutContent }], + ['app/page.tsx', { content: rootPageContent }], + ['theme.ts', { content: themeContent }], + ['next-env.d.ts', { content: nextTypesContent }], + ['next.config.mjs', { content: nextConfigContent }], + ['.eslintrc.json', { content: eslintConfigContent }], + ['tsconfig.json', { content: tsConfigContent }], + ['package.json', { content: JSON.stringify(packageJson, null, 2) }], + ['.gitignore', { content: gitignoreTemplate }], + // ... + ]); +} diff --git a/packages/create-toolpad-app/src/index.ts b/packages/create-toolpad-app/src/index.ts index 8a5c2c7dbd0..e8c17ef0631 100644 --- a/packages/create-toolpad-app/src/index.ts +++ b/packages/create-toolpad-app/src/index.ts @@ -7,12 +7,14 @@ import yargs from 'yargs'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { errorFrom } from '@toolpad/utils/errors'; -import { execaCommand } from 'execa'; +import { execa } from 'execa'; import { satisfies } from 'semver'; import { readJsonFile } from '@toolpad/utils/fs'; import invariant from 'invariant'; import { bashResolvePath } from '@toolpad/utils/cli'; import { PackageJson } from './packageType'; +import generateProject from './generateProject'; +import writeFiles from './writeFiles'; import { downloadAndExtractExample } from './examples'; type PackageManager = 'npm' | 'pnpm' | 'yarn'; @@ -36,7 +38,7 @@ function getPackageManager(): PackageManager { return 'npm'; } } - return 'yarn'; + return 'npm'; } // From https://github.com/vercel/next.js/blob/canary/packages/create-next-app/helpers/is-folder-empty.ts @@ -91,7 +93,7 @@ const validatePath = async (relativePath: string): Promise => if (await isFolderEmpty(absolutePath)) { return true; } - return `${chalk.red('error')} - The directory at ${chalk.blue( + return `${chalk.red('error')} - The directory at ${chalk.cyan( absolutePath, )} contains files that could conflict. Either use a new directory, or remove conflicting files.`; } catch (rawError: unknown) { @@ -112,7 +114,7 @@ const scaffoldProject = async (absolutePath: string, installFlag: boolean): Prom console.log(); // eslint-disable-next-line no-console console.log( - `${chalk.blue('info')} - Creating Toolpad Studio project in ${chalk.blue(absolutePath)}`, + `${chalk.cyan('info')} - Creating Toolpad Studio project in ${chalk.cyan(absolutePath)}`, ); // eslint-disable-next-line no-console console.log(); @@ -132,11 +134,11 @@ const scaffoldProject = async (absolutePath: string, installFlag: boolean): Prom const DEFAULT_GENERATED_GITIGNORE_FILE = '.gitignore'; // eslint-disable-next-line no-console - console.log(`${chalk.blue('info')} - Initializing package.json file`); + console.log(`${chalk.cyan('info')} - Initializing package.json file`); await fs.writeFile(path.join(absolutePath, 'package.json'), JSON.stringify(packageJson, null, 2)); // eslint-disable-next-line no-console - console.log(`${chalk.blue('info')} - Initializing .gitignore file`); + console.log(`${chalk.cyan('info')} - Initializing .gitignore file`); await fs.copyFile( path.resolve(__dirname, `./gitignoreTemplate`), path.join(absolutePath, DEFAULT_GENERATED_GITIGNORE_FILE), @@ -144,13 +146,11 @@ const scaffoldProject = async (absolutePath: string, installFlag: boolean): Prom if (installFlag) { // eslint-disable-next-line no-console - console.log(`${chalk.blue('info')} - Installing dependencies`); + console.log(`${chalk.cyan('info')} - Installing dependencies`); // eslint-disable-next-line no-console console.log(); - const installVerb = packageManager === 'yarn' ? 'add' : 'install'; - const command = `${packageManager} ${installVerb} @toolpad/studio`; - await execaCommand(command, { stdio: 'inherit', cwd: absolutePath }); + await execa(packageManager, ['install'], { stdio: 'inherit', cwd: absolutePath }); // eslint-disable-next-line no-console console.log(); @@ -161,6 +161,35 @@ const scaffoldProject = async (absolutePath: string, installFlag: boolean): Prom } }; +const scaffoldCoreProject = async (absolutePath: string): Promise => { + // eslint-disable-next-line no-console + console.log(); + // eslint-disable-next-line no-console + console.log( + `${chalk.cyan('info')} - Creating Toolpad Core project in ${chalk.cyan(absolutePath)}`, + ); + // eslint-disable-next-line no-console + console.log(); + const files = generateProject({ name: path.basename(absolutePath) }); + await writeFiles(absolutePath, files); + + // eslint-disable-next-line no-console + console.log(`${chalk.cyan('info')} - Installing dependencies`); + // eslint-disable-next-line no-console + console.log(); + + await execa(packageManager, ['install'], { stdio: 'inherit', cwd: absolutePath }); + + // eslint-disable-next-line no-console + console.log(); + // eslint-disable-next-line no-console + console.log( + `${chalk.green('success')} - Created Toolpad Core project at ${chalk.cyan(absolutePath)}`, + ); + // eslint-disable-next-line no-console + console.log(); +}; + // Run the CLI interaction with Inquirer.js const run = async () => { const pkgJson: PackageJson = (await readJsonFile( @@ -187,6 +216,11 @@ const run = async () => { type: 'string', describe: 'The path where the Toolpad Studio project directory will be created', }) + .option('core', { + type: 'boolean', + describe: 'Create a new project with Toolpad Core', + default: false, + }) .option('install', { type: 'boolean', describe: 'Install dependencies', @@ -197,11 +231,11 @@ const run = async () => { describe: 'The name of one of the available examples. See https://github.com/mui/mui-toolpad/tree/master/examples.', }) - .help().argv; const pathArg = args._?.[0] as string; const installFlag = args.install as boolean; + const coreFlag = args.core as boolean; if (pathArg) { const pathValidOrError = await validatePath(pathArg); @@ -233,6 +267,8 @@ const run = async () => { if (args.example) { await downloadAndExtractExample(absolutePath, args.example); + } else if (coreFlag) { + await scaffoldCoreProject(absolutePath); } else { await scaffoldProject(absolutePath, installFlag); } diff --git a/packages/create-toolpad-app/src/writeFiles.ts b/packages/create-toolpad-app/src/writeFiles.ts new file mode 100644 index 00000000000..5d35d4dcba5 --- /dev/null +++ b/packages/create-toolpad-app/src/writeFiles.ts @@ -0,0 +1,22 @@ +import path from 'path'; +import fs from 'fs/promises'; + +export default async function writeFiles( + absolutePath: string, + files: Map, +): Promise { + // Get all directories and deduplicate + const dirs = new Set(Array.from(files.keys(), (filePath) => path.dirname(filePath))); + + // Create directories, use recursive option to create parent directories + await Promise.all( + Array.from(dirs, (dirPath) => fs.mkdir(path.join(absolutePath, dirPath), { recursive: true })), + ); + + // Write all the files + await Promise.all( + Array.from(files.entries(), ([filePath, { content }]) => + fs.writeFile(path.join(absolutePath, filePath), content), + ), + ); +}