UNPKG

one-file-cli

Version:

Run shadcn/ui React components instantly with zero config - perfect for quick prototypes

866 lines (726 loc) โ€ข 27.5 kB
#!/usr/bin/env node // one-file.js - Two-command system // Usage: // node one-file.js init (install dependencies once) // node one-file.js main.tsx (run instantly) const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const os = require('os'); const homeDir = os.homedir(); const templateDir = path.join(homeDir, '.one-file-template'); const sourceTemplateDir = path.join(__dirname, 'template'); function detectPackageManager(dir) { // Check for lock files in priority order if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) { return 'pnpm'; } if (fs.existsSync(path.join(dir, 'yarn.lock'))) { return 'yarn'; } if (fs.existsSync(path.join(dir, 'package-lock.json'))) { return 'npm'; } // Fallback to environment check return process.env.npm_execpath && process.env.npm_execpath.includes('pnpm') ? 'pnpm' : 'npm'; } // Detect package manager based on user's directory first const packageManager = detectPackageManager(process.cwd()); function copyRecursive(src, dest) { const stats = fs.statSync(src); if (path.basename(src) === 'node_modules') { return; } if (stats.isDirectory()) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } const items = fs.readdirSync(src); items.forEach(item => { copyRecursive(path.join(src, item), path.join(dest, item)); }); } else { fs.copyFileSync(src, dest); } } function createTemplate() { console.log('๐Ÿ“ Creating template directory...'); if (!fs.existsSync(sourceTemplateDir)) { console.error('โŒ Template directory not found. Please ensure ./template/ exists.'); process.exit(1); } copyRecursive(sourceTemplateDir, templateDir); console.log(`โœ… Template created at: ${templateDir}`); } async function installDeps() { return new Promise((resolve, reject) => { console.log('๐Ÿ“ฆ Installing dependencies (this may take a minute)...'); const install = spawn(packageManager, ['install'], { cwd: templateDir, stdio: 'inherit', shell: true }); install.on('close', (code) => { if (code === 0) { console.log('โœ… Dependencies installed! You can now run files instantly.'); resolve(); } else { reject(new Error(`${packageManager} install failed with code ${code}`)); } }); install.on('error', reject); }); } function updateMainTsx(userFilePath) { const mainTsxPath = path.join(templateDir, 'src', 'main.tsx'); const absoluteUserPath = path.resolve(userFilePath); const relativePath = path.relative(path.join(templateDir, 'src'), absoluteUserPath).replace(/\\/g, '/'); // Read the original main.tsx if (!fs.existsSync(mainTsxPath)) { throw new Error('Template main.tsx not found'); } let mainTsxContent = fs.readFileSync(mainTsxPath, 'utf8'); // Replace the App import with the user's file const updatedContent = mainTsxContent.replace( /import App from ["']\.\/App\.tsx?["'];?/, `import App from '${relativePath}';` ); fs.writeFileSync(mainTsxPath, updatedContent); return mainTsxPath; } function updateViteConfig() { const viteConfigPath = path.join(templateDir, 'vite.config.ts'); const vitestConfigPath = path.join(templateDir, 'vitest.config.ts'); if (!fs.existsSync(viteConfigPath)) { console.warn('โš ๏ธ Vite config not found'); return null; } // Get the current working directory where the script is executed const currentDir = process.cwd(); // Update Vite config let viteConfigContent = ` import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import path from 'path'; export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), "~": "${currentDir.replace(/\\/g, '/')}" }, modules: [ path.resolve(__dirname, "node_modules"), "${currentDir.replace(/\\/g, '/')}/node_modules", "${path.join(templateDir, 'node_modules').replace(/\\/g, '/')}" ] } }); `; fs.writeFileSync(viteConfigPath, viteConfigContent); // Update Vitest config if it exists if (fs.existsSync(vitestConfigPath)) { let vitestConfigContent = ` import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true, setupFiles: './src/setup-tests.ts', }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), "~": "${currentDir.replace(/\\/g, '/')}" }, modules: [ path.resolve(__dirname, "node_modules"), "${currentDir.replace(/\\/g, '/')}/node_modules", "${path.join(templateDir, 'node_modules').replace(/\\/g, '/')}" ] }, }); `; fs.writeFileSync(vitestConfigPath, vitestConfigContent); } return viteConfigPath; } function createUserDirLink(userDir, templateDir) { const userDirLink = path.join(templateDir, '~'); if (fs.existsSync(userDirLink)) { // Remove existing link if (process.platform === 'win32') { fs.rmSync(userDirLink, { recursive: true, force: true }); } else { fs.unlinkSync(userDirLink); } } try { if (process.platform === 'win32') { const { execSync } = require('child_process'); execSync(`mklink /J "${userDirLink}" "${userDir}"`, { stdio: 'ignore' }); } else { fs.symlinkSync(userDir, userDirLink); } return userDirLink; } catch (error) { console.warn('โš ๏ธ Could not create user directory link:', error.message); return null; } } function createNodeModulesLink(userDir, templateDir) { if (!fs.existsSync(path.join(userDir, 'package.json'))) { console.log('๐Ÿ“ฆ No package.json found in user directory, using template dependencies only'); return null; } if (!fs.existsSync(path.join(userDir, 'node_modules'))) { console.log('โš ๏ธ No node_modules found in user directory. Please run your package manager\'s install command first.'); } return null; } function createLocalTsConfig(userDir, templateDir) { const templateTsConfigPath = path.join(templateDir, 'tsconfig.json'); const localTsConfigPath = path.join(userDir, 'tsconfig.json'); const config = { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": [ "ES2020", "DOM", "DOM.Iterable" ], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "node", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "baseUrl": "..\\..\\..\\..\\..\\.one-file-template", "paths": { "@/*": ["src/*"], "~/*": ["../one-file-cli/test-demo/*"], "*": ["node_modules/*"] } }, "include": [ "*.tsx", "*.ts", "..\\..\\..\\..\\..\\.one-file-template\\src" ] }; fs.writeFileSync(localTsConfigPath, JSON.stringify(config, null, 2)); return localTsConfigPath; } async function installUserDependencies(userDir, templateDir) { const userPackageJsonPath = path.join(userDir, 'package.json'); if (!fs.existsSync(userPackageJsonPath)) { return null; // No user dependencies to install } try { const userPackageJson = JSON.parse(fs.readFileSync(userPackageJsonPath, 'utf8')); const templatePackageJson = JSON.parse(fs.readFileSync(path.join(templateDir, 'package.json'), 'utf8')); // Get all dependencies that aren't already in the template const newDeps = {}; ['dependencies', 'devDependencies'].forEach(depType => { if (userPackageJson[depType]) { Object.entries(userPackageJson[depType]).forEach(([pkg, version]) => { if (!templatePackageJson.dependencies?.[pkg] && !templatePackageJson.devDependencies?.[pkg]) { newDeps[pkg] = version; } }); } }); if (Object.keys(newDeps).length === 0) { return null; // No new dependencies to install } // Create a temporary package.json with the new dependencies const tempPackageJson = { ...templatePackageJson, dependencies: { ...templatePackageJson.dependencies, ...newDeps } }; const tempPackageJsonPath = path.join(templateDir, 'package.json'); const originalPackageJson = fs.readFileSync(tempPackageJsonPath, 'utf8'); // Write temporary package.json with user dependencies fs.writeFileSync(tempPackageJsonPath, JSON.stringify(tempPackageJson, null, 2)); console.log('๐Ÿ“ฆ Installing additional dependencies from user project...'); // Install the new dependencies await new Promise((resolve, reject) => { const install = spawn(packageManager, ['install'], { cwd: templateDir, stdio: 'inherit', shell: true }); install.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Failed to install dependencies (code ${code})`)); } }); install.on('error', reject); }); return originalPackageJson; // Return original content for cleanup } catch (error) { console.error('โš ๏ธ Warning: Failed to process user dependencies:', error.message); return null; } } async function runFile(userFile) { if (!fs.existsSync(templateDir)) { console.error('โŒ Template not found. Please run: node one-file.js init'); process.exit(1); } if (!fs.existsSync(path.join(templateDir, 'node_modules'))) { console.error('โŒ Dependencies not installed. Please run: node one-file.js init'); process.exit(1); } const absoluteUserFile = path.resolve(userFile); const userDir = path.dirname(absoluteUserFile); const mainTsxPath = path.join(templateDir, 'src', 'main.tsx'); const viteConfigPath = path.join(templateDir, 'vite.config.ts'); let originalMainTsx = null; let originalViteConfig = null; let localTsConfigPath = null; let nodeModulesLinkPath = null; let userDirLinkPath = null; try { // Backup original files if (fs.existsSync(mainTsxPath)) { originalMainTsx = fs.readFileSync(mainTsxPath, 'utf8'); } if (fs.existsSync(viteConfigPath)) { originalViteConfig = fs.readFileSync(viteConfigPath, 'utf8'); } // Update main.tsx to import the user's file updateMainTsx(absoluteUserFile); console.log(`๐Ÿ“„ Updated main.tsx to import ${userFile}`); // Update Vite config to include ~ alias updateViteConfig(); console.log('โš™๏ธ Updated Vite config with ~ alias for user directory'); // Create user directory symlink as ~ userDirLinkPath = createUserDirLink(userDir, templateDir); if (userDirLinkPath) { console.log('๐Ÿ”— Created ~ symlink to user directory'); } // Create TypeScript and node_modules support localTsConfigPath = createLocalTsConfig(userDir, templateDir); console.log('โš™๏ธ Created local tsconfig.json with ~ support'); nodeModulesLinkPath = createNodeModulesLink(userDir, templateDir); if (nodeModulesLinkPath) { console.log('๐Ÿ”— Created node_modules link for type resolution'); } console.log('๐Ÿ”ฅ Starting Vite with native hot reload...'); const vite = spawn(packageManager, ['run', 'dev'], { cwd: templateDir, stdio: 'inherit', shell: true }); const cleanup = () => { console.log('\n๐Ÿ”ฅ Keeping files in place...'); vite.kill(); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); vite.on('error', (error) => { console.error('โŒ Failed to start dev server:', error.message); cleanup(); }); } catch (error) { console.error('โŒ Error:', error.message); process.exit(1); } } async function buildFile(userFile) { if (!fs.existsSync(templateDir)) { console.error('โŒ Template not found. Please run: node one-file.js init'); process.exit(1); } if (!fs.existsSync(path.join(templateDir, 'node_modules'))) { console.error('โŒ Dependencies not installed. Please run: node one-file.js init'); process.exit(1); } const absoluteUserFile = path.resolve(userFile); const userDir = path.dirname(absoluteUserFile); const mainTsxPath = path.join(templateDir, 'src', 'main.tsx'); const viteConfigPath = path.join(templateDir, 'vite.config.ts'); const buildOutputDir = path.join(userDir, 'dist'); let originalMainTsx = null; let originalViteConfig = null; let localTsConfigPath = null; let nodeModulesLinkPath = null; let userDirLinkPath = null; let originalPackageJson = null; try { // Backup original files if (fs.existsSync(mainTsxPath)) { originalMainTsx = fs.readFileSync(mainTsxPath, 'utf8'); } if (fs.existsSync(viteConfigPath)) { originalViteConfig = fs.readFileSync(viteConfigPath, 'utf8'); } // Install user dependencies if any originalPackageJson = await installUserDependencies(userDir, templateDir); // Update main.tsx to import the user's file updateMainTsx(absoluteUserFile); console.log(`๐Ÿ“„ Updated main.tsx to import ${userFile}`); // Update Vite config to include ~ alias updateViteConfig(); console.log('โš™๏ธ Updated Vite config with ~ alias for user directory'); // Create user directory symlink as ~ userDirLinkPath = createUserDirLink(userDir, templateDir); if (userDirLinkPath) { console.log('๐Ÿ”— Created ~ symlink to user directory'); } // Create TypeScript and node_modules support localTsConfigPath = createLocalTsConfig(userDir, templateDir); console.log('โš™๏ธ Created local tsconfig.json with ~ support'); nodeModulesLinkPath = createNodeModulesLink(userDir, templateDir); if (nodeModulesLinkPath) { console.log('๐Ÿ”— Created merged package.json'); // Install dependencies from merged package.json console.log('๐Ÿ“ฆ Installing all dependencies...'); await new Promise((resolve, reject) => { const install = spawn(packageManager, ['install'], { cwd: templateDir, stdio: 'inherit', shell: true }); install.on('close', (code) => { if (code === 0) { console.log('โœ… Successfully installed all dependencies'); resolve(); } else { reject(new Error(`Failed to install dependencies (code ${code})`)); } }); install.on('error', reject); }); } console.log('๐Ÿ—๏ธ Building production files...'); // Run the build command const build = spawn(packageManager, ['run', 'build'], { cwd: templateDir, stdio: 'inherit', shell: true }); await new Promise((resolve, reject) => { build.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Build failed with code ${code}`)); } }); build.on('error', reject); }); // Copy build output to user directory const templateDistDir = path.join(templateDir, 'dist'); if (fs.existsSync(templateDistDir)) { if (fs.existsSync(buildOutputDir)) { fs.rmSync(buildOutputDir, { recursive: true, force: true }); } copyRecursive(templateDistDir, buildOutputDir); console.log(`โœ… Build complete! Output saved to: ${buildOutputDir}`); } else { throw new Error('Build output not found'); } } catch (error) { console.error('โŒ Build failed:', error.message); process.exit(1); } finally { // Cleanup console.log('๐Ÿงน Cleaning up...'); if (originalMainTsx !== null) { fs.writeFileSync(mainTsxPath, originalMainTsx); } if (originalViteConfig !== null) { fs.writeFileSync(viteConfigPath, originalViteConfig); } if (originalPackageJson !== null) { fs.writeFileSync(path.join(templateDir, 'package.json'), originalPackageJson); // Reinstall original dependencies try { await new Promise((resolve, reject) => { const install = spawn(packageManager, ['install'], { cwd: templateDir, stdio: 'inherit', shell: true }); install.on('close', (code) => code === 0 ? resolve() : reject()); install.on('error', reject); }); } catch (error) { console.warn('โš ๏ธ Warning: Failed to restore original dependencies'); } } if (localTsConfigPath && fs.existsSync(localTsConfigPath)) { fs.unlinkSync(localTsConfigPath); } if (nodeModulesLinkPath && fs.existsSync(nodeModulesLinkPath)) { // Remove merged package.json fs.unlinkSync(nodeModulesLinkPath); // Restore original package.json and reinstall original dependencies if (fs.existsSync(path.join(templateDir, 'package.json.backup'))) { fs.copyFileSync( path.join(templateDir, 'package.json.backup'), path.join(templateDir, 'package.json') ); fs.unlinkSync(path.join(templateDir, 'package.json.backup')); // Reinstall original dependencies try { await new Promise((resolve, reject) => { const install = spawn(packageManager, ['install'], { cwd: templateDir, stdio: 'inherit', shell: true }); install.on('close', (code) => code === 0 ? resolve() : reject()); install.on('error', reject); }); } catch (error) { console.warn('โš ๏ธ Warning: Failed to restore original dependencies'); } } } if (userDirLinkPath && fs.existsSync(userDirLinkPath)) { if (process.platform === 'win32') { fs.rmSync(userDirLinkPath, { recursive: true, force: true }); } else { fs.unlinkSync(userDirLinkPath); } } } } async function initCommand() { try { if (fs.existsSync(templateDir)) { console.log('๐Ÿงน Removing existing template...'); fs.rmSync(templateDir, { recursive: true, force: true }); } createTemplate(); await installDeps(); console.log('\n๐ŸŽ‰ Setup complete! Now you can run:'); console.log(` node one-file.js main.tsx`); console.log('\n๐Ÿ’ก Import aliases:'); console.log(' @ - template components (e.g., @/components/ui/button)'); console.log(' ~ - your local files (e.g., ~/components/hello)'); } catch (error) { console.error('โŒ Init failed:', error.message); process.exit(1); } } async function main() { const command = process.argv[2]; if (command === '--version' || command === '-v') { const packageJson = require('./package.json'); console.log(packageJson.version); process.exit(0); } if (!command) { console.log(` ๐ŸŽฏ one-file - Run .tsx files with zero config Commands: node one-file.js init Install dependencies (run once) node one-file.js <file.tsx> Run a .tsx file instantly node one-file.js build <file.tsx> Build for production node one-file.js test <file.tsx> Run tests node one-file.js --version Show version number Workflow: 1. node one-file.js init # Install deps once (~30 seconds) 2. node one-file.js main.tsx # Run instantly (< 3 seconds) 3. node one-file.js build main.tsx # Build for production 4. node one-file.js test main.tsx # Run tests Features: โœ… Zero config React + TypeScript + Tailwind CSS v4 โœ… Native Vite hot reload (no file copying!) โœ… Built-in UI components (Button, Input) โœ… Auto cleanup when done โœ… Direct file import - Vite handles everything โœ… Dual import aliases: @ (template) and ~ (local) โœ… Production build support with static file output โœ… Test support with Vitest `); process.exit(1); } if (command === 'init') { await initCommand(); } else if (command === 'build') { const file = process.argv[3]; if (!file) { console.error('โŒ Please specify a file to build: node one-file.js build <file.tsx>'); process.exit(1); } await buildFile(file); } else if (command === 'test') { const file = process.argv[3]; if (!file) { console.error('โŒ Please specify a file to test: node one-file.js test <file.tsx>'); process.exit(1); } await testFile(file); } else { await runFile(command); } } async function testFile(userFile) { if (!fs.existsSync(templateDir)) { console.error('โŒ Template not found. Please run: node one-file.js init'); process.exit(1); } if (!fs.existsSync(path.join(templateDir, 'node_modules'))) { console.error('โŒ Dependencies not installed. Please run: node one-file.js init'); process.exit(1); } const absoluteUserFile = path.resolve(userFile); const userDir = path.dirname(absoluteUserFile); const mainTsxPath = path.join(templateDir, 'src', 'main.tsx'); const viteConfigPath = path.join(templateDir, 'vite.config.ts'); const vitestConfigPath = path.join(templateDir, 'vitest.config.ts'); const setupFilePath = path.join(templateDir, 'src', 'setup-tests.ts'); const templateTestFile = path.join(templateDir, 'src', 'user-test', path.basename(userFile)); let originalMainTsx = null; let originalViteConfig = null; let originalVitestConfig = null; let localTsConfigPath = null; let nodeModulesLinkPath = null; let userDirLinkPath = null; try { // Copy test file to template directory if (!fs.existsSync(absoluteUserFile)) { console.error(`โŒ Test file not found: ${absoluteUserFile}`); process.exit(1); } // Create user-test directory if it doesn't exist const userTestDir = path.dirname(templateTestFile); if (!fs.existsSync(userTestDir)) { fs.mkdirSync(userTestDir, { recursive: true }); } fs.copyFileSync(absoluteUserFile, templateTestFile); console.log('๐Ÿ“„ Copied test file to template directory'); // Backup original files if (fs.existsSync(mainTsxPath)) { originalMainTsx = fs.readFileSync(mainTsxPath, 'utf8'); } if (fs.existsSync(viteConfigPath)) { originalViteConfig = fs.readFileSync(viteConfigPath, 'utf8'); } if (fs.existsSync(vitestConfigPath)) { originalVitestConfig = fs.readFileSync(vitestConfigPath, 'utf8'); // Update Vitest config to include the user's test file const updatedVitestConfig = originalVitestConfig.replace( /test:\s*{[^}]*}/, `test: { environment: 'jsdom', globals: true, setupFiles: '${setupFilePath.replace(/\\/g, '/')}', include: ['${templateTestFile.replace(/\\/g, '/')}'], root: '${templateDir.replace(/\\/g, '/')}' }` ); fs.writeFileSync(vitestConfigPath, updatedVitestConfig); console.log('โš™๏ธ Updated Vitest config to run user test file'); } // Create merged package.json with user dependencies nodeModulesLinkPath = await createNodeModulesLink(userDir, templateDir); if (nodeModulesLinkPath) { console.log('๐Ÿ“ฆ Created merged package.json with template and user dependencies'); // Install all dependencies console.log('๐Ÿ“ฆ Installing all dependencies...'); await new Promise((resolve, reject) => { const install = spawn(packageManager, ['install'], { cwd: templateDir, stdio: 'inherit', shell: true }); install.on('close', (code) => { if (code === 0) { console.log('โœ… Successfully installed all dependencies'); resolve(); } else { reject(new Error(`Failed to install dependencies (code ${code})`)); } }); install.on('error', reject); }); } // Update Vite config to use current directory updateViteConfig(); console.log('โš™๏ธ Updated Vite config with current directory alias'); // Create TypeScript config with current directory support localTsConfigPath = createLocalTsConfig(userDir, templateDir); console.log('โš™๏ธ Created local tsconfig.json with path support'); // Create user directory symlink as ~ userDirLinkPath = createUserDirLink(userDir, templateDir); if (userDirLinkPath) { console.log('๐Ÿ”— Created ~ symlink to user directory'); } console.log('๐Ÿงช Running tests...'); // Run the test command const test = spawn(packageManager, ['run', 'test'], { cwd: templateDir, stdio: 'inherit', shell: true, env: { ...process.env, NODE_PATH: process.cwd() } }); await new Promise((resolve, reject) => { test.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Tests failed with code ${code}`)); } }); test.on('error', reject); }); console.log('โœ… Tests completed successfully!'); } catch (error) { console.error('โŒ Tests failed:', error.message); process.exit(1); } finally { // Cleanup console.log('๐Ÿงน Cleaning up...'); // Remove the copied test file and its directory if (fs.existsSync(templateTestFile)) { fs.unlinkSync(templateTestFile); const userTestDir = path.dirname(templateTestFile); if (fs.existsSync(userTestDir)) { fs.rmdirSync(userTestDir, { recursive: true }); } } if (originalMainTsx !== null) { fs.writeFileSync(mainTsxPath, originalMainTsx); } if (originalViteConfig !== null) { fs.writeFileSync(viteConfigPath, originalViteConfig); } if (originalVitestConfig !== null) { fs.writeFileSync(vitestConfigPath, originalVitestConfig); } if (localTsConfigPath && fs.existsSync(localTsConfigPath)) { fs.unlinkSync(localTsConfigPath); } if (userDirLinkPath && fs.existsSync(userDirLinkPath)) { if (process.platform === 'win32') { fs.rmSync(userDirLinkPath, { recursive: true, force: true }); } else { fs.unlinkSync(userDirLinkPath); } } } } main();