UNPKG

@isthatuzii/create-nano-app

Version:

Desktop application scaffolding tool for the Nano Framework

606 lines (510 loc) • 19.8 kB
import prompts from 'prompts'; import kleur from 'kleur'; import fs from 'fs-extra'; import path from 'path'; import { execa } from 'execa'; import ora from 'ora'; import updateNotifier from 'update-notifier'; import boxen from 'boxen'; import { fileURLToPath } from 'url'; import { showStartupAnimation, showWelcomeMessage, showBrandingBoxes, showCompletionAnimation, showProjectReadyBanner, COMPANY_INFO } from './branding.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Framework configurations const FRAMEWORKS = { 'solid-js-ts': { name: 'SolidJS (TypeScript)', color: 'cyan', dependencies: ['solid-js'], devDependencies: ['vite-plugin-solid', 'typescript', '@types/node'], vitePlugin: 'solid' }, 'solid-js': { name: 'SolidJS (JavaScript)', color: 'blue', dependencies: ['solid-js'], devDependencies: ['vite-plugin-solid'], vitePlugin: 'solid' } }; const PACKAGE_MANAGERS = { npm: { name: 'npm', lockFile: 'package-lock.json' }, yarn: { name: 'yarn', lockFile: 'yarn.lock' }, pnpm: { name: 'pnpm', lockFile: 'pnpm-lock.yaml' }, bun: { name: 'bun', lockFile: 'bun.lockb' } }; // Load package.json for version info const packageJsonPath = path.join(__dirname, '..', 'package.json'); const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); export async function createNanoApp(projectName) { // Check for updates await checkForUpdates(); // Show animated startup await showStartupAnimation(); // Show welcome message showWelcomeMessage(); // Show company branding boxes showBrandingBoxes(); // Validate project name if (!isValidPackageName(projectName)) { console.error(kleur.red(`Invalid project name: ${projectName}`)); process.exit(1); } const targetDir = path.resolve(process.cwd(), projectName); // Check if directory already exists if (fs.existsSync(targetDir)) { const { overwrite } = await prompts({ type: 'confirm', name: 'overwrite', message: `Directory ${kleur.yellow(projectName)} already exists. Overwrite?`, initial: false }); if (!overwrite) { console.log(kleur.yellow('Operation cancelled.')); process.exit(0); } await fs.remove(targetDir); } // Interactive prompts const responses = await prompts([ { type: 'select', name: 'framework', message: 'Select a frontend framework:', choices: Object.entries(FRAMEWORKS).map(([key, framework]) => ({ title: kleur[framework.color](framework.name), value: key })), initial: 0 }, { type: 'select', name: 'packageManager', message: 'Select a package manager:', choices: Object.entries(PACKAGE_MANAGERS).map(([key, pm]) => ({ title: pm.name, value: key })), initial: 0 }, { type: 'confirm', name: 'useTemplate', message: 'Include template UI components?', initial: true }, { type: 'confirm', name: 'installDeps', message: 'Install dependencies?', initial: true }, { type: 'confirm', name: 'installCargoWatch', message: 'Install cargo-watch for hot reload development? (Recommended)', initial: true }, { type: 'confirm', name: 'initGit', message: 'Initialize Git repository?', initial: true }, { type: 'confirm', name: 'setupVSCode', message: 'Setup VS Code workspace configuration?', initial: true }, { type: 'confirm', name: 'runHealthCheck', message: 'Run project health check after creation?', initial: true } ]); if (!responses.framework) { console.log(kleur.yellow('Operation cancelled.')); process.exit(0); } const { framework, packageManager, useTemplate, installDeps, installCargoWatch, initGit, setupVSCode, runHealthCheck } = responses; const selectedFramework = FRAMEWORKS[framework]; console.log(`\n${kleur.green('āœ“')} Creating ${kleur.cyan(projectName)} with ${kleur[selectedFramework.color](selectedFramework.name)}...\n`); // Create project await createProject(projectName, targetDir, framework, packageManager, useTemplate); // Install dependencies if (installDeps) { await installDependencies(targetDir, packageManager); } // Install cargo-watch if (installCargoWatch) { await installCargoWatch(); } // Initialize Git repository if (initGit) { await initializeGitRepository(targetDir, projectName); } // Setup VS Code workspace if (setupVSCode) { await setupVSCodeWorkspace(targetDir, framework); } // Run project health check if (runHealthCheck) { await runProjectHealthCheck(targetDir, projectName); } // Show completion animation await showCompletionAnimation(); // Show project ready banner showProjectReadyBanner(projectName); // Show completion message showCompletionMessage(projectName, packageManager, installDeps, installCargoWatch, initGit, setupVSCode, runHealthCheck); } async function createProject(projectName, targetDir, framework, packageManager, useTemplate) { const spinner = ora('Copying project files...').start(); try { // Create target directory await fs.ensureDir(targetDir); // Copy template files const templateDir = path.join(__dirname, '..', 'templates', framework); await fs.copy(templateDir, targetDir); // Copy common files (Rust backend) const commonDir = path.join(__dirname, '..', 'templates', 'common'); await fs.copy(commonDir, targetDir, { overwrite: false }); // Update package.json with project details await updatePackageJson(targetDir, projectName, framework, packageManager); // Update Cargo.toml await updateCargoToml(targetDir, projectName); // Process README template await processReadmeTemplate(targetDir, projectName, framework, packageManager); // Copy template UI if requested if (!useTemplate) { await removeTemplateUI(targetDir); } spinner.succeed('Project files copied'); } catch (error) { spinner.fail('Failed to copy project files'); throw error; } } async function updatePackageJson(targetDir, projectName, framework, packageManager) { const packageJsonPath = path.join(targetDir, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); const selectedFramework = FRAMEWORKS[framework]; // Update package.json packageJson.name = projectName; packageJson.dependencies = { ...packageJson.dependencies, ...selectedFramework.dependencies.reduce((acc, dep) => { acc[dep] = 'latest'; return acc; }, {}) }; packageJson.devDependencies = { ...packageJson.devDependencies, ...selectedFramework.devDependencies.reduce((acc, dep) => { acc[dep] = 'latest'; return acc; }, {}) }; // Add package manager specific scripts if (packageManager === 'pnpm') { packageJson.scripts = { ...packageJson.scripts, 'nano:hot': 'concurrently "pnpm dev" "cargo watch -x run"' }; } else if (packageManager === 'yarn') { packageJson.scripts = { ...packageJson.scripts, 'nano:hot': 'concurrently "yarn dev" "cargo watch -x run"' }; } await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } async function updateCargoToml(targetDir, projectName) { const cargoTomlPath = path.join(targetDir, 'Cargo.toml'); let cargoToml = await fs.readFile(cargoTomlPath, 'utf-8'); // Update package name cargoToml = cargoToml.replace(/name = ".*"/, `name = "${projectName}"`); cargoToml = cargoToml.replace(/name = ".*"/, `name = "${projectName}"`, 'g'); await fs.writeFile(cargoTomlPath, cargoToml); } async function removeTemplateUI(targetDir) { const templateUIDir = path.join(targetDir, 'src', 'template-ui'); const templateHtml = path.join(targetDir, 'template.html'); const templateConfig = path.join(targetDir, 'vite.template.config.js'); await Promise.all([ fs.remove(templateUIDir).catch(() => {}), fs.remove(templateHtml).catch(() => {}), fs.remove(templateConfig).catch(() => {}) ]); // Update package.json to remove template scripts const packageJsonPath = path.join(targetDir, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); delete packageJson.scripts.template; delete packageJson.scripts['template:build']; delete packageJson.scripts['template:nano']; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } async function installDependencies(targetDir, packageManager) { const spinner = ora(`Installing dependencies with ${packageManager}...`).start(); try { await execa(packageManager, ['install'], { cwd: targetDir, stdio: 'pipe' }); spinner.succeed('Dependencies installed'); } catch (error) { spinner.fail(`Failed to install dependencies with ${packageManager}`); console.log(kleur.yellow('\nYou can install dependencies manually:')); console.log(kleur.gray(` cd ${path.basename(targetDir)}`)); console.log(kleur.gray(` ${packageManager} install`)); } } async function installCargoWatch() { const spinner = ora('Installing cargo-watch...').start(); try { // Check if cargo-watch is already installed await execa('cargo', ['watch', '--version'], { stdio: 'pipe' }); spinner.succeed('cargo-watch already installed'); return; } catch { // Not installed, install it try { await execa('cargo', ['install', 'cargo-watch'], { stdio: 'pipe' }); spinner.succeed('cargo-watch installed'); } catch (error) { spinner.fail('Failed to install cargo-watch'); console.log(kleur.yellow('\nYou can install cargo-watch manually:')); console.log(kleur.gray(' cargo install cargo-watch')); console.log(kleur.dim(' (Required for hot reload development with npm run nano:hot)')); } } } function showCompletionMessage(projectName, packageManager, installDeps, installCargoWatch, initGit, setupVSCode, runHealthCheck) { console.log(kleur.bold('šŸš€ Quick Start Commands:\n')); console.log(`${kleur.gray('1.')} ${kleur.cyan('cd')} ${kleur.yellow(projectName)}`); if (!installDeps) { console.log(`${kleur.gray('2.')} ${kleur.cyan(packageManager + ' install')} ${kleur.dim('# Install dependencies')}`); } if (!installCargoWatch) { console.log(`${kleur.gray('3.')} ${kleur.cyan('cargo install cargo-watch')} ${kleur.dim('# For hot reload development')}`); } console.log(`${kleur.gray('4.')} ${kleur.cyan('cargo check')} ${kleur.dim('# Verify Rust code')}`); console.log(`${kleur.gray('5.')} ${kleur.cyan(packageManager + ' run nano')} ${kleur.dim('# Build and run app')}`); if (installCargoWatch) { console.log(`${kleur.gray('6.')} ${kleur.cyan(packageManager + ' run nano:hot')} ${kleur.dim('# Development with hot reload')}`); } else { console.log(`${kleur.gray('6.')} ${kleur.cyan(packageManager + ' run nano:hot')} ${kleur.dim('# Hot reload (needs cargo-watch)')}`); } console.log(`${kleur.gray('7.')} ${kleur.cyan(packageManager + ' run nano:release')} ${kleur.dim('# Build portable release')}`); if (setupVSCode) { console.log(`\n${kleur.blue('šŸ“ VS Code:')} Workspace configured with Rust extensions and debug settings`); } if (initGit) { console.log(`${kleur.green('šŸ”„ Git:')} Repository initialized with initial commit`); } console.log(`\n${kleur.dim('šŸ“š Resources:')}`); console.log(`${kleur.dim(' Documentation:')} https://nano-framework.dev`); console.log(`${kleur.dim(' Issues:')} https://github.com/imperium-industries/nano/issues`); console.log(`${kleur.dim(' Support:')} ${COMPANY_INFO.email}`); console.log(`\n${kleur.dim('⚔ Made with precision by')} ${kleur.bold(COMPANY_INFO.name)} ${kleur.dim('|')} ${kleur.italic(COMPANY_INFO.subsidiary)}`); console.log(`${kleur.dim(' Professional development tools for modern applications')}\n`); } async function processReadmeTemplate(targetDir, projectName, framework, packageManager) { const readmePath = path.join(targetDir, 'README.md'); let readme = await fs.readFile(readmePath, 'utf-8'); const selectedFramework = FRAMEWORKS[framework]; const selectedPM = PACKAGE_MANAGERS[packageManager]; const frameworkDocs = { 'solid-js': 'https://solidjs.com/docs', 'react': 'https://reactjs.org/docs', 'vue': 'https://vuejs.org/guide/' }; const installCommands = { npm: 'npm install', yarn: 'yarn', pnpm: 'pnpm install', bun: 'bun install' }; // Replace template variables readme = readme .replace(/\{\{PROJECT_NAME\}\}/g, projectName) .replace(/\{\{FRAMEWORK\}\}/g, selectedFramework.name) .replace(/\{\{PACKAGE_MANAGER\}\}/g, packageManager) .replace(/\{\{INSTALL_COMMAND\}\}/g, installCommands[packageManager]) .replace(/\{\{FRAMEWORK_DOCS\}\}/g, frameworkDocs[framework]); await fs.writeFile(readmePath, readme); } async function checkForUpdates() { try { const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 // Check daily }); if (notifier.update) { console.log(boxen( kleur.yellow.bold('šŸ”„ Update Available!\n\n') + kleur.white(`Current version: ${notifier.update.current}\n`) + kleur.green(`Latest version: ${notifier.update.latest}\n\n`) + kleur.cyan('Run: npm install -g @isthatuzii/create-nanoframework-app@latest\n') + kleur.dim('to get the latest features and improvements'), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow' } )); // Wait a moment for user to see the update notice await new Promise(resolve => setTimeout(resolve, 2000)); } } catch (error) { // Silently fail update check } } async function initializeGitRepository(targetDir, projectName) { const spinner = ora('Initializing Git repository...').start(); try { // Initialize git repo await execa('git', ['init'], { cwd: targetDir, stdio: 'pipe' }); // Create initial commit await execa('git', ['add', '.'], { cwd: targetDir, stdio: 'pipe' }); await execa('git', ['commit', '-m', `šŸŽ‰ Initial commit - ${projectName} created with Nano Framework\n\nGenerated by Imperium Industries | Microcode Labs\nContact: ${COMPANY_INFO.email}`], { cwd: targetDir, stdio: 'pipe' }); spinner.succeed('Git repository initialized with initial commit'); } catch (error) { spinner.fail('Failed to initialize Git repository'); console.log(kleur.yellow('\nYou can initialize Git manually:')); console.log(kleur.gray(` cd ${projectName}`)); console.log(kleur.gray(' git init')); console.log(kleur.gray(' git add .')); console.log(kleur.gray(' git commit -m "Initial commit"')); } } async function setupVSCodeWorkspace(targetDir, framework) { const spinner = ora('Setting up VS Code workspace...').start(); try { const vscodeDir = path.join(targetDir, '.vscode'); await fs.ensureDir(vscodeDir); // VS Code settings const settings = { "rust-analyzer.checkOnSave.command": "clippy", "rust-analyzer.cargo.features": "all", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": true }, "files.exclude": { "**/target": true, "**/node_modules": true, "**/dist": true }, "search.exclude": { "**/target": true, "**/node_modules": true, "**/dist": true } }; await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, { spaces: 2 }); // Recommended extensions const extensions = { "recommendations": [ "rust-lang.rust-analyzer", "vadimcn.vscode-lldb", "serayuzgur.crates", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode", "ms-vscode.vscode-typescript-next" ] }; // Add framework-specific extensions if (framework === 'solid-js') { extensions.recommendations.push("solid.vscode-solid"); } else if (framework === 'react') { extensions.recommendations.push("ms-vscode.vscode-react-javascript"); } else if (framework === 'vue') { extensions.recommendations.push("Vue.volar"); } await fs.writeJson(path.join(vscodeDir, 'extensions.json'), extensions, { spaces: 2 }); // Debug configuration const launch = { "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Debug Rust Backend", "cargo": { "args": ["build", "--bin", "nano"], "filter": { "name": "nano", "kind": "bin" } }, "args": [], "cwd": "${workspaceFolder}" } ] }; await fs.writeJson(path.join(vscodeDir, 'launch.json'), launch, { spaces: 2 }); spinner.succeed('VS Code workspace configured'); } catch (error) { spinner.fail('Failed to setup VS Code workspace'); console.log(kleur.yellow('\nVS Code setup failed, but you can configure it manually later.')); } } async function runProjectHealthCheck(targetDir, projectName) { const spinner = ora('Running project health check...').start(); try { const issues = []; // Check Rust installation try { const rustc = await execa('rustc', ['--version'], { stdio: 'pipe' }); const cargo = await execa('cargo', ['--version'], { stdio: 'pipe' }); } catch { issues.push('āŒ Rust not found - install from https://rustup.rs/'); } // Check Node.js version try { const node = await execa('node', ['--version'], { stdio: 'pipe' }); const version = parseInt(node.stdout.replace('v', '')); if (version < 18) { issues.push('āš ļø Node.js version < 18 detected - some features may not work'); } } catch { issues.push('āŒ Node.js not found'); } // Check project structure const requiredFiles = ['Cargo.toml', 'package.json', 'nano.config.json']; for (const file of requiredFiles) { if (!fs.existsSync(path.join(targetDir, file))) { issues.push(`āŒ Missing required file: ${file}`); } } spinner.stop(); if (issues.length === 0) { console.log(kleur.green('āœ… Project health check passed!')); } else { console.log(kleur.yellow('\nšŸ„ Health Check Issues:')); issues.forEach(issue => console.log(' ' + issue)); console.log(kleur.dim('\nSome issues were found. Please resolve them before developing.')); } } catch (error) { spinner.fail('Health check failed'); } } function isValidPackageName(projectName) { return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName); }