Skip to content

Commit 9cda44e

Browse files
committed
sync versions script
1 parent a245202 commit 9cda44e

4 files changed

Lines changed: 339 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"prettier": "lage prettier",
2828
"prettier-fix": "lage prettier-fix",
2929
"publish:beachball": "beachball publish --bump-deps -m\"📦 applying package updates ***NO_CI***\" --verbose",
30+
"sync-npm-versions": "node ./scripts/sync-npm-versions.js",
3031
"test": "lage test",
3132
"test-links": "markdown-link-check"
3233
},
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# NPM Version Sync Script
2+
3+
This script helps synchronize local package.json versions with the latest versions published on NPM.
4+
5+
## Purpose
6+
7+
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.
8+
9+
## Usage
10+
11+
### From the root directory:
12+
```bash
13+
# Run the sync script
14+
yarn sync-npm-versions
15+
16+
# Or run directly with node
17+
node scripts/sync-npm-versions.js
18+
```
19+
20+
### From the scripts directory:
21+
```bash
22+
node sync-npm-versions.js
23+
```
24+
25+
## What it does
26+
27+
1. **Scans the workspace** - Finds all package.json files in the project
28+
2. **Checks NPM registry** - Fetches the latest version for each public package
29+
3. **Compares versions** - Uses semantic versioning comparison
30+
4. **Updates package.json** - Updates local files when NPM has a newer version
31+
5. **Provides feedback** - Shows what was updated and what was skipped
32+
33+
## Features
34+
35+
- **Skips private packages** - Won't try to check packages marked as private
36+
- **Handles missing packages** - Gracefully handles packages not found on NPM
37+
- **Semantic version comparison** - Properly compares version numbers (e.g., 1.10.0 > 1.2.0)
38+
- **Rate limiting** - Adds small delays to avoid overwhelming NPM registry
39+
- **Detailed logging** - Shows progress and results for each package
40+
41+
## Output Examples
42+
43+
```
44+
🚀 Starting version sync for /path/to/project
45+
📁 Found 45 package.json files
46+
47+
📂 Processing packages/components/Avatar/package.json
48+
🔍 Checking @fluentui-react-native/avatar (current: 1.12.7)
49+
📦 Updating @fluentui-react-native/avatar: 1.12.7 → 1.12.8
50+
51+
📂 Processing packages/framework/use-slot/package.json
52+
🔍 Checking @fluentui-react-native/use-slot (current: 0.6.2)
53+
✅ @fluentui-react-native/use-slot is up to date (0.6.2)
54+
55+
✨ Sync complete! Updated 1 package(s).
56+
```
57+
58+
## Testing
59+
60+
Run the test suite to verify the script works correctly:
61+
62+
```bash
63+
node scripts/test-sync-npm-versions.js
64+
```
65+
66+
## Important Notes
67+
68+
- **Review changes** - Always review the changes before committing
69+
- **Test compatibility** - Run tests after updating to ensure compatibility
70+
- **Backup recommended** - Consider backing up your workspace before running
71+
- **Network required** - Script needs internet access to check NPM registry
72+
73+
## Troubleshooting
74+
75+
### Common Issues
76+
77+
1. **NPM registry timeouts** - Script includes retry logic and rate limiting
78+
2. **Package not found** - Private or scoped packages may not be accessible
79+
3. **Version format issues** - Script handles standard semver formats
80+
81+
### Error Messages
82+
83+
- `⚠️ Could not fetch version for package-name` - Package not found on NPM or network issue
84+
- `⚠️ No version found in package.json` - Package.json missing version field
85+
- `⚠️ Local version (X.X.X) is newer than NPM (Y.Y.Y)` - Local version ahead of NPM
86+
87+
## Integration
88+
89+
This script can be integrated into CI/CD pipelines or pre-publish workflows to ensure version consistency before releases.

