#!/usr/bin/env node import { Command } from "commander"; import prompts from "prompts"; import chalk from "chalk"; import path from "path"; import fs from "fs/promises"; import { execCommand, log, logSuccess, logError, logWarning } from "./utils.js"; import { globalCssTemplate } from "./templates/globalCss.js"; import { metroConfigTemplate } from "./templates/metroConfig.js"; import { appLayoutTemplate } from "./templates/appLayout.js"; const program = new Command(); interface ProjectConfig { projectName: string; projectPath: string; } async function createExpoApp(config: ProjectConfig): Promise { log("\nšŸ“¦ Step 1: Creating Expo app...\n"); await execCommand( `bunx create-expo-app@latest ${config.projectName} --template blank-typescript`, process.cwd(), "Creating Expo app" ); } async function installExpoRouter(config: ProjectConfig): Promise { log("\n🧭 Step 2: Installing Expo Router...\n"); await execCommand( "bunx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar", config.projectPath, "Installing Expo Router dependencies" ); // Update package.json main field for Expo Router const packageJsonPath = path.join(config.projectPath, "package.json"); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); packageJson.main = "expo-router/entry"; await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); // Create app directory structure const appDir = path.join(config.projectPath, "app"); await fs.mkdir(appDir, { recursive: true }); // Create a simple index.tsx const indexContent = `import { View, Text } from 'react-native'; export default function HomeScreen() { return ( Welcome to Expo + Uniwind + HeroUI! ); } `; await fs.writeFile(path.join(appDir, "index.tsx"), indexContent); logSuccess("Installed and configured Expo Router"); } async function installUniwind(config: ProjectConfig): Promise { log("\nšŸŽØ Step 3: Installing Uniwind and Tailwind CSS...\n"); await execCommand( "bun add uniwind tailwindcss", config.projectPath, "Installing Uniwind and Tailwind CSS" ); logSuccess("Installed Uniwind and Tailwind CSS"); } async function setupGlobalCSS(config: ProjectConfig): Promise { log("\nšŸ“ Step 4: Setting up global.css...\n"); // Create global.css in project root const globalCssPath = path.join(config.projectPath, "global.css"); await fs.writeFile(globalCssPath, globalCssTemplate); logSuccess("Created global.css with Tailwind, Uniwind, and HeroUI imports"); } async function setupMetroConfig(config: ProjectConfig): Promise { log("\nāš™ļø Step 5: Configuring Metro bundler...\n"); // Create metro.config.js directly with our template const metroConfigPath = path.join(config.projectPath, "metro.config.js"); await fs.writeFile(metroConfigPath, metroConfigTemplate); logSuccess("Configured Metro with Uniwind support"); } async function installHeroUI(config: ProjectConfig): Promise { log("\nšŸ“š Step 6: Installing HeroUI Native and dependencies...\n"); // Install HeroUI Native await execCommand( "bun add heroui-native", config.projectPath, "Installing heroui-native" ); // Install peer dependencies await execCommand( "bun add react-native-screens@~4.16.0 react-native-reanimated@~4.1.1 react-native-gesture-handler@^2.28.0 react-native-worklets@0.5.1 react-native-safe-area-context@~5.6.0 react-native-svg@15.12.1 tailwind-variants@^3.2.2 tailwind-merge@^3.4.0 @gorhom/bottom-sheet@^5", config.projectPath, "Installing peer dependencies" ); logSuccess("Installed all required dependencies"); } async function setupAppLayout(config: ProjectConfig): Promise { log("\nšŸš€ Step 7: Setting up app layout with providers...\n"); const appDir = path.join(config.projectPath, "app"); const layoutPath = path.join(appDir, "_layout.tsx"); // Write the app layout with providers await fs.writeFile(layoutPath, appLayoutTemplate); logSuccess("Configured app/_layout.tsx with HeroUINativeProvider and GestureHandlerRootView"); } async function finalizeSetup(config: ProjectConfig): Promise { log("\n✨ Finalizing setup...\n"); logWarning("Important: You need to run the Metro server to generate TypeScript typings:"); console.log(chalk.dim(` cd ${config.projectName}`)); console.log(chalk.dim(" bunx expo start")); console.log(); logSuccess("Project created successfully!"); console.log(); console.log(chalk.bold.cyan("Next steps:")); console.log(chalk.dim(` cd ${config.projectName}`)); console.log(chalk.dim(" bunx expo start")); console.log(); console.log(chalk.gray("Happy coding! šŸŽ‰")); } async function createProject(): Promise { console.log( chalk.bold.cyan("\nšŸš€ Welcome to ahmtshn-create-app!\n") ); console.log(chalk.gray("This will create an Expo + Uniwind + HeroUI Native project.\n")); // Prompt for project name const response = await prompts({ type: "text", name: "projectName", message: "What is your project name?", initial: "my-app", validate: (value) => { if (!value) return "Project name is required"; if (!/^[a-z0-9-_]+$/i.test(value)) { return "Project name can only contain letters, numbers, hyphens, and underscores"; } return true; }, }); if (!response.projectName) { logError("Project creation cancelled"); process.exit(1); } const config: ProjectConfig = { projectName: response.projectName, projectPath: path.join(process.cwd(), response.projectName), }; try { // Check if directory already exists try { await fs.access(config.projectPath); logError(`Directory "${config.projectName}" already exists!`); process.exit(1); } catch { // Directory doesn't exist, which is good } // Run all setup steps await createExpoApp(config); await installExpoRouter(config); await installUniwind(config); await setupGlobalCSS(config); await setupMetroConfig(config); await installHeroUI(config); await setupAppLayout(config); await finalizeSetup(config); } catch (error) { logError("Failed to create project"); console.error(error); process.exit(1); } } program .name("ahmtshn-create-app") .description("CLI to quickly bootstrap Expo + Uniwind + HeroUI Native projects") .version("1.0.0") .action(createProject); program.parse();