Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"prettier": "lage prettier",
"prettier-fix": "lage prettier-fix",
"publish:beachball": "beachball publish --bump-deps -m\"📦 applying package updates ***NO_CI***\" --verbose",
"sync-npm-versions": "node ./scripts/sync-npm-versions.js",
"test": "lage test",
"test-links": "markdown-link-check"
},
Expand Down
89 changes: 89 additions & 0 deletions scripts/README-sync-npm-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# NPM Version Sync Script

This script helps synchronize local package.json versions with the latest versions published on NPM.

## Purpose

In monorepo environments, it's common for local package versions to fall behind what's actually published to NPM. This script automatically detects such discrepancies and updates the local package.json files to match the NPM registry.

## Usage

### From the root directory:
```bash
# Run the sync script
yarn sync-npm-versions

# Or run directly with node
node scripts/sync-npm-versions.js
```

### From the scripts directory:
```bash
node sync-npm-versions.js
```

## What it does

1. **Scans the workspace** - Finds all package.json files in the project
2. **Checks NPM registry** - Fetches the latest version for each public package
3. **Compares versions** - Uses semantic versioning comparison
4. **Updates package.json** - Updates local files when NPM has a newer version
5. **Provides feedback** - Shows what was updated and what was skipped

## Features

- **Skips private packages** - Won't try to check packages marked as private
- **Handles missing packages** - Gracefully handles packages not found on NPM
- **Semantic version comparison** - Properly compares version numbers (e.g., 1.10.0 > 1.2.0)
- **Rate limiting** - Adds small delays to avoid overwhelming NPM registry
- **Detailed logging** - Shows progress and results for each package

## Output Examples

```
🚀 Starting version sync for /path/to/project
📁 Found 45 package.json files

📂 Processing packages/components/Avatar/package.json
🔍 Checking @fluentui-react-native/avatar (current: 1.12.7)
📦 Updating @fluentui-react-native/avatar: 1.12.7 → 1.12.8

📂 Processing packages/framework/use-slot/package.json
🔍 Checking @fluentui-react-native/use-slot (current: 0.6.2)
✅ @fluentui-react-native/use-slot is up to date (0.6.2)

✨ Sync complete! Updated 1 package(s).
```

## Testing

Run the test suite to verify the script works correctly:

```bash
node scripts/test-sync-npm-versions.js
```

## Important Notes

- **Review changes** - Always review the changes before committing
- **Test compatibility** - Run tests after updating to ensure compatibility
- **Backup recommended** - Consider backing up your workspace before running
- **Network required** - Script needs internet access to check NPM registry

## Troubleshooting

### Common Issues

1. **NPM registry timeouts** - Script includes retry logic and rate limiting
2. **Package not found** - Private or scoped packages may not be accessible
3. **Version format issues** - Script handles standard semver formats

### Error Messages

- `⚠️ Could not fetch version for package-name` - Package not found on NPM or network issue
- `⚠️ No version found in package.json` - Package.json missing version field
- `⚠️ Local version (X.X.X) is newer than NPM (Y.Y.Y)` - Local version ahead of NPM

## Integration

This script can be integrated into CI/CD pipelines or pre-publish workflows to ensure version consistency before releases.
186 changes: 186 additions & 0 deletions scripts/sync-npm-versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

/**
* Compares two semver version strings
* @param {string} version1 - First version string
* @param {string} version2 - Second version string
* @returns {number} - Returns 1 if version1 > version2, -1 if version1 < version2, 0 if equal
*/
function compareVersions(version1, version2) {
// Remove pre-release identifiers for comparison
const cleanVersion1 = version1.split('-')[0];
const cleanVersion2 = version2.split('-')[0];

const v1parts = cleanVersion1.split('.').map(Number);
const v2parts = cleanVersion2.split('.').map(Number);

for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const v1part = v1parts[i] || 0;
const v2part = v2parts[i] || 0;

if (v1part > v2part) return 1;
if (v1part < v2part) return -1;
}

// If base versions are equal, check pre-release
if (cleanVersion1 !== cleanVersion2) {
return 0; // Base versions are different, already handled above
}

// If one has pre-release and other doesn't, release version is higher
const v1hasPrerelease = version1.includes('-');
const v2hasPrerelease = version2.includes('-');

if (v1hasPrerelease && !v2hasPrerelease) return -1;
if (!v1hasPrerelease && v2hasPrerelease) return 1;

return 0;
}

/**
* Gets the latest version of a package from NPM
* @param {string} packageName - Name of the package
* @returns {string|null} - Latest version string or null if not found
*/
async function getLatestNpmVersion(packageName) {
try {
const command = `npm view ${packageName} version`;
const output = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
return output.trim();
} catch (error) {
console.warn(`⚠️ Could not fetch version for ${packageName}: ${error.message}`);
return null;
}
}

/**
* Finds all package.json files in the workspace
* @param {string} dir - Directory to search
* @param {string[]} results - Array to store results
* @returns {string[]} - Array of package.json file paths
*/
function findPackageJsonFiles(dir, results = []) {
const files = fs.readdirSync(dir);

for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
// Skip node_modules directories
if (file !== 'node_modules' && file !== '.git') {
findPackageJsonFiles(fullPath, results);
}
} else if (file === 'package.json') {
results.push(fullPath);
}
}

