@isthatuzii/create-nano-app
Version:
Desktop application scaffolding tool for the Nano Framework
606 lines (510 loc) ⢠19.8 kB
JavaScript
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);
}