@bushizhanghan/dsh-publish-gate 0.3.0 → 0.3.2
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 +4 -4
- package/lib/engine.js +59 -1
- package/lib/index.js +12 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
37
37
|
| `publish_gate_market` | 校验市场条目 YAML(url/name/category/description 双语);传 `dir` 则改查只有线上才能回答的三件事:这个仓库**已经在目录里**(再开 PR 就是重复条目)、另一个条目也在发同一个 npm 包、仓库还**不满 24 小时**(提交门禁会失败并自己重跑) |
|
|
38
38
|
| `publish_gate_next_version` | 当前版本、是否已被 registry 占用、建议 bump 到多少(本地比 npm 旧时从 npm 的 latest 往上加,不会推荐一个已被占用的版本) |
|
|
39
39
|
| `publish_gate_doctor` | 扫整个目录,按 blocker 数排序,附带每个插件的真实包体积,看哪个还不能发 |
|
|
40
|
-
| `publish_gate_commands` |
|
|
40
|
+
| `publish_gate_commands` | 按顺序列出发布命令,并标出这台机器已经走到哪一步。命令是**这台机器上真能跑的形式**:Windows 下 `npm publish` 走 `cmd /c`(PowerShell 的 `npm.ps1` 常被执行策略拦掉),`npm publish` 后面紧跟一条 `publish_gate_verify` |
|
|
41
41
|
|
|
42
42
|
## 检查清单
|
|
43
43
|
|
|
@@ -135,7 +135,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
135
135
|
|
|
136
136
|
### 代码卫生
|
|
137
137
|
|
|
138
|
-
`lib-debugger`(error)、`lib-console-log`、`lib-hardcoded-path`(`C:\Users\...` 硬编码)、`lib-url-pathname`(`new URL(...).pathname` 在中文路径上会百分号编码,得用 `fileURLToPath
|
|
138
|
+
`lib-debugger`(error)、`lib-console-log`、`lib-hardcoded-path`(`C:\Users\...` 硬编码)、`lib-url-pathname`(`new URL(...).pathname` 在中文路径上会百分号编码,得用 `fileURLToPath`)、`node-too-old`(`engines.node` 写着 `>=18`,`lib/` 里却用了 `Object.groupBy`(21)或 `Promise.withResolvers`(22)。npm 对 `engines` 只是警告,照样装上,然后在 import 那一刻抛——用户看到的是「插件坏了」,不是版本不匹配)。字符串和注释里的同名文字不算。
|
|
139
139
|
|
|
140
140
|
### 构建产物:`src/` 和 `lib/` 谁更新
|
|
141
141
|
|
|
@@ -183,8 +183,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
183
183
|
## 开发
|
|
184
184
|
|
|
185
185
|
```sh
|
|
186
|
-
node --test #
|
|
187
|
-
node test/integration.test.mjs #
|
|
186
|
+
node --test # 65 个引擎单测
|
|
187
|
+
node test/integration.test.mjs # 26 条集成断言(9 个真实插件作 fixture)
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
## License
|
package/lib/engine.js
CHANGED
|
@@ -1202,6 +1202,63 @@ export function checkLibHygiene(dir) {
|
|
|
1202
1202
|
return out
|
|
1203
1203
|
}
|
|
1204
1204
|
|
|
1205
|
+
/**
|
|
1206
|
+
* Features that need a newer Node than the manifest promises.
|
|
1207
|
+
*
|
|
1208
|
+
* `engines.node` is only advisory — npm warns and installs anyway — so a plugin
|
|
1209
|
+
* that says `>=20` and uses `Object.groupBy` (21) breaks on exactly the Node it
|
|
1210
|
+
* advertised. Each entry is [name, regex, minimum major], newest evidence only.
|
|
1211
|
+
*/
|
|
1212
|
+
const NODE_FEATURES = [
|
|
1213
|
+
['Object.groupBy / Map.groupBy', /\b(?:Object|Map)\.groupBy\b/, 21],
|
|
1214
|
+
['Array.fromAsync', /\bArray\.fromAsync\b/, 22],
|
|
1215
|
+
['Promise.withResolvers', /\bPromise\.withResolvers\b/, 22],
|
|
1216
|
+
['import.meta.dirname', /\bimport\.meta\.dirname\b/, 21],
|
|
1217
|
+
['Array.prototype.toSorted / toReversed / toSpliced / with', /\.(?:toSorted|toReversed|toSpliced)\(|\.with\(/, 20],
|
|
1218
|
+
['structuredClone', /\bstructuredClone\s*\(/, 17],
|
|
1219
|
+
]
|
|
1220
|
+
|
|
1221
|
+
/** `>=22.1.0`, `^20`, `>=18 <21` -> the first major it allows. */
|
|
1222
|
+
const minNodeMajor = (range) => {
|
|
1223
|
+
if (typeof range !== 'string') return null
|
|
1224
|
+
const m = /(\d+)/.exec(range)
|
|
1225
|
+
return m ? Number(m[1]) : null
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
export function checkNodeVersion(dir) {
|
|
1229
|
+
const declared = minNodeMajor(readPkgJson(dir)?.engines?.node)
|
|
1230
|
+
if (declared === null) return []
|
|
1231
|
+
const out = []
|
|
1232
|
+
const seen = new Set()
|
|
1233
|
+
for (const file of walkFiles(join(dir, 'lib'), /\.(?:js|mjs|cjs)$/)) {
|
|
1234
|
+
const text = safeText(file)
|
|
1235
|
+
if (text === null) continue
|
|
1236
|
+
// Strings are stripped first: a tool description that names a feature is
|
|
1237
|
+
// prose, not a call (the same false positive require-in-esm had).
|
|
1238
|
+
const code = text
|
|
1239
|
+
.split('\n')
|
|
1240
|
+
.map(stripComment)
|
|
1241
|
+
.join('\n')
|
|
1242
|
+
.replace(/'(?:[^'\\\n]|\\.)*'/g, "''")
|
|
1243
|
+
.replace(/"(?:[^"\\\n]|\\.)*"/g, '""')
|
|
1244
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, '``')
|
|
1245
|
+
for (const [name, re, needs] of NODE_FEATURES) {
|
|
1246
|
+
if (needs <= declared || seen.has(name) || !re.test(code)) continue
|
|
1247
|
+
seen.add(name)
|
|
1248
|
+
out.push(
|
|
1249
|
+
F(
|
|
1250
|
+
'node-too-old',
|
|
1251
|
+
'warn',
|
|
1252
|
+
`engines.node says ${JSON.stringify(readPkgJson(dir).engines.node)} but lib/ uses ${name} (needs >=${needs})`,
|
|
1253
|
+
'npm only warns about engines, so it installs on the older Node and throws at import time — the consumer sees a broken plugin, not a version mismatch.',
|
|
1254
|
+
`Raise engines.node to >=${needs}, or stop using ${name}.`,
|
|
1255
|
+
),
|
|
1256
|
+
)
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return out
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1205
1262
|
const INJECT_HINTS = [
|
|
1206
1263
|
['tools', /ctx\.tools\b/],
|
|
1207
1264
|
['web', /ctx\.web\b/],
|
|
@@ -1564,6 +1621,7 @@ export function runStaticChecks(dir) {
|
|
|
1564
1621
|
...checkInjectCoverage(dir),
|
|
1565
1622
|
...checkClientHalf(dir),
|
|
1566
1623
|
...checkLibHygiene(dir),
|
|
1624
|
+
...checkNodeVersion(dir),
|
|
1567
1625
|
...checkBuildFreshness(dir),
|
|
1568
1626
|
...checkDocs(dir),
|
|
1569
1627
|
...checkTests(dir),
|
|
@@ -1586,7 +1644,7 @@ const RULE_ORDER = [
|
|
|
1586
1644
|
'inject-unused-service',
|
|
1587
1645
|
'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
|
|
1588
1646
|
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type',
|
|
1589
|
-
'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'stale-build', 'hand-edited-build',
|
|
1647
|
+
'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
|
|
1590
1648
|
'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
|
|
1591
1649
|
'pack-unexpected-dir', 'pack-script-build', 'pack-unavailable',
|
|
1592
1650
|
'no-license', 'license-mismatch', 'version-taken', 'registry-unreachable', 'publish-not-served',
|
package/lib/index.js
CHANGED
|
@@ -379,11 +379,21 @@ export function apply(ctx, config = {}) {
|
|
|
379
379
|
},
|
|
380
380
|
{
|
|
381
381
|
name: 'npm publish',
|
|
382
|
-
command:
|
|
382
|
+
command: process.platform === 'win32' ? 'cmd /c "npm publish --access public"' : 'npm publish --access public',
|
|
383
383
|
done: false,
|
|
384
|
-
note:
|
|
384
|
+
note:
|
|
385
|
+
`a bare (unscoped) name can return 403 with a granular token — "${json.name}" is ${json.name.startsWith('@') ? 'scoped, fine' : 'UNSCOPED, rename it'}` +
|
|
386
|
+
(process.platform === 'win32'
|
|
387
|
+
? '. On Windows the bare form fails under PowerShell when npm.ps1 is blocked by execution policy, so it is wrapped in cmd /c'
|
|
388
|
+
: '') +
|
|
389
|
+
'. Run `npm publish --dry-run` first if you want to see the file list without spending the version.',
|
|
385
390
|
},
|
|
386
391
|
{
|
|
392
|
+
name: 'verify the published bytes',
|
|
393
|
+
command: `publish_gate_verify({ dir: "${dir.replace(/\\/g, '\\\\')}" })`,
|
|
394
|
+
done: false,
|
|
395
|
+
note: 'the registry answers 202 and then 404s for about a minute, so a mismatch right after publish is normal; the shasum comparison is exact once it settles',
|
|
396
|
+
}, {
|
|
387
397
|
name: 'market entry',
|
|
388
398
|
command: `write data/plugins/${owner}__${repo}.yml with url / name / category / description{en,zh}`,
|
|
389
399
|
done: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Helps DSH plugin AUTHORS publish their plugins: a gate that turns every silent failure in the publish path into a deterministic check. Manifest layer — package.json completeness, BOM, YAML/JSON parseability, entry paths that point at no file, a patch the manifest never declares, patch insert correctness, a build whose lib/ is older than src/, inject services that are missing or misspelled, npm scope permissions, GitHub repo and token state, market entry format, LICENSE/README, tests. Artifact layer — runs `npm pack --dry-run` and reads what npm will really ship: code missing from the tarball, an unpacked patch, oversize packages, dev directories. Runtime layer — imports the entry in an isolated child process, calls apply(ctx), and reports tools that never register, missing execute, duplicate names, throwing presentCall and card kinds the host does not render. Browser half — loads the client bundle against a fake window.__ModuleLoader__ and reports a bundle that never loads, returns no apply, throws, registers nothing, or uses a service its own inject array omits, because a panel that never appears looks like a UI bug, not a packaging one. Then GitHub + npm + market, and a post-publish shasum check that proves the registry holds these exact bytes. 帮助插件作者一键发布:写完插件后跑一遍门禁(清单 / 产物 / 运行时 / 浏览器半边),把发布链路上的每个静默坑变成确定性检查,然后 GitHub + npm + 市场一键三连。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|