scripts/sync-npm-versions.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { execSync } = require('child_process');
6+
7+
/**
8+
* Compares two semver version strings
9+
* @param {string} version1 - First version string
10+
* @param {string} version2 - Second version string
11+
* @returns {number} - Returns 1 if version1 > version2, -1 if version1 < version2, 0 if equal
12+
*/
13+
function compareVersions(version1, version2) {
14+
// Remove pre-release identifiers for comparison
15+
const cleanVersion1 = version1.split('-')[0];
16+
const cleanVersion2 = version2.split('-')[0];
17+
18+
const v1parts = cleanVersion1.split('.').map(Number);
19+
const v2parts = cleanVersion2.split('.').map(Number);
20+
21+
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
22+
const v1part = v1parts[i] || 0;
23+
const v2part = v2parts[i] || 0;
24+
25+
if (v1part > v2part) return 1;
26+
if (v1part < v2part) return -1;
27+
}
28+
29+
// If base versions are equal, check pre-release
30+
if (cleanVersion1 !== cleanVersion2) {
31+
return 0; // Base versions are different, already handled above
32+
}
33+
34+
// If one has pre-release and other doesn't, release version is higher
35+
const v1hasPrerelease = version1.includes('-');
36+
const v2hasPrerelease = version2.includes('-');
37+
38+
if (v1hasPrerelease && !v2hasPrerelease) return -1;
39+
if (!v1hasPrerelease && v2hasPrerelease) return 1;
40+
41+
return 0;
42+
}
43+
44+
/**
45+
* Gets the latest version of a package from NPM
46+
* @param {string} packageName - Name of the package
47+
* @returns {string|null} - Latest version string or null if not found
48+
*/
49+
async function getLatestNpmVersion(packageName) {
50+
try {
51+
const command = `npm view ${packageName} version`;
52+
const output = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
53+
return output.trim();
54+
} catch (error) {
55+
console.warn(`⚠️ Could not fetch version for ${packageName}: ${error.message}`);
56+
return null;
57+
}
58+
}
59+
60+
/**
61+
* Finds all package.json files in the workspace
62+
* @param {string} dir - Directory to search
63+
* @param {string[]} results - Array to store results
64+
* @returns {string[]} - Array of package.json file paths
65+
*/
66+
function findPackageJsonFiles(dir, results = []) {
67+
const files = fs.readdirSync(dir);
68+
69+
for (const file of files) {
70+
const fullPath = path.join(dir, file);
71+
const stat = fs.statSync(fullPath);
72+
73+
if (stat.isDirectory()) {
74+
// Skip node_modules directories
75+
if (file !== 'node_modules' && file !== '.git') {
76+
findPackageJsonFiles(fullPath, results);
77+
}
78+
} else if (file === 'package.json') {
79+
results.push(fullPath);
80+
}
81+
}
82+
83+
return results;
84+
}
85+
86+
/**
87+
* Updates package.json version if NPM has a newer version
88+
* @param {string} packageJsonPath - Path to package.json file
89+
* @returns {Promise<boolean>} - True if updated, false otherwise
90+
*/
91+
async function updatePackageVersion(packageJsonPath) {
92+
try {
93+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
94+
95+
// Skip private packages and packages without a name
96+
if (packageJson.private || !packageJson.name) {
97+
return false;
98+
}
99+
100+
const currentVersion = packageJson.version;
101+
if (!currentVersion) {
102+
console.warn(`⚠️ No version found in ${packageJsonPath}`);
103+
return false;
104+
}
105+
106+
console.log(`🔍 Checking ${packageJson.name} (current: ${currentVersion})`);
107+
108+
const latestVersion = await getLatestNpmVersion(packageJson.name);
109+
if (!latestVersion) {
110+
return false;
111+
}
112+
113+
const comparison = compareVersions(latestVersion, currentVersion);
114+
115+
if (comparison > 0) {
116+
console.log(`📦 Updating ${packageJson.name}: ${currentVersion}${latestVersion}`);
117+
118+
// Update the version
119+
packageJson.version = latestVersion;
120+
121+
// Write the updated package.json
122+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
123+
124+
return true;
125+
} else if (comparison === 0) {
126+
console.log(`✅ ${packageJson.name} is up to date (${currentVersion})`);
127+
} else {
128+
console.log(`⚠️ ${packageJson.name} local version (${currentVersion}) is newer than NPM (${latestVersion})`);
129+
}
130+
131+
return false;
132+
} catch (error) {
133+
console.error(`❌ Error processing ${packageJsonPath}:`, error.message);
134+
return false;
135+
}
136+
}
137+
138+
/**
139+
* Main function to sync all package versions
140+
*/
141+
async function syncAllPackageVersions() {
142+
const rootDir = path.resolve(__dirname, '..');
143+
console.log(`🚀 Starting version sync for ${rootDir}`);
144+
145+
const packageJsonFiles = findPackageJsonFiles(rootDir);
146+
console.log(`📁 Found ${packageJsonFiles.length} package.json files`);
147+
148+
let updatedCount = 0;
149+
150+
for (const packageJsonPath of packageJsonFiles) {
151+
const relativePath = path.relative(rootDir, packageJsonPath);
152+
console.log(`\n📂 Processing ${relativePath}`);
153+
154+
const wasUpdated = await updatePackageVersion(packageJsonPath);
155+
if (wasUpdated) {
156+
updatedCount++;
157+
}
158+
159+
// Add a small delay to avoid overwhelming NPM registry
160+
await new Promise(resolve => setTimeout(resolve, 100));
161+
}
162+
163+
console.log(`\n✨ Sync complete! Updated ${updatedCount} package(s).`);
164+
165+
if (updatedCount > 0) {
166+
console.log('\n💡 Don\'t forget to:');
167+
console.log(' 1. Review the changes');
168+
console.log(' 2. Run tests to ensure compatibility');
169+
console.log(' 3. Commit the updated versions');
170+
}
171+
}
172+
173+
// Run the script if called directly
174+
if (require.main === module) {
175+
syncAllPackageVersions().catch(error => {
176+
console.error('❌ Script failed:', error);
177+
process.exit(1);
178+
});
179+
}
180+
181+
module.exports = {
182+
syncAllPackageVersions,
183+
updatePackageVersion,
184+
getLatestNpmVersion,
185+
compareVersions
186+
};

