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:
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