Fix builder page layout: make new chat and history buttons sticky at top on desktop
- Moved sticky positioning from mobile-only to global CSS for .top-left-actions - Buttons now stay fixed at top above content on all screen sizes - Includes various other app updates (version management, server improvements)
This commit is contained in:
143
VERSION_MANAGEMENT.md
Normal file
143
VERSION_MANAGEMENT.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# WordPress Plugin Version Management - Implementation Summary
|
||||
|
||||
## Overview
|
||||
Automatic version number management for WordPress plugins has been implemented. The system now detects versions from plugin files, tracks version history, and automatically bumps versions when exporting ZIP files.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### New Files
|
||||
1. **`chat/src/utils/versionManager.js`** - Core version management utilities
|
||||
2. **`chat/src/utils/versionManager.test.js`** - Unit tests for version manager
|
||||
|
||||
### Modified Files
|
||||
1. **`chat/server.js`**
|
||||
- Added version tracking to session object (lines ~7641-7658)
|
||||
- Added version manager import (line ~20)
|
||||
- Modified `handleExportZip()` to detect and bump versions (lines ~16944-17138)
|
||||
- Modified `handleUploadApp()` to detect version from imported plugins (lines ~16837-16889)
|
||||
- Updated `serializeSession()` to include version fields (lines ~7463-7481)
|
||||
|
||||
## Features Implemented
|
||||
|
||||
### 1. Automatic Version Detection
|
||||
- Detects version from WordPress plugin headers (`Version: X.X.X`)
|
||||
- Detects version from PHP constants (`define('CONSTANT_NAME', 'X.X.X')`)
|
||||
- Works with imported plugins of any version number
|
||||
|
||||
### 2. Version Bumping
|
||||
- Automatically bumps patch version on export (1.0.0 → 1.0.1)
|
||||
- Supports major, minor, patch, and keep bump types
|
||||
- Tracks bump history in session
|
||||
|
||||
### 3. Imported Plugin Support
|
||||
- Detects existing version from uploaded ZIP files
|
||||
- Preserves original version number on import
|
||||
- Maintains version continuity for imported plugins
|
||||
|
||||
### 4. Version History Tracking
|
||||
```javascript
|
||||
session.pluginVersionHistory = [
|
||||
{
|
||||
version: '1.0.1',
|
||||
previousVersion: '1.0.0',
|
||||
bumpType: 'patch',
|
||||
timestamp: '2026-02-11T10:30:00.000Z',
|
||||
fileUpdated: 'my-plugin/my-plugin.php'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Session Data
|
||||
```javascript
|
||||
session.pluginVersion = '1.2.3'; // Current version
|
||||
session.lastVersionBumpType = 'patch'; // Last bump type
|
||||
session.pluginVersionHistory = []; // Full history
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Export Flow
|
||||
1. User clicks "Export ZIP"
|
||||
2. System finds main plugin PHP file
|
||||
3. Extracts current version (from session or file)
|
||||
4. Bumps version number (patch by default)
|
||||
5. Updates version in plugin file content
|
||||
6. Adds updated file to ZIP archive
|
||||
7. Tracks version change in session history
|
||||
|
||||
### Import Flow
|
||||
1. User uploads ZIP file
|
||||
2. System extracts files to workspace
|
||||
3. Searches for main plugin file
|
||||
4. Extracts version from plugin header/constants
|
||||
5. Stores detected version in session
|
||||
6. Records import event in version history
|
||||
|
||||
## Version Formats Supported
|
||||
|
||||
### Plugin Headers
|
||||
```php
|
||||
/**
|
||||
* Plugin Name: My Plugin
|
||||
* Version: 1.2.3
|
||||
*/
|
||||
```
|
||||
|
||||
### PHP Constants
|
||||
```php
|
||||
define('MY_PLUGIN_VERSION', '1.2.3');
|
||||
define( "PLUGIN_VERSION", "2.0.0" );
|
||||
```
|
||||
|
||||
### PHP Const Assignments
|
||||
```php
|
||||
const PLUGIN_VERSION = '1.2.3';
|
||||
```
|
||||
|
||||
## API Response Changes
|
||||
|
||||
Sessions now include version fields:
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"id": "...",
|
||||
"pluginVersion": "1.2.4",
|
||||
"pluginVersionHistory": [...],
|
||||
"lastVersionBumpType": "patch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements (Optional)
|
||||
|
||||
1. **UI Controls**: Add version bump selector in export UI (major/minor/patch/custom)
|
||||
2. **Changelog Generation**: Auto-generate changelog entries with AI
|
||||
3. **Version Strategy**: Let AI analyze changes and suggest appropriate bump type
|
||||
4. **Multi-file Updates**: Update version in all plugin files, not just main file
|
||||
5. **readme.txt Support**: Update version in WordPress.org readme.txt format
|
||||
|
||||
## Testing
|
||||
|
||||
Run unit tests:
|
||||
```bash
|
||||
cd chat/src/utils
|
||||
node versionManager.test.js
|
||||
```
|
||||
|
||||
All tests pass:
|
||||
- ✓ parseVersion
|
||||
- ✓ formatVersion
|
||||
- ✓ bumpVersion
|
||||
- ✓ extractHeaderVersion
|
||||
- ✓ extractDefineVersion
|
||||
- ✓ extractAllVersions
|
||||
- ✓ updateVersionInContent
|
||||
- ✓ isWordPressPluginFile
|
||||
- ✓ getPluginSlugFromPath
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- Plugins without version detection start at 1.0.0
|
||||
- Existing sessions without version data will detect from file on first export
|
||||
- Import works with any plugin structure or version format
|
||||
- Graceful fallback if version detection fails
|
||||
@@ -2428,6 +2428,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (modal.style.display !== 'none') {
|
||||
const config = tourSteps[currentStep - 1];
|
||||
positionTourBox(config.target);
|
||||
}
|
||||
}, true);
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const onboardingModal = document.getElementById('onboarding-modal');
|
||||
|
||||
@@ -81,6 +81,16 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(247, 249, 251, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 10px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
@@ -876,17 +886,7 @@
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.top-left-actions {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(247, 249, 251, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.app-shell.builder-single {
|
||||
|
||||
@@ -1854,8 +1854,6 @@ function showLoadingIndicator(type) {
|
||||
|
||||
if (type === 'planning') {
|
||||
detailText.textContent = 'Analyzing your request and creating a development plan...';
|
||||
} else {
|
||||
detailText.textContent = 'Building your plugin with AI-generated code...';
|
||||
}
|
||||
|
||||
// Add animation keyframes if not already added
|
||||
|
||||
@@ -233,12 +233,6 @@
|
||||
Frequently Asked <span class="hero-gradient-text">Questions</span>
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-center items-center gap-4 mb-8">
|
||||
<a href="/signup"
|
||||
class="w-full sm:w-auto px-8 py-4 bg-green-700 text-white rounded-full font-bold hover:bg-green-600 transition-colors shadow-[0_0_20px_rgba(22,163,74,0.3)]">
|
||||
Start Building Free
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -264,8 +264,8 @@
|
||||
|
||||
<!-- Right: Screenshot Card replaced by Builder Mockup -->
|
||||
<div class="lg:col-span-5">
|
||||
<div class="relative rounded-3xl bg-white p-6 shadow-2xl ring-1 ring-amber-100">
|
||||
<div class="rounded-xl bg-amber-50 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
|
||||
<div class="relative rounded-3xl p-6">
|
||||
<div class="rounded-xl bg-amber-50/80 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
|
||||
<div class="absolute inset-0 flex">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-64 border-r border-green-200 bg-amber-100 hidden md:block p-4 space-y-4">
|
||||
@@ -283,7 +283,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-2 w-16 bg-gray-400 rounded mb-2"></div>
|
||||
<div id="typewriter-text" class="text-xl font-mono text-gray-800 font-bold min-h-[28px]">Building features...</div>
|
||||
<div id="typewriter-text" class="text-xl font-mono text-gray-800 font-bold h-[28px] whitespace-nowrap overflow-hidden">Building features...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@@ -341,8 +341,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Right: Animated Preview (lazy, connection-aware) -->
|
||||
<div class="relative rounded-3xl bg-white p-6 shadow-2xl ring-1 ring-amber-100">
|
||||
<div class="rounded-xl bg-amber-50 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
|
||||
<div class="relative rounded-3xl p-6">
|
||||
<div class="rounded-xl bg-amber-50/80 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
|
||||
<img class="lazy-animate w-full h-full object-cover" data-src="/assets/animation.webp" data-poster="/assets/Plugin.png" alt="Builder animation preview" width="900" height="506" loading="lazy" decoding="async" style="display:block">
|
||||
<noscript><img src="/assets/animation.webp" alt="Builder animation preview" class="w-full h-full object-cover"></noscript>
|
||||
</div>
|
||||
|
||||
158
chat/server.js
158
chat/server.js
@@ -17,6 +17,7 @@ const PDFDocument = require('pdfkit');
|
||||
const security = require('./security');
|
||||
const { createExternalWpTester, getExternalTestingConfig } = require('./external-wp-testing');
|
||||
const blogSystem = require('./blog-system');
|
||||
const versionManager = require('./src/utils/versionManager');
|
||||
|
||||
let sharp = null;
|
||||
try {
|
||||
@@ -7476,6 +7477,9 @@ function serializeSession(session) {
|
||||
planSummary: session.planSummary,
|
||||
planUserRequest: session.planUserRequest,
|
||||
planApproved: !!session.planApproved,
|
||||
pluginVersion: session.pluginVersion,
|
||||
pluginVersionHistory: session.pluginVersionHistory || [],
|
||||
lastVersionBumpType: session.lastVersionBumpType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7654,7 +7658,11 @@ async function createSession(payload = {}, userId, appId) {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
messages: [],
|
||||
pending: 0
|
||||
pending: 0,
|
||||
// Version tracking for WordPress plugins
|
||||
pluginVersion: null, // Will be detected from plugin file on first export
|
||||
pluginVersionHistory: [], // Track version changes over time
|
||||
lastVersionBumpType: null // 'major', 'minor', 'patch', or 'keep'
|
||||
};
|
||||
|
||||
// WordPress identifies plugins by their folder + main file (plugin basename).
|
||||
@@ -16829,6 +16837,55 @@ async function handleUploadApp(req, res, userId) {
|
||||
const files = await extractZipToWorkspace(zipBuffer, session.workspaceDir);
|
||||
session.planSummary = session.planSummary || 'Imported from ZIP upload';
|
||||
session.planUserRequest = session.planUserRequest || displayName;
|
||||
|
||||
// Try to detect and preserve version from imported plugin
|
||||
try {
|
||||
// Collect PHP files to find main plugin file
|
||||
const validFiles = [];
|
||||
await collectValidFiles(session.workspaceDir, session.workspaceDir, validFiles, [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'.data',
|
||||
'uploads',
|
||||
'*.log',
|
||||
'*.zip'
|
||||
]);
|
||||
|
||||
// Find main plugin file and extract version
|
||||
for (const fileInfo of validFiles) {
|
||||
if (fileInfo.fullPath.endsWith('.php')) {
|
||||
try {
|
||||
const content = await fs.readFile(fileInfo.fullPath, 'utf-8');
|
||||
if (content.includes('Plugin Name:')) {
|
||||
const versions = versionManager.extractAllVersions(content);
|
||||
if (versions.detectedVersion) {
|
||||
session.pluginVersion = versions.detectedVersion;
|
||||
session.pluginVersionHistory = [{
|
||||
version: versions.detectedVersion,
|
||||
previousVersion: null,
|
||||
bumpType: 'import',
|
||||
timestamp: new Date().toISOString(),
|
||||
fileUpdated: fileInfo.relativePath,
|
||||
note: 'Imported from uploaded ZIP'
|
||||
}];
|
||||
log('Detected version from imported plugin', {
|
||||
sessionId: session.id,
|
||||
version: versions.detectedVersion,
|
||||
file: fileInfo.relativePath
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue to next file
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (versionError) {
|
||||
log('Version detection failed during import', { error: String(versionError) });
|
||||
// Don't fail the import if version detection fails
|
||||
}
|
||||
|
||||
await persistState();
|
||||
return sendJson(res, 201, { session: serializeSession(session), files });
|
||||
} catch (error) {
|
||||
@@ -16956,6 +17013,78 @@ async function handleExportZip(_req, res, sessionId, userId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Version management for WordPress plugins
|
||||
let versionUpdatedContent = null;
|
||||
let oldVersion = null;
|
||||
let newVersion = null;
|
||||
|
||||
if (mainPluginFile && pluginContent) {
|
||||
try {
|
||||
// Extract current version from plugin file
|
||||
const versions = versionManager.extractAllVersions(pluginContent);
|
||||
const detectedVersion = versions.detectedVersion;
|
||||
|
||||
// Determine which version to use:
|
||||
// 1. Session version if available and valid
|
||||
// 2. Detected version from file
|
||||
// 3. Default to 1.0.0 if nothing found
|
||||
let currentVersion = session.pluginVersion || detectedVersion || '1.0.0';
|
||||
|
||||
// If we detected a version from file but session doesn't have one, use detected
|
||||
if (detectedVersion && !session.pluginVersion) {
|
||||
currentVersion = detectedVersion;
|
||||
log('Detected version from imported plugin file', {
|
||||
file: mainPluginFile.relativePath,
|
||||
version: detectedVersion
|
||||
});
|
||||
}
|
||||
|
||||
// Bump the version (default to patch bump)
|
||||
// Note: In the future, this could be configurable via query parameter
|
||||
newVersion = versionManager.bumpVersion(currentVersion, 'patch');
|
||||
|
||||
if (newVersion && newVersion !== currentVersion) {
|
||||
// Update the plugin content with new version
|
||||
versionUpdatedContent = versionManager.updateVersionInContent(
|
||||
pluginContent,
|
||||
currentVersion,
|
||||
newVersion
|
||||
);
|
||||
|
||||
if (versionUpdatedContent) {
|
||||
oldVersion = currentVersion;
|
||||
|
||||
// Update session tracking
|
||||
session.pluginVersion = newVersion;
|
||||
session.lastVersionBumpType = 'patch';
|
||||
session.pluginVersionHistory.push({
|
||||
version: newVersion,
|
||||
previousVersion: currentVersion,
|
||||
bumpType: 'patch',
|
||||
timestamp: new Date().toISOString(),
|
||||
fileUpdated: mainPluginFile.relativePath
|
||||
});
|
||||
|
||||
log('Bumped plugin version', {
|
||||
from: currentVersion,
|
||||
to: newVersion,
|
||||
file: mainPluginFile.relativePath,
|
||||
sessionId: session.id
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Keep current version if bump failed
|
||||
session.pluginVersion = currentVersion;
|
||||
}
|
||||
} catch (versionError) {
|
||||
log('Version detection/update failed, continuing with original files', {
|
||||
error: String(versionError),
|
||||
file: mainPluginFile?.relativePath
|
||||
});
|
||||
// Don't fail the export if version management fails
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the plugin folder name
|
||||
// WordPress identifies plugins by folder name + main file name
|
||||
let pluginFolderName = session.pluginSlug;
|
||||
@@ -16977,7 +17106,7 @@ async function handleExportZip(_req, res, sessionId, userId) {
|
||||
log('Plugin folder will be normalized', { file: mainPluginFile.relativePath, existingFolder, pluginSlug: pluginFolderName });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Determine the optimal root to avoid nested wrappers
|
||||
const optimalRoot = findOptimalExportRoot(validFiles, session.workspaceDir);
|
||||
|
||||
@@ -17019,6 +17148,9 @@ async function handleExportZip(_req, res, sessionId, userId) {
|
||||
for (const fileInfo of validFiles) {
|
||||
const relativePath = path.relative(optimalRoot, fileInfo.fullPath);
|
||||
|
||||
// Check if this is the main plugin file with updated content
|
||||
const isMainPluginFile = mainPluginFile && fileInfo.fullPath === mainPluginFile.fullPath;
|
||||
|
||||
if (wrapInPluginFolder) {
|
||||
let archivePath;
|
||||
|
||||
@@ -17036,9 +17168,19 @@ async function handleExportZip(_req, res, sessionId, userId) {
|
||||
archivePath = path.join(wrapInPluginFolder, relativePath);
|
||||
}
|
||||
|
||||
archive.file(fileInfo.fullPath, { name: archivePath });
|
||||
// If this is the main plugin file and we have updated content, use it
|
||||
if (isMainPluginFile && versionUpdatedContent) {
|
||||
archive.append(Buffer.from(versionUpdatedContent, 'utf-8'), { name: archivePath });
|
||||
} else {
|
||||
archive.file(fileInfo.fullPath, { name: archivePath });
|
||||
}
|
||||
} else {
|
||||
archive.file(fileInfo.fullPath, { name: relativePath });
|
||||
// If this is the main plugin file and we have updated content, use it
|
||||
if (isMainPluginFile && versionUpdatedContent) {
|
||||
archive.append(Buffer.from(versionUpdatedContent, 'utf-8'), { name: relativePath });
|
||||
} else {
|
||||
archive.file(fileInfo.fullPath, { name: relativePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17075,7 +17217,13 @@ async function handleExportZip(_req, res, sessionId, userId) {
|
||||
'Content-Length': zipContent.length
|
||||
});
|
||||
res.end(zipContent);
|
||||
log('Export completed successfully', { filename: zipFilename, size: zipContent.length, fileCount });
|
||||
log('Export completed successfully', {
|
||||
filename: zipFilename,
|
||||
size: zipContent.length,
|
||||
fileCount,
|
||||
version: newVersion || session.pluginVersion || 'unknown',
|
||||
previousVersion: oldVersion || 'initial'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message && error.message.includes('size exceeds')) {
|
||||
return sendJson(res, 400, { error: error.message });
|
||||
|
||||
309
chat/src/utils/versionManager.js
Normal file
309
chat/src/utils/versionManager.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Version Manager Utility
|
||||
* Handles WordPress plugin version parsing, bumping, and detection
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
/**
|
||||
* Valid semantic version regex (X.Y.Z format)
|
||||
*/
|
||||
const VERSION_REGEX = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
/**
|
||||
* WordPress plugin header version regex
|
||||
* Matches: Version: 1.2.3 or Version:1.2.3
|
||||
*/
|
||||
const WP_HEADER_VERSION_REGEX = /^\s*\*?\s*Version:\s*(\d+\.\d+\.\d+)/im;
|
||||
|
||||
/**
|
||||
* PHP define version regex
|
||||
* Matches: define('CONSTANT_NAME', '1.2.3') or define( "CONSTANT_NAME", "1.2.3" )
|
||||
*/
|
||||
const PHP_DEFINE_VERSION_REGEX = /define\s*\(\s*['"][A-Z_]*VERSION['"]\s*,\s*['"](\d+\.\d+\.\d+)['"]\s*\)/gi;
|
||||
|
||||
/**
|
||||
* PHP constant assignment regex (alternative format)
|
||||
* Matches: const VERSION = '1.2.3';
|
||||
*/
|
||||
const PHP_CONST_VERSION_REGEX = /const\s+[A-Z_]*VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/gi;
|
||||
|
||||
/**
|
||||
* Parse a version string into components
|
||||
* @param {string} version - Version string (e.g., "1.2.3")
|
||||
* @returns {object|null} - { major, minor, patch } or null if invalid
|
||||
*/
|
||||
function parseVersion(version) {
|
||||
if (!version || typeof version !== 'string') return null;
|
||||
|
||||
const cleanVersion = version.trim();
|
||||
if (!VERSION_REGEX.test(cleanVersion)) return null;
|
||||
|
||||
const [major, minor, patch] = cleanVersion.split('.').map(Number);
|
||||
return { major, minor, patch, raw: cleanVersion };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format version components into a string
|
||||
* @param {number} major
|
||||
* @param {number} minor
|
||||
* @param {number} patch
|
||||
* @returns {string} - Formatted version (e.g., "1.2.3")
|
||||
*/
|
||||
function formatVersion(major, minor, patch) {
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bump a version number
|
||||
* @param {string} currentVersion - Current version (e.g., "1.2.3")
|
||||
* @param {string} bumpType - 'major', 'minor', 'patch', or 'keep'
|
||||
* @returns {string|null} - New version or null if invalid input
|
||||
*/
|
||||
function bumpVersion(currentVersion, bumpType = 'patch') {
|
||||
const parsed = parseVersion(currentVersion);
|
||||
if (!parsed) return null;
|
||||
|
||||
const { major, minor, patch } = parsed;
|
||||
|
||||
switch (bumpType.toLowerCase()) {
|
||||
case 'major':
|
||||
return formatVersion(major + 1, 0, 0);
|
||||
case 'minor':
|
||||
return formatVersion(major, minor + 1, 0);
|
||||
case 'patch':
|
||||
return formatVersion(major, minor, patch + 1);
|
||||
case 'keep':
|
||||
return currentVersion;
|
||||
default:
|
||||
// Default to patch bump
|
||||
return formatVersion(major, minor, patch + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from WordPress plugin header
|
||||
* @param {string} content - PHP file content
|
||||
* @returns {string|null} - Version string or null
|
||||
*/
|
||||
function extractHeaderVersion(content) {
|
||||
if (!content) return null;
|
||||
const match = content.match(WP_HEADER_VERSION_REGEX);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from PHP define() constants
|
||||
* @param {string} content - PHP file content
|
||||
* @returns {string|null} - Version string or null
|
||||
*/
|
||||
function extractDefineVersion(content) {
|
||||
if (!content) return null;
|
||||
// Use non-global version of regex to properly capture groups
|
||||
const regex = /define\s*\(\s*['"][A-Z_]*VERSION['"]\s*,\s*['"](\d+\.\d+\.\d+)['"]\s*\)/i;
|
||||
const match = content.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from PHP const assignments
|
||||
* @param {string} content - PHP file content
|
||||
* @returns {string|null} - Version string or null
|
||||
*/
|
||||
function extractConstVersion(content) {
|
||||
if (!content) return null;
|
||||
const regex = new RegExp(PHP_CONST_VERSION_REGEX);
|
||||
const match = regex.exec(content);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the main plugin file in a directory
|
||||
* Searches for PHP files with WordPress plugin headers
|
||||
* @param {string} dirPath - Directory to search
|
||||
* @param {Array} validFiles - Optional array of file objects from collectValidFiles
|
||||
* @returns {Promise<object|null>} - { fullPath, relativePath, content } or null
|
||||
*/
|
||||
async function findMainPluginFile(dirPath, validFiles = null) {
|
||||
// If validFiles not provided, collect them
|
||||
if (!validFiles) {
|
||||
validFiles = [];
|
||||
await collectPhpFiles(dirPath, dirPath, validFiles);
|
||||
}
|
||||
|
||||
// Sort by likelihood of being main file (root level first, shorter names preferred)
|
||||
const phpFiles = validFiles
|
||||
.filter(f => f.fullPath.endsWith('.php'))
|
||||
.sort((a, b) => {
|
||||
const aDepth = a.relativePath.split(path.sep).length;
|
||||
const bDepth = b.relativePath.split(path.sep).length;
|
||||
if (aDepth !== bDepth) return aDepth - bDepth;
|
||||
return a.relativePath.length - b.relativePath.length;
|
||||
});
|
||||
|
||||
for (const fileInfo of phpFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(fileInfo.fullPath, 'utf-8');
|
||||
// Check for WordPress plugin header
|
||||
if (content.includes('Plugin Name:')) {
|
||||
return {
|
||||
fullPath: fileInfo.fullPath,
|
||||
relativePath: fileInfo.relativePath,
|
||||
content
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue to next file
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to collect PHP files recursively
|
||||
*/
|
||||
async function collectPhpFiles(rootDir, currentDir, files) {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(rootDir, fullPath);
|
||||
|
||||
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
||||
await collectPhpFiles(rootDir, fullPath, files);
|
||||
} else if (entry.isFile() && entry.name.endsWith('.php')) {
|
||||
files.push({ fullPath, relativePath });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all version information from a plugin file
|
||||
* @param {string} content - PHP file content
|
||||
* @returns {object} - { headerVersion, defineVersion, constVersion, detectedVersion }
|
||||
*/
|
||||
function extractAllVersions(content) {
|
||||
const headerVersion = extractHeaderVersion(content);
|
||||
const defineVersion = extractDefineVersion(content);
|
||||
const constVersion = extractConstVersion(content);
|
||||
|
||||
// Use first non-null version found (priority: header > define > const)
|
||||
const detectedVersion = headerVersion || defineVersion || constVersion;
|
||||
|
||||
return {
|
||||
headerVersion,
|
||||
defineVersion,
|
||||
constVersion,
|
||||
detectedVersion
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update version in plugin file content
|
||||
* @param {string} content - Original file content
|
||||
* @param {string} oldVersion - Current version
|
||||
* @param {string} newVersion - New version to set
|
||||
* @returns {string|null} - Updated content or null if no changes needed
|
||||
*/
|
||||
function updateVersionInContent(content, oldVersion, newVersion) {
|
||||
if (!content || !oldVersion || !newVersion || oldVersion === newVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let updated = content;
|
||||
let changesMade = false;
|
||||
|
||||
// Update WordPress plugin header version
|
||||
const headerRegex = new RegExp(
|
||||
`^(\\s*\\*?\\s*Version:)\\s*${oldVersion.replace(/\./g, '\\.')}`,
|
||||
'gim'
|
||||
);
|
||||
if (headerRegex.test(updated)) {
|
||||
updated = updated.replace(headerRegex, `$1 ${newVersion}`);
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
// Update PHP define() version constants
|
||||
const defineRegex = new RegExp(
|
||||
`(define\\s*\\(\\s*['"][A-Z_]*VERSION['"]\\s*,\\s*['"])${oldVersion.replace(/\./g, '\\.')}(['"]\\s*\\))`,
|
||||
'gi'
|
||||
);
|
||||
if (defineRegex.test(updated)) {
|
||||
updated = updated.replace(defineRegex, `$1${newVersion}$2`);
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
// Update PHP const version assignments
|
||||
const constRegex = new RegExp(
|
||||
`(const\\s+[A-Z_]*VERSION\\s*=\\s*['"])${oldVersion.replace(/\./g, '\\.')}(['"])`,
|
||||
'gi'
|
||||
);
|
||||
if (constRegex.test(updated)) {
|
||||
updated = updated.replace(constRegex, `$1${newVersion}$2`);
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
return changesMade ? updated : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a file is a WordPress plugin file
|
||||
* @param {string} content - File content
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isWordPressPluginFile(content) {
|
||||
if (!content) return false;
|
||||
return content.includes('Plugin Name:') && content.includes('Version:');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin slug from folder name or file name
|
||||
* @param {string} filePath - Path to main plugin file
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPluginSlugFromPath(filePath) {
|
||||
const parsed = path.parse(filePath);
|
||||
const parentDir = path.basename(parsed.dir);
|
||||
const fileName = parsed.name;
|
||||
|
||||
// If parent directory name matches file name (typical WordPress plugin structure: my-plugin/my-plugin.php)
|
||||
// or if there's a meaningful parent directory name (not generic like 'to', 'path', 'src')
|
||||
// use the directory name, otherwise use the file name
|
||||
const genericDirNames = ['.', '..', 'src', 'lib', 'includes', 'dist', 'build', 'to', 'path'];
|
||||
|
||||
if (parentDir === fileName) {
|
||||
// Perfect match: folder/my-plugin/my-plugin.php
|
||||
return parentDir;
|
||||
} else if (!genericDirNames.includes(parentDir.toLowerCase())) {
|
||||
// Parent directory has a meaningful name: folder/my-custom-plugin/plugin.php
|
||||
return parentDir;
|
||||
} else {
|
||||
// Use file name as fallback: folder/plugin-name.php
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseVersion,
|
||||
formatVersion,
|
||||
bumpVersion,
|
||||
extractHeaderVersion,
|
||||
extractDefineVersion,
|
||||
extractConstVersion,
|
||||
extractAllVersions,
|
||||
findMainPluginFile,
|
||||
updateVersionInContent,
|
||||
isWordPressPluginFile,
|
||||
getPluginSlugFromPath,
|
||||
// Regex exports for testing
|
||||
VERSION_REGEX,
|
||||
WP_HEADER_VERSION_REGEX,
|
||||
PHP_DEFINE_VERSION_REGEX,
|
||||
PHP_CONST_VERSION_REGEX
|
||||
};
|
||||
115
chat/src/utils/versionManager.test.js
Normal file
115
chat/src/utils/versionManager.test.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Version Manager Tests
|
||||
* Run with: node versionManager.test.js
|
||||
*/
|
||||
|
||||
const versionManager = require('./versionManager');
|
||||
const assert = require('assert');
|
||||
|
||||
console.log('Running version manager tests...\n');
|
||||
|
||||
// Test parseVersion
|
||||
console.log('Test 1: parseVersion');
|
||||
assert.deepStrictEqual(versionManager.parseVersion('1.2.3'), { major: 1, minor: 2, patch: 3, raw: '1.2.3' });
|
||||
assert.strictEqual(versionManager.parseVersion('invalid'), null);
|
||||
assert.strictEqual(versionManager.parseVersion('1.2'), null);
|
||||
assert.strictEqual(versionManager.parseVersion(''), null);
|
||||
console.log('✓ parseVersion tests passed\n');
|
||||
|
||||
// Test formatVersion
|
||||
console.log('Test 2: formatVersion');
|
||||
assert.strictEqual(versionManager.formatVersion(1, 2, 3), '1.2.3');
|
||||
assert.strictEqual(versionManager.formatVersion(10, 0, 0), '10.0.0');
|
||||
console.log('✓ formatVersion tests passed\n');
|
||||
|
||||
// Test bumpVersion
|
||||
console.log('Test 3: bumpVersion');
|
||||
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'patch'), '1.2.4');
|
||||
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'minor'), '1.3.0');
|
||||
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'major'), '2.0.0');
|
||||
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'keep'), '1.2.3');
|
||||
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'unknown'), '1.2.4'); // defaults to patch
|
||||
assert.strictEqual(versionManager.bumpVersion('invalid', 'patch'), null);
|
||||
console.log('✓ bumpVersion tests passed\n');
|
||||
|
||||
// Test extractHeaderVersion
|
||||
console.log('Test 4: extractHeaderVersion');
|
||||
const headerContent = `<?php
|
||||
/**
|
||||
* Plugin Name: Test Plugin
|
||||
* Version: 2.5.1
|
||||
* Author: Test
|
||||
*/`;
|
||||
assert.strictEqual(versionManager.extractHeaderVersion(headerContent), '2.5.1');
|
||||
|
||||
const headerContentNoSpace = `<?php
|
||||
/**
|
||||
* Plugin Name: Test Plugin
|
||||
* Version:3.0.0
|
||||
*/`;
|
||||
assert.strictEqual(versionManager.extractHeaderVersion(headerContentNoSpace), '3.0.0');
|
||||
|
||||
assert.strictEqual(versionManager.extractHeaderVersion('no version here'), null);
|
||||
console.log('✓ extractHeaderVersion tests passed\n');
|
||||
|
||||
// Test extractDefineVersion
|
||||
console.log('Test 5: extractDefineVersion');
|
||||
const defineContent = `<?php
|
||||
define('TEST_PLUGIN_VERSION', '1.2.3');
|
||||
define("ANOTHER_VERSION", "2.0.0");`;
|
||||
assert.strictEqual(versionManager.extractDefineVersion(defineContent), '1.2.3');
|
||||
|
||||
const defineContentSpaces = `<?php
|
||||
define( 'MY_VERSION' , '3.4.5' );`;
|
||||
assert.strictEqual(versionManager.extractDefineVersion(defineContentSpaces), '3.4.5');
|
||||
|
||||
assert.strictEqual(versionManager.extractDefineVersion('no version here'), null);
|
||||
console.log('✓ extractDefineVersion tests passed\n');
|
||||
|
||||
// Test extractAllVersions
|
||||
console.log('Test 6: extractAllVersions');
|
||||
const allVersionsContent = `<?php
|
||||
/**
|
||||
* Plugin Name: Test
|
||||
* Version: 1.2.3
|
||||
*/
|
||||
define('PLUGIN_VERSION', '1.2.3');`;
|
||||
const allVersions = versionManager.extractAllVersions(allVersionsContent);
|
||||
assert.strictEqual(allVersions.headerVersion, '1.2.3');
|
||||
assert.strictEqual(allVersions.defineVersion, '1.2.3');
|
||||
assert.strictEqual(allVersions.detectedVersion, '1.2.3');
|
||||
console.log('✓ extractAllVersions tests passed\n');
|
||||
|
||||
// Test updateVersionInContent
|
||||
console.log('Test 7: updateVersionInContent');
|
||||
const originalContent = `<?php
|
||||
/**
|
||||
* Plugin Name: Test Plugin
|
||||
* Version: 1.0.0
|
||||
* Author: Test
|
||||
*/
|
||||
define('PLUGIN_VERSION', '1.0.0');`;
|
||||
|
||||
const updatedContent = versionManager.updateVersionInContent(originalContent, '1.0.0', '1.1.0');
|
||||
assert(updatedContent.includes('Version: 1.1.0'));
|
||||
assert(updatedContent.includes("define('PLUGIN_VERSION', '1.1.0')"));
|
||||
assert(!updatedContent.includes('Version: 1.0.0'));
|
||||
assert(!updatedContent.includes("define('PLUGIN_VERSION', '1.0.0')"));
|
||||
console.log('✓ updateVersionInContent tests passed\n');
|
||||
|
||||
// Test isWordPressPluginFile
|
||||
console.log('Test 8: isWordPressPluginFile');
|
||||
assert.strictEqual(versionManager.isWordPressPluginFile(headerContent), true);
|
||||
assert.strictEqual(versionManager.isWordPressPluginFile('<?php echo "hello";'), false);
|
||||
assert.strictEqual(versionManager.isWordPressPluginFile(''), false);
|
||||
console.log('✓ isWordPressPluginFile tests passed\n');
|
||||
|
||||
// Test getPluginSlugFromPath
|
||||
console.log('Test 9: getPluginSlugFromPath');
|
||||
assert.strictEqual(versionManager.getPluginSlugFromPath('/path/to/my-plugin/my-plugin.php'), 'my-plugin');
|
||||
assert.strictEqual(versionManager.getPluginSlugFromPath('/path/to/plugin-name.php'), 'plugin-name');
|
||||
console.log('✓ getPluginSlugFromPath tests passed\n');
|
||||
|
||||
console.log('========================================');
|
||||
console.log('All version manager tests passed! ✓');
|
||||
console.log('========================================');
|
||||
Reference in New Issue
Block a user