UNPKG

wp-plugin-init

Version:

CLI to scaffold a PHP plugin boilerplate structure

163 lines (135 loc) 6.91 kB
#!/usr/bin/env node import fs from 'fs-extra'; import path from 'path'; import chalk from 'chalk'; import { fileURLToPath } from 'url'; import { glob } from 'glob'; import { exec } from 'child_process'; import util from 'util'; const execPromise = util.promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const args = process.argv.slice(2); const pluginName = args.find((a) => !a.startsWith('-')); // --local=/path/to/wpsuitepress-framework uses a local path repo instead // of the GitHub VCS — handy for developing the framework before publishing. const localFlag = args.find((a) => a.startsWith('--local=')); const localFrameworkPath = localFlag ? localFlag.split('=')[1] : null; if (!pluginName) { console.log(chalk.red('❌ Please provide a plugin name. e.g. wp-plugin-init my-plugin')); process.exit(1); } if (!/^[a-z][a-z0-9-]*$/.test(pluginName)) { console.log(chalk.red('❌ Plugin name must be lowercase kebab-case (letters, numbers, dashes), starting with a letter.')); process.exit(1); } // ---- Derived identifiers ------------------------------------------------- // "Suite One" const readablePluginName = pluginName .split(/[-_]/) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); const classPrefix = readablePluginName.replace(/\s+/g, ''); // SuiteOne const constantPrefix = pluginName.replace(/[-\s]/g, '_').toUpperCase(); // SUITE_ONE const snakeName = pluginName.replace(/[-\s]/g, '_').toLowerCase(); // suite_one const pluginGlobal = `${classPrefix}Settings`; // SuiteOneSettings const targetDir = path.join(process.cwd(), pluginName); const templateDir = path.join(__dirname, 'template'); // Placeholder → value map, applied to every text file. const replacements = [ [/__PLUGIN_TITLE__/g, readablePluginName], [/__PLUGIN_NAME__/g, pluginName], [/__CONST_PREFIX__/g, constantPrefix], [/__SNAKE_NAME__/g, snakeName], [/__PLUGIN_GLOBAL__/g, pluginGlobal], [/PluginsNameSpaces/g, classPrefix], ]; const TEXT_EXTENSIONS = new Set([ '.php', '.js', '.mjs', '.vue', '.json', '.md', '.txt', '.css', '.html', ]); function applyReplacements(content) { return replacements.reduce((acc, [pattern, value]) => acc.replace(pattern, value), content); } (async () => { try { if (await fs.pathExists(targetDir)) { console.log(chalk.red(`❌ Folder '${pluginName}' already exists.`)); return; } console.log(chalk.cyan(`\n🚀 Scaffolding '${readablePluginName}'...\n`)); await fs.copy(templateDir, targetDir); // Rename the main plugin file. const oldMain = path.join(targetDir, 'wp-plugins-boilerplate.php'); const newMain = path.join(targetDir, `${pluginName}.php`); if (await fs.pathExists(oldMain)) { await fs.rename(oldMain, newMain); } // Replace placeholders across every text file. const files = glob.sync('**/*', { cwd: targetDir, nodir: true, dot: true, ignore: ['node_modules/**', 'vendor/**'], }); for (const rel of files) { const file = path.join(targetDir, rel); if (!TEXT_EXTENSIONS.has(path.extname(file))) { continue; } const content = await fs.readFile(file, 'utf8'); await fs.writeFile(file, applyReplacements(content)); } console.log(chalk.green('✅ Files generated and placeholders replaced.')); console.log(chalk.gray(` Namespace : ${classPrefix}\\`)); console.log(chalk.gray(` Framework : ${classPrefix}\\Framework\\ (scoped on install)`)); console.log(chalk.gray(` Constants : ${constantPrefix}_VERSION, ${constantPrefix}_PATH, ...`)); console.log(chalk.gray(` REST base : /wp-json/${pluginName}/v1`)); console.log(chalk.gray(` WP-CLI : wp ${pluginName} <command>`)); // Remove stale lockfiles so dependencies resolve fresh. await fs.remove(path.join(targetDir, 'composer.lock')); // For local framework development, point composer at a path repo. if (localFrameworkPath) { const composerPath = path.join(targetDir, 'composer.json'); const composer = await fs.readJson(composerPath); composer.repositories = [ { type: 'path', url: path.resolve(localFrameworkPath), options: { symlink: false } }, ]; composer['minimum-stability'] = 'dev'; await fs.writeJson(composerPath, composer, { spaces: 2 }); console.log(chalk.gray(` Using local framework: ${path.resolve(localFrameworkPath)}`)); } // ---- Composer install (pulls + scopes the framework) ------------- try { console.log(chalk.yellow('\n📦 Installing Composer dependencies...')); // --no-dev keeps the shipped vendor/ lean; the scoping script runs // via post-install-cmd and rewrites the bundled framework namespace. await execPromise('composer install --no-interaction --no-dev --optimize-autoloader', { cwd: targetDir }); console.log(chalk.green('✅ Composer dependencies installed and framework scoped.')); } catch (error) { console.error(chalk.red('⚠️ composer install failed.')); console.error(chalk.gray(error.stderr || error.message || error)); console.log(chalk.yellow(' The wpsuitepress/framework repo must be reachable (or pass --local=/path/to/framework).')); console.log(chalk.yellow(' Then run "composer install" inside the plugin directory.')); } // ---- Frontend assets --------------------------------------------- try { console.log(chalk.yellow('\n📦 Installing NPM packages...')); await execPromise('npm install', { cwd: targetDir }); console.log(chalk.yellow('🏗️ Building production assets...')); await execPromise('npm run build', { cwd: targetDir }); console.log(chalk.green('✅ Frontend assets built.')); } catch (err) { console.error(chalk.red('⚠️ Frontend build failed.')); console.error(chalk.gray(err.stderr || err.message || err)); } console.log(chalk.green(`\n🎉 Plugin '${pluginName}' is ready!`)); console.log(chalk.cyan('\nNext steps:')); console.log(chalk.gray(` cd ${pluginName}`)); console.log(chalk.gray(' npm run dev # Vite dev server with HMR')); console.log(chalk.gray(' composer install # re-run if the framework repo was offline')); console.log(''); } catch (err) { console.error(chalk.red('❌ Error creating plugin:'), err); process.exit(1); } })();