@mindbase/node-tools 1.1.0 → 1.3.0
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/README.md +170 -0
- package/bin/clear.js +9 -18
- package/bin/publish.js +236 -0
- package/package.json +8 -4
- package/src/clear/ui.js +183 -204
- package/src/common/ui/screen.js +26 -2
- package/src/common/ui/utils.js +31 -0
- package/src/git-log/ui.js +3 -10
- package/src/publish/core/builder.js +61 -0
- package/src/publish/core/dependency.js +137 -0
- package/src/publish/core/detector.js +76 -0
- package/src/publish/core/scanner.js +91 -0
- package/src/publish/core/version.js +80 -0
- package/src/publish/publisher.js +157 -0
- package/src/publish/registry/adapters/base.js +51 -0
- package/src/publish/registry/adapters/codeup.js +97 -0
- package/src/publish/registry/adapters/npm.js +122 -0
- package/src/publish/registry/global-config.js +275 -0
- package/src/publish/registry/project-config.js +172 -0
- package/src/publish/registry/registry-manager.js +118 -0
- package/src/publish/ui.js +452 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 依赖关系分析模块
|
|
3
|
+
* 分析包之间的依赖关系并计算发布顺序(拓扑排序)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 分析包之间的依赖关系
|
|
8
|
+
* @param {Array<Object>} packages - 包列表
|
|
9
|
+
* @returns {Object} 分析结果
|
|
10
|
+
*/
|
|
11
|
+
function analyzeDependencies(packages) {
|
|
12
|
+
// 构建包名到包的映射
|
|
13
|
+
const packageMap = new Map();
|
|
14
|
+
for (const pkg of packages) {
|
|
15
|
+
packageMap.set(pkg.name, pkg);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 构建依赖图
|
|
19
|
+
const graph = new Map();
|
|
20
|
+
const inDegree = new Map();
|
|
21
|
+
|
|
22
|
+
for (const pkg of packages) {
|
|
23
|
+
graph.set(pkg.name, new Set());
|
|
24
|
+
inDegree.set(pkg.name, 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 分析依赖关系
|
|
28
|
+
for (const pkg of packages) {
|
|
29
|
+
const allDeps = {
|
|
30
|
+
...pkg.dependencies,
|
|
31
|
+
...pkg.peerDependencies
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
for (const depName of Object.keys(allDeps)) {
|
|
35
|
+
// 只关心内部依赖
|
|
36
|
+
if (packageMap.has(depName)) {
|
|
37
|
+
const adjList = graph.get(pkg.name);
|
|
38
|
+
if (!adjList.has(depName)) {
|
|
39
|
+
adjList.add(depName);
|
|
40
|
+
inDegree.set(depName, inDegree.get(depName) + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 拓扑排序(Kahn 算法)
|
|
47
|
+
const order = [];
|
|
48
|
+
const queue = [];
|
|
49
|
+
|
|
50
|
+
// 找出入度为 0 的节点
|
|
51
|
+
for (const [name, degree] of inDegree) {
|
|
52
|
+
if (degree === 0) {
|
|
53
|
+
queue.push(name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
while (queue.length > 0) {
|
|
58
|
+
const current = queue.shift();
|
|
59
|
+
order.push(current);
|
|
60
|
+
|
|
61
|
+
// 移除当前节点的所有出边
|
|
62
|
+
const neighbors = graph.get(current);
|
|
63
|
+
for (const neighbor of neighbors) {
|
|
64
|
+
const newDegree = inDegree.get(neighbor) - 1;
|
|
65
|
+
inDegree.set(neighbor, newDegree);
|
|
66
|
+
|
|
67
|
+
if (newDegree === 0) {
|
|
68
|
+
queue.push(neighbor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 检测循环依赖
|
|
74
|
+
const hasCycle = order.length !== packages.length;
|
|
75
|
+
const cycles = hasCycle ? findCycles(graph, inDegree) : [];
|
|
76
|
+
|
|
77
|
+
// 将包名转换为包对象
|
|
78
|
+
const orderedPackages = order
|
|
79
|
+
.map(name => packageMap.get(name))
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
order: orderedPackages,
|
|
84
|
+
hasCycle,
|
|
85
|
+
cycles,
|
|
86
|
+
graph
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 查找循环依赖
|
|
92
|
+
* @param {Map} graph - 依赖图
|
|
93
|
+
* @param {Map} inDegree - 入度表
|
|
94
|
+
* @returns {Array<Array<string>>} 循环列表
|
|
95
|
+
*/
|
|
96
|
+
function findCycles(graph, inDegree) {
|
|
97
|
+
const cycles = [];
|
|
98
|
+
const visited = new Set();
|
|
99
|
+
const recursionStack = new Set();
|
|
100
|
+
|
|
101
|
+
function dfs(node, path) {
|
|
102
|
+
if (recursionStack.has(node)) {
|
|
103
|
+
// 找到循环
|
|
104
|
+
const cycleStart = path.indexOf(node);
|
|
105
|
+
const cycle = path.slice(cycleStart);
|
|
106
|
+
cycle.push(node);
|
|
107
|
+
cycles.push(cycle);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (visited.has(node)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
visited.add(node);
|
|
116
|
+
recursionStack.add(node);
|
|
117
|
+
|
|
118
|
+
const neighbors = graph.get(node) || new Set();
|
|
119
|
+
for (const neighbor of neighbors) {
|
|
120
|
+
dfs(neighbor, [...path, node]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
recursionStack.delete(node);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const node of graph.keys()) {
|
|
127
|
+
if (!visited.has(node)) {
|
|
128
|
+
dfs(node, []);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return cycles;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
analyzeDependencies
|
|
137
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工作空间检测模块
|
|
3
|
+
* 检测当前目录是单项目还是工作空间(npm workspace/pnpm/lerna)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { existsSync, readFileSync } = require('fs');
|
|
7
|
+
const { join } = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 检测工作空间类型
|
|
11
|
+
* @param {string} rootPath - 项目根路径
|
|
12
|
+
* @returns {Promise<Object>} 检测结果
|
|
13
|
+
*/
|
|
14
|
+
async function detectWorkspace(rootPath) {
|
|
15
|
+
const packageJsonPath = join(rootPath, 'package.json');
|
|
16
|
+
|
|
17
|
+
if (!existsSync(packageJsonPath)) {
|
|
18
|
+
return {
|
|
19
|
+
type: 'none',
|
|
20
|
+
patterns: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
25
|
+
|
|
26
|
+
// 1. 检测 npm/yarn workspaces (package.json)
|
|
27
|
+
if (packageJson.workspaces) {
|
|
28
|
+
const patterns = Array.isArray(packageJson.workspaces)
|
|
29
|
+
? packageJson.workspaces
|
|
30
|
+
: packageJson.workspaces.packages || [];
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
type: 'npm',
|
|
34
|
+
patterns: patterns.length > 0 ? patterns : ['packages/*'],
|
|
35
|
+
packageJson
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. 检测 pnpm workspace
|
|
40
|
+
const pnpmWorkspacePath = join(rootPath, 'pnpm-workspace.yaml');
|
|
41
|
+
if (existsSync(pnpmWorkspacePath)) {
|
|
42
|
+
const yaml = require('js-yaml');
|
|
43
|
+
const pnpmConfig = yaml.load(readFileSync(pnpmWorkspacePath, 'utf-8'));
|
|
44
|
+
const patterns = pnpmConfig.packages || ['packages/*'];
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
type: 'pnpm',
|
|
48
|
+
patterns,
|
|
49
|
+
packageJson
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. 检测 lerna
|
|
54
|
+
const lernaJsonPath = join(rootPath, 'lerna.json');
|
|
55
|
+
if (existsSync(lernaJsonPath)) {
|
|
56
|
+
const lernaConfig = JSON.parse(readFileSync(lernaJsonPath, 'utf-8'));
|
|
57
|
+
const patterns = lernaConfig.packages || ['packages/*'];
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
type: 'lerna',
|
|
61
|
+
patterns,
|
|
62
|
+
packageJson
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. 单项目模式
|
|
67
|
+
return {
|
|
68
|
+
type: 'single',
|
|
69
|
+
patterns: [],
|
|
70
|
+
packageJson
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
detectWorkspace
|
|
76
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 子项目扫描模块
|
|
3
|
+
* 扫描工作空间下的所有子项目
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { readFileSync, existsSync } = require('fs');
|
|
7
|
+
const { join } = require('path');
|
|
8
|
+
const { globSync } = require('glob');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 扫描工作空间中的所有包
|
|
12
|
+
* @param {string} rootPath - 工作空间根路径
|
|
13
|
+
* @param {Array<string>} patterns - glob 模式数组
|
|
14
|
+
* @returns {Array<Object>} 包列表
|
|
15
|
+
*/
|
|
16
|
+
async function scanPackages(rootPath, patterns) {
|
|
17
|
+
if (patterns.length === 0) {
|
|
18
|
+
// 单项目模式
|
|
19
|
+
return [scanSinglePackage(rootPath)];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const packages = [];
|
|
23
|
+
const seen = new Set(); // 避免重复
|
|
24
|
+
|
|
25
|
+
for (const pattern of patterns) {
|
|
26
|
+
const matches = globSync(pattern, {
|
|
27
|
+
cwd: rootPath,
|
|
28
|
+
absolute: true,
|
|
29
|
+
onlyDirectories: true
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
for (const match of matches) {
|
|
33
|
+
const packageJsonPath = join(match, 'package.json');
|
|
34
|
+
|
|
35
|
+
if (!existsSync(packageJsonPath)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
40
|
+
|
|
41
|
+
// 跳过 private 包
|
|
42
|
+
if (packageJson.private) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 避免重复
|
|
47
|
+
const key = `${packageJson.name}@${match}`;
|
|
48
|
+
if (seen.has(key)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
seen.add(key);
|
|
52
|
+
|
|
53
|
+
packages.push({
|
|
54
|
+
name: packageJson.name,
|
|
55
|
+
version: packageJson.version,
|
|
56
|
+
path: match,
|
|
57
|
+
packageJson,
|
|
58
|
+
scripts: packageJson.scripts || {},
|
|
59
|
+
dependencies: packageJson.dependencies || {},
|
|
60
|
+
peerDependencies: packageJson.peerDependencies || {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return packages;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 扫描单个项目
|
|
70
|
+
* @param {string} rootPath - 项目路径
|
|
71
|
+
* @returns {Object} 包信息
|
|
72
|
+
*/
|
|
73
|
+
function scanSinglePackage(rootPath) {
|
|
74
|
+
const packageJsonPath = join(rootPath, 'package.json');
|
|
75
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
name: packageJson.name,
|
|
79
|
+
version: packageJson.version,
|
|
80
|
+
path: rootPath,
|
|
81
|
+
packageJson,
|
|
82
|
+
scripts: packageJson.scripts || {},
|
|
83
|
+
dependencies: packageJson.dependencies || {},
|
|
84
|
+
peerDependencies: packageJson.peerDependencies || {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
scanPackages,
|
|
90
|
+
scanSinglePackage
|
|
91
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本号管理模块
|
|
3
|
+
* 处理版本号升级和 package.json 更新
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const semver = require('semver');
|
|
7
|
+
const { readFileSync, writeFileSync } = require('fs');
|
|
8
|
+
const { join } = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 升级版本号
|
|
12
|
+
* @param {string} version - 当前版本号
|
|
13
|
+
* @param {string} increment - 升级类型 (major/minor/patch/prerelease)
|
|
14
|
+
* @param {string} customVersion - 自定义版本号(当 increment 为 'custom' 时使用)
|
|
15
|
+
* @returns {string} 新版本号
|
|
16
|
+
*/
|
|
17
|
+
function bumpVersion(version, increment, customVersion = null) {
|
|
18
|
+
if (increment === 'custom') {
|
|
19
|
+
if (!customVersion) {
|
|
20
|
+
throw new Error('自定义版本号不能为空');
|
|
21
|
+
}
|
|
22
|
+
// 验证版本号格式
|
|
23
|
+
if (!semver.valid(customVersion)) {
|
|
24
|
+
throw new Error(`无效的版本号: ${customVersion}`);
|
|
25
|
+
}
|
|
26
|
+
return customVersion;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const newVersion = semver.inc(version, increment);
|
|
30
|
+
if (!newVersion) {
|
|
31
|
+
throw new Error(`无法升级版本号: ${version} + ${increment}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return newVersion;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 更新 package.json 的版本号
|
|
39
|
+
* @param {string} pkgPath - 包路径
|
|
40
|
+
* @param {string} newVersion - 新版本号
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async function updatePackageJson(pkgPath, newVersion) {
|
|
44
|
+
const packageJsonPath = join(pkgPath, 'package.json');
|
|
45
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
46
|
+
|
|
47
|
+
packageJson.version = newVersion;
|
|
48
|
+
|
|
49
|
+
writeFileSync(
|
|
50
|
+
packageJsonPath,
|
|
51
|
+
JSON.stringify(packageJson, null, 2) + '\n',
|
|
52
|
+
'utf-8'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 验证版本号是否有效
|
|
58
|
+
* @param {string} version - 版本号
|
|
59
|
+
* @returns {boolean} 是否有效
|
|
60
|
+
*/
|
|
61
|
+
function isValidVersion(version) {
|
|
62
|
+
return semver.valid(version) !== null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 比较两个版本号
|
|
67
|
+
* @param {string} v1 - 版本号1
|
|
68
|
+
* @param {string} v2 - 版本号2
|
|
69
|
+
* @returns {number} 1(v1>v2), 0(v1=v2), -1(v1<v2)
|
|
70
|
+
*/
|
|
71
|
+
function compareVersions(v1, v2) {
|
|
72
|
+
return semver.compare(v1, v2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
bumpVersion,
|
|
77
|
+
updatePackageJson,
|
|
78
|
+
isValidVersion,
|
|
79
|
+
compareVersions
|
|
80
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布编排器
|
|
3
|
+
* 协调构建、版本更新和多源发布
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const prompts = require('prompts');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { detectBuildScripts, runBuild } = require('./core/builder.js');
|
|
9
|
+
const { updatePackageJson } = require('./core/version.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 发布编排器类
|
|
13
|
+
*/
|
|
14
|
+
class Publisher {
|
|
15
|
+
constructor(registryManager) {
|
|
16
|
+
this.registryManager = registryManager;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 多源发布(串行)
|
|
21
|
+
*/
|
|
22
|
+
async publishToRegistries(pkg, registries, options) {
|
|
23
|
+
const results = [];
|
|
24
|
+
|
|
25
|
+
for (const { registry, token } of registries) {
|
|
26
|
+
const adapter = this.registryManager.createAdapter(registry, token);
|
|
27
|
+
const result = await adapter.publish(pkg, options);
|
|
28
|
+
|
|
29
|
+
results.push({
|
|
30
|
+
registry: registry.name,
|
|
31
|
+
success: result.success,
|
|
32
|
+
error: result.error
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 失败时询问是否继续
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
const shouldContinue = await this.askPublishFailed(registry.name, result.error);
|
|
38
|
+
if (!shouldContinue) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 单包发布流程
|
|
49
|
+
*/
|
|
50
|
+
async publishPackage(pkg, registries, options) {
|
|
51
|
+
// 1. 检测构建脚本
|
|
52
|
+
const buildScript = detectBuildScripts(pkg.scripts);
|
|
53
|
+
|
|
54
|
+
// 2. 执行构建
|
|
55
|
+
if (buildScript) {
|
|
56
|
+
console.log(chalk.cyan(`\n执行构建: npm run ${buildScript}`));
|
|
57
|
+
|
|
58
|
+
const buildResult = await runBuild(pkg.path, buildScript);
|
|
59
|
+
|
|
60
|
+
if (!buildResult.success) {
|
|
61
|
+
const shouldContinue = await this.askBuildFailed(pkg.name, buildResult.error);
|
|
62
|
+
if (!shouldContinue) {
|
|
63
|
+
return { skipped: true, reason: '构建失败' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. 更新版本号
|
|
69
|
+
if (pkg.newVersion) {
|
|
70
|
+
console.log(chalk.cyan(`\n更新版本号: ${pkg.version} → ${pkg.newVersion}`));
|
|
71
|
+
await updatePackageJson(pkg.path, pkg.newVersion);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. 多源发布
|
|
75
|
+
const publishResults = await this.publishToRegistries(pkg, registries, options);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
package: pkg.name,
|
|
79
|
+
buildSuccess: true,
|
|
80
|
+
publishResults
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 批量发布(工作空间模式)
|
|
86
|
+
*/
|
|
87
|
+
async publishPackages(packages, registries, options) {
|
|
88
|
+
const results = [];
|
|
89
|
+
|
|
90
|
+
for (const pkg of packages) {
|
|
91
|
+
console.log(chalk.bold(`\n[${results.length + 1}/${packages.length}] 发布: ${pkg.name}`));
|
|
92
|
+
|
|
93
|
+
const result = await this.publishPackage(pkg, registries, options);
|
|
94
|
+
results.push(result);
|
|
95
|
+
|
|
96
|
+
if (result.skipped) {
|
|
97
|
+
console.log(chalk.yellow(`跳过: ${result.reason}`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 构建失败询问
|
|
106
|
+
*/
|
|
107
|
+
async askBuildFailed(packageName, error) {
|
|
108
|
+
console.log(chalk.red(`\n构建失败: ${packageName}`));
|
|
109
|
+
console.log(chalk.red(`错误: ${error}`));
|
|
110
|
+
|
|
111
|
+
const { choice } = await prompts({
|
|
112
|
+
type: 'select',
|
|
113
|
+
name: 'choice',
|
|
114
|
+
message: '是否继续发布?',
|
|
115
|
+
choices: [
|
|
116
|
+
{ title: '重试构建', value: 'retry' },
|
|
117
|
+
{ title: '跳过构建,继续发布', value: 'continue' },
|
|
118
|
+
{ title: '取消', value: 'cancel' }
|
|
119
|
+
],
|
|
120
|
+
initial: 1
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (choice === 'cancel') {
|
|
124
|
+
return false;
|
|
125
|
+
} else if (choice === 'retry') {
|
|
126
|
+
// TODO: 实现重试逻辑
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 发布失败询问
|
|
135
|
+
*/
|
|
136
|
+
async askPublishFailed(registryName, error) {
|
|
137
|
+
console.log(chalk.red(`\n发布到 ${registryName} 失败`));
|
|
138
|
+
console.log(chalk.red(`错误: ${error}`));
|
|
139
|
+
|
|
140
|
+
const { choice } = await prompts({
|
|
141
|
+
type: 'select',
|
|
142
|
+
name: 'choice',
|
|
143
|
+
message: '是否继续?',
|
|
144
|
+
choices: [
|
|
145
|
+
{ title: '继续发布到其他源', value: 'continue' },
|
|
146
|
+
{ title: '取消', value: 'cancel' }
|
|
147
|
+
],
|
|
148
|
+
initial: 0
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return choice === 'continue';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
Publisher
|
|
157
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 基础适配器类
|
|
3
|
+
* 所有发布源适配器的基类
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class BaseAdapter {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} config - 注册源配置
|
|
9
|
+
* @param {string} token - 认证 token
|
|
10
|
+
*/
|
|
11
|
+
constructor(config, token) {
|
|
12
|
+
this.registry = config.url;
|
|
13
|
+
this.token = token;
|
|
14
|
+
this.type = config.type;
|
|
15
|
+
this.name = config.name;
|
|
16
|
+
this.id = config.id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 发布包(必须实现)
|
|
21
|
+
* @param {Object} pkg - 包信息
|
|
22
|
+
* @param {Object} options - 发布选项
|
|
23
|
+
* @returns {Promise<Object>} 发布结果
|
|
24
|
+
*/
|
|
25
|
+
async publish(pkg, options) {
|
|
26
|
+
throw new Error('publish() must be implemented by subclass');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 验证 token 有效性(必须实现)
|
|
31
|
+
* @returns {Promise<Object>} 验证结果
|
|
32
|
+
*/
|
|
33
|
+
async validateToken() {
|
|
34
|
+
throw new Error('validateToken() must be implemented by subclass');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 检查包是否存在(可选)
|
|
39
|
+
* @param {string} name - 包名
|
|
40
|
+
* @param {string} version - 版本号
|
|
41
|
+
* @returns {Promise<Object>} 检查结果
|
|
42
|
+
*/
|
|
43
|
+
async checkPackage(name, version) {
|
|
44
|
+
// 默认实现:不支持检查
|
|
45
|
+
return { supported: false };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
BaseAdapter
|
|
51
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codeup 适配器
|
|
3
|
+
* 支持发布到 Codeup 内部 npm 源
|
|
4
|
+
*
|
|
5
|
+
* 注意:这是一个示例实现,实际使用时需要根据 Codeup 的具体 API 调整
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { BaseAdapter } = require('./base.js');
|
|
9
|
+
|
|
10
|
+
class CodeupAdapter extends BaseAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* 发布包到 Codeup
|
|
13
|
+
*/
|
|
14
|
+
async publish(pkg, options) {
|
|
15
|
+
const { tag = 'latest', dryRun = false } = options;
|
|
16
|
+
|
|
17
|
+
if (dryRun) {
|
|
18
|
+
return { success: true, message: 'Dry run mode' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// TODO: 根据 Codeup 的实际 API 实现
|
|
23
|
+
// 这里提供一个示例实现框架
|
|
24
|
+
|
|
25
|
+
// 方式 1: 使用 npm CLI(如果 Codeup 兼容 npm registry)
|
|
26
|
+
// const { execaCommand } = require('execa');
|
|
27
|
+
// await execaCommand('npm publish --registry ' + this.registry, {
|
|
28
|
+
// cwd: pkg.path,
|
|
29
|
+
// env: { NPM_TOKEN: this.token }
|
|
30
|
+
// });
|
|
31
|
+
|
|
32
|
+
// 方式 2: 使用 Codeup 专用 API
|
|
33
|
+
// const response = await fetch(`${this.registry}/api/publish`, {
|
|
34
|
+
// method: 'POST',
|
|
35
|
+
// headers: {
|
|
36
|
+
// 'Authorization': `Bearer ${this.token}`,
|
|
37
|
+
// 'Content-Type': 'application/json'
|
|
38
|
+
// },
|
|
39
|
+
// body: JSON.stringify({
|
|
40
|
+
// name: pkg.name,
|
|
41
|
+
// version: pkg.version,
|
|
42
|
+
// tag
|
|
43
|
+
// })
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
// 如果需要 fetch,请确保 Node 版本 >= 18 或安装 node-fetch
|
|
47
|
+
throw new Error('Codeup 适配器需要根据实际 API 实现发布逻辑');
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: error.message
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 验证 token 有效性
|
|
59
|
+
*/
|
|
60
|
+
async validateToken() {
|
|
61
|
+
try {
|
|
62
|
+
// TODO: 根据 Codeup 的实际 API 实现
|
|
63
|
+
// 示例:
|
|
64
|
+
// const response = await fetch(`${this.registry}/api/user/me`, {
|
|
65
|
+
// headers: { 'Authorization': `Bearer ${this.token}` }
|
|
66
|
+
// });
|
|
67
|
+
// return response.ok
|
|
68
|
+
// ? { valid: true }
|
|
69
|
+
// : { valid: false, error: 'Invalid token' };
|
|
70
|
+
|
|
71
|
+
throw new Error('Codeup 适配器需要根据实际 API 实现 token 验证逻辑');
|
|
72
|
+
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
valid: false,
|
|
76
|
+
error: error.message
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 检查包是否存在
|
|
83
|
+
*/
|
|
84
|
+
async checkPackage(name, version) {
|
|
85
|
+
try {
|
|
86
|
+
// TODO: 根据 Codeup 的实际 API 实现
|
|
87
|
+
throw new Error('Codeup 适配器需要根据实际 API 实现包检查逻辑');
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { exists: false, error: error.message };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
CodeupAdapter
|
|
97
|
+
};
|