@bushizhanghan/dsh-publish-gate 0.1.2 → 0.1.4
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 +8 -6
- package/lib/engine.js +73 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,14 +47,15 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
47
47
|
| `no-exports` | 缺 `exports["."]`,宿主解析不到入口 |
|
|
48
48
|
| `client-without-export` | 声明了 client 平台却没导出 `./client`,宿主半边在、侧栏永不出现 |
|
|
49
49
|
| `not-esm` | 没 `"type": "module"`,`import` 在加载时炸 |
|
|
50
|
-
| `insert-
|
|
50
|
+
| `no-insert-row` | patch 里只有 override,永远不挂载(页面报的仍然是这条——顶层 `- id:` 是 patch 不是挂载) |
|
|
51
|
+
| `patch-not-mount` | info:这些顶层 `- id:` 是在覆盖已有条目,合法,但**不是**挂载 |
|
|
51
52
|
| `insert-missing-name` / `insert-name-mismatch` | 挂载名缺失或与 package.json 不一致 |
|
|
52
|
-
| `no-insert-row` | patch 里只有 override,永远不挂载 |
|
|
53
53
|
| `no-patch-file` | 有 `lib/index.js` 但没有 `cordis.patch.yml` |
|
|
54
54
|
| `no-name-export` / `no-apply-export` | 入口协议不完整 |
|
|
55
55
|
| `require-in-esm` | ESM 里 `require()` → `ERR_AMBIGUOUS_MODULE_SYNTAX`,报错不带行号 |
|
|
56
56
|
| `no-license` | 缺 LICENSE |
|
|
57
57
|
| `version-taken` | 这个版本已经在 registry 上,再发是 403 |
|
|
58
|
+
| `registry-unreachable` | registry 回了 500/429 —— 版本检查**没跑**,所以别把它当干净 |
|
|
58
59
|
|
|
59
60
|
### 值得在发布前知道的(warn / info)
|
|
60
61
|
`unscoped-name`(granular token 的 403)、`repo-missing`、`patch-name-unresolvable`(junction 放错层级)、`no-readme` / `readme-no-install` / `readme-no-profile`、`license-mismatch`、`no-tests` / `no-test-files` / `no-test-script`、`no-engines`、`bad-repo-url`、`no-git` / `no-gitignore`,以及市场条目的 `market-*` 全系列。
|
|
@@ -69,9 +70,10 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
69
70
|
fail with a parse error that never names the BOM.
|
|
70
71
|
✗ files does not include "lib" [files-omits-lib]
|
|
71
72
|
The published tarball will contain no code.
|
|
72
|
-
✗
|
|
73
|
-
A
|
|
74
|
-
|
|
73
|
+
✗ cordis.patch.yml has no insert row [no-insert-row]
|
|
74
|
+
A patch with only overrides never mounts your plugin. This file has 1
|
|
75
|
+
top-level `- id:` row — those patch entries that already exist in the tree,
|
|
76
|
+
they do not add one.
|
|
75
77
|
✗ lib/index.js calls require() inside an ESM module [require-in-esm]
|
|
76
78
|
4 blocker(s) — fix before publishing
|
|
77
79
|
```
|
|
@@ -88,7 +90,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
88
90
|
## 开发
|
|
89
91
|
|
|
90
92
|
```sh
|
|
91
|
-
node --test
|
|
93
|
+
node --test # 24 个测试(含 4 个真实插件作 fixture)
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
## License
|
package/lib/engine.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// unscoped npm name that returned E403, a git push that failed on the network
|
|
7
7
|
// while the REST API worked. All of them are silent until the moment they cost
|
|
8
8
|
// you a publish, so they are all checkable before you spend it.
|
|
9
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
10
10
|
import { join, resolve } from 'node:path'
|
|
11
11
|
import { homedir } from 'node:os'
|
|
12
12
|
|
|
@@ -64,6 +64,9 @@ export function checkPackageJson(dir) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
for (const field of REQUIRED) {
|
|
67
|
+
// `main` is optional when the package resolves through exports["."]:
|
|
68
|
+
// an exports-only manifest is the modern shape and loads fine.
|
|
69
|
+
if (field === 'main' && json.exports?.['.']) continue
|
|
67
70
|
if (json[field] === undefined || json[field] === null || json[field] === '') {
|
|
68
71
|
out.push(F('missing-field', 'error', `package.json is missing "${field}"`, `npm warns but publishes; consumers then cannot resolve it.`, `Add "${field}".`))
|
|
69
72
|
}
|
|
@@ -100,9 +103,15 @@ export function checkPackageJson(dir) {
|
|
|
100
103
|
)
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
// files — the classic: lib/ omitted, so the published tarball has no code
|
|
106
|
+
// files — the classic: lib/ omitted, so the published tarball has no code.
|
|
107
|
+
// "lib/", "./lib" and "lib/**/*.js" all pack lib/, so match the entry rather
|
|
108
|
+
// than the exact string "lib".
|
|
104
109
|
if (Array.isArray(json.files)) {
|
|
105
|
-
|
|
110
|
+
const packsLib = json.files.some((f) => {
|
|
111
|
+
const norm = String(f).replace(/^\.\//, '').replace(/\/+$/, '')
|
|
112
|
+
return norm === 'lib' || norm === 'lib/**/*' || norm.startsWith('lib/')
|
|
113
|
+
})
|
|
114
|
+
if (!packsLib) {
|
|
106
115
|
out.push(
|
|
107
116
|
F(
|
|
108
117
|
'files-omits-lib',
|
|
@@ -201,6 +210,7 @@ export function checkPatchYml(dir) {
|
|
|
201
210
|
|
|
202
211
|
const lines = text.split('\n')
|
|
203
212
|
const insertRows = []
|
|
213
|
+
const patchRows = []
|
|
204
214
|
let inInsert = false
|
|
205
215
|
let insertIndent = 0
|
|
206
216
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -215,7 +225,12 @@ export function checkPatchYml(dir) {
|
|
|
215
225
|
if (idMatch) {
|
|
216
226
|
const indent = idMatch[1].length
|
|
217
227
|
const id = idMatch[2].replace(/^['"]|['"]$/g, '')
|
|
218
|
-
|
|
228
|
+
// A row at column 0 is a patch on an existing entry — the shape every
|
|
229
|
+
// profile patch is made of. Only a row nested inside "- insert:" mounts
|
|
230
|
+
// a package, and calling the patch row a mis-indented insert told users
|
|
231
|
+
// to indent it, which would insert a duplicate of an existing id.
|
|
232
|
+
if (inInsert && indent > insertIndent) insertRows.push({ line: i + 1, indent, id })
|
|
233
|
+
else patchRows.push({ line: i + 1, indent, id })
|
|
219
234
|
continue
|
|
220
235
|
}
|
|
221
236
|
// a top-level key (indent 0, no dash) closes the insert sequence
|
|
@@ -228,41 +243,37 @@ export function checkPatchYml(dir) {
|
|
|
228
243
|
'no-insert-row',
|
|
229
244
|
'error',
|
|
230
245
|
'cordis.patch.yml has no insert row',
|
|
231
|
-
|
|
246
|
+
`A patch with only overrides never mounts your plugin.${patchRows.length > 0 ? ` This file has ${patchRows.length} top-level \`- id:\` row(s) — those patch entries that already exist in the tree, they do not add one.` : ''}`,
|
|
232
247
|
'Add:\n- insert:\n - id: <your-plugin-id>\n name: "<package-name>"',
|
|
233
248
|
),
|
|
234
249
|
)
|
|
235
250
|
return out
|
|
236
251
|
}
|
|
237
252
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
),
|
|
251
|
-
)
|
|
252
|
-
}
|
|
253
|
+
// Patching is legitimate (that is how a profile overrides a model or a
|
|
254
|
+
// theme), but it must not be mistaken for a mount.
|
|
255
|
+
if (patchRows.length > 0) {
|
|
256
|
+
out.push(
|
|
257
|
+
F(
|
|
258
|
+
'patch-not-mount',
|
|
259
|
+
'info',
|
|
260
|
+
`${patchRows.length} top-level \`- id:\` row(s) patch existing entries (line ${patchRows.map((r) => r.line).join(', ')})`,
|
|
261
|
+
'These rows override an entry that is already in the tree. They are not mounts: if you meant to add this package, it needs an "- insert:" row.',
|
|
262
|
+
'Nothing to do if you are overriding. If not, move the row under "- insert:".',
|
|
263
|
+
),
|
|
264
|
+
)
|
|
253
265
|
}
|
|
254
266
|
|
|
255
267
|
// every insert row must carry a name that matches package.json
|
|
256
|
-
for (
|
|
257
|
-
const
|
|
258
|
-
if (!m || m[1].length <= 2) continue
|
|
268
|
+
for (const row of insertRows) {
|
|
269
|
+
const i = row.line - 1
|
|
259
270
|
const nameMatch = /^\s*name:\s*(.+?)\s*$/.exec(lines[i + 1] || '')
|
|
260
271
|
if (!nameMatch) {
|
|
261
272
|
out.push(
|
|
262
273
|
F(
|
|
263
274
|
'insert-missing-name',
|
|
264
275
|
'error',
|
|
265
|
-
`insert row on line ${
|
|
276
|
+
`insert row on line ${row.line} has no name: field`,
|
|
266
277
|
'The loader mounts by package name; without it the row resolves to nothing.',
|
|
267
278
|
`Add the next line: name: "${name || '<package-name>'}"`,
|
|
268
279
|
),
|
|
@@ -379,12 +390,28 @@ export function checkNpmState(dir, { token, registry = 'https://registry.npmjs.o
|
|
|
379
390
|
}
|
|
380
391
|
|
|
381
392
|
/** Version-collision check: the same version already on the registry. */
|
|
382
|
-
export async function checkVersionTaken(name, version, { registry = 'https://registry.npmjs.org' } = {}) {
|
|
393
|
+
export async function checkVersionTaken(name, version, { registry = 'https://registry.npmjs.org', timeoutMs = 8000 } = {}) {
|
|
383
394
|
const out = []
|
|
384
395
|
try {
|
|
385
|
-
const res = await fetch(`${registry.replace(/\/$/, '')}/${name.replace('/', '%2f')}`, {
|
|
396
|
+
const res = await fetch(`${registry.replace(/\/$/, '')}/${name.replace('/', '%2f')}`, {
|
|
397
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
398
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
399
|
+
})
|
|
386
400
|
if (res.status === 404) return out // never published: fine
|
|
387
|
-
if (!res.ok)
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
// A 500 or a rate limit is not proof the version is free. Saying nothing
|
|
403
|
+
// here made the gate report "clean" for a check that never ran.
|
|
404
|
+
out.push(
|
|
405
|
+
F(
|
|
406
|
+
'registry-unreachable',
|
|
407
|
+
'warn',
|
|
408
|
+
`could not read ${name} from the registry (HTTP ${res.status})`,
|
|
409
|
+
'The version-taken check did not run, so this is NOT a clean version check. npm itself will still refuse a duplicate on publish.',
|
|
410
|
+
'Re-run the gate later, or check https://www.npmjs.com/package/' + name + '?activeTab=versions by hand.',
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
return out
|
|
414
|
+
}
|
|
388
415
|
const json = await res.json()
|
|
389
416
|
if (json?.versions?.[version]) {
|
|
390
417
|
out.push(
|
|
@@ -489,8 +516,17 @@ export function checkTests(dir) {
|
|
|
489
516
|
out.push(F('no-tests', 'warn', 'no test/ directory', 'A plugin with no tests ships rules nobody has run; the market does not require tests, but reviewers notice.', 'Add node --test tests.'))
|
|
490
517
|
return out
|
|
491
518
|
}
|
|
492
|
-
|
|
493
|
-
|
|
519
|
+
// Any test file counts: naming is the author's business, and demanding
|
|
520
|
+
// engine.test.mjs specifically told authors to rename working tests.
|
|
521
|
+
const testFiles = (() => {
|
|
522
|
+
try {
|
|
523
|
+
return readdirSync(testDir).filter((f) => /\.(test|spec)\.(mjs|js|cjs)$/.test(f))
|
|
524
|
+
} catch {
|
|
525
|
+
return []
|
|
526
|
+
}
|
|
527
|
+
})()
|
|
528
|
+
const hasTest = testFiles.length > 0
|
|
529
|
+
if (!hasTest) out.push(F('no-test-files', 'warn', 'test/ has no *.test.mjs or *.spec.mjs', 'Add at least an engine test so the rules are exercised.', 'Add test/engine.test.mjs.'))
|
|
494
530
|
const pkgJson = (() => {
|
|
495
531
|
const t = safeText(join(dir, 'package.json'))
|
|
496
532
|
try {
|
|
@@ -499,7 +535,13 @@ export function checkTests(dir) {
|
|
|
499
535
|
return null
|
|
500
536
|
}
|
|
501
537
|
})()
|
|
502
|
-
if (!pkgJson?.scripts?.test) out.push(F('no-test-script', 'info', 'package.json has no "test" script', 'npm test does nothing, so CI and reviewers cannot run your checks.', 'Add "scripts": { "test": "node --test
|
|
538
|
+
if (!pkgJson?.scripts?.test) out.push(F('no-test-script', 'info', 'package.json has no "test" script', 'npm test does nothing, so CI and reviewers cannot run your checks.', 'Add "scripts": { "test": "node --test" }.'))
|
|
539
|
+
// Node 22+ loads a positional argument to --test as a module path instead of
|
|
540
|
+
// globbing it, so `node --test test/` dies with MODULE_NOT_FOUND on every run
|
|
541
|
+
// while the tests themselves are fine. Auto-discovery needs no argument.
|
|
542
|
+
const testScript = pkgJson?.scripts?.test || ''
|
|
543
|
+
const dirArg = testScript.match(/\bnode\s+--test\s+(?!-)(\S+)/)
|
|
544
|
+
if (dirArg) out.push(F('test-script-dir-arg', 'warn', `test script passes a path to node --test ("${dirArg[1]}")`, 'Node 22+ treats that argument as a module path, not a directory to glob: `node --test test/` throws MODULE_NOT_FOUND, so npm test can never pass even though the tests are fine.', 'Drop the path — "test": "node --test" auto-discovers test/**/*.test.mjs.'))
|
|
503
545
|
return out
|
|
504
546
|
}
|
|
505
547
|
|
|
@@ -523,7 +565,7 @@ const RULE_ORDER = [
|
|
|
523
565
|
'missing-field', 'not-esm', 'files-omits-lib', 'files-omits-patch', 'no-exports', 'client-without-export',
|
|
524
566
|
'no-patch-file', 'no-insert-row', 'insert-wrong-indent', 'insert-missing-name', 'insert-name-mismatch',
|
|
525
567
|
'patch-name-unresolvable', 'no-name-export', 'no-apply-export', 'no-inject', 'require-in-esm',
|
|
526
|
-
'no-license', 'license-mismatch', 'version-taken', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
568
|
+
'no-license', 'license-mismatch', 'version-taken', 'registry-unreachable', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
527
569
|
'market-no-name', 'market-name-shape', 'market-no-category', 'market-bad-category', 'market-no-desc',
|
|
528
570
|
'market-no-en', 'market-no-zh', 'unscoped-name', 'no-git', 'no-readme', 'readme-no-install', 'readme-no-profile',
|
|
529
571
|
'no-tests', 'no-test-files', 'no-test-script', 'no-engines', 'bad-repo-url', 'no-gitignore',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Helps DSH plugin AUTHORS publish their plugins: a one-command gate that turns every silent failure in the publish path into a deterministic check — package.json completeness, BOM, YAML/JSON parseability, junction and patch insert correctness, npm scope permissions, GitHub repo and token state, market entry format, LICENSE/README, test suite. 帮助插件作者一键发布:写完插件后跑一遍门禁,把发布链路上的每个静默坑变成确定性检查,然后 GitHub + npm + 市场一键三连。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"test": "node --test
|
|
49
|
+
"test": "node --test"
|
|
50
50
|
}
|
|
51
51
|
}
|