soso-ppm 2.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yaml +24 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/bin/soso.js +78 -0
- package/lib/cache.js +196 -0
- package/lib/commands/clean.js +22 -0
- package/lib/commands/info.js +67 -0
- package/lib/commands/install.js +220 -0
- package/lib/commands/publish.js +143 -0
- package/lib/commands/update.js +111 -0
- package/lib/config.js +126 -0
- package/lib/lockfile.js +136 -0
- package/lib/resolver.js +139 -0
- package/lib/utils/git.js +136 -0
- package/lib/utils/help.js +53 -0
- package/lib/utils/logger.js +50 -0
- package/package.json +39 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const semver = require('semver');
|
|
6
|
+
const { loadRegistry, saveRegistry } = require('../config');
|
|
7
|
+
const { success, error, info, warn } = require('../utils/logger');
|
|
8
|
+
const git = require('../utils/git');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Publish package to registry
|
|
12
|
+
*/
|
|
13
|
+
async function publish(args) {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
16
|
+
|
|
17
|
+
// Load package.json
|
|
18
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
19
|
+
throw new Error('No package.json found in current directory');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
23
|
+
|
|
24
|
+
// Validate package.json
|
|
25
|
+
validatePackageJson(packageJson);
|
|
26
|
+
|
|
27
|
+
const { name, version } = packageJson;
|
|
28
|
+
|
|
29
|
+
info(`Publishing ${name}@${version}...`);
|
|
30
|
+
|
|
31
|
+
// Check if Git repository
|
|
32
|
+
const gitDir = path.join(cwd, '.git');
|
|
33
|
+
if (!fs.existsSync(gitDir)) {
|
|
34
|
+
throw new Error('Not a git repository. Initialize with: git init');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if working directory is clean
|
|
38
|
+
const isClean = await git.isClean(cwd);
|
|
39
|
+
if (!isClean) {
|
|
40
|
+
throw new Error('Working directory is not clean. Commit or stash changes before publishing.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get git remote URL
|
|
44
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
45
|
+
if (!remoteUrl) {
|
|
46
|
+
throw new Error('No git remote configured. Add remote with: git remote add origin <url>');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
info(`Git remote: ${remoteUrl}`);
|
|
50
|
+
|
|
51
|
+
// Load registry
|
|
52
|
+
const registry = loadRegistry();
|
|
53
|
+
|
|
54
|
+
// Check if package exists
|
|
55
|
+
if (!registry.packages[name]) {
|
|
56
|
+
registry.packages[name] = {
|
|
57
|
+
name,
|
|
58
|
+
versions: {}
|
|
59
|
+
};
|
|
60
|
+
info(`Adding new package: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if version already exists
|
|
64
|
+
if (registry.packages[name].versions[version]) {
|
|
65
|
+
throw new Error(`Version ${version} already published. Update version in package.json.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create git tag
|
|
69
|
+
const tag = `v${version}`;
|
|
70
|
+
info(`Creating tag: ${tag}`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await git.createTag(tag, `Release ${version}`, cwd);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw new Error(`Failed to create tag: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Push tag to remote
|
|
79
|
+
info('Pushing tag to remote...');
|
|
80
|
+
try {
|
|
81
|
+
await git.pushTags(cwd);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// Rollback: delete local tag
|
|
84
|
+
await git.execGit(['tag', '-d', tag], { cwd });
|
|
85
|
+
throw new Error(`Failed to push tag: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update registry
|
|
89
|
+
registry.packages[name].versions[version] = {
|
|
90
|
+
version,
|
|
91
|
+
gitUrl: remoteUrl,
|
|
92
|
+
tag,
|
|
93
|
+
dependencies: packageJson.dependencies || {},
|
|
94
|
+
publishedAt: new Date().toISOString()
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
saveRegistry(registry);
|
|
98
|
+
|
|
99
|
+
success(`Published ${name}@${version}`);
|
|
100
|
+
info(`Package available at: ${remoteUrl}#${tag}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate package.json
|
|
105
|
+
*/
|
|
106
|
+
function validatePackageJson(packageJson) {
|
|
107
|
+
// Check required fields
|
|
108
|
+
if (!packageJson.name) {
|
|
109
|
+
throw new Error('package.json missing required field: name');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!packageJson.version) {
|
|
113
|
+
throw new Error('package.json missing required field: version');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Validate name
|
|
117
|
+
const nameRegex = /^(@[a-z0-9-]+\/)?[a-z0-9-]+$/;
|
|
118
|
+
if (!nameRegex.test(packageJson.name)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
'Invalid package name. Use lowercase letters, numbers, and hyphens. ' +
|
|
121
|
+
'Scoped packages: @scope/name'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate version
|
|
126
|
+
if (!semver.valid(packageJson.version)) {
|
|
127
|
+
throw new Error(`Invalid version: ${packageJson.version}. Must follow semver (e.g., 1.2.3)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get git remote URL
|
|
133
|
+
*/
|
|
134
|
+
async function getGitRemoteUrl(cwd) {
|
|
135
|
+
try {
|
|
136
|
+
const output = await git.execGit(['remote', 'get-url', 'origin'], { cwd });
|
|
137
|
+
return output.trim();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { publish };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const semver = require('semver');
|
|
6
|
+
const { loadRegistry } = require('../config');
|
|
7
|
+
const { install } = require('./install');
|
|
8
|
+
const { success, info, warn } = require('../utils/logger');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Update dependencies
|
|
12
|
+
*/
|
|
13
|
+
async function update(args) {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
16
|
+
|
|
17
|
+
// Load package.json
|
|
18
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
19
|
+
throw new Error('No package.json found in current directory');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
23
|
+
const dependencies = packageJson.dependencies || {};
|
|
24
|
+
|
|
25
|
+
if (Object.keys(dependencies).length === 0) {
|
|
26
|
+
info('No dependencies to update');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Determine what to update
|
|
31
|
+
let specificPackage = null;
|
|
32
|
+
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
33
|
+
specificPackage = args[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const registry = loadRegistry();
|
|
37
|
+
let updated = false;
|
|
38
|
+
|
|
39
|
+
if (specificPackage) {
|
|
40
|
+
// Update specific package
|
|
41
|
+
if (!dependencies[specificPackage]) {
|
|
42
|
+
throw new Error(`Package ${specificPackage} not found in dependencies`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
info(`Checking for updates to ${specificPackage}...`);
|
|
46
|
+
const newVersion = await findLatestVersion(specificPackage, dependencies[specificPackage], registry);
|
|
47
|
+
|
|
48
|
+
if (newVersion) {
|
|
49
|
+
dependencies[specificPackage] = `^${newVersion}`;
|
|
50
|
+
info(`Updated ${specificPackage} to ^${newVersion}`);
|
|
51
|
+
updated = true;
|
|
52
|
+
} else {
|
|
53
|
+
info(`${specificPackage} is already at latest version`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// Update all packages
|
|
57
|
+
info('Checking for updates...');
|
|
58
|
+
|
|
59
|
+
for (const [name, range] of Object.entries(dependencies)) {
|
|
60
|
+
const newVersion = await findLatestVersion(name, range, registry);
|
|
61
|
+
|
|
62
|
+
if (newVersion) {
|
|
63
|
+
dependencies[name] = `^${newVersion}`;
|
|
64
|
+
info(`Updated ${name} to ^${newVersion}`);
|
|
65
|
+
updated = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (updated) {
|
|
71
|
+
// Save updated package.json
|
|
72
|
+
packageJson.dependencies = dependencies;
|
|
73
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
74
|
+
|
|
75
|
+
// Reinstall
|
|
76
|
+
info('Reinstalling dependencies...');
|
|
77
|
+
await install([]);
|
|
78
|
+
|
|
79
|
+
success('Dependencies updated');
|
|
80
|
+
} else {
|
|
81
|
+
success('All dependencies are up to date');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Find latest version satisfying range
|
|
87
|
+
*/
|
|
88
|
+
async function findLatestVersion(name, range, registry) {
|
|
89
|
+
const packageData = registry.packages[name];
|
|
90
|
+
if (!packageData) {
|
|
91
|
+
warn(`Package ${name} not found in registry`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const versions = Object.keys(packageData.versions).sort(semver.rcompare);
|
|
96
|
+
|
|
97
|
+
// Find latest version matching range
|
|
98
|
+
const currentVersion = semver.maxSatisfying(versions, range);
|
|
99
|
+
|
|
100
|
+
// Find absolute latest version
|
|
101
|
+
const latestVersion = versions[0];
|
|
102
|
+
|
|
103
|
+
// Only update if there's a newer version
|
|
104
|
+
if (currentVersion && semver.gt(latestVersion, currentVersion)) {
|
|
105
|
+
return latestVersion;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { update };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get platform-specific configuration directory
|
|
9
|
+
*/
|
|
10
|
+
function getConfigDir() {
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'soso');
|
|
13
|
+
}
|
|
14
|
+
return path.join(os.homedir(), '.soso');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get cache directory
|
|
19
|
+
*/
|
|
20
|
+
function getCacheDir() {
|
|
21
|
+
return path.join(getConfigDir(), 'cache');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get registry file path
|
|
26
|
+
*/
|
|
27
|
+
function getRegistryPath() {
|
|
28
|
+
return path.join(getConfigDir(), 'registry.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get user config file path (.sosorc)
|
|
33
|
+
*/
|
|
34
|
+
function getUserConfigPath() {
|
|
35
|
+
return path.join(os.homedir(), '.sosorc');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure directory exists
|
|
40
|
+
*/
|
|
41
|
+
function ensureDir(dirPath) {
|
|
42
|
+
if (!fs.existsSync(dirPath)) {
|
|
43
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize configuration directories
|
|
49
|
+
*/
|
|
50
|
+
function initConfig() {
|
|
51
|
+
ensureDir(getConfigDir());
|
|
52
|
+
ensureDir(getCacheDir());
|
|
53
|
+
|
|
54
|
+
const registryPath = getRegistryPath();
|
|
55
|
+
if (!fs.existsSync(registryPath)) {
|
|
56
|
+
fs.writeFileSync(registryPath, JSON.stringify({ packages: {} }, null, 2));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load registry configuration
|
|
62
|
+
*/
|
|
63
|
+
function loadRegistry() {
|
|
64
|
+
const registryPath = getRegistryPath();
|
|
65
|
+
if (!fs.existsSync(registryPath)) {
|
|
66
|
+
return { packages: {} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const content = fs.readFileSync(registryPath, 'utf8');
|
|
71
|
+
return JSON.parse(content);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Failed to load registry: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Save registry configuration
|
|
79
|
+
*/
|
|
80
|
+
function saveRegistry(registry) {
|
|
81
|
+
const registryPath = getRegistryPath();
|
|
82
|
+
ensureDir(path.dirname(registryPath));
|
|
83
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Load user configuration (.sosorc)
|
|
88
|
+
*/
|
|
89
|
+
function loadUserConfig() {
|
|
90
|
+
const configPath = getUserConfigPath();
|
|
91
|
+
if (!fs.existsSync(configPath)) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
97
|
+
const config = {};
|
|
98
|
+
|
|
99
|
+
// Parse simple KEY=VALUE format
|
|
100
|
+
content.split('\n').forEach(line => {
|
|
101
|
+
line = line.trim();
|
|
102
|
+
if (line && !line.startsWith('#')) {
|
|
103
|
+
const [key, ...valueParts] = line.split('=');
|
|
104
|
+
if (key && valueParts.length > 0) {
|
|
105
|
+
config[key.trim()] = valueParts.join('=').trim();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return config;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`Failed to load user config: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
getConfigDir,
|
|
118
|
+
getCacheDir,
|
|
119
|
+
getRegistryPath,
|
|
120
|
+
getUserConfigPath,
|
|
121
|
+
ensureDir,
|
|
122
|
+
initConfig,
|
|
123
|
+
loadRegistry,
|
|
124
|
+
saveRegistry,
|
|
125
|
+
loadUserConfig
|
|
126
|
+
};
|
package/lib/lockfile.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { debug } = require('./utils/logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Lockfile manager for deterministic installs
|
|
9
|
+
*/
|
|
10
|
+
class Lockfile {
|
|
11
|
+
constructor(projectPath) {
|
|
12
|
+
this.projectPath = projectPath;
|
|
13
|
+
this.lockfilePath = path.join(projectPath, 'soso-lock.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if lockfile exists
|
|
18
|
+
*/
|
|
19
|
+
exists() {
|
|
20
|
+
return fs.existsSync(this.lockfilePath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read lockfile
|
|
25
|
+
*/
|
|
26
|
+
read() {
|
|
27
|
+
if (!this.exists()) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(this.lockfilePath, 'utf8');
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`Failed to read lockfile: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write lockfile
|
|
41
|
+
*/
|
|
42
|
+
write(packages) {
|
|
43
|
+
debug(`Writing lockfile with ${Object.keys(packages).length} packages`);
|
|
44
|
+
|
|
45
|
+
const lockfile = {
|
|
46
|
+
lockfileVersion: 1,
|
|
47
|
+
packages: {}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Sort packages alphabetically for deterministic output
|
|
51
|
+
const sortedNames = Object.keys(packages).sort();
|
|
52
|
+
|
|
53
|
+
for (const name of sortedNames) {
|
|
54
|
+
const pkg = packages[name];
|
|
55
|
+
lockfile.packages[name] = {
|
|
56
|
+
version: pkg.version,
|
|
57
|
+
resolved: pkg.resolved,
|
|
58
|
+
integrity: pkg.integrity
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Include dependencies if present
|
|
62
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
|
|
63
|
+
lockfile.packages[name].dependencies = pkg.dependencies;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const content = JSON.stringify(lockfile, null, 2) + '\n';
|
|
68
|
+
fs.writeFileSync(this.lockfilePath, content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate lockfile integrity
|
|
73
|
+
*/
|
|
74
|
+
validate(lockfileData) {
|
|
75
|
+
if (!lockfileData) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (lockfileData.lockfileVersion !== 1) {
|
|
80
|
+
throw new Error('Unsupported lockfile version');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!lockfileData.packages || typeof lockfileData.packages !== 'object') {
|
|
84
|
+
throw new Error('Invalid lockfile format: missing packages');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get package from lockfile
|
|
92
|
+
*/
|
|
93
|
+
getPackage(lockfileData, name) {
|
|
94
|
+
if (!lockfileData || !lockfileData.packages) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return lockfileData.packages[name] || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if lockfile matches current dependencies
|
|
102
|
+
*/
|
|
103
|
+
matches(lockfileData, dependencies) {
|
|
104
|
+
if (!lockfileData || !lockfileData.packages) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const lockfileNames = new Set(Object.keys(lockfileData.packages));
|
|
109
|
+
const currentNames = new Set(Object.keys(dependencies));
|
|
110
|
+
|
|
111
|
+
// Quick check: same number of packages
|
|
112
|
+
if (lockfileNames.size !== currentNames.size) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check all dependencies are in lockfile
|
|
117
|
+
for (const name of currentNames) {
|
|
118
|
+
if (!lockfileNames.has(name)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Delete lockfile
|
|
128
|
+
*/
|
|
129
|
+
delete() {
|
|
130
|
+
if (this.exists()) {
|
|
131
|
+
fs.unlinkSync(this.lockfilePath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { Lockfile };
|
package/lib/resolver.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const semver = require('semver');
|
|
4
|
+
const { debug } = require('./utils/logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve dependency tree to flat structure
|
|
8
|
+
* Implements highest-compatible-version strategy
|
|
9
|
+
*/
|
|
10
|
+
class Resolver {
|
|
11
|
+
constructor(registry) {
|
|
12
|
+
this.registry = registry;
|
|
13
|
+
this.resolved = new Map(); // package name -> resolved version
|
|
14
|
+
this.pending = new Map(); // package name -> requested ranges
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve all dependencies from root package
|
|
19
|
+
*/
|
|
20
|
+
async resolve(dependencies) {
|
|
21
|
+
if (!dependencies || Object.keys(dependencies).length === 0) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// First pass: collect all version requirements
|
|
26
|
+
await this.collectRequirements(dependencies);
|
|
27
|
+
|
|
28
|
+
// Second pass: resolve conflicts
|
|
29
|
+
this.resolveConflicts();
|
|
30
|
+
|
|
31
|
+
return Object.fromEntries(this.resolved);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively collect all dependency requirements
|
|
36
|
+
*/
|
|
37
|
+
async collectRequirements(dependencies, visited = new Set()) {
|
|
38
|
+
for (const [name, range] of Object.entries(dependencies)) {
|
|
39
|
+
debug(`Collecting requirement: ${name}@${range}`);
|
|
40
|
+
|
|
41
|
+
// Track this requirement
|
|
42
|
+
if (!this.pending.has(name)) {
|
|
43
|
+
this.pending.set(name, []);
|
|
44
|
+
}
|
|
45
|
+
this.pending.get(name).push(range);
|
|
46
|
+
|
|
47
|
+
// Avoid infinite loops
|
|
48
|
+
const key = `${name}@${range}`;
|
|
49
|
+
if (visited.has(key)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
visited.add(key);
|
|
53
|
+
|
|
54
|
+
// Get available versions from registry
|
|
55
|
+
const versions = await this.getAvailableVersions(name);
|
|
56
|
+
if (versions.length === 0) {
|
|
57
|
+
throw new Error(`Package not found in registry: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find highest version matching this range
|
|
61
|
+
const version = semver.maxSatisfying(versions, range);
|
|
62
|
+
if (!version) {
|
|
63
|
+
throw new Error(`No version of ${name} satisfies ${range}. Available: ${versions.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get package metadata to find its dependencies
|
|
67
|
+
const pkgInfo = await this.getPackageInfo(name, version);
|
|
68
|
+
|
|
69
|
+
// Recursively collect dependencies
|
|
70
|
+
if (pkgInfo.dependencies) {
|
|
71
|
+
await this.collectRequirements(pkgInfo.dependencies, visited);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve version conflicts
|
|
78
|
+
*/
|
|
79
|
+
resolveConflicts() {
|
|
80
|
+
for (const [name, ranges] of this.pending.entries()) {
|
|
81
|
+
debug(`Resolving ${name} with ranges: ${ranges.join(', ')}`);
|
|
82
|
+
|
|
83
|
+
// Get all available versions
|
|
84
|
+
const packageData = this.registry.packages[name];
|
|
85
|
+
if (!packageData) {
|
|
86
|
+
throw new Error(`Package not found: ${name}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const versions = Object.keys(packageData.versions);
|
|
90
|
+
|
|
91
|
+
// Find highest version that satisfies ALL ranges
|
|
92
|
+
let resolvedVersion = null;
|
|
93
|
+
|
|
94
|
+
for (const version of versions.sort(semver.rcompare)) {
|
|
95
|
+
const satisfiesAll = ranges.every(range => semver.satisfies(version, range));
|
|
96
|
+
if (satisfiesAll) {
|
|
97
|
+
resolvedVersion = version;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!resolvedVersion) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Cannot resolve ${name}: conflicting version ranges ${ranges.join(', ')}. ` +
|
|
105
|
+
`Available versions: ${versions.join(', ')}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.resolved.set(name, resolvedVersion);
|
|
110
|
+
debug(`Resolved ${name} to ${resolvedVersion}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get available versions for a package
|
|
116
|
+
*/
|
|
117
|
+
async getAvailableVersions(name) {
|
|
118
|
+
const packageData = this.registry.packages[name];
|
|
119
|
+
if (!packageData || !packageData.versions) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return Object.keys(packageData.versions).sort(semver.rcompare);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get package info for specific version
|
|
128
|
+
*/
|
|
129
|
+
async getPackageInfo(name, version) {
|
|
130
|
+
const packageData = this.registry.packages[name];
|
|
131
|
+
if (!packageData || !packageData.versions || !packageData.versions[version]) {
|
|
132
|
+
throw new Error(`Version ${version} not found for package ${name}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return packageData.versions[version];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { Resolver };
|