Skip to content

Commit

Permalink
[cli] Refactor setup (#4597)
Browse files Browse the repository at this point in the history
  • Loading branch information
bharatkashyap authored Jan 23, 2025
1 parent f7e47ef commit ca6354f
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 229 deletions.
52 changes: 52 additions & 0 deletions packages/create-toolpad-app/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import chalk from 'chalk';
import { execa } from 'execa';
import type { GenerateProjectOptions } from './types';
import generateProject from './generateProject';
import writeFiles from './writeFiles';

export async function scaffoldCoreProject(options: GenerateProjectOptions): Promise<void> {
// 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(options.absolutePath)}`,
);
// eslint-disable-next-line no-console
console.log();

const packageManager = options.packageManager;

const files = generateProject(options);
await writeFiles(options.absolutePath, files);

if (options.install) {
// 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: options.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(options.absolutePath)}`,
);
// eslint-disable-next-line no-console
console.log();

if (options.auth) {
// eslint-disable-next-line no-console
console.log(
`${chalk.cyan('info')} - Bootstrapped ${chalk.cyan('env.local')} with empty values. See https://authjs.dev/getting-started on how to add your credentials.`,
);
// eslint-disable-next-line no-console
console.log();
}
}
265 changes: 39 additions & 226 deletions packages/create-toolpad-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,220 +1,32 @@
#!/usr/bin/env node

import * as fs from 'fs/promises';
import { constants as fsConstants } from 'fs';
import path from 'path';
import yargs from 'yargs';
import { input, confirm, select, checkbox } from '@inquirer/prompts';
import chalk from 'chalk';
import { errorFrom } from '@toolpad/utils/errors';
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 type { SupportedAuthProvider } from '@toolpad/core/SignInPage';
import generateProject from './generateProject';
import generateStudioProject from './generateStudioProject';
import writeFiles from './writeFiles';
import { bashResolvePath } from '@toolpad/utils/cli';
import { downloadAndExtractExample } from './examples';
import type { PackageJson } from './templates/packageType';
import type {
SupportedFramework,
SupportedRouter,
PackageManager,
GenerateProjectOptions,
} from './types';

/**
* Find package.json of the create-toolpad-app package
*/
async function findCtaPackageJson() {
const ctaPackageJsonPath = path.resolve(__dirname, '../package.json');
const content = await fs.readFile(ctaPackageJsonPath, 'utf8');
const packageJson = JSON.parse(content);
return packageJson;
}
import type { SupportedFramework, SupportedRouter, GenerateProjectOptions } from './types';
import { findCtaPackageJson, getPackageManager } from './package';
import { scaffoldCoreProject } from './core';
import { scaffoldStudioProject } from './studio';
import { validatePath } from './validation';

declare global {
interface Error {
code?: unknown;
}
}

function getPackageManager(): PackageManager {
const userAgent = process.env.npm_config_user_agent;

if (userAgent) {
if (userAgent.startsWith('yarn')) {
return 'yarn';
}
if (userAgent.startsWith('pnpm')) {
return 'pnpm';
}
if (userAgent.startsWith('npm')) {
return 'npm';
}
}
return 'npm';
}

// From https://github.com/vercel/next.js/blob/canary/packages/create-next-app/helpers/is-folder-empty.ts
async function isFolderEmpty(pathDir: string): Promise<boolean> {
const validFiles = [
'.DS_Store',
'.git',
'.gitattributes',
'.gitignore',
'.gitlab-ci.yml',
'.hg',
'.hgcheck',
'.hgignore',
'.idea',
'.npmignore',
'.travis.yml',
'LICENSE',
'Thumbs.db',
'docs',
'mkdocs.yml',
'npm-debug.log',
'yarn-debug.log',
'yarn-error.log',
'yarnrc.yml',
'.yarn',
];

const conflicts = await fs.readdir(pathDir);

conflicts
.filter((file) => !validFiles.includes(file))
// Support IntelliJ IDEA-based editors
.filter((file) => !/\.iml$/.test(file));

if (conflicts.length > 0) {
return false;
}
return true;
}

// Detect the package manager
const packageManager = getPackageManager();

const validatePath = async (relativePath: string): Promise<boolean | string> => {
const absolutePath = bashResolvePath(relativePath);

try {
await fs.access(absolutePath, fsConstants.F_OK);

// Directory exists, verify if it's empty to proceed

if (await isFolderEmpty(absolutePath)) {
return true;
}
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) {
// Directory does not exist, create it
const error = errorFrom(rawError);
if (error.code === 'ENOENT') {
await fs.mkdir(absolutePath, { recursive: true });
return true;
}
// Unexpected error, let it bubble up and crash the process
throw error;
}
};

// Create a new `package.json` file and install dependencies
const scaffoldStudioProject = async (absolutePath: string, installFlag: boolean): Promise<void> => {
// eslint-disable-next-line no-console
console.log();
// eslint-disable-next-line no-console
console.log(
`${chalk.cyan('info')} - Creating Toolpad Studio project in ${chalk.cyan(absolutePath)}`,
);
// eslint-disable-next-line no-console
console.log();
const options: GenerateProjectOptions = {
name: path.basename(absolutePath),
absolutePath,
projectType: 'studio',
packageManager,
};
const files = generateStudioProject(options);

await writeFiles(absolutePath, files);

if (installFlag) {
// 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')} - Dependencies installed successfully!`);
// eslint-disable-next-line no-console
console.log();
}
};