scripts/test-sync-npm-versions.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const { compareVersions, getLatestNpmVersion } = require('./sync-npm-versions');
2+
3+
/**
4+
* Test the compareVersions function
5+
*/
6+
function testCompareVersions() {
7+
console.log('Testing compareVersions function...');
8+
9+
const tests = [
10+
{ v1: '1.0.0', v2: '1.0.0', expected: 0 },
11+
{ v1: '1.0.1', v2: '1.0.0', expected: 1 },
12+
{ v1: '1.0.0', v2: '1.0.1', expected: -1 },
13+
{ v1: '2.0.0', v2: '1.9.9', expected: 1 },
14+
{ v1: '1.10.0', v2: '1.2.0', expected: 1 },
15+
{ v1: '1.0.0', v2: '1.0.0-beta', expected: 1 }, // Will treat -beta as 0
16+
];
17+
18+
tests.forEach(({ v1, v2, expected }, index) => {
19+
const result = compareVersions(v1, v2);
20+
const passed = result === expected;
21+
console.log(`Test ${index + 1}: ${v1} vs ${v2} = ${result} (expected ${expected}) ${passed ? '✅' : '❌'}`);
22+
});
23+
}
24+
25+
/**
26+
* Test the NPM version fetching (with a known package)
27+
*/
28+
async function testNpmVersionFetch() {
29+
console.log('\nTesting NPM version fetch...');
30+
31+
try {
32+
const version = await getLatestNpmVersion('react');
33+
console.log(`✅ Successfully fetched React version: ${version}`);
34+
} catch (error) {
35+
console.log(`❌ Failed to fetch React version: ${error.message}`);
36+
}
37+
38+
try {
39+
const version = await getLatestNpmVersion('this-package-definitely-does-not-exist-12345');
40+
console.log(`⚠️ Unexpectedly found version for non-existent package: ${version}`);
41+
} catch (error) {
42+
console.log(`✅ Correctly failed to fetch non-existent package`);
43+
}
44+
}
45+
46+
async function runTests() {
47+
console.log('🧪 Running tests for sync-npm-versions.js\n');
48+
49+
testCompareVersions();
50+
await testNpmVersionFetch();
51+
52+
console.log('\n✨ Tests complete!');
53+
}
54+
55+
// Run tests if called directly
56+
if (require.main === module) {
57+
runTests().catch(error => {
58+
console.error('❌ Test failed:', error);
59+
process.exit(1);
60+
});
61+
}
62+
63+
module.exports = { runTests };

0 commit comments

Comments
 (0)