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
JavaScript
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 };