dsh-plugin-update 0.1.0 → 0.1.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/README.md +45 -8
- package/derive-client-values.mjs +171 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npm install dsh-plugin-update
|
|
|
22
22
|
包根即宿主侧入口(`package.json` 的 `exports` 只暴露包根与 `./package.json`):
|
|
23
23
|
|
|
24
24
|
- 包根(宿主侧用):建更新能力、拼电话名、给调用方回电话名与处理器。宿主侧用标准模块写法,直接 `import`。
|
|
25
|
-
- `dist/client.js
|
|
25
|
+
- `dist/client.js`(客户端侧用):电话名拼法、轮询时间口径、手工兜底命令形状。客户端侧**在构建期派生取值**(把本文件打包一次,把你自己的前缀代进去,得到三个电话名与轮询间隔的常量),面板界面不进包。
|
|
26
26
|
|
|
27
27
|
## 3. 三步接入(双入口)
|
|
28
28
|
|
|
@@ -59,17 +59,54 @@ for (const [name, handler] of Object.entries(update.handlers)) {
|
|
|
59
59
|
这一步得到该插件的一组电话名与处理器:`notes.updateStatus`、`notes.updateCheck`、`notes.updateInstall`。
|
|
60
60
|
单例复用键强制含插件标识,多插件不串内存状态与锁。其余配置(官方源、目录根、超时、轮询)全可选并带默认值,默认值等于现状,不传即走现状。
|
|
61
61
|
|
|
62
|
-
第 3
|
|
62
|
+
第 3 步:客户端侧接线(构建期派生取值)。
|
|
63
|
+
|
|
64
|
+
这一步的目标只有一句:**面板里不要写死电话名与轮询间隔,全部从本包派生出来**。做法是构建时把本包的客户端入口打包一次,把你的前缀代进去,在你的仓库里生成一个小文件,里面是三个电话名与轮询间隔的常量;面板直接引用这些常量。
|
|
65
|
+
|
|
66
|
+
在第 2 步已有的配置上再补两处接线:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// 生成出来的文件里长这样(名字随你,这里是本仓用的形状):
|
|
70
|
+
// export const UPD_STATUS = 'notes.updateStatus'
|
|
71
|
+
// export const UPD_CHECK = 'notes.updateCheck'
|
|
72
|
+
// export const UPD_INSTALL = 'notes.updateInstall'
|
|
73
|
+
// export const UPD_POLL = 1000
|
|
74
|
+
//
|
|
75
|
+
// 面板里这样用(宿主侧用同一套 phoneNames,两边必须同前缀):
|
|
76
|
+
host.call(UPD_STATUS, {}) // 查状态
|
|
77
|
+
host.call(UPD_CHECK, {}) // 查新版
|
|
78
|
+
host.call(UPD_INSTALL, { checkId, requestId }) // 装更新
|
|
79
|
+
setInterval(readStatus, UPD_POLL) // 轮询间隔
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
面板不再出现 `'notes.updateStatus'` 这样的字面量,也不再出现写死的 `1000`。以后要换前缀或换轮询间隔,只改本包的配置再重新派生一次,面板一行都不用动。
|
|
83
|
+
|
|
84
|
+
派生要用的三样取值都在本包的客户端入口里,按名字取即可:
|
|
63
85
|
|
|
64
86
|
```js
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
assertPollInterval(1000) // 面板轮询间隔,不得小于 250 毫秒
|
|
70
|
-
manualCommand({ profileName, latestVersion, installedVersion, runningVersion, jobTargetVersion, blockedReason, sourceInstall: false, targetPackageName: 'my-notes-plugin', registryUrl: 'https://registry.npmjs.org/' })
|
|
87
|
+
buildClientPhoneNames('notes') // 电话名拼法,与宿主侧同一套(前缀 + 点 + 动作名)
|
|
88
|
+
CLIENT_POLL.defaultMs // 面板轮询间隔默认值(1 秒)
|
|
89
|
+
CLIENT_POLL.minMs // 轮询间隔下限(250 毫秒,低于它要报错而不是静默取整)
|
|
90
|
+
manualCommand({ ... }) // 手工兜底命令的形状,与宿主侧同一套政策
|
|
71
91
|
```
|
|
72
92
|
|
|
93
|
+
注意 `manualCommand` 一般不用你在面板里算:宿主每次回包都会带一条现成的手工命令(见第 9 节),面板拿到就展示,不要自己拼。
|
|
94
|
+
|
|
95
|
+
**参考实现**:本包自带集成工具 `derive-client-values.mjs`,在你自己的插件仓库里跑一条命令即可(它的输入是包里的 `dist/client.js`,纯 JS,不需要你自己有 TypeScript):
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
node node_modules/dsh-plugin-update/derive-client-values.mjs --prefix notes --out scripts/generated/updateClient.derived.js
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
生成的 `UPD_STATUS` / `UPD_CHECK` / `UPD_INSTALL` / `UPD_POLL` 就是上面那段面板示例要用的常量。以后换前缀或升级本包,重新跑一次这条命令即可。
|
|
102
|
+
|
|
103
|
+
自定义项与注意点:
|
|
104
|
+
|
|
105
|
+
- `--prefix` 必填,必须与宿主侧 `createHostUpdate` 传的 `prefix` 一致,否则面板调的电话名和宿主注册的电话名对不上。
|
|
106
|
+
- `--out` 必填,且故意不给默认值:默认值会覆盖别人的文件,写错比报错更糟。
|
|
107
|
+
- 工具用 `esbuild` 只做「打包一次」这件事(构建期使用,不引入运行期依赖)。esbuild 优先从本包自己的 `node_modules` 找,找不到再从你的仓库找;两处都没有时它会打印一句明确的安装提示,不静默失败。
|
|
108
|
+
- `--dry-run` 只打印生成内容、不写文件,用来先看一眼再决定。
|
|
109
|
+
|
|
73
110
|
客户端只做三件事:照前缀拼电话名、按间隔轮询查状态、装不上时展示手工兜底命令与待重启提示(见第 9、10 节)。
|
|
74
111
|
`pluginId` 必填(非空字符串且不含路径分隔符),`prefix` 不传即 `wf`,新插件务必传自己的前缀。
|
|
75
112
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* derive-client-values.mjs —— 从更新包生成「面板要用的取值」文件(集成文档第 3 节第 3 步的参考实现)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么要有这个文件:面板里不该写死电话名('notes.updateStatus' 这类字面量)与轮询间隔,
|
|
6
|
+
* 否则换前缀、换间隔就得改多处、还容易改漏。取值只有更新包这一个来源,
|
|
7
|
+
* 所以做法是:构建时把本包的客户端入口打包一次,把消费方自己的前缀代进去,
|
|
8
|
+
* 在消费方仓库里生成一个小文件,里面是三个电话名与轮询间隔的常量,面板直接引用这些常量。
|
|
9
|
+
*
|
|
10
|
+
* 用法(在你自己的插件仓库里跑):
|
|
11
|
+
* node node_modules/dsh-plugin-update/derive-client-values.mjs \
|
|
12
|
+
* --prefix notes \
|
|
13
|
+
* --out scripts/generated/updateClient.derived.js
|
|
14
|
+
*
|
|
15
|
+
* 参数:
|
|
16
|
+
* --prefix <前缀> 必填。你在宿主侧 createHostUpdate 里传的那个 prefix,两处必须一致。
|
|
17
|
+
* --out <文件路径> 必填。生成到哪里。故意不给默认值:默认值会覆盖别人的文件,写错更糟。
|
|
18
|
+
* --package <标识> 可选。写进生成文件的封面注释,方便以后认出来是谁生成的。默认取消费方 package.json 的 name。
|
|
19
|
+
* --dry-run 只打印要生成的内容,不写文件。
|
|
20
|
+
*
|
|
21
|
+
* 生成的形状(具体值随你的前缀):
|
|
22
|
+
* export const UPD_STATUS = 'notes.updateStatus'
|
|
23
|
+
* export const UPD_CHECK = 'notes.updateCheck'
|
|
24
|
+
* export const UPD_INSTALL = 'notes.updateInstall'
|
|
25
|
+
* export const UPD_POLL = 1000
|
|
26
|
+
* export const UPD_POLL_MIN = 250
|
|
27
|
+
*
|
|
28
|
+
* 依赖说明:本脚本用 esbuild 只做「打包一次」这件事,不引入任何运行期依赖。
|
|
29
|
+
* esbuild 优先从本包自己的 node_modules 找;找不到再从你的仓库里找(你自己的 devDependencies 里有也行)。
|
|
30
|
+
* 两个地方都没有时打印一句明确的安装提示,不静默失败。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
34
|
+
import { createRequire } from 'node:module'
|
|
35
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
36
|
+
import { fileURLToPath } from 'node:url'
|
|
37
|
+
|
|
38
|
+
const PKG_DIR = dirname(fileURLToPath(import.meta.url))
|
|
39
|
+
const PKG_MANIFEST = JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8'))
|
|
40
|
+
|
|
41
|
+
function usage(message) {
|
|
42
|
+
if (message) console.error('错误:' + message)
|
|
43
|
+
console.error('用法:node ' + join(PKG_DIR.split(/[\\/]/).pop(), 'derive-client-values.mjs') + ' --prefix <前缀> --out <文件路径> [--package <标识>] [--dry-run]')
|
|
44
|
+
process.exit(2)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------- 参数 ----------
|
|
48
|
+
let prefix = ''
|
|
49
|
+
let outFile = ''
|
|
50
|
+
let consumerName = ''
|
|
51
|
+
let dryRun = false
|
|
52
|
+
const argv = process.argv.slice(2)
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const arg = argv[i]
|
|
55
|
+
if (arg === '--prefix') { prefix = argv[++i] || '' }
|
|
56
|
+
else if (arg === '--out') { outFile = argv[++i] || '' }
|
|
57
|
+
else if (arg === '--package') { consumerName = argv[++i] || '' }
|
|
58
|
+
else if (arg === '--dry-run') { dryRun = true }
|
|
59
|
+
else if (arg === '-h' || arg === '--help') { usage('') }
|
|
60
|
+
else { usage('不认识的参数 ' + arg) }
|
|
61
|
+
}
|
|
62
|
+
if (!prefix) usage('必须给 --prefix(要与宿主侧 createHostUpdate 的 prefix 一致)')
|
|
63
|
+
if (!outFile) usage('必须给 --out(生成到哪里;故意不给默认值,免得覆盖别人的文件)')
|
|
64
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(prefix)) usage('--prefix 只允许字母、数字、下划线、点与短横(电话名是「前缀.动作名」,前缀里不该有别的字符)')
|
|
65
|
+
if (prefix.includes('.')) usage('--prefix 不能含点:点留作前缀与动作名之间的分隔符')
|
|
66
|
+
|
|
67
|
+
// ---------- 找 esbuild ----------
|
|
68
|
+
function loadEsbuild() {
|
|
69
|
+
const candidates = [join(PKG_DIR, 'package.json'), join(process.cwd(), 'package.json')]
|
|
70
|
+
for (const from of candidates) {
|
|
71
|
+
if (!existsSync(from)) continue
|
|
72
|
+
try {
|
|
73
|
+
const require = createRequire(from)
|
|
74
|
+
return require('esbuild')
|
|
75
|
+
} catch {
|
|
76
|
+
// 换下一个候选位置
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.error('错误:找不到 esbuild(本脚本用它把客户端入口打包一次,只做构建期使用,不引入运行期依赖)。')
|
|
80
|
+
console.error('修法:在你自己的插件仓库里装一次开发依赖 —— npm install --save-dev esbuild')
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------- 打包客户端入口 ----------
|
|
85
|
+
const esbuild = loadEsbuild()
|
|
86
|
+
const entry = join(PKG_DIR, 'src', 'client.ts')
|
|
87
|
+
const sourceOfTruth = existsSync(entry) ? entry : join(PKG_DIR, 'dist', 'client.js')
|
|
88
|
+
if (!existsSync(sourceOfTruth)) {
|
|
89
|
+
console.error('错误:包里既没有 src/client.ts 也没有 dist/client.js,包可能不完整(重装一次试试)。')
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let built
|
|
94
|
+
try {
|
|
95
|
+
built = esbuild.buildSync({
|
|
96
|
+
entryPoints: [sourceOfTruth],
|
|
97
|
+
bundle: true,
|
|
98
|
+
format: 'esm',
|
|
99
|
+
platform: 'neutral',
|
|
100
|
+
target: 'es2020',
|
|
101
|
+
write: false,
|
|
102
|
+
})
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('错误:打包客户端入口失败:' + ((error && error.message) || error))
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
if (!built.outputFiles || built.outputFiles.length !== 1) {
|
|
108
|
+
console.error('错误:打包产物数量不对(期望恰好 1 个文件),包可能不完整。')
|
|
109
|
+
process.exit(1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let body = Buffer.from(built.outputFiles[0].contents).toString('utf8').replace(/\r\n/g, '\n')
|
|
113
|
+
// 注释里的路径归一:不同目录重跑时打包器会写出相对路径形态,统一成包名开头,保证消费方重跑零差异。
|
|
114
|
+
body = body.replace(/^\/\/ (\.\.\/)+packages\//gm, '// packages/')
|
|
115
|
+
|
|
116
|
+
// 打包器收尾会写一个模块级的 `export { ... };` 块。那个块拼进插件主文件闭包是语法错误
|
|
117
|
+
// (闭包里只能是声明),所以拆掉它;被改名导出的符号在下面补回本名。
|
|
118
|
+
const exportBlock = body.match(/export\s*\{([\s\S]*?)\}\s*;?\s*$/)
|
|
119
|
+
const aliases = []
|
|
120
|
+
if (exportBlock) {
|
|
121
|
+
for (const item of exportBlock[1].split(',')) {
|
|
122
|
+
const piece = item.trim()
|
|
123
|
+
if (!piece) continue
|
|
124
|
+
const alias = piece.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/)
|
|
125
|
+
if (alias && alias[1] !== alias[2]) aliases.push(alias)
|
|
126
|
+
}
|
|
127
|
+
body = body.slice(0, exportBlock.index)
|
|
128
|
+
}
|
|
129
|
+
body = body.replace(/\s+$/, '') + '\n'
|
|
130
|
+
|
|
131
|
+
// ---------- 组装 ----------
|
|
132
|
+
const who = consumerName || (function () {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8')).name || '(未指明)'
|
|
135
|
+
} catch {
|
|
136
|
+
return '(未指明)'
|
|
137
|
+
}
|
|
138
|
+
})()
|
|
139
|
+
|
|
140
|
+
const header =
|
|
141
|
+
'// 由 ' + PKG_MANIFEST.name + '@' + PKG_MANIFEST.version + ' 的集成工具生成,人手不改。\n' +
|
|
142
|
+
'// 生成命令:node ' + PKG_MANIFEST.name + '/derive-client-values.mjs --prefix ' + prefix + ' --out <本文件路径>\n' +
|
|
143
|
+
'// 生成对象:' + who + '。改了前缀或想升级本包,重新跑一次这条命令即可。\n'
|
|
144
|
+
|
|
145
|
+
const aliasExports = aliases.map(([local, name]) => 'export const ' + name + ' = ' + local + '\n').join('')
|
|
146
|
+
|
|
147
|
+
const tail =
|
|
148
|
+
'\n// ---- 取值:从更新包的客户端入口算出本插件要用的电话名与轮询间隔 ----\n' +
|
|
149
|
+
'// 面板只该用下面这几个常量,不要再写死电话名字面量与轮询数字。\n' +
|
|
150
|
+
'const UPD_PHONE_NAMES = buildClientPhoneNames(' + JSON.stringify(prefix) + ')\n' +
|
|
151
|
+
'const UPD_POLL_MS = CLIENT_POLL.defaultMs\n' +
|
|
152
|
+
'const UPD_POLL_MIN_MS = CLIENT_POLL.minMs\n' +
|
|
153
|
+
'export const UPD_STATUS = UPD_PHONE_NAMES.updateStatus\n' +
|
|
154
|
+
'export const UPD_CHECK = UPD_PHONE_NAMES.updateCheck\n' +
|
|
155
|
+
'export const UPD_INSTALL = UPD_PHONE_NAMES.updateInstall\n' +
|
|
156
|
+
'export const UPD_POLL = UPD_POLL_MS\n' +
|
|
157
|
+
'export const UPD_POLL_MIN = UPD_POLL_MIN_MS\n'
|
|
158
|
+
|
|
159
|
+
const output = header + body + aliasExports + tail
|
|
160
|
+
|
|
161
|
+
if (dryRun) {
|
|
162
|
+
process.stdout.write(output)
|
|
163
|
+
process.exit(0)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const target = isAbsolute(outFile) ? outFile : resolve(process.cwd(), outFile)
|
|
167
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
168
|
+
writeFileSync(target, output, 'utf8')
|
|
169
|
+
console.log('已生成:' + target)
|
|
170
|
+
console.log(' 前缀 ' + prefix + ' → 电话名 ' + ['updateStatus', 'updateCheck', 'updateInstall'].map((a) => prefix + '.' + a).join('、') + ',轮询 ' + 1000 + ' 毫秒')
|
|
171
|
+
console.log(' 面板里请引用 UPD_STATUS / UPD_CHECK / UPD_INSTALL / UPD_POLL,不要再写字面量。')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-update",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "王辰浩",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
+
"derive-client-values.mjs",
|
|
17
18
|
"event-list.template.json",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|