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
536 lines (456 loc) • 15.1 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const inquirer = require('inquirer');
const ora = require('ora');
async function validateProject(options = {}) {
const spinner = ora('Validating MusPE project...').start();
try {
const projectRoot = findProjectRoot();
if (!projectRoot) {
spinner.fail('Not in a MusPE project directory');
return false;
}
const issues = [];
const warnings = [];
// Check configuration
const configIssues = await validateConfiguration(projectRoot);
issues.push(...configIssues.errors);
warnings.push(...configIssues.warnings);
// Check dependencies
const depIssues = await validateDependencies(projectRoot);
issues.push(...depIssues.errors);
warnings.push(...depIssues.warnings);
// Check file structure
const structureIssues = await validateFileStructure(projectRoot);
issues.push(...structureIssues.errors);
warnings.push(...structureIssues.warnings);
// Check code quality
const codeIssues = await validateCodeQuality(projectRoot);
issues.push(...codeIssues.errors);
warnings.push(...codeIssues.warnings);
// Display results
spinner.succeed('Project validation completed');
if (issues.length === 0 && warnings.length === 0) {
console.log(chalk.green('✅ Project validation passed!'));
console.log(chalk.gray('No issues found.'));
} else {
if (issues.length > 0) {
console.log(chalk.red(`\n❌ Found ${issues.length} error(s):`));
issues.forEach(issue => {
console.log(chalk.red(` • ${issue.message}`));
if (issue.fix) {
console.log(chalk.gray(` Fix: ${issue.fix}`));
}
});
}
if (warnings.length > 0) {
console.log(chalk.yellow(`\n⚠️ Found ${warnings.length} warning(s):`));
warnings.forEach(warning => {
console.log(chalk.yellow(` • ${warning.message}`));
if (warning.suggestion) {
console.log(chalk.gray(` Suggestion: ${warning.suggestion}`));
}
});
}
// Offer to fix issues
if (options.fix && issues.length > 0) {
const { shouldFix } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldFix',
message: 'Would you like to automatically fix the issues that can be auto-fixed?',
default: true
}
]);
if (shouldFix) {
await autoFixIssues(projectRoot, issues.filter(issue => issue.autoFixable));
}
}
}
return issues.length === 0;
} catch (error) {
spinner.fail('Validation failed');
console.error(chalk.red(error.message));
return false;
}
}
async function validateConfiguration(projectRoot) {
const errors = [];
const warnings = [];
// Check muspe.config.js
const configPath = path.join(projectRoot, 'muspe.config.js');
if (!await fs.pathExists(configPath)) {
errors.push({
message: 'Missing muspe.config.js file',
fix: 'Run "muspe init" to create configuration',
autoFixable: true
});
} else {
try {
const config = require(configPath);
// Validate required fields
if (!config.template) {
warnings.push({
message: 'Template not specified in config',
suggestion: 'Add template field to muspe.config.js'
});
}
if (!config.framework) {
warnings.push({
message: 'Framework not specified in config',
suggestion: 'Add framework field to muspe.config.js'
});
}
// Validate server config
if (config.server && config.server.port) {
if (typeof config.server.port !== 'number' || config.server.port < 1000 || config.server.port > 65535) {
errors.push({
message: 'Invalid server port in configuration',
fix: 'Set port to a valid number between 1000-65535'
});
}
}
} catch (error) {
errors.push({
message: 'Invalid muspe.config.js syntax',
fix: 'Check JavaScript syntax in configuration file'
});
}
}
// Check package.json
const packagePath = path.join(projectRoot, 'package.json');
if (!await fs.pathExists(packagePath)) {
errors.push({
message: 'Missing package.json file',
fix: 'Run "npm init" to create package.json',
autoFixable: true
});
} else {
try {
const pkg = await fs.readJSON(packagePath);
if (!pkg.name) {
warnings.push({
message: 'Package name not specified',
suggestion: 'Add name field to package.json'
});
}
if (!pkg.muspe) {
warnings.push({
message: 'MusPE metadata missing from package.json',
suggestion: 'Add muspe field with template and framework info'
});
}
} catch (error) {
errors.push({
message: 'Invalid package.json syntax',
fix: 'Check JSON syntax in package.json'
});
}
}
return { errors, warnings };
}
async function validateDependencies(projectRoot) {
const errors = [];
const warnings = [];
const packagePath = path.join(projectRoot, 'package.json');
if (await fs.pathExists(packagePath)) {
try {
const pkg = await fs.readJSON(packagePath);
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
// Check for required MusPE dependencies
const requiredDeps = ['@material/web', 'swiper'];
const recommendedDeps = ['tailwindcss', 'vite'];
requiredDeps.forEach(dep => {
if (!deps[dep]) {
errors.push({
message: `Missing required dependency: ${dep}`,
fix: `Run "npm install ${dep}"`,
autoFixable: true
});
}
});
recommendedDeps.forEach(dep => {
if (!deps[dep]) {
warnings.push({
message: `Missing recommended dependency: ${dep}`,
suggestion: `Consider installing ${dep} for better development experience`
});
}
});
// Check for outdated dependencies
const criticalDeps = ['@material/web', 'swiper', 'vite'];
const outdatedVersions = {
'@material/web': '1.0.0',
'swiper': '11.0.0',
'vite': '5.0.0'
};
criticalDeps.forEach(dep => {
if (deps[dep] && outdatedVersions[dep]) {
const currentVersion = deps[dep].replace(/[^0-9.]/g, '');
const requiredVersion = outdatedVersions[dep];
if (isVersionOlder(currentVersion, requiredVersion)) {
warnings.push({
message: `Outdated dependency: ${dep} (${currentVersion} < ${requiredVersion})`,
suggestion: `Update to latest version: npm install ${dep}@latest`
});
}
}
});
} catch (error) {
errors.push({
message: 'Unable to read package.json dependencies',
fix: 'Check package.json file syntax'
});
}
}
return { errors, warnings };
}
async function validateFileStructure(projectRoot) {
const errors = [];
const warnings = [];
// Required directories
const requiredDirs = [
'src',
'src/components',
'src/pages',
'src/styles',
'src/scripts',
'public'
];
// Optional but recommended directories
const recommendedDirs = [
'src/layouts',
'src/templates',
'src/assets',
'src/utils',
'src/services'
];
// Check required directories
for (const dir of requiredDirs) {
const dirPath = path.join(projectRoot, dir);
if (!await fs.pathExists(dirPath)) {
errors.push({
message: `Missing required directory: ${dir}`,
fix: `Create directory: mkdir -p ${dir}`,
autoFixable: true
});
}
}
// Check recommended directories
for (const dir of recommendedDirs) {
const dirPath = path.join(projectRoot, dir);
if (!await fs.pathExists(dirPath)) {
warnings.push({
message: `Missing recommended directory: ${dir}`,
suggestion: `Create directory for better organization: mkdir ${dir}`
});
}
}
// Check for essential files
const requiredFiles = [
'public/index.html',
'src/scripts/main.js',
'src/styles/main.css'
];
for (const file of requiredFiles) {
const filePath = path.join(projectRoot, file);
if (!await fs.pathExists(filePath)) {
errors.push({
message: `Missing required file: ${file}`,
fix: `Create file: touch ${file}`,
autoFixable: true
});
}
}
return { errors, warnings };
}
async function validateCodeQuality(projectRoot) {
const errors = [];
const warnings = [];
// Check for common code issues in main files
const mainJsPath = path.join(projectRoot, 'src/scripts/main.js');
if (await fs.pathExists(mainJsPath)) {
try {
const content = await fs.readFile(mainJsPath, 'utf-8');
// Check for console.log statements
if (content.includes('console.log') && !content.includes('// DEBUG')) {
warnings.push({
message: 'Console.log statements found in main.js',
suggestion: 'Remove or comment console.log statements for production'
});
}
// Check for proper ES6 imports
if (content.includes('require(') && content.includes('import ')) {
warnings.push({
message: 'Mixed import styles (require/import) found',
suggestion: 'Use consistent import style (prefer ES6 imports)'
});
}
// Check for Material.io imports
if (!content.includes('@material/web')) {
warnings.push({
message: 'Material.io components not imported',
suggestion: 'Import Material.io components for enhanced UI'
});
}
} catch (error) {
warnings.push({
message: 'Unable to analyze main.js code quality',
suggestion: 'Check file syntax and permissions'
});
}
}
// Check HTML validation
const indexPath = path.join(projectRoot, 'public/index.html');
if (await fs.pathExists(indexPath)) {
try {
const content = await fs.readFile(indexPath, 'utf-8');
// Check for proper viewport meta tag
if (!content.includes('name="viewport"')) {
errors.push({
message: 'Missing viewport meta tag for mobile responsiveness',
fix: 'Add <meta name="viewport" content="width=device-width, initial-scale=1.0">',
autoFixable: true
});
}
// Check for proper charset
if (!content.includes('charset="UTF-8"')) {
warnings.push({
message: 'Missing or incorrect charset declaration',
suggestion: 'Add <meta charset="UTF-8"> for proper character encoding'
});
}
// Check for accessibility
if (!content.includes('lang=')) {
warnings.push({
message: 'Missing language attribute on html element',
suggestion: 'Add lang="en" to <html> for accessibility'
});
}
} catch (error) {
warnings.push({
message: 'Unable to analyze index.html',
suggestion: 'Check file syntax and permissions'
});
}
}
return { errors, warnings };
}
async function autoFixIssues(projectRoot, fixableIssues) {
const spinner = ora('Auto-fixing issues...').start();
try {
for (const issue of fixableIssues) {
await autoFixIssue(projectRoot, issue);
}
spinner.succeed(`Auto-fixed ${fixableIssues.length} issue(s)`);
} catch (error) {
spinner.fail('Auto-fix failed');
console.error(chalk.red(error.message));
}
}
async function autoFixIssue(projectRoot, issue) {
switch (issue.message) {
case 'Missing muspe.config.js file':
await createDefaultConfig(projectRoot);
break;
case 'Missing package.json file':
await createDefaultPackageJson(projectRoot);
break;
case 'Missing viewport meta tag for mobile responsiveness':
await fixViewportMeta(projectRoot);
break;
default:
if (issue.message.includes('Missing required directory:')) {
const dir = issue.message.split(': ')[1];
await fs.ensureDir(path.join(projectRoot, dir));
} else if (issue.message.includes('Missing required file:')) {
const file = issue.message.split(': ')[1];
await fs.ensureFile(path.join(projectRoot, file));
}
break;
}
}
async function createDefaultConfig(projectRoot) {
const config = `module.exports = {
template: 'mobile',
framework: 'tailwind',
server: {
port: 3000,
host: 'localhost',
open: true
},
build: {
outDir: 'dist',
minify: true,
sourcemap: true
},
pwa: {
enabled: false,
manifest: './public/manifest.json',
serviceWorker: './src/sw.js'
}
};`;
await fs.writeFile(path.join(projectRoot, 'muspe.config.js'), config);
}
async function createDefaultPackageJson(projectRoot) {
const pkg = {
name: path.basename(projectRoot),
version: '1.0.0',
description: 'A MusPE application',
main: 'src/scripts/main.js',
scripts: {
start: 'muspe serve',
build: 'muspe build',
dev: 'muspe serve --open'
},
dependencies: {},
devDependencies: {},
muspe: {
template: 'mobile',
framework: 'tailwind',
version: '1.0.0'
}
};
await fs.writeJSON(path.join(projectRoot, 'package.json'), pkg, { spaces: 2 });
}
async function fixViewportMeta(projectRoot) {
const indexPath = path.join(projectRoot, 'public/index.html');
if (await fs.pathExists(indexPath)) {
let content = await fs.readFile(indexPath, 'utf-8');
if (!content.includes('name="viewport"')) {
// Add viewport meta tag after charset
content = content.replace(
/<meta charset="UTF-8">/,
'<meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">'
);
await fs.writeFile(indexPath, content);
}
}
}
function findProjectRoot() {
let currentDir = process.cwd();
while (currentDir !== path.dirname(currentDir)) {
const configPath = path.join(currentDir, 'muspe.config.js');
const packagePath = path.join(currentDir, 'package.json');
if (fs.existsSync(configPath) ||
(fs.existsSync(packagePath) &&
JSON.parse(fs.readFileSync(packagePath, 'utf-8')).muspe)) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
return null;
}
function isVersionOlder(current, required) {
const currentParts = current.split('.').map(Number);
const requiredParts = required.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, requiredParts.length); i++) {
const currentPart = currentParts[i] || 0;
const requiredPart = requiredParts[i] || 0;
if (currentPart < requiredPart) return true;
if (currentPart > requiredPart) return false;
}
return false;
}
module.exports = { validateProject };