graftthis
Version:
A collection of utility tools for working with the RWSDK (Redwood SDK)
481 lines (398 loc) • 16.9 kB
JavaScript
#!/usr/bin/env node
/**
* GraftThis CLI
*
* A command-line tool for installing and managing RWSDK utility tools.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const config = {
toolsDir: path.join(__dirname, 'tools'),
defaultInstallPath: process.cwd()
};
// Ensure tools directory exists
if (!fs.existsSync(config.toolsDir)) {
fs.mkdirSync(config.toolsDir, { recursive: true });
}
/**
* Main function to process command line arguments
*/
function main() {
const args = process.argv.slice(2);
const command = args[0];
if (!command) {
// Default behavior: install all tools
installAllTools();
return;
}
// Process specific commands
switch (command) {
case 'routes':
installGenerateRoutesTool();
break;
case 'component':
installComponentGeneratorTool();
break;
case 'tailwind':
installTailwindSetup();
break;
case 'shadcn':
installShadcnSetup();
break;
case 'help':
showHelp();
break;
default:
console.error(`Unknown command: ${command}`);
showHelp();
process.exit(1);
}
}
/**
* Show help information
*/
function showHelp() {
console.log('\nGraftThis - Utility tools for RWSDK');
console.log('\nUsage:');
console.log(' npx graftthis Install all tools');
console.log(' npx graftthis routes Install routes generator');
console.log(' npx graftthis component Install component generator');
console.log(' npx graftthis tailwind Set up Tailwind CSS for your project');
console.log(' npx graftthis shadcn Set up shadcn UI components for your project');
console.log(' npx graftthis help Show this help message');
}
/**
* Install all available tools
*/
function installAllTools() {
console.log('Installing all GraftThis...');
// Install all available tools
installGenerateRoutesTool();
installComponentGeneratorTool();
installTailwindSetup();
installShadcnSetup();
console.log('\nAll tools installed successfully!');
}
/**
* Install the generateRoutes tool to the current project
*/
function installGenerateRoutesTool() {
const targetPath = config.defaultInstallPath;
const toolPath = path.join(config.toolsDir, 'generateRoutes');
console.log('Installing generateRoutes tool...');
try {
// Create src/scripts directory if it doesn't exist
const scriptsDir = path.join(targetPath, 'src', 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
// Copy generateRoutes.ts to src/scripts directory
const sourcePath = path.join(toolPath, 'generateRoutes.ts');
const destPath = path.join(scriptsDir, 'generateRoutes.ts');
if (!fs.existsSync(sourcePath)) {
console.error(`Error: generateRoutes.ts not found at ${sourcePath}`);
process.exit(1);
}
fs.copyFileSync(sourcePath, destPath);
console.log(`✓ Copied generateRoutes.ts to ${destPath}`);
// Add script to package.json
addScriptToPackageJson(targetPath, 'routes', 'npx tsx src/scripts/generateRoutes.ts');
console.log('✓ generateRoutes tool installed successfully!');
} catch (error) {
console.error(`Error installing generateRoutes tool: ${error.message}`);
process.exit(1);
}
}
/**
* Add a script to the target project's package.json
* @param {string} projectPath - Path to the project
* @param {string} scriptName - Name of the script to add
* @param {string} scriptCommand - Command to run for the script
*/
function addScriptToPackageJson(projectPath, scriptName, scriptCommand) {
const packageJsonPath = path.join(projectPath, 'package.json');
try {
if (!fs.existsSync(packageJsonPath)) {
console.error(`Error: package.json not found at ${packageJsonPath}`);
return;
}
// Read and parse the project's package.json
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// Initialize scripts section if it doesn't exist
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Add or update the script
packageJson.scripts[scriptName] = scriptCommand;
// Write the updated package.json back to the file
// Preserve formatting by using the same spacing as the original file
const spacing = packageJsonContent.includes(' "') ? 2 : packageJsonContent.includes(' "') ? 4 : 2;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, spacing));
console.log(`✓ Added '${scriptName}' script to package.json: '${scriptCommand}'`);
} catch (error) {
console.error(`Error adding script to package.json: ${error.message}`);
}
}
/**
* Install the component generator tool to the current project
*/
function installComponentGeneratorTool() {
const targetPath = config.defaultInstallPath;
const toolPath = path.join(config.toolsDir, 'componentGenerator');
console.log('Installing component generator tool...');
try {
// Check if plopfile.mjs exists in the project root
const plopfilePath = path.join(targetPath, 'plopfile.mjs');
const sourcePlopfilePath = path.join(toolPath, 'plopfile.mjs');
if (!fs.existsSync(sourcePlopfilePath)) {
console.error(`Error: plopfile.mjs not found at ${sourcePlopfilePath}`);
process.exit(1);
}
// Copy plopfile.mjs to project root
fs.copyFileSync(sourcePlopfilePath, plopfilePath);
console.log(`✓ Copied plopfile.mjs to ${plopfilePath}`);
// Create plop-templates directory and copy templates
const templateSourceDir = path.join(toolPath, 'plop-templates');
const templateTargetDir = path.join(targetPath, 'plop-templates');
if (fs.existsSync(templateSourceDir)) {
// Create the target directory if it doesn't exist
if (!fs.existsSync(templateTargetDir)) {
fs.mkdirSync(templateTargetDir, { recursive: true });
}
// Copy the component templates directory
const componentSourceDir = path.join(templateSourceDir, 'components');
const componentTargetDir = path.join(templateTargetDir, 'component');
if (fs.existsSync(componentSourceDir)) {
if (!fs.existsSync(componentTargetDir)) {
fs.mkdirSync(componentTargetDir, { recursive: true });
}
// Copy all template files
const templateFiles = fs.readdirSync(componentSourceDir);
templateFiles.forEach(file => {
const sourcePath = path.join(componentSourceDir, file);
const targetPath = path.join(componentTargetDir, file);
fs.copyFileSync(sourcePath, targetPath);
console.log(`✓ Copied template ${file} to ${targetPath}`);
});
}
}
// Add scripts to package.json
addScriptToPackageJson(targetPath, 'plop', 'plop');
addScriptToPackageJson(targetPath, 'component', 'plop component');
addScriptToPackageJson(targetPath, 'restructure', 'plop restructure');
addScriptToPackageJson(targetPath, 'restructure-all', 'plop restructure-all');
// Check if plop is installed and install it if needed
try {
// Check if plop is installed
const plopInstalled = checkPlopInstalled(targetPath);
if (!plopInstalled) {
console.log('\n⚠️ Plop is not installed in this project. Installing plop...');
try {
// Run the pnpm install command to install plop
const { execSync } = require('child_process');
execSync('pnpm install -D plop', {
cwd: targetPath,
stdio: 'inherit' // Show the output to the user
});
console.log('\n✅ Plop installed successfully!\n');
} catch (error) {
console.error(`\n❌ Error installing plop: ${error.message}`);
console.log('\n⚠️ Please install plop manually by running:');
console.log('\n pnpm install -D plop\n');
}
} else {
console.log('\n✅ Plop is already installed. You\'re all set!\n');
}
} catch (error) {
// Ignore errors when checking for plop
console.error(`Error checking for plop: ${error.message}`);
}
console.log('✓ Component generator tool installed successfully!');
} catch (error) {
console.error(`Error installing component generator tool: ${error.message}`);
process.exit(1);
}
}
/**
* Check if plop is installed in the project
* @param {string} projectPath - Path to the project
* @returns {boolean} - Whether plop is installed
*/
function checkPlopInstalled(projectPath) {
const packageJsonPath = path.join(projectPath, 'package.json');
try {
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// Check if plop is in dependencies or devDependencies
const hasPlopDep = packageJson.dependencies && packageJson.dependencies.plop;
const hasPlopDevDep = packageJson.devDependencies && packageJson.devDependencies.plop;
return hasPlopDep || hasPlopDevDep;
}
} catch (error) {
// Ignore errors when checking for plop
}
return false;
}
/**
* Check if Tailwind CSS dependencies are installed in the project
* @param {string} projectPath - Path to the project
* @returns {boolean} - Whether Tailwind dependencies are installed
*/
function checkTailwindInstalled(projectPath) {
const packageJsonPath = path.join(projectPath, 'package.json');
try {
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// Check if tailwindcss and @tailwindcss/vite are in dependencies or devDependencies
const hasTailwindDep = packageJson.dependencies && packageJson.dependencies.tailwindcss;
const hasTailwindDevDep = packageJson.devDependencies && packageJson.devDependencies.tailwindcss;
const hasTailwindViteDep = packageJson.dependencies && packageJson.dependencies['@tailwindcss/vite'];
const hasTailwindViteDevDep = packageJson.devDependencies && packageJson.devDependencies['@tailwindcss/vite'];
// Return true if both packages are installed (in either dependencies or devDependencies)
return (hasTailwindDep || hasTailwindDevDep) && (hasTailwindViteDep || hasTailwindViteDevDep);
}
} catch (error) {
// Ignore errors when checking for tailwind
}
return false;
}
/**
* Install and set up Tailwind CSS for an RWSDK project
*/
function installTailwindSetup() {
const targetPath = config.defaultInstallPath;
console.log('Setting up Tailwind CSS for your RWSDK project...');
try {
// Step 1: Check if the project has the required files
const viteConfigPath = path.join(targetPath, 'vite.config.mts');
const documentPath = path.join(targetPath, 'src', 'app', 'Document.tsx');
if (!fs.existsSync(viteConfigPath)) {
console.error(`Error: vite.config.mts not found at ${viteConfigPath}`);
console.error('Make sure you are in an RWSDK project directory.');
process.exit(1);
}
if (!fs.existsSync(documentPath)) {
console.error(`Error: Document.tsx not found at ${documentPath}`);
console.error('Make sure you are in an RWSDK project directory.');
process.exit(1);
}
// Step 2: Create the styles.css file
const stylesDir = path.join(targetPath, 'src', 'app');
const stylesPath = path.join(stylesDir, 'styles.css');
if (!fs.existsSync(stylesDir)) {
fs.mkdirSync(stylesDir, { recursive: true });
}
fs.writeFileSync(stylesPath, '@import "tailwindcss";');
console.log(`✓ Created styles.css file at ${stylesPath}`);
// Step 3: Update the vite.config.mts file
let viteConfig = fs.readFileSync(viteConfigPath, 'utf8');
// Check if tailwindcss is already imported
if (!viteConfig.includes("import tailwindcss from '@tailwindcss/vite'")) {
// Add the import statement at the top of the file
viteConfig = "import tailwindcss from '@tailwindcss/vite'\n" + viteConfig;
console.log('✓ Added tailwindcss import to vite.config.mts');
}
// Check if the environments config exists
if (!viteConfig.includes('environments:')) {
// Add the environments config
viteConfig = viteConfig.replace(
'export default defineConfig({',
'export default defineConfig({\n environments: {\n ssr: {},\n },'
);
console.log('✓ Added environments config to vite.config.mts');
}
// Check if tailwindcss is already in the plugins array
if (!viteConfig.includes('tailwindcss()')) {
// Add tailwindcss to the plugins array
viteConfig = viteConfig.replace(
/plugins:\s*\[([^\]]*)\]/,
(match, plugins) => {
if (plugins.trim().endsWith(',')) {
return `plugins: [${plugins} tailwindcss()]`;
} else if (plugins.trim()) {
return `plugins: [${plugins}, tailwindcss()]`;
} else {
return `plugins: [tailwindcss()]`;
}
}
);
console.log('✓ Added tailwindcss to plugins array in vite.config.mts');
}
// Write the updated vite.config.mts file
fs.writeFileSync(viteConfigPath, viteConfig);
// Step 4: Update the Document.tsx file
let documentContent = fs.readFileSync(documentPath, 'utf8');
// Check if styles are already imported
if (!documentContent.includes("import styles from './styles.css?url'")) {
// Always add the import statement at the very top of the file
documentContent = `import styles from './styles.css?url';\n\n${documentContent}`;
console.log('✓ Added styles import to Document.tsx');
// Double-check that the import was added
if (!documentContent.includes("import styles from './styles.css?url'")) {
console.log('⚠️ Warning: Import may not have been added correctly. Trying alternative method...');
// Try a more direct approach by splitting into lines
const lines = documentContent.split('\n');
lines.unshift(`import styles from './styles.css?url';`);
documentContent = lines.join('\n');
console.log('✓ Added styles import using alternative method');
}
}
// Check if the link tag is already in the head
if (!documentContent.includes('<link rel="stylesheet" href={styles}')) {
// Add the link tag to the head
documentContent = documentContent.replace(
/<head>(\s*)/,
'<head>$1<link rel="stylesheet" href={styles} />$1'
);
console.log('✓ Added stylesheet link to Document.tsx');
}
// Write the updated Document.tsx file
fs.writeFileSync(documentPath, documentContent);
// Step 5: Check if dependencies are installed and install them if needed
const tailwindInstalled = checkTailwindInstalled(targetPath);
console.log('\n✓ Tailwind CSS setup complete!');
if (!tailwindInstalled) {
console.log('\n⚠️ Installing required dependencies...');
try {
// Run the pnpm install command as regular dependencies (not dev dependencies)
const { execSync } = require('child_process');
execSync('pnpm install tailwindcss @tailwindcss/vite', {
cwd: targetPath,
stdio: 'inherit' // Show the output to the user
});
console.log('\n✅ Tailwind dependencies installed successfully!\n');
} catch (error) {
console.error(`\n❌ Error installing dependencies: ${error.message}`);
console.log('\n⚠️ Please install the dependencies manually by running:');
console.log('\n pnpm install tailwindcss @tailwindcss/vite\n');
}
} else {
console.log('\n✅ Tailwind dependencies are already installed. You\'re all set!\n');
}
} catch (error) {
console.error(`Error setting up Tailwind CSS: ${error.message}`);
process.exit(1);
}
}
/**
* Run the shadcn setup directly on the current project
*/
function installShadcnSetup() {
const targetPath = config.defaultInstallPath;
const toolPath = path.join(config.toolsDir, 'shadcnSetup');
console.log('Setting up shadcn for your project...');
try {
// Require the shadcnSetup module
const shadcnSetup = require(toolPath);
// Run the setup directly
shadcnSetup.install();
} catch (error) {
console.error(`Error setting up shadcn: ${error.message}`);
process.exit(1);
}
}
// Start the CLI
main();