@dsh-workbench/module-tools 0.3.11 → 1.0.0-beta.1
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 +21 -0
- package/README.md +223 -177
- package/bin/dsh-wb-module.mjs +89 -34
- package/package.json +22 -3
- package/src/arch.mjs +158 -129
- package/src/build.mjs +6 -30
- package/src/changelog.mjs +166 -0
- package/src/check-flags.mjs +0 -7
- package/src/check.mjs +115 -53
- package/src/checks/alias.mjs +0 -9
- package/src/checks/arch.mjs +3 -25
- package/src/checks/bundles.mjs +2 -34
- package/src/checks/cards.mjs +26 -30
- package/src/checks/contracts.mjs +72 -219
- package/src/checks/kit.mjs +2 -42
- package/src/checks/lint.mjs +9 -82
- package/src/checks/markers.mjs +48 -17
- package/src/checks/primitives.mjs +0 -39
- package/src/checks/protocol.mjs +0 -18
- package/src/checks/source.mjs +103 -51
- package/src/checks/standalone.mjs +4 -14
- package/src/checks/style.mjs +84 -230
- package/src/css-injection.mjs +0 -11
- package/src/css-shell.mjs +3 -31
- package/src/datasource-file.mjs +104 -0
- package/src/declared-tools.mjs +0 -24
- package/src/design.mjs +116 -38
- package/src/discover.mjs +2 -10
- package/src/font-scale.mjs +1 -61
- package/src/init.mjs +2 -17
- package/src/lint.mjs +24 -144
- package/src/market-index.mjs +569 -0
- package/src/market-init.mjs +80 -0
- package/src/primitives-surface.mjs +2 -28
- package/src/publish.mjs +11 -25
- package/src/release-changelog.mjs +29 -0
- package/src/release-journal.mjs +131 -0
- package/src/release-report.mjs +99 -0
- package/src/release-util.mjs +87 -0
- package/src/release.mjs +1250 -0
- package/src/seed.mjs +0 -25
- package/src/shell-type-files.mjs +3 -30
- package/src/sync.mjs +41 -24
- package/template/AGENTS.md.tpl +33 -0
- package/template/CHANGELOG.md.tpl +7 -0
- package/template/README.md.tpl +6 -33
- package/template/package.json.tpl +2 -2
- package/template/src/client.tsx.tpl +1 -18
- package/template/src/host.ts.tpl +2 -15
- package/template/types/globals.d.ts +1 -14
- package/template-diy/AGENTS.md.tpl +33 -0
- package/template-diy/CHANGELOG.md.tpl +7 -0
- package/template-diy/README.md.tpl +4 -133
- package/template-diy/package.json.tpl +2 -2
- package/template-diy/src/client.tsx.tpl +1 -29
- package/template-diy/src/host.ts.tpl +1 -15
- package/template-diy/src/plugin.test.ts.tpl +1 -1
- package/template-diy/types/globals.d.ts +1 -14
- package/template-diy/types/styles.d.ts +0 -10
|
@@ -1,33 +1,16 @@
|
|
|
1
|
-
// dsh 原语本包的**可见面**:从装机那份类型面(`lib/types/**/*.d.ts`)取出导出名,按名字形状分桶。
|
|
2
|
-
//
|
|
3
|
-
// 为什么读类型面而不是源码:这个包发布出去的 `files` 只有
|
|
4
|
-
// `lib/index.js` / `lib/**/*.css` / `lib/types/**/*.d.ts` —— **源码树不随包发布**
|
|
5
|
-
// (`package.json` 的 `exports['./src/*']` 在装机包里是死条目),类型面是唯一机器可读的清单。
|
|
6
|
-
//
|
|
7
|
-
// 为什么不上 TypeScript API:module-tools 自己没有 `typescript` 依赖(只有 `typescript-eslint`
|
|
8
|
-
// 的 peer),而这份 barrel 是 tsc 生成的、形状稳定:`export { A, B } from './X.tsx'` +
|
|
9
|
-
// `export type { … }`(滤掉)+ 少量 `export declare const X` + 一条 `export *`(要跟着走)。
|
|
10
|
-
// 代价是"上游换了 emit 形状没人知道"—— 由 selfcheck 那条"清单非空 / 含 Button / `Icon*Regular` ≥ 90"
|
|
11
|
-
// 兜住(`.scratch/dsh-primitives-face/spec.md` §3.4 / §8.1)。
|
|
12
|
-
//
|
|
13
|
-
// 分桶**只按名字形状**,不判用途:`Icon*` → 图标;`^use[A-Z]` → hooks;首字母大写 → 组件;
|
|
14
|
-
// 其余(常量、纯函数)→ 工具。"这件是不是该用、该怎么用"是判断,留给 agent / 人(TOOLING.md 那条口径)。
|
|
15
|
-
|
|
16
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
17
2
|
import { createRequire } from 'node:module';
|
|
18
3
|
import { dirname, join } from 'node:path';
|
|
19
4
|
|
|
20
5
|
export const PRIMITIVES_SPECIFIER = '@deepseek-ai/dsh-client-ui-primitives';
|
|
21
6
|
|
|
22
|
-
/** 从 `from`(一个包的目录)解析这个原语包;解析不到返回 undefined。 */
|
|
23
7
|
export function installedPrimitives(from) {
|
|
24
8
|
try {
|
|
25
9
|
const require = createRequire(join(from, 'package.json'));
|
|
26
10
|
const manifest = require.resolve(`${PRIMITIVES_SPECIFIER}/package.json`);
|
|
27
11
|
const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
28
12
|
const version = typeof parsed.version === 'string' ? parsed.version : undefined;
|
|
29
|
-
|
|
30
|
-
// `require.resolve` 给的是 realpath)—— agent 照着能直接打开,也短得多。
|
|
13
|
+
|
|
31
14
|
const viaNodeModules = join(from, 'node_modules', ...PRIMITIVES_SPECIFIER.split('/'));
|
|
32
15
|
const dir = existsSync(join(viaNodeModules, 'package.json')) ? viaNodeModules : dirname(manifest);
|
|
33
16
|
return { manifest, dir, version };
|
|
@@ -36,7 +19,6 @@ export function installedPrimitives(from) {
|
|
|
36
19
|
}
|
|
37
20
|
}
|
|
38
21
|
|
|
39
|
-
/** 外壳 manifest 里钉的那份原语版本(`devDependencies` 那一格)—— 模板盖章与漂移 note 都用它。 */
|
|
40
22
|
export function pinnedPrimitivesVersion(manifestPath) {
|
|
41
23
|
try {
|
|
42
24
|
const pinned = JSON.parse(readFileSync(manifestPath, 'utf8')).devDependencies?.[PRIMITIVES_SPECIFIER];
|
|
@@ -46,12 +28,10 @@ export function pinnedPrimitivesVersion(manifestPath) {
|
|
|
46
28
|
}
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
/** `export … from '…'` 一条条看:`export type` 滤掉、`export *` 跟着走、其余按名字拆。 */
|
|
50
31
|
const STATEMENT = /export\s+(type\s+)?(\*|\{[^}]*\})\s*from\s*'([^']+)'/g;
|
|
51
32
|
const DECLARED = /^export declare (?:const|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
52
33
|
const MAX_STAR_DEPTH = 3;
|
|
53
34
|
|
|
54
|
-
/** 说明符落到同目录的 `.d.ts`:`./icons/index.tsx` → `<types>/icons/index.d.ts`。 */
|
|
55
35
|
function dtsTargetOf(fromFile, specifier) {
|
|
56
36
|
const base = join(dirname(fromFile), specifier);
|
|
57
37
|
const stripped = base.replace(/\.(tsx?|jsx?|mjs|cjs)$/, '');
|
|
@@ -84,18 +64,12 @@ function collect(file, origin, acc, depth) {
|
|
|
84
64
|
if (target !== undefined) collect(target, origin, acc, depth + 1);
|
|
85
65
|
continue;
|
|
86
66
|
}
|
|
87
|
-
|
|
88
|
-
// `lib/types/Button.d.ts` —— 这条信息就是"agent 想用哪个直接去看"的落点,别丢。
|
|
67
|
+
|
|
89
68
|
const declaredIn = dtsTargetOf(file, match[3]) ?? file;
|
|
90
69
|
for (const name of namesOf(match[2])) acc.set(name, { origin, file: declaredIn });
|
|
91
70
|
}
|
|
92
71
|
}
|
|
93
72
|
|
|
94
|
-
/**
|
|
95
|
-
* 装机那份类型面的导出,按名字形状分成四桶。
|
|
96
|
-
* `typesDir` = 原语包的 `lib/types`;读不到入口时抛(调用方给人话,不给空表)。
|
|
97
|
-
* `files` = 名字 → 声明它的那份 `.d.ts` 的绝对路径(188 枚图标同指 `icons/index.d.ts`)。
|
|
98
|
-
*/
|
|
99
73
|
export function surfaceOf(typesDir) {
|
|
100
74
|
const entry = join(typesDir, 'index.d.ts');
|
|
101
75
|
if (!existsSync(entry)) throw new Error(`类型面不在:${entry}`);
|
package/src/publish.mjs
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
// 发货前那一张清单:门禁 + 三张表 + `npm pack --dry-run` 的清单。
|
|
2
|
-
//
|
|
3
|
-
// ── 为什么要有这个命令 ────────────────────────────────────────────────────────
|
|
4
|
-
//
|
|
5
|
-
// 打包这件事有两条**静默**的坑,都真发生过:
|
|
6
|
-
// · `files` 里漏一个目录(`template-diy/` 那次):在仓里跑永远看不出问题,装出来的那份
|
|
7
|
-
// `init --template diy` 直接报"模板不存在";
|
|
8
|
-
// · 产物/声明对不上(`exports` 指向的文件没被 `files` 带上):装上去加载器找不到入口。
|
|
9
|
-
// 两条都不会在开发时冒头,只会在"别人装到那一刻"炸。所以把它们变成发货前的一条命令。
|
|
10
|
-
//
|
|
11
|
-
// 这条命令**只做 dry run**:真发布要凭据、要人点头,不在这里做。它跑完会打印你该跑的那一条。
|
|
12
|
-
|
|
13
|
-
import { execFileSync } from 'node:child_process';
|
|
14
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
15
2
|
import { tmpdir } from 'node:os';
|
|
16
3
|
import { join, relative } from 'node:path';
|
|
17
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { execaSync } from 'execa';
|
|
18
6
|
|
|
19
7
|
import { DESIGN_FILE } from './design.mjs';
|
|
8
|
+
import { publishTagOf } from './release-util.mjs';
|
|
20
9
|
|
|
21
|
-
/** 门禁那几条命令自己会 `process.exit`(它们本来就是 CLI),所以这里**走子进程** ——
|
|
22
|
-
* 直接 import 进来调,成功那次也会把整条 publish 结束掉(实测:清单还没打印就退出了)。 */
|
|
23
10
|
const CLI = fileURLToPath(new URL('../bin/dsh-wb-module.mjs', import.meta.url));
|
|
24
11
|
|
|
25
12
|
function runCli(root, argv) {
|
|
26
13
|
try {
|
|
27
|
-
|
|
14
|
+
execaSync(process.execPath, [CLI, ...argv, '--cwd', root], { stdio: 'inherit' });
|
|
28
15
|
} catch {
|
|
29
16
|
console.error(`✖ ${argv.join(' ')} 没过 —— 清单先不打印了,把上面那些红字处理掉`);
|
|
30
17
|
process.exit(1);
|
|
@@ -37,13 +24,11 @@ function readManifest(root) {
|
|
|
37
24
|
|
|
38
25
|
function packList(root) {
|
|
39
26
|
const cache = join(tmpdir(), 'dsh-wb-pack-cache');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const raw = execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts', '--cache', cache], {
|
|
27
|
+
|
|
28
|
+
const raw = execaSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts', '--cache', cache], {
|
|
43
29
|
cwd: root,
|
|
44
|
-
encoding: 'utf8',
|
|
45
30
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
-
});
|
|
31
|
+
}).stdout;
|
|
47
32
|
const start = raw.search(/[[{]/);
|
|
48
33
|
if (start === -1) throw new Error(`npm pack 没有输出 JSON:${raw.slice(0, 200)}`);
|
|
49
34
|
const parsed = JSON.parse(raw.slice(start));
|
|
@@ -62,8 +47,7 @@ export async function publish({ root }) {
|
|
|
62
47
|
const isModule = typeof unit === 'string' && unit !== '';
|
|
63
48
|
|
|
64
49
|
if (isModule) {
|
|
65
|
-
|
|
66
|
-
// 不先构建就会拿"还没构建"的半成品去跑门禁,红得莫名其妙。
|
|
50
|
+
|
|
67
51
|
runCli(root, ['build']);
|
|
68
52
|
runCli(root, ['check']);
|
|
69
53
|
if (existsSync(join(root, DESIGN_FILE))) runCli(root, ['design', 'check']);
|
|
@@ -100,7 +84,6 @@ export async function publish({ root }) {
|
|
|
100
84
|
if (readmeOnDisk && !paths.includes('README.md')) problems.push('README.md 在盘上却没进包');
|
|
101
85
|
if (!readmeOnDisk) console.log('○ 包根下没有 README.md —— 发出去的包在源上会是一页空白,建议补一份');
|
|
102
86
|
|
|
103
|
-
// `.tpl` 在 `template*/` 里是**本体**(init 拷过去才剥壳),只有在别处冒出来才算混进脏东西。
|
|
104
87
|
const inTemplate = (one) => templateDirs.some((dir) => one.startsWith(`${dir}/`));
|
|
105
88
|
const leftovers = paths.filter((one) => !inTemplate(one) && (one.endsWith('.tpl') || one.includes('.init-')));
|
|
106
89
|
if (leftovers.length > 0) problems.push(`包里混进了不该发的东西:${leftovers.slice(0, 3).join('、')}`);
|
|
@@ -129,6 +112,9 @@ export async function publish({ root }) {
|
|
|
129
112
|
return;
|
|
130
113
|
}
|
|
131
114
|
console.log('✔ 清单干净。真要发就自己跑(凭据在你手上):');
|
|
132
|
-
|
|
115
|
+
const tag = publishTagOf(manifest.version);
|
|
116
|
+
console.log(
|
|
117
|
+
` npm publish${manifest.publishConfig?.registry === undefined ? '' : ` --registry ${manifest.publishConfig.registry}`}${tag === undefined ? '' : ` --tag ${tag}`}`,
|
|
118
|
+
);
|
|
133
119
|
console.log(` 包目录:${relative(process.cwd(), root) === '' ? '.' : relative(process.cwd(), root)}`);
|
|
134
120
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { changelogProblemOf } from './changelog.mjs';
|
|
2
|
+
|
|
3
|
+
function manifestAuthor(manifest) {
|
|
4
|
+
const author = manifest?.author;
|
|
5
|
+
if (typeof author === 'string') return author.trim();
|
|
6
|
+
if (author !== null && typeof author === 'object' && typeof author.name === 'string') return author.name.trim();
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function configValue(exec, root, key) {
|
|
11
|
+
const result = exec('git', ['-C', root, 'config', key], { capture: true, allowFailure: true });
|
|
12
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function authorChangeOf(root, manifest, exec) {
|
|
16
|
+
if (manifestAuthor(manifest) !== '') return {};
|
|
17
|
+
const name = configValue(exec, root, 'user.name');
|
|
18
|
+
if (name === '') return {};
|
|
19
|
+
const email = configValue(exec, root, 'user.email');
|
|
20
|
+
return {
|
|
21
|
+
author: email === '' ? name : `${name} <${email}>`,
|
|
22
|
+
change: 'author ← git config',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function changelogGateOf(root, version) {
|
|
27
|
+
const problem = changelogProblemOf(root, version);
|
|
28
|
+
return problem === undefined ? { ok: true } : { ok: false, ...problem };
|
|
29
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const FINGERPRINT_FILES = [
|
|
6
|
+
'package.json',
|
|
7
|
+
'design.json',
|
|
8
|
+
'dist/host.js',
|
|
9
|
+
'client.js',
|
|
10
|
+
'cordis.patch.yml',
|
|
11
|
+
'README.md',
|
|
12
|
+
'CHANGELOG.md',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function statePathOf(home, unit) {
|
|
16
|
+
return join(home, 'workbench', 'releases', `${unit}.json`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readState(home, unit) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(statePathOf(home, unit), 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writeState(home, unit, state) {
|
|
28
|
+
const file = statePathOf(home, unit);
|
|
29
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
30
|
+
const temp = `${file}.tmp`;
|
|
31
|
+
writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`);
|
|
32
|
+
renameSync(temp, file);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function removeState(home, unit) {
|
|
36
|
+
try {
|
|
37
|
+
rmSync(statePathOf(home, unit));
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function artifactFingerprintOf(root) {
|
|
44
|
+
const hash = createHash('sha256');
|
|
45
|
+
for (const rel of FINGERPRINT_FILES) {
|
|
46
|
+
hash.update(`${rel}:`);
|
|
47
|
+
try {
|
|
48
|
+
hash.update(readFileSync(join(root, rel)));
|
|
49
|
+
} catch {
|
|
50
|
+
hash.update('missing');
|
|
51
|
+
}
|
|
52
|
+
hash.update('\n');
|
|
53
|
+
}
|
|
54
|
+
return hash.digest('hex').slice(0, 16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resumableState(home, unit) {
|
|
58
|
+
const state = readState(home, unit);
|
|
59
|
+
if (state === undefined || typeof state !== 'object' || state === null) return undefined;
|
|
60
|
+
const run = state.run;
|
|
61
|
+
if (run === undefined || typeof run !== 'object' || run === null) return undefined;
|
|
62
|
+
if (run.state === 'done') return undefined;
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function startRun(home, unit, init) {
|
|
67
|
+
const current = readState(home, unit) ?? {};
|
|
68
|
+
const previous = current.run;
|
|
69
|
+
const resuming = init.resume === true && previous !== undefined;
|
|
70
|
+
const run = {
|
|
71
|
+
...(resuming ? previous : {}),
|
|
72
|
+
id: resuming && typeof previous.id === 'string' ? previous.id : `run-${randomUUID()}`,
|
|
73
|
+
state: 'running',
|
|
74
|
+
phase: 'preflight',
|
|
75
|
+
attempts: resuming ? Number(previous.attempts ?? 0) + 1 : 0,
|
|
76
|
+
pid: process.pid,
|
|
77
|
+
startedAt: resuming && typeof previous.startedAt === 'string' ? previous.startedAt : new Date().toISOString(),
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
...init.fields,
|
|
80
|
+
};
|
|
81
|
+
writeState(home, unit, {
|
|
82
|
+
...current,
|
|
83
|
+
target: init.target,
|
|
84
|
+
name: init.name,
|
|
85
|
+
...(init.version === undefined ? {} : { version: init.version }),
|
|
86
|
+
...(typeof current.status === 'string' ? {} : { status: 'intent' }),
|
|
87
|
+
at: new Date().toISOString(),
|
|
88
|
+
run,
|
|
89
|
+
});
|
|
90
|
+
return run;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function advanceRun(home, unit, patch) {
|
|
94
|
+
const current = readState(home, unit);
|
|
95
|
+
if (current === undefined || typeof current !== 'object' || current === null) return undefined;
|
|
96
|
+
const run = {
|
|
97
|
+
...(current.run ?? {}),
|
|
98
|
+
...patch,
|
|
99
|
+
pid: process.pid,
|
|
100
|
+
updatedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
writeState(home, unit, {
|
|
103
|
+
...current,
|
|
104
|
+
...(patch.version === undefined ? {} : { version: patch.version }),
|
|
105
|
+
...(patch.status === undefined ? {} : { status: patch.status }),
|
|
106
|
+
run,
|
|
107
|
+
});
|
|
108
|
+
return run;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function interruptRun(home, unit, failure) {
|
|
112
|
+
const current = readState(home, unit) ?? {};
|
|
113
|
+
const run = {
|
|
114
|
+
...(current.run ?? {}),
|
|
115
|
+
state: 'interrupted',
|
|
116
|
+
...(typeof failure.phase === 'string' ? { phase: failure.phase } : {}),
|
|
117
|
+
lastError: {
|
|
118
|
+
class: failure.class ?? 'transient',
|
|
119
|
+
code: failure.code ?? '',
|
|
120
|
+
text: failure.text ?? '',
|
|
121
|
+
at: new Date().toISOString(),
|
|
122
|
+
},
|
|
123
|
+
updatedAt: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
writeState(home, unit, { ...current, run });
|
|
126
|
+
return run;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function finishRun(home, unit, patch = {}) {
|
|
130
|
+
return advanceRun(home, unit, { ...patch, state: 'done' });
|
|
131
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
function httpsUrlOf(url) {
|
|
2
|
+
const text = String(url).trim();
|
|
3
|
+
if (!text.startsWith('git@')) return text;
|
|
4
|
+
const rest = text.slice(4);
|
|
5
|
+
const at = rest.indexOf(':');
|
|
6
|
+
if (at === -1) return text;
|
|
7
|
+
return `https://${rest.slice(0, at)}/${rest.slice(at + 1)}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function shortSha(value) {
|
|
11
|
+
return typeof value === 'string' && value.length >= 7 ? value.slice(0, 7) : String(value ?? '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function gitInstallSpecOf(info, version) {
|
|
15
|
+
const base = `git+${httpsUrlOf(info.url)}#${info.tagOf(version)}`;
|
|
16
|
+
return info.packagePath === '.' ? base : `${base}&path:/${info.packagePath}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function marketInstallOf(url, commit, path) {
|
|
20
|
+
const base = `git+${httpsUrlOf(url)}#${commit}`;
|
|
21
|
+
const suffix = String(path ?? '').replace(/^\/+/, '');
|
|
22
|
+
return suffix === '' || suffix === '.' ? base : `${base}&path:/${suffix}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installOf(info, name, version) {
|
|
26
|
+
if (info.kind === 'tarball') return `dsh plugin --profile workbench add ${info.fileOf(version)}`;
|
|
27
|
+
if (info.kind === 'git') return `dsh plugin --profile workbench add ${gitInstallSpecOf(info, version)}`;
|
|
28
|
+
return `dsh plugin --profile workbench add ${name}@${version}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function stateOf(info, manifest, version, status) {
|
|
32
|
+
const base = {
|
|
33
|
+
target: info.target,
|
|
34
|
+
name: manifest.name,
|
|
35
|
+
version,
|
|
36
|
+
status,
|
|
37
|
+
at: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
if (info.kind === 'registry') base.registry = info.registry;
|
|
40
|
+
if (info.kind === 'tarball') base.file = info.fileOf(version);
|
|
41
|
+
if (info.kind === 'git') {
|
|
42
|
+
base.remote = info.url;
|
|
43
|
+
base.tag = info.tagOf(version);
|
|
44
|
+
}
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function emit(out, json, value) {
|
|
49
|
+
if (json) out(JSON.stringify(value));
|
|
50
|
+
else human(out, value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function human(out, summary) {
|
|
54
|
+
if (summary.target === 'local') {
|
|
55
|
+
out('没发,留在本机。');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (summary.dryRun === true) {
|
|
59
|
+
out(`○ 只到预览(dry-run):${summary.name}@${summary.version} → ${summary.target}`);
|
|
60
|
+
if (summary.changes.length > 0) out(` 会改:${summary.changes.join(';')}`);
|
|
61
|
+
if (summary.files !== undefined) out(` 包内容:${summary.files.join(' · ')}`);
|
|
62
|
+
if (summary.market?.planned === true) out(` 会更新市场索引:${summary.market.url} → ${summary.market.entry}`);
|
|
63
|
+
if (summary.install !== '') out(` 安装:${summary.install}`);
|
|
64
|
+
out(' 什么都没写');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (summary.target === 'market') {
|
|
68
|
+
const market = summary.market ?? {};
|
|
69
|
+
out(
|
|
70
|
+
`✔ 已发布 ${summary.name}@${summary.version} → ${market.url}` +
|
|
71
|
+
`(${market.branch === undefined ? '默认分支' : market.branch})`,
|
|
72
|
+
);
|
|
73
|
+
out(` 产物:${String(market.path ?? '').replace(/^\/*/, '')} · commit ${shortSha(market.commit)}`);
|
|
74
|
+
out(' 提交:产物与 market.json 两个 commit 都已推上去');
|
|
75
|
+
if (summary.changes.length > 0) out(` 改了:${summary.changes.join(';')}`);
|
|
76
|
+
if (summary.files !== undefined) out(` 包内容:${summary.files.join(' · ')}`);
|
|
77
|
+
out(` 安装:${summary.install}`);
|
|
78
|
+
out(' 已从仓库侧核过产物');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const where =
|
|
82
|
+
summary.target === 'tarball'
|
|
83
|
+
? summary.file
|
|
84
|
+
: summary.target === 'git'
|
|
85
|
+
? `${summary.git.url}(tag ${summary.git.tag})`
|
|
86
|
+
: summary.registry;
|
|
87
|
+
out(`✔ 已发布 ${summary.name}@${summary.version} → ${where}`);
|
|
88
|
+
if (summary.changes.length > 0) out(` 改了:${summary.changes.join(';')}`);
|
|
89
|
+
if (summary.files !== undefined) out(` 包内容:${summary.files.join(' · ')}`);
|
|
90
|
+
if (summary.market !== undefined) {
|
|
91
|
+
out(
|
|
92
|
+
summary.market.updated === true
|
|
93
|
+
? ` 市场索引:${summary.market.url}${summary.market.branch === undefined ? '' : `(${summary.market.branch})`} · ${summary.market.entry}`
|
|
94
|
+
: ` 市场索引没更新:${summary.market.reason ?? '(未说明)'}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
out(` 安装:${summary.install}`);
|
|
98
|
+
out(' 已从出口侧核过包内容');
|
|
99
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { execaSync } from 'execa';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
|
|
7
|
+
export class ReleaseError extends Error {
|
|
8
|
+
constructor(message, code = 1) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ReleaseError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ResumableError extends ReleaseError {
|
|
16
|
+
constructor(message, resume) {
|
|
17
|
+
super(message, 4);
|
|
18
|
+
this.name = 'ResumableError';
|
|
19
|
+
this.resume = resume;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toJSON() {
|
|
23
|
+
return { ok: false, resumable: true, text: this.message, resume: this.resume };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NeedsUser extends Error {
|
|
28
|
+
constructor(step, text, command, extra = {}) {
|
|
29
|
+
super(text);
|
|
30
|
+
this.name = 'NeedsUser';
|
|
31
|
+
this.step = step;
|
|
32
|
+
this.text = text;
|
|
33
|
+
this.command = command;
|
|
34
|
+
this.extra = extra;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
step: this.step,
|
|
40
|
+
text: this.text,
|
|
41
|
+
...(this.command === undefined ? {} : { command: this.command }),
|
|
42
|
+
...this.extra,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function tailOf(text) {
|
|
48
|
+
return String(text ?? '')
|
|
49
|
+
.trim()
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter((one) => one.trim() !== '')
|
|
52
|
+
.slice(-6)
|
|
53
|
+
.join(' | ');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function publishTagOf(version) {
|
|
57
|
+
const prerelease = semver.prerelease(String(version ?? '').trim());
|
|
58
|
+
if (prerelease === null) return undefined;
|
|
59
|
+
return typeof prerelease[0] === 'string' ? prerelease[0] : 'next';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function defaultExec(command, argv, { cwd, capture = false, allowFailure = false } = {}) {
|
|
63
|
+
const cache = join(tmpdir(), 'dsh-wb-npm-cache');
|
|
64
|
+
const logs = join(tmpdir(), 'dsh-wb-npm-logs');
|
|
65
|
+
mkdirSync(cache, { recursive: true });
|
|
66
|
+
mkdirSync(logs, { recursive: true });
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = execaSync(command, argv, {
|
|
70
|
+
cwd,
|
|
71
|
+
env: { ...process.env, npm_config_cache: cache, npm_config_logs_dir: logs },
|
|
72
|
+
reject: false,
|
|
73
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (allowFailure) return { status: 1, stdout: '', stderr: String(error) };
|
|
77
|
+
throw new ReleaseError(`跑不起 ${command}:${String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
const status = result.exitCode ?? 1;
|
|
80
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
81
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
82
|
+
if (status !== 0 && !allowFailure) {
|
|
83
|
+
const text = tailOf(`${stdout}\n${stderr}`);
|
|
84
|
+
throw new ReleaseError(`${command} ${argv.join(' ')} 没过${text === '' ? '' : `:${text}`}`);
|
|
85
|
+
}
|
|
86
|
+
return { status, stdout, stderr };
|
|
87
|
+
}
|