UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

705 lines (595 loc) โ€ข 19.1 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const inquirer = require('inquirer'); const ora = require('ora'); const spawn = require('cross-spawn'); const { initCordova } = require('./cordova'); async function addFeature(feature, options) { const projectRoot = findProjectRoot(); if (!projectRoot) { console.log(chalk.red('Not in a MusPE project directory')); return; } const config = await loadConfig(projectRoot); const availableFeatures = [ 'pwa', 'testing', 'eslint', 'prettier', 'typescript', 'tailwind', 'bootstrap', 'analytics', 'seo', 'deployment', 'cordova' ]; if (!availableFeatures.includes(feature)) { console.log(chalk.red(`Unknown feature: ${feature}`)); console.log(chalk.gray(`Available features: ${availableFeatures.join(', ')}`)); return; } const spinner = ora(`Adding ${feature} feature...`).start(); try { switch (feature) { case 'pwa': await addPWAFeature(projectRoot, config, options); break; case 'testing': await addTestingFeature(projectRoot, config, options); break; case 'eslint': await addESLintFeature(projectRoot, config, options); break; case 'prettier': await addPrettierFeature(projectRoot, config, options); break; case 'typescript': await addTypeScriptFeature(projectRoot, config, options); break; case 'tailwind': await addTailwindFeature(projectRoot, config, options); break; case 'bootstrap': await addBootstrapFeature(projectRoot, config, options); break; case 'analytics': await addAnalyticsFeature(projectRoot, config, options); break; case 'seo': await addSEOFeature(projectRoot, config, options); break; case 'deployment': await addDeploymentFeature(projectRoot, config, options); break; case 'cordova': spinner.stop(); await initCordova(options); break; } spinner.succeed(`${feature} feature added successfully`); if (options.config) { await configureFeature(feature, projectRoot); } } catch (error) { spinner.fail(`Failed to add ${feature} feature`); console.error(chalk.red(error.message)); } } function findProjectRoot() { let currentDir = process.cwd(); while (currentDir !== path.parse(currentDir).root) { const configPath = path.join(currentDir, 'muspe.config.js'); if (fs.existsSync(configPath)) { return currentDir; } currentDir = path.dirname(currentDir); } return null; } async function loadConfig(projectRoot) { const configPath = path.join(projectRoot, 'muspe.config.js'); if (await fs.pathExists(configPath)) { try { delete require.cache[require.resolve(configPath)]; return require(configPath); } catch (error) { return {}; } } return {}; } async function updateConfig(projectRoot, updates) { const configPath = path.join(projectRoot, 'muspe.config.js'); const config = await loadConfig(projectRoot); const updatedConfig = { ...config, ...updates }; const configContent = `module.exports = ${JSON.stringify(updatedConfig, null, 2) .replace(/"/g, "'") .replace(/'([a-zA-Z_][a-zA-Z0-9_]*)':/g, '$1:')};`; await fs.writeFile(configPath, configContent); } async function updatePackageJson(projectRoot, updates) { const packageJsonPath = path.join(projectRoot, 'package.json'); const packageJson = await fs.readJSON(packageJsonPath); const updatedPackageJson = { ...packageJson, dependencies: { ...packageJson.dependencies, ...updates.dependencies }, devDependencies: { ...packageJson.devDependencies, ...updates.devDependencies }, scripts: { ...packageJson.scripts, ...updates.scripts } }; await fs.writeJSON(packageJsonPath, updatedPackageJson, { spaces: 2 }); } async function addPWAFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿ“ฑ Adding PWA features...')); // Update config await updateConfig(projectRoot, { pwa: { enabled: true, manifest: './public/manifest.json', serviceWorker: './src/sw.js', ...config.pwa } }); // Generate manifest.json const packageJson = await fs.readJSON(path.join(projectRoot, 'package.json')); const manifest = { name: packageJson.name, short_name: packageJson.name, description: packageJson.description || `${packageJson.name} - PWA`, start_url: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#3b82f6', icons: [ { src: './assets/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' }, { src: './assets/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' } ] }; await fs.writeJSON(path.join(projectRoot, 'public/manifest.json'), manifest, { spaces: 2 }); // Generate service worker const serviceWorker = `// MusPE Service Worker const CACHE_NAME = '${packageJson.name}-v1'; const urlsToCache = [ '/', '/src/styles/main.css', '/src/scripts/main.js', '/manifest.json' ]; self.addEventListener('install', (event) => { console.log('Service Worker installing...'); event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { console.log('Caching app shell'); return cache.addAll(urlsToCache); }) ); }); self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then((response) => { // Return cached version or fetch from network return response || fetch(event.request); }) ); }); self.addEventListener('activate', (event) => { console.log('Service Worker activating...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) ); });`; await fs.writeFile(path.join(projectRoot, 'public/sw.js'), serviceWorker); // Update HTML to include manifest and service worker registration const indexPath = path.join(projectRoot, 'public/index.html'); if (await fs.pathExists(indexPath)) { let htmlContent = await fs.readFile(indexPath, 'utf8'); // Add manifest link if not present if (!htmlContent.includes('rel="manifest"')) { htmlContent = htmlContent.replace( '</head>', ' <link rel="manifest" href="./manifest.json">\n <meta name="theme-color" content="#3b82f6">\n</head>' ); } // Add service worker registration if not present if (!htmlContent.includes('serviceWorker.register')) { const swScript = ` <script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js') .then((registration) => { console.log('SW registered: ', registration); }) .catch((registrationError) => { console.log('SW registration failed: ', registrationError); }); }); } </script>`; htmlContent = htmlContent.replace('</body>', `${swScript}\n</body>`); } await fs.writeFile(indexPath, htmlContent); } console.log(chalk.green('โœ… PWA features added')); console.log(chalk.gray(' โ€ข manifest.json generated')); console.log(chalk.gray(' โ€ข Service worker created')); console.log(chalk.gray(' โ€ข HTML updated with PWA meta tags')); } async function addTestingFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿงช Adding testing framework...')); await updatePackageJson(projectRoot, { devDependencies: { 'jest': '^29.5.0', '@testing-library/dom': '^9.3.0', '@testing-library/jest-dom': '^5.16.5' }, scripts: { 'test': 'jest', 'test:watch': 'jest --watch', 'test:coverage': 'jest --coverage' } }); // Create Jest config const jestConfig = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'], testMatch: [ '<rootDir>/src/**/__tests__/**/*.{js,jsx}', '<rootDir>/src/**/*.{test,spec}.{js,jsx}' ], collectCoverageFrom: [ 'src/**/*.{js,jsx}', '!src/index.js', '!src/setupTests.js' ] }; await fs.writeJSON(path.join(projectRoot, 'jest.config.json'), jestConfig, { spaces: 2 }); // Create setup file const setupTests = `// Jest setup file import '@testing-library/jest-dom'; // Mock functions for testing environment Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), removeListener: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), }); // Mock localStorage const localStorageMock = { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), }; global.localStorage = localStorageMock; // Mock fetch global.fetch = jest.fn();`; await fs.writeFile(path.join(projectRoot, 'src/setupTests.js'), setupTests); // Create example test const exampleTest = `// Example test file import { AppHeader } from '../components/AppHeader'; describe('AppHeader', () => { test('should create an AppHeader instance', () => { const header = new AppHeader({ title: 'Test App' }); expect(header.title).toBe('Test App'); }); test('should render header element', () => { const header = new AppHeader({ title: 'Test App' }); const element = header.render(); expect(element.tagName).toBe('HEADER'); expect(element.className).toBe('app-header'); expect(element.querySelector('h1').textContent).toBe('Test App'); }); });`; await fs.ensureDir(path.join(projectRoot, 'src/components/__tests__')); await fs.writeFile(path.join(projectRoot, 'src/components/__tests__/AppHeader.test.js'), exampleTest); console.log(chalk.green('โœ… Testing framework added')); console.log(chalk.gray(' โ€ข Jest configured')); console.log(chalk.gray(' โ€ข Testing Library setup')); console.log(chalk.gray(' โ€ข Example tests created')); } async function addESLintFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿ” Adding ESLint...')); await updatePackageJson(projectRoot, { devDependencies: { 'eslint': '^8.45.0', 'eslint-config-standard': '^17.1.0', 'eslint-plugin-import': '^2.27.5', 'eslint-plugin-node': '^11.1.0', 'eslint-plugin-promise': '^6.1.1' }, scripts: { 'lint': 'eslint src/', 'lint:fix': 'eslint src/ --fix' } }); const eslintConfig = { env: { browser: true, es2021: true, node: true }, extends: ['standard'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, rules: { 'no-console': 'warn', 'no-unused-vars': 'warn' } }; await fs.writeJSON(path.join(projectRoot, '.eslintrc.json'), eslintConfig, { spaces: 2 }); const eslintIgnore = `node_modules/ dist/ build/ *.min.js`; await fs.writeFile(path.join(projectRoot, '.eslintignore'), eslintIgnore); console.log(chalk.green('โœ… ESLint added')); } async function addPrettierFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿ’… Adding Prettier...')); await updatePackageJson(projectRoot, { devDependencies: { 'prettier': '^3.0.0' }, scripts: { 'format': 'prettier --write src/', 'format:check': 'prettier --check src/' } }); const prettierConfig = { semi: true, trailingComma: 'es5', singleQuote: true, printWidth: 80, tabWidth: 2 }; await fs.writeJSON(path.join(projectRoot, '.prettierrc'), prettierConfig, { spaces: 2 }); const prettierIgnore = `node_modules/ dist/ build/ *.min.js *.min.css`; await fs.writeFile(path.join(projectRoot, '.prettierignore'), prettierIgnore); console.log(chalk.green('โœ… Prettier added')); } async function addTailwindFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐ŸŽจ Adding Tailwind CSS...')); await updatePackageJson(projectRoot, { devDependencies: { 'tailwindcss': '^3.3.0', 'autoprefixer': '^10.4.14', 'postcss': '^8.4.24' } }); // Update config await updateConfig(projectRoot, { framework: 'tailwind' }); // Generate Tailwind config files (already done in create command) console.log(chalk.green('โœ… Tailwind CSS added')); } async function addBootstrapFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿ…ฑ๏ธ Adding Bootstrap...')); await updatePackageJson(projectRoot, { dependencies: { 'bootstrap': '^5.3.0' } }); await updateConfig(projectRoot, { framework: 'bootstrap' }); console.log(chalk.green('โœ… Bootstrap added')); } async function addAnalyticsFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿ“Š Adding Analytics...')); const { trackingId } = await inquirer.prompt([ { type: 'input', name: 'trackingId', message: 'Enter your Google Analytics tracking ID (optional):', default: '' } ]); const analyticsScript = `// Analytics utility class Analytics { constructor(trackingId) { this.trackingId = trackingId; this.init(); } init() { if (this.trackingId && typeof gtag !== 'undefined') { gtag('config', this.trackingId); } } trackEvent(action, category, label, value) { if (typeof gtag !== 'undefined') { gtag('event', action, { event_category: category, event_label: label, value: value }); } console.log('Analytics Event:', { action, category, label, value }); } trackPageView(path) { if (typeof gtag !== 'undefined') { gtag('config', this.trackingId, { page_path: path }); } console.log('Page View:', path); } } const analytics = new Analytics('${trackingId}'); if (typeof module !== 'undefined' && module.exports) { module.exports = { Analytics, analytics }; } if (typeof window !== 'undefined') { window.Analytics = Analytics; window.analytics = analytics; }`; await fs.ensureDir(path.join(projectRoot, 'src/utils')); await fs.writeFile(path.join(projectRoot, 'src/utils/analytics.js'), analyticsScript); console.log(chalk.green('โœ… Analytics added')); } async function addSEOFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿ” Adding SEO utilities...')); const seoUtils = `// SEO utilities class SEO { static updateTitle(title) { document.title = title; } static updateMeta(name, content) { let meta = document.querySelector(\`meta[name="\${name}"]\`); if (!meta) { meta = document.createElement('meta'); meta.name = name; document.head.appendChild(meta); } meta.content = content; } static updateOGMeta(property, content) { let meta = document.querySelector(\`meta[property="\${property}"]\`); if (!meta) { meta = document.createElement('meta'); meta.property = property; document.head.appendChild(meta); } meta.content = content; } static setCanonical(url) { let link = document.querySelector('link[rel="canonical"]'); if (!link) { link = document.createElement('link'); link.rel = 'canonical'; document.head.appendChild(link); } link.href = url; } static generateSchema(type, data) { const script = document.createElement('script'); script.type = 'application/ld+json'; script.innerHTML = JSON.stringify({ '@context': 'https://schema.org', '@type': type, ...data }); document.head.appendChild(script); } } if (typeof module !== 'undefined' && module.exports) { module.exports = SEO; } if (typeof window !== 'undefined') { window.SEO = SEO; }`; await fs.ensureDir(path.join(projectRoot, 'src/utils')); await fs.writeFile(path.join(projectRoot, 'src/utils/seo.js'), seoUtils); console.log(chalk.green('โœ… SEO utilities added')); } async function addDeploymentFeature(projectRoot, config, options) { console.log(chalk.blue('\n๐Ÿš€ Adding deployment configuration...')); // Add deployment scripts to package.json await updatePackageJson(projectRoot, { scripts: { 'deploy:netlify': 'npm run build && npx netlify deploy --prod --dir=dist', 'deploy:vercel': 'npm run build && npx vercel --prod', 'deploy:github': 'npm run build && npx gh-pages -d dist' } }); // Create deployment configurations const netlifyToml = `[build] command = "npm run build" publish = "dist" [build.environment] NODE_VERSION = "18" [[redirects]] from = "/*" to = "/index.html" status = 200`; await fs.writeFile(path.join(projectRoot, 'netlify.toml'), netlifyToml); const vercelJson = { version: 2, builds: [ { src: "package.json", use: "@vercel/static-build", config: { distDir: "dist" } } ], routes: [ { src: "/(.*)", dest: "/index.html" } ] }; await fs.writeJSON(path.join(projectRoot, 'vercel.json'), vercelJson, { spaces: 2 }); console.log(chalk.green('โœ… Deployment configuration added')); console.log(chalk.gray(' โ€ข Netlify configuration')); console.log(chalk.gray(' โ€ข Vercel configuration')); console.log(chalk.gray(' โ€ข Deployment scripts added')); } async function configureFeature(feature, projectRoot) { console.log(chalk.cyan(`\nโš™๏ธ Configuring ${feature}...`)); switch (feature) { case 'pwa': const { appName, themeColor } = await inquirer.prompt([ { type: 'input', name: 'appName', message: 'App name for PWA manifest:', default: path.basename(projectRoot) }, { type: 'input', name: 'themeColor', message: 'Theme color (hex):', default: '#3b82f6' } ]); // Update manifest with user preferences const manifestPath = path.join(projectRoot, 'public/manifest.json'); const manifest = await fs.readJSON(manifestPath); manifest.name = appName; manifest.short_name = appName; manifest.theme_color = themeColor; await fs.writeJSON(manifestPath, manifest, { spaces: 2 }); console.log(chalk.green('โœ… PWA configured')); break; default: console.log(chalk.gray(`No additional configuration needed for ${feature}`)); } } module.exports = { addFeature };