const scaffoldCoreProject = async (options: GenerateProjectOptions): Promise<void> => {
// 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(options.absolutePath)}`,
);
// eslint-disable-next-line no-console
console.log();
const pkg = await findCtaPackageJson();
if (!options.coreVersion) {
options.coreVersion = pkg.version;
}
const files = generateProject(options);
await writeFiles(options.absolutePath, files);

if (options.install) {
// 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: options.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(options.absolutePath)}`,
);
// eslint-disable-next-line no-console
console.log();

if (options.auth) {
// eslint-disable-next-line no-console
console.log(
`${chalk.cyan('info')} - Bootstrapped ${chalk.cyan('env.local')} with empty values. See https://authjs.dev/getting-started on how to add your credentials.`,
);
// eslint-disable-next-line no-console
console.log();
}
};

// Run the CLI interaction with Inquirer.js
const run = async () => {
const pkgJson: PackageJson = (await readJsonFile(
path.resolve(__dirname, `../package.json`),
)) as any;
const pkgJson = await findCtaPackageJson();
const packageManager = getPackageManager();

invariant(pkgJson.engines?.node, 'Missing node version in package.json');

// check the node version before create
if (!satisfies(process.version, pkgJson.engines.node)) {
// eslint-disable-next-line no-console
console.log(
Expand Down Expand Up @@ -259,27 +71,29 @@ const run = async () => {
const example = args.example as string;

if (pathArg) {
const pathValidOrError = await validatePath(pathArg);
if (typeof pathValidOrError === 'string') {
// eslint-disable-next-line no-console
console.log();
// eslint-disable-next-line no-console
console.log(pathValidOrError);
// eslint-disable-next-line no-console
console.log();
process.exit(1);
if (!example) {
const pathValidOrError = await validatePath(pathArg, true);
if (typeof pathValidOrError === 'string') {
// eslint-disable-next-line no-console
console.log();
// eslint-disable-next-line no-console
console.log(pathValidOrError);
// eslint-disable-next-line no-console
console.log();
process.exit(1);
}
} else {
await validatePath(pathArg);
}
}
let projectPath = pathArg;

let projectPath = pathArg;
if (!pathArg) {
projectPath = await input({
message: example
? `Enter path of directory to download example "${chalk.cyan(example)}" into`
: 'Enter path of directory to bootstrap new app',
// This check is only necessary if an empty app is being bootstrapped,
// not if an example is being downloaded.
validate: example ? () => true : validatePath,
validate: (pathInput) => validatePath(pathInput, !example),
default: '.',
});
}
Expand All @@ -289,16 +103,11 @@ const run = async () => {
let hasNodemailerProvider = false;
let hasPasskeyProvider = false;

// If the user has provided an example, download and extract it
if (example) {
await downloadAndExtractExample(absolutePath, example);
}

// If the studio flag is set, create a new project with Toolpad Studio
else if (studioFlag) {
await scaffoldStudioProject(absolutePath, installFlag);
} else if (studioFlag) {
await scaffoldStudioProject(absolutePath, installFlag, packageManager);
} else {
// Otherwise, create a new project with Toolpad Core
const frameworkOption: SupportedFramework = await select({
message: 'Which framework would you like to use?',
default: 'nextjs',
Expand Down Expand Up @@ -367,10 +176,10 @@ const run = async () => {
hasPasskeyProvider = authProviderOptions?.includes('passkey');
}

const options = {
const options: GenerateProjectOptions = {
name: path.basename(absolutePath),
absolutePath,
coreVersion: args.coreVersion,
coreVersion: args.coreVersion ?? pkgJson.version,
router: routerOption,
framework: frameworkOption,
auth: authFlag,
Expand All @@ -379,18 +188,22 @@ const run = async () => {
hasCredentialsProvider: authProviderOptions?.includes('credentials'),
hasNodemailerProvider,
hasPasskeyProvider,
packageManager,
};

await scaffoldCoreProject(options);
}

const changeDirectoryInstruction =
/* `path.relative` is truth-y if the relative path
* between `absolutePath` and `process.cwd()`
* is not empty
*/
path.relative(process.cwd(), absolutePath)
? ` cd ${path.relative(process.cwd(), absolutePath)}\n`
: '';
let changeDirectoryInstruction = '';
if (example) {
if (!path.relative(process.cwd(), absolutePath)) {
changeDirectoryInstruction = ` cd ./${example}\n`;
} else {
changeDirectoryInstruction = ` cd ${path.basename(absolutePath)}/${example}\n`;
}
} else if (path.relative(process.cwd(), absolutePath)) {
changeDirectoryInstruction = ` cd ${path.relative(process.cwd(), absolutePath)}\n`;
}

const installInstruction = example || !installFlag ? ` ${packageManager} install\n` : '';

Expand Down
Loading

0 comments on commit ca6354f

Please sign in to comment.