return results;
}

/**
* Updates package.json version if NPM has a newer version
* @param {string} packageJsonPath - Path to package.json file
* @returns {Promise<boolean>} - True if updated, false otherwise
*/
async function updatePackageVersion(packageJsonPath) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));

// Skip private packages and packages without a name
if (packageJson.private || !packageJson.name) {
return false;
}

const currentVersion = packageJson.version;
if (!currentVersion) {
console.warn(`⚠️ No version found in ${packageJsonPath}`);
return false;
}

console.log(`🔍 Checking ${packageJson.name} (current: ${currentVersion})`);

const latestVersion = await getLatestNpmVersion(packageJson.name);
if (!latestVersion) {
return false;
}

const comparison = compareVersions(latestVersion, currentVersion);

if (comparison > 0) {
console.log(`📦 Updating ${packageJson.name}: ${currentVersion} → ${latestVersion}`);

// Update the version
packageJson.version = latestVersion;

// Write the updated package.json
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');

return true;
} else if (comparison === 0) {
console.log(`✅ ${packageJson.name} is up to date (${currentVersion})`);
} else {
console.log(`⚠️ ${packageJson.name} local version (${currentVersion}) is newer than NPM (${latestVersion})`);
}

return false;
} catch (error) {
console.error(`❌ Error processing ${packageJsonPath}:`, error.message);
return false;
}
}

/**
* Main function to sync all package versions
*/
async function syncAllPackageVersions() {
const rootDir = path.resolve(__dirname, '..');
console.log(`🚀 Starting version sync for ${rootDir}`);

const packageJsonFiles = findPackageJsonFiles(rootDir);
console.log(`📁 Found ${packageJsonFiles.length} package.json files`);

let updatedCount = 0;

for (const packageJsonPath of packageJsonFiles) {
const relativePath = path.relative(rootDir, packageJsonPath);
console.log(`\n📂 Processing ${relativePath}`);

const wasUpdated = await updatePackageVersion(packageJsonPath);
if (wasUpdated) {
updatedCount++;
}

// Add a small delay to avoid overwhelming NPM registry
await new Promise(resolve => setTimeout(resolve, 100));
}

console.log(`\n✨ Sync complete! Updated ${updatedCount} package(s).`);

if (updatedCount > 0) {
console.log('\n💡 Don\'t forget to:');
console.log(' 1. Review the changes');
console.log(' 2. Run tests to ensure compatibility');
console.log(' 3. Commit the updated versions');
}
}

// Run the script if called directly
if (require.main === module) {
syncAllPackageVersions().catch(error => {
console.error('❌ Script failed:', error);
process.exit(1);
});
}

module.exports = {
syncAllPackageVersions,
updatePackageVersion,
getLatestNpmVersion,
compareVersions
};
63 changes: 63 additions & 0 deletions scripts/test-sync-npm-versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const { compareVersions, getLatestNpmVersion } = require('./sync-npm-versions');

/**
* Test the compareVersions function
*/
function testCompareVersions() {
console.log('Testing compareVersions function...');

const tests = [
{ v1: '1.0.0', v2: '1.0.0', expected: 0 },
{ v1: '1.0.1', v2: '1.0.0', expected: 1 },
{ v1: '1.0.0', v2: '1.0.1', expected: -1 },
{ v1: '2.0.0', v2: '1.9.9', expected: 1 },
{ v1: '1.10.0', v2: '1.2.0', expected: 1 },
{ v1: '1.0.0', v2: '1.0.0-beta', expected: 1 }, // Will treat -beta as 0
];

tests.forEach(({ v1, v2, expected }, index) => {
const result = compareVersions(v1, v2);
const passed = result === expected;
console.log(`Test ${index + 1}: ${v1} vs ${v2} = ${result} (expected ${expected}) ${passed ? '✅' : '❌'}`);
});
}

/**
* Test the NPM version fetching (with a known package)
*/
async function testNpmVersionFetch() {
console.log('\nTesting NPM version fetch...');

try {
const version = await getLatestNpmVersion('react');
console.log(`✅ Successfully fetched React version: ${version}`);
} catch (error) {
console.log(`❌ Failed to fetch React version: ${error.message}`);
}

try {
const version = await getLatestNpmVersion('this-package-definitely-does-not-exist-12345');
console.log(`⚠️ Unexpectedly found version for non-existent package: ${version}`);
} catch (error) {
console.log(`✅ Correctly failed to fetch non-existent package`);
}
}

async function runTests() {
console.log('🧪 Running tests for sync-npm-versions.js\n');

testCompareVersions();
await testNpmVersionFetch();

console.log('\n✨ Tests complete!');
}

// Run tests if called directly
if (require.main === module) {
runTests().catch(error => {
console.error('❌ Test failed:', error);
process.exit(1);
});
}

module.exports = { runTests };
Loading