@mindbase/mindbase 1.0.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/LICENSE +201 -0
- package/bin/gitlog.ts +3 -0
- package/bin/iconfont-editor.ts +3 -0
- package/bin/index.ts +29 -0
- package/bin/nodeclear.ts +3 -0
- package/bin/npmpublish.ts +3 -0
- package/bin/shared.ts +141 -0
- package/package.json +63 -0
- package/scripts/ensure-tsx.cjs +18 -0
- package/src/clear/app.ts +152 -0
- package/src/clear/config.ts +108 -0
- package/src/clear/file-cleaner.ts +137 -0
- package/src/clear/index.ts +28 -0
- package/src/clear/scanner.ts +154 -0
- package/src/gitlog/app.ts +272 -0
- package/src/gitlog/config.ts +91 -0
- package/src/gitlog/display.ts +178 -0
- package/src/gitlog/index.ts +5 -0
- package/src/gitlog/log-fetcher.ts +150 -0
- package/src/gitlog/pager.ts +178 -0
- package/src/gitlog/scanner.ts +60 -0
- package/src/iconfont/frontend/index.html +12 -0
- package/src/iconfont/frontend/src/App.vue +14 -0
- package/src/iconfont/frontend/src/main.ts +8 -0
- package/src/iconfont/frontend/src/views/IconEditor.vue +819 -0
- package/src/iconfont/frontend/vite.config.ts +15 -0
- package/src/iconfont/lib/css-generator.js +37 -0
- package/src/iconfont/lib/font-builder.js +82 -0
- package/src/iconfont/lib/glyph-extractor.js +69 -0
- package/src/iconfont/server/api.js +256 -0
- package/src/iconfont/server/index.js +64 -0
- package/src/index.ts +0 -0
- package/src/publish/app.ts +316 -0
- package/src/publish/builder.ts +120 -0
- package/src/publish/dependency.ts +144 -0
- package/src/publish/detector.ts +93 -0
- package/src/publish/index.ts +66 -0
- package/src/publish/npm-query.ts +244 -0
- package/src/publish/registry/adapters/npm.ts +128 -0
- package/src/publish/registry/registry-manager.ts +107 -0
- package/src/publish/scanner.ts +90 -0
- package/src/publish/types.ts +98 -0
- package/src/publish/version.ts +108 -0
- package/src/shared/config-manager.ts +57 -0
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import * as clack from '@clack/prompts';
|
|
2
|
+
import { noteBox } from '@mindbase/cli-ui';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { RegistryManager } from './registry/registry-manager.js';
|
|
5
|
+
import {
|
|
6
|
+
detectWorkspace,
|
|
7
|
+
scanPackages,
|
|
8
|
+
analyzeDependencies,
|
|
9
|
+
bumpVersions,
|
|
10
|
+
applyVersionChanges,
|
|
11
|
+
NpmPublisher,
|
|
12
|
+
queryPackage,
|
|
13
|
+
type Workspace,
|
|
14
|
+
type Package,
|
|
15
|
+
type DependencyAnalysis,
|
|
16
|
+
type BumpType,
|
|
17
|
+
} from './index.js';
|
|
18
|
+
|
|
19
|
+
/** 统一取消处理 */
|
|
20
|
+
function handleCancel<T>(result: T | symbol): T {
|
|
21
|
+
if (clack.isCancel(result)) {
|
|
22
|
+
clack.cancel('操作已取消');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 运行 npmpublish 主流程
|
|
30
|
+
*/
|
|
31
|
+
export async function runApp(scanPath: string, options: { dryRun?: boolean } = {}): Promise<void> {
|
|
32
|
+
// 阶段 1: 扫描(带进度)
|
|
33
|
+
const s = clack.spinner();
|
|
34
|
+
s.start('检测工作空间...');
|
|
35
|
+
const workspace = await detectWorkspace(scanPath);
|
|
36
|
+
s.message(`扫描包(${workspace.type} 模式)...`);
|
|
37
|
+
const packages = await scanPackages(workspace);
|
|
38
|
+
s.message('分析依赖关系...');
|
|
39
|
+
const analysis = analyzeDependencies(packages);
|
|
40
|
+
s.stop(`找到 ${packages.length} 个包`);
|
|
41
|
+
|
|
42
|
+
if (packages.length === 0) {
|
|
43
|
+
clack.log.warn('未找到可发布的包');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 展示概览
|
|
48
|
+
showOverview(packages, analysis);
|
|
49
|
+
|
|
50
|
+
// 阶段 2: 主循环
|
|
51
|
+
let selectedPackages: Package[] = [];
|
|
52
|
+
let versionChanges = new Map<Package, string>();
|
|
53
|
+
|
|
54
|
+
while (true) {
|
|
55
|
+
const action = handleCancel(
|
|
56
|
+
await clack.select({
|
|
57
|
+
message: '选择操作',
|
|
58
|
+
options: [
|
|
59
|
+
{ value: 'select', label: '选择包', hint: `已选 ${selectedPackages.length} 个` },
|
|
60
|
+
{ value: 'version', label: '版本管理', hint: versionChanges.size > 0 ? `${versionChanges.size} 个变更` : '' },
|
|
61
|
+
{ value: 'publish', label: '发布', hint: options.dryRun ? '(dry-run)' : '' },
|
|
62
|
+
{ value: 'info', label: '查看包信息', hint: '查询 npm registry' },
|
|
63
|
+
{ value: 'quit', label: '退出' },
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
) as string;
|
|
67
|
+
|
|
68
|
+
switch (action) {
|
|
69
|
+
case 'select':
|
|
70
|
+
selectedPackages = await selectPackages(packages);
|
|
71
|
+
// 选中包后重置版本变更
|
|
72
|
+
versionChanges.clear();
|
|
73
|
+
break;
|
|
74
|
+
case 'version':
|
|
75
|
+
if (selectedPackages.length === 0) {
|
|
76
|
+
clack.log.warn('请先选择包');
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
versionChanges = await manageVersions(selectedPackages);
|
|
80
|
+
break;
|
|
81
|
+
case 'publish':
|
|
82
|
+
if (selectedPackages.length === 0) {
|
|
83
|
+
clack.log.warn('请先选择包');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
await publishPackages(selectedPackages, analysis, versionChanges, options.dryRun);
|
|
87
|
+
break;
|
|
88
|
+
case 'info':
|
|
89
|
+
await showPackageInfo(packages);
|
|
90
|
+
break;
|
|
91
|
+
case 'quit':
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 展示工作空间概览
|
|
99
|
+
*/
|
|
100
|
+
function showOverview(packages: Package[], analysis: DependencyAnalysis): void {
|
|
101
|
+
const overview = packages.map(p => {
|
|
102
|
+
const priv = p.private ? '(private)' : '';
|
|
103
|
+
const build = p.hasBuildScript ? '[build]' : '';
|
|
104
|
+
return `${p.name}@${p.version} ${priv} ${build}`;
|
|
105
|
+
}).join('\n');
|
|
106
|
+
const depInfo = analysis.hasCycle
|
|
107
|
+
? `⚠ 存在循环依赖: ${analysis.cycles.map(c => c.join(' → ')).join(', ')}`
|
|
108
|
+
: '无循环依赖';
|
|
109
|
+
noteBox(overview + `\n\n${depInfo}`, '工作空间概览');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 选择包(支持全选)
|
|
114
|
+
*/
|
|
115
|
+
async function selectPackages(packages: Package[]): Promise<Package[]> {
|
|
116
|
+
const publicPkgs = packages.filter(p => !p.private);
|
|
117
|
+
const ALL_VALUE = '__ALL__';
|
|
118
|
+
|
|
119
|
+
const selected = handleCancel(
|
|
120
|
+
await clack.multiselect({
|
|
121
|
+
message: '选择要操作的包(空格切换,a 全选)',
|
|
122
|
+
options: [
|
|
123
|
+
{ value: ALL_VALUE, label: '全选', hint: `共 ${publicPkgs.length} 个公开包` },
|
|
124
|
+
...publicPkgs.map(p => ({
|
|
125
|
+
value: p.name,
|
|
126
|
+
label: `${p.name}@${p.version}`,
|
|
127
|
+
hint: p.hasBuildScript ? 'has build' : '',
|
|
128
|
+
})),
|
|
129
|
+
],
|
|
130
|
+
required: true,
|
|
131
|
+
})
|
|
132
|
+
) as string[];
|
|
133
|
+
|
|
134
|
+
if (selected.includes(ALL_VALUE)) {
|
|
135
|
+
return publicPkgs;
|
|
136
|
+
}
|
|
137
|
+
return publicPkgs.filter(p => selected.includes(p.name));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 版本管理:单选 bump 类型 → 展示可编辑版本号 → 确认
|
|
142
|
+
*/
|
|
143
|
+
async function manageVersions(packages: Package[]): Promise<Map<Package, string>> {
|
|
144
|
+
// 单选 bump 类型
|
|
145
|
+
const bumpType = handleCancel(
|
|
146
|
+
await clack.select({
|
|
147
|
+
message: '选择版本递增类型',
|
|
148
|
+
options: [
|
|
149
|
+
{ value: 'patch', label: 'patch', hint: '补丁版本 (x.y.Z)' },
|
|
150
|
+
{ value: 'minor', label: 'minor', hint: '次版本 (x.Y.0)' },
|
|
151
|
+
{ value: 'major', label: 'major', hint: '主版本 (X.0.0)' },
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
) as BumpType;
|
|
155
|
+
|
|
156
|
+
// 计算新版本
|
|
157
|
+
const changes = bumpVersions(packages, bumpType);
|
|
158
|
+
|
|
159
|
+
// 展示可编辑的版本号(逐个编辑)
|
|
160
|
+
const finalChanges = new Map<Package, string>();
|
|
161
|
+
|
|
162
|
+
for (const [pkg, newVersion] of changes) {
|
|
163
|
+
const edited = handleCancel(
|
|
164
|
+
await clack.text({
|
|
165
|
+
message: `${pkg.name}`,
|
|
166
|
+
initialValue: newVersion,
|
|
167
|
+
validate: (v) => {
|
|
168
|
+
if (!v || !/^\d+\.\d+\.\d+/.test(v)) return '无效版本号';
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
) as string;
|
|
172
|
+
finalChanges.set(pkg, edited);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 预览
|
|
176
|
+
const preview = packages.map(p => {
|
|
177
|
+
const newVer = finalChanges.get(p);
|
|
178
|
+
return newVer ? `${p.name}: ${p.version} → ${newVer}` : `${p.name}: ${p.version} (不变)`;
|
|
179
|
+
}).join('\n');
|
|
180
|
+
noteBox(preview, '版本变更预览');
|
|
181
|
+
|
|
182
|
+
// 确认写入
|
|
183
|
+
const confirmed = handleCancel(
|
|
184
|
+
await clack.confirm({ message: '确认写入版本变更?' })
|
|
185
|
+
) as boolean;
|
|
186
|
+
|
|
187
|
+
if (confirmed) {
|
|
188
|
+
applyVersionChanges(finalChanges);
|
|
189
|
+
clack.log.success('版本已更新');
|
|
190
|
+
}
|
|
191
|
+
return finalChanges;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 发布包(按依赖顺序,带进度)
|
|
196
|
+
*/
|
|
197
|
+
async function publishPackages(
|
|
198
|
+
packages: Package[],
|
|
199
|
+
analysis: DependencyAnalysis,
|
|
200
|
+
versionChanges: Map<Package, string>,
|
|
201
|
+
dryRun?: boolean,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const publishOrder = analysis.order.filter(p => packages.some(sp => sp.name === p.name));
|
|
204
|
+
|
|
205
|
+
const confirmed = handleCancel(
|
|
206
|
+
await clack.confirm({
|
|
207
|
+
message: `确认发布 ${publishOrder.length} 个包?${dryRun ? ' (dry-run)' : ''}`,
|
|
208
|
+
})
|
|
209
|
+
) as boolean;
|
|
210
|
+
|
|
211
|
+
if (!confirmed) return;
|
|
212
|
+
|
|
213
|
+
const publisher = new NpmPublisher('https://registry.npmjs.org/');
|
|
214
|
+
const s = clack.spinner();
|
|
215
|
+
s.start('发布中...');
|
|
216
|
+
|
|
217
|
+
const results = { success: 0, failed: 0, failures: [] as string[] };
|
|
218
|
+
|
|
219
|
+
for (const pkg of publishOrder) {
|
|
220
|
+
s.message(`发布: ${pkg.name}`);
|
|
221
|
+
try {
|
|
222
|
+
const result = await publisher.publish(pkg.path, { dryRun });
|
|
223
|
+
if (result.success > 0) {
|
|
224
|
+
results.success++;
|
|
225
|
+
} else {
|
|
226
|
+
results.failed++;
|
|
227
|
+
results.failures.push(`${pkg.name}: ${result.failures[0]?.error || '未知错误'}`);
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
results.failed++;
|
|
231
|
+
results.failures.push(`${pkg.name}: ${err}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
s.stop(`发布完成: ✓${results.success} ✗${results.failed}`);
|
|
236
|
+
|
|
237
|
+
if (results.failures.length > 0) {
|
|
238
|
+
noteBox(results.failures.join('\n'), '失败详情');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 查看包信息
|
|
244
|
+
*/
|
|
245
|
+
async function showPackageInfo(packages: Package[]): Promise<void> {
|
|
246
|
+
const name = handleCancel(
|
|
247
|
+
await clack.select({
|
|
248
|
+
message: '选择要查看的包',
|
|
249
|
+
options: packages.map(p => ({
|
|
250
|
+
value: p.name,
|
|
251
|
+
label: p.name,
|
|
252
|
+
hint: p.version,
|
|
253
|
+
})),
|
|
254
|
+
})
|
|
255
|
+
) as string;
|
|
256
|
+
|
|
257
|
+
const s = clack.spinner();
|
|
258
|
+
s.start(`查询 ${name}...`);
|
|
259
|
+
try {
|
|
260
|
+
const info = await queryPackage(name);
|
|
261
|
+
s.stop();
|
|
262
|
+
if (!info.exists) {
|
|
263
|
+
clack.log.warn(`${name} 尚未发布到 npm`);
|
|
264
|
+
} else {
|
|
265
|
+
noteBox([
|
|
266
|
+
`名称: ${info.name}`,
|
|
267
|
+
`最新版本: ${info.latestVersion}`,
|
|
268
|
+
`Beta: ${info.betaVersion || '无'}`,
|
|
269
|
+
`所有版本: ${info.allVersions.length} 个`,
|
|
270
|
+
`描述: ${info.description || '无'}`,
|
|
271
|
+
].join('\n'), 'npm 包信息');
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
s.stop(`查询失败: ${err}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 显示配置
|
|
280
|
+
*/
|
|
281
|
+
export async function showConfig(): Promise<void> {
|
|
282
|
+
const mgr = new RegistryManager();
|
|
283
|
+
const registries = mgr.getAllRegistries();
|
|
284
|
+
noteBox(
|
|
285
|
+
registries.map(r => `${r.id}: ${r.url}${r.isDefault ? ' (默认)' : ''}`).join('\n'),
|
|
286
|
+
'注册源配置'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 交互式选择子命令(不带子命令时)
|
|
292
|
+
*/
|
|
293
|
+
export async function interactiveMenu(): Promise<void> {
|
|
294
|
+
const action = handleCancel(
|
|
295
|
+
await clack.select({
|
|
296
|
+
message: '选择操作',
|
|
297
|
+
options: [
|
|
298
|
+
{ value: 'publish', label: '发布包', hint: '扫描并发布工作空间中的包' },
|
|
299
|
+
{ value: 'config', label: '查看配置', hint: '显示注册源配置' },
|
|
300
|
+
{ value: 'quit', label: '退出' },
|
|
301
|
+
],
|
|
302
|
+
})
|
|
303
|
+
) as string;
|
|
304
|
+
|
|
305
|
+
switch (action) {
|
|
306
|
+
case 'publish': {
|
|
307
|
+
await runApp(resolve('.'));
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case 'config':
|
|
311
|
+
await showConfig();
|
|
312
|
+
break;
|
|
313
|
+
case 'quit':
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { Package } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 构建信息
|
|
7
|
+
*/
|
|
8
|
+
export interface BuildInfo {
|
|
9
|
+
/** 包名 */
|
|
10
|
+
name: string;
|
|
11
|
+
/** 是否有构建脚本 */
|
|
12
|
+
hasBuildScript: boolean;
|
|
13
|
+
/** 构建脚本命令 */
|
|
14
|
+
buildScript?: string;
|
|
15
|
+
/** 输出目录 */
|
|
16
|
+
outDir?: string;
|
|
17
|
+
/** 是否需要构建 */
|
|
18
|
+
needsBuild: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 检测包的构建配置
|
|
23
|
+
*/
|
|
24
|
+
export function detectBuildConfig(pkg: Package): BuildInfo {
|
|
25
|
+
const packageJsonPath = join(pkg.path, 'package.json');
|
|
26
|
+
|
|
27
|
+
if (!existsSync(packageJsonPath)) {
|
|
28
|
+
return {
|
|
29
|
+
name: pkg.name,
|
|
30
|
+
hasBuildScript: false,
|
|
31
|
+
needsBuild: false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
37
|
+
const scripts = packageJson.scripts || {};
|
|
38
|
+
|
|
39
|
+
const hasBuildScript = !!scripts.build;
|
|
40
|
+
const buildScript = scripts.build;
|
|
41
|
+
|
|
42
|
+
// 检测输出目录
|
|
43
|
+
let outDir: string | undefined;
|
|
44
|
+
if (packageJson.exports) {
|
|
45
|
+
// 检查 exports 配置
|
|
46
|
+
const exports = packageJson.exports;
|
|
47
|
+
if (typeof exports === 'string') {
|
|
48
|
+
outDir = exports.split('/')[0];
|
|
49
|
+
} else if (exports['.'] && typeof exports['.'] === 'string') {
|
|
50
|
+
outDir = exports['.'].split('/')[0];
|
|
51
|
+
} else if (exports['./package'] && typeof exports['./package'] === 'object') {
|
|
52
|
+
outDir = exports['./package'].import?.split('/')[0];
|
|
53
|
+
}
|
|
54
|
+
} else if (packageJson.main) {
|
|
55
|
+
outDir = packageJson.main.split('/')[0];
|
|
56
|
+
} else if (packageJson.types || packageJson.typings) {
|
|
57
|
+
outDir = (packageJson.types || packageJson.typings).split('/')[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 常见的输出目录
|
|
61
|
+
const commonOutDirs = ['dist', 'build', 'out', 'lib', 'es'];
|
|
62
|
+
if (!outDir) {
|
|
63
|
+
for (const dir of commonOutDirs) {
|
|
64
|
+
if (existsSync(join(pkg.path, dir))) {
|
|
65
|
+
outDir = dir;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: pkg.name,
|
|
73
|
+
hasBuildScript,
|
|
74
|
+
buildScript,
|
|
75
|
+
outDir,
|
|
76
|
+
needsBuild: hasBuildScript && !isBuildOutputExists(pkg, outDir)
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return {
|
|
80
|
+
name: pkg.name,
|
|
81
|
+
hasBuildScript: false,
|
|
82
|
+
needsBuild: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 检查构建输出是否存在
|
|
89
|
+
*/
|
|
90
|
+
function isBuildOutputExists(pkg: Package, outDir?: string): boolean {
|
|
91
|
+
if (!outDir) return false;
|
|
92
|
+
|
|
93
|
+
const outPath = join(pkg.path, outDir);
|
|
94
|
+
return existsSync(outPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 批量检测构建配置
|
|
99
|
+
*/
|
|
100
|
+
export function detectBuildConfigs(packages: Package[]): BuildInfo[] {
|
|
101
|
+
return packages.map(pkg => detectBuildConfig(pkg));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 过滤需要构建的包
|
|
106
|
+
*/
|
|
107
|
+
export function filterPackagesNeedingBuild(packages: Package[]): Package[] {
|
|
108
|
+
return packages.filter(pkg => {
|
|
109
|
+
const info = detectBuildConfig(pkg);
|
|
110
|
+
return info.needsBuild;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取构建脚本命令
|
|
116
|
+
*/
|
|
117
|
+
export function getBuildScript(pkg: Package): string | undefined {
|
|
118
|
+
const info = detectBuildConfig(pkg);
|
|
119
|
+
return info.buildScript;
|
|
120
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Package, DependencyAnalysis } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 分析包之间的依赖关系
|
|
5
|
+
*/
|
|
6
|
+
export function analyzeDependencies(packages: Package[]): DependencyAnalysis {
|
|
7
|
+
const pkgMap = new Map(packages.map(p => [p.name, p]));
|
|
8
|
+
const graph = new Map<string, Set<string>>();
|
|
9
|
+
|
|
10
|
+
// 构建依赖图
|
|
11
|
+
for (const pkg of packages) {
|
|
12
|
+
const deps = new Set<string>();
|
|
13
|
+
|
|
14
|
+
// 查找包内部依赖
|
|
15
|
+
for (const depName of pkg.dependencies) {
|
|
16
|
+
const depPkg = pkgMap.get(depName);
|
|
17
|
+
if (depPkg && !depPkg.private && depPkg.name !== pkg.name) {
|
|
18
|
+
deps.add(depName);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
graph.set(pkg.name, deps);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 拓扑排序
|
|
26
|
+
const order: Package[] = [];
|
|
27
|
+
const visited = new Set<string>();
|
|
28
|
+
const temp = new Set<string>();
|
|
29
|
+
const cycles: string[][] = [];
|
|
30
|
+
|
|
31
|
+
function visit(pkgName: string, path: string[]): void {
|
|
32
|
+
if (temp.has(pkgName)) {
|
|
33
|
+
// 发现循环依赖
|
|
34
|
+
const cycleStart = path.indexOf(pkgName);
|
|
35
|
+
const cycle = [...path.slice(cycleStart), pkgName];
|
|
36
|
+
cycles.push(cycle);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (visited.has(pkgName)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
temp.add(pkgName);
|
|
45
|
+
|
|
46
|
+
const deps = graph.get(pkgName);
|
|
47
|
+
if (deps) {
|
|
48
|
+
for (const dep of deps) {
|
|
49
|
+
visit(dep, [...path, pkgName]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
temp.delete(pkgName);
|
|
54
|
+
visited.add(pkgName);
|
|
55
|
+
|
|
56
|
+
const pkg = pkgMap.get(pkgName);
|
|
57
|
+
if (pkg) {
|
|
58
|
+
order.push(pkg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const pkg of packages) {
|
|
63
|
+
if (!visited.has(pkg.name)) {
|
|
64
|
+
visit(pkg.name, []);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 反转顺序,使依赖最少的最先发布
|
|
69
|
+
order.reverse();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
order,
|
|
73
|
+
graph,
|
|
74
|
+
hasCycle: cycles.length > 0,
|
|
75
|
+
cycles
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 检查是否有循环依赖
|
|
81
|
+
*/
|
|
82
|
+
export function checkCircularDependencies(analysis: DependencyAnalysis): boolean {
|
|
83
|
+
return analysis.hasCycle;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 获取依赖树(用于显示)
|
|
88
|
+
*/
|
|
89
|
+
export function getDependencyTree(
|
|
90
|
+
packages: Package[],
|
|
91
|
+
analysis: DependencyAnalysis
|
|
92
|
+
): Array<{ pkg: Package; deps: Package[] }> {
|
|
93
|
+
const pkgMap = new Map(packages.map(p => [p.name, p]));
|
|
94
|
+
|
|
95
|
+
return analysis.order.map(pkg => {
|
|
96
|
+
const depNames = analysis.graph.get(pkg.name) || new Set();
|
|
97
|
+
const deps = Array.from(depNames)
|
|
98
|
+
.map(name => pkgMap.get(name))
|
|
99
|
+
.filter((p): p is Package => !!p);
|
|
100
|
+
|
|
101
|
+
return { pkg, deps };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 按依赖层级分组
|
|
107
|
+
*/
|
|
108
|
+
export function groupByLevel(
|
|
109
|
+
packages: Package[],
|
|
110
|
+
analysis: DependencyAnalysis
|
|
111
|
+
): Package[][] {
|
|
112
|
+
const levels: Package[][] = [];
|
|
113
|
+
const placed = new Set<string>();
|
|
114
|
+
|
|
115
|
+
const pkgMap = new Map(packages.map(p => [p.name, p]));
|
|
116
|
+
|
|
117
|
+
// 找出没有内部依赖的包(第一层)
|
|
118
|
+
const findLevel = (): Package[] => {
|
|
119
|
+
const level: Package[] = [];
|
|
120
|
+
|
|
121
|
+
for (const pkg of analysis.order) {
|
|
122
|
+
if (placed.has(pkg.name)) continue;
|
|
123
|
+
|
|
124
|
+
const depNames = analysis.graph.get(pkg.name) || new Set();
|
|
125
|
+
const internalDeps = Array.from(depNames).filter(name => pkgMap.has(name));
|
|
126
|
+
|
|
127
|
+
// 所有内部依赖都已放置
|
|
128
|
+
if (internalDeps.every(dep => placed.has(dep))) {
|
|
129
|
+
level.push(pkg);
|
|
130
|
+
placed.add(pkg.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return level;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
while (placed.size < analysis.order.length) {
|
|
138
|
+
const level = findLevel();
|
|
139
|
+
if (level.length === 0) break;
|
|
140
|
+
levels.push(level);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return levels;
|
|
144
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { Workspace, WorkspaceType } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 检测工作空间类型
|
|
7
|
+
*/
|
|
8
|
+
export async function detectWorkspace(rootPath: string): Promise<Workspace> {
|
|
9
|
+
const packageJsonPath = join(rootPath, 'package.json');
|
|
10
|
+
|
|
11
|
+
if (!existsSync(packageJsonPath)) {
|
|
12
|
+
return {
|
|
13
|
+
type: 'single',
|
|
14
|
+
rootPath,
|
|
15
|
+
patterns: ['package.json'],
|
|
16
|
+
isNpmWorkspace: false
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
21
|
+
const workspacesConfig = packageJson.workspaces;
|
|
22
|
+
|
|
23
|
+
let type: WorkspaceType = 'single';
|
|
24
|
+
let patterns: string[] = ['package.json'];
|
|
25
|
+
let isNpmWorkspace = false;
|
|
26
|
+
|
|
27
|
+
if (workspacesConfig) {
|
|
28
|
+
isNpmWorkspace = true;
|
|
29
|
+
if (Array.isArray(workspacesConfig)) {
|
|
30
|
+
patterns = workspacesConfig;
|
|
31
|
+
type = 'workspace';
|
|
32
|
+
} else if (workspacesConfig.packages && Array.isArray(workspacesConfig.packages)) {
|
|
33
|
+
patterns = workspacesConfig.packages;
|
|
34
|
+
type = 'workspace';
|
|
35
|
+
} else {
|
|
36
|
+
// npm workspaces without packages array
|
|
37
|
+
patterns = ['**/package.json'];
|
|
38
|
+
type = 'workspace';
|
|
39
|
+
}
|
|
40
|
+
} else if (packageJson.name === '@root/workspace' || existsSync(join(rootPath, 'pnpm-workspace.yaml'))) {
|
|
41
|
+
// 检测 pnpm workspace
|
|
42
|
+
type = 'monorepo';
|
|
43
|
+
patterns = ['**/package.json'];
|
|
44
|
+
} else if (existsSync(join(rootPath, 'lerna.json'))) {
|
|
45
|
+
// 检测 lerna monorepo
|
|
46
|
+
type = 'monorepo';
|
|
47
|
+
const lernaConfig = JSON.parse(readFileSync(join(rootPath, 'lerna.json'), 'utf-8'));
|
|
48
|
+
patterns = lernaConfig.packages || ['packages/*'];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
type,
|
|
53
|
+
rootPath,
|
|
54
|
+
patterns,
|
|
55
|
+
isNpmWorkspace
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 检查包是否已准备好发布
|
|
61
|
+
*/
|
|
62
|
+
export function checkPublishReadiness(pkg: any): {
|
|
63
|
+
ready: boolean;
|
|
64
|
+
issues: string[];
|
|
65
|
+
} {
|
|
66
|
+
const issues: string[] = [];
|
|
67
|
+
|
|
68
|
+
if (!pkg.name) {
|
|
69
|
+
issues.push('缺少包名');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!pkg.version) {
|
|
73
|
+
issues.push('缺少版本号');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pkg.private) {
|
|
77
|
+
issues.push('私有包,不能发布');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 检查是否有必要的元数据
|
|
81
|
+
if (!pkg.description) {
|
|
82
|
+
issues.push('缺少描述信息');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!pkg.repository && !pkg.license) {
|
|
86
|
+
issues.push('缺少仓库信息或许可证');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ready: issues.length === 0,
|
|
91
|
+
issues
|
|
92
|
+
};
|
|
93
|
+
}
|