@bushizhanghan/dsh-publish-gate 0.19.0 → 0.21.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/CHANGELOG.md +24 -0
- package/README.md +12 -8
- package/lib/engine.js +224 -138
- package/lib/index.js +31 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
- The runtime layer no longer assumes the entry is `lib/index.js`. It resolves `exports["."]` →
|
|
6
|
+
`main` → `lib/index.js`. Measured against the thirty-one plugins installed in this profile, the
|
|
7
|
+
old guess produced **twenty false "entry throws when imported" errors**; two of those plugins ship
|
|
8
|
+
`index.mjs` with no `lib/` at all.
|
|
9
|
+
- `entry-missing` (error): the manifest names no file that exists, so there is nothing to import.
|
|
10
|
+
- `entry-import-unresolved` (warn): the entry imports a bare package that is not installed beside
|
|
11
|
+
it and is a host-provided peer (`@deepseek-ai/*` or a declared `peerDependencies` entry). That is
|
|
12
|
+
not a defect — but a bare import of something the host does *not* provide fails identically.
|
|
13
|
+
- `lib-console-log` reports **once per file with a count**, and never for the client bundle. One
|
|
14
|
+
bundled plugin carried 459 `console.log` calls and produced 459 warnings, which is how a gate gets
|
|
15
|
+
ignored; in a browser bundle `console.log` is the debug channel, not host-log residue.
|
|
16
|
+
- `unused-dependency` is skipped for packages that ship a client bundle: the bundler inlines those
|
|
17
|
+
dependencies, so the code is in the bundle and "lib/ never imports it" is true but not actionable.
|
|
18
|
+
|
|
19
|
+
## 0.20.0
|
|
20
|
+
|
|
21
|
+
- `publish_gate_commands` now agrees with the rules: a prerelease version gets `npm publish --tag next`
|
|
22
|
+
instead of a bare publish that would move `latest`.
|
|
23
|
+
- New step `tag the shipped commit` (`git tag v<version> && git push origin v<version>`). The gate has
|
|
24
|
+
warned about a missing tag since 0.5.0 and the command list never mentioned tagging — I wrote a
|
|
25
|
+
separate script to do it because the instructions never said to.
|
|
26
|
+
|
|
3
27
|
## 0.19.0
|
|
4
28
|
|
|
5
29
|
- `prerelease-published-as-latest` (warn): a `-beta` / `-rc` version with no `publishConfig.tag`.
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
45
45
|
| `publish_gate_market` | 校验市场条目 YAML(url/name/category/description 双语);传 `dir` 则改查只有线上才能回答的三件事:这个仓库**已经在目录里**(再开 PR 就是重复条目)、另一个条目也在发同一个 npm 包、仓库还**不满 24 小时**(提交门禁会失败并自己重跑) |
|
|
46
46
|
| `publish_gate_next_version` | 当前版本、是否已被 registry 占用、建议 bump 到多少(本地比 npm 旧时从 npm 的 latest 往上加,不会推荐一个已被占用的版本) |
|
|
47
47
|
| `publish_gate_doctor` | 扫整个目录,按 blocker 数排序,附带每个插件的真实包体积,看哪个还不能发 |
|
|
48
|
-
| `publish_gate_commands` | 按顺序列出发布命令,并标出这台机器已经走到哪一步。命令是**这台机器上真能跑的形式**:Windows 下 `npm publish` 走 `cmd /c`(PowerShell 的 `npm.ps1` 常被执行策略拦掉),`npm publish` 后面紧跟一条 `publish_gate_verify
|
|
48
|
+
| `publish_gate_commands` | 按顺序列出发布命令,并标出这台机器已经走到哪一步。命令是**这台机器上真能跑的形式**:Windows 下 `npm publish` 走 `cmd /c`(PowerShell 的 `npm.ps1` 常被执行策略拦掉),`npm publish` 后面紧跟一条 `publish_gate_verify`;版本是预发版时命令自带 `--tag next`;最后一步是给发出去的 commit 打 tag |
|
|
49
49
|
|
|
50
50
|
## 检查清单
|
|
51
51
|
|
|
@@ -65,7 +65,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
65
65
|
| `not-esm` | 没 `"type": "module"`,`import` 在加载时炸 |
|
|
66
66
|
| `no-insert-row` | patch 里只有 override,永远不挂载(页面报的仍然是这条——顶层 `- id:` 是 patch 不是挂载) |
|
|
67
67
|
| `bundle-patch-not-declared` | 有 `cordis.patch.yml` 但清单里没有 `dsh.bundle.patch`——patch 是死的,装了不挂载 |
|
|
68
|
-
| `entry-path-missing` | `main` / `exports["."]` / `exports["./client"]` 指的文件不存在,宿主解析到空 |
|
|
68
|
+
| `entry-path-missing` | `main` / `exports["."]` / `exports["./client"]` 指的文件不存在,宿主解析到空 |
|
|
69
|
+
| `entry-missing` | 清单里没有任何一个存在的入口文件——连 `import` 的机会都没有 |
|
|
69
70
|
| `inject-missing-*` | 用了 `ctx.tools` / `ctx.web` / `ctx.webServer` / `ctx.setInterval` 却没在 `inject` 里声明,cordis 永远不激活 |
|
|
70
71
|
| `patch-not-mount` | info:这些顶层 `- id:` 是在覆盖已有条目,合法,但**不是**挂载 |
|
|
71
72
|
| `insert-missing-name` / `insert-name-mismatch` | 挂载名缺失或与 package.json 不一致 |
|
|
@@ -103,11 +104,14 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
103
104
|
|
|
104
105
|
### 运行时层:`apply` 之后到底注册了什么
|
|
105
106
|
|
|
106
|
-
前两层都是读文件,读不出「装上能不能用」。这一层在独立子进程里 `import()` 入口、用假 ctx 调一次 `apply(ctx)`,把 `ctx.tools.register` 收到的每一条拿回来检查。20 秒超时,你的插件代码不在宿主进程里跑。
|
|
107
|
+
前两层都是读文件,读不出「装上能不能用」。这一层在独立子进程里 `import()` 入口、用假 ctx 调一次 `apply(ctx)`,把 `ctx.tools.register` 收到的每一条拿回来检查。20 秒超时,你的插件代码不在宿主进程里跑。
|
|
108
|
+
|
|
109
|
+
入口按 `exports["."]` → `main` → `lib/index.js` 的顺序解析。**早先写死 `lib/index.js`,拿三十一个真实已装插件一测,二十个报"入口抛异常"——其中两个的入口是 `index.mjs`,根本没有 `lib/`。**误报比漏报更贵:一个满屏假错误的门禁,第二次就没人跑了。
|
|
107
110
|
|
|
108
111
|
| 规则 | 抓什么 |
|
|
109
112
|
|---|---|
|
|
110
|
-
| `entry-import-failed` | `import()` 入口就抛——装上必定不加载 |
|
|
113
|
+
| `entry-import-failed` | `import()` 入口就抛——装上必定不加载 |
|
|
114
|
+
| `entry-import-unresolved` | 入口 import 了一个没装在旁边的包(`@deepseek-ai/*` 这类宿主提供的 peer)——**warn**,不是缺陷 |
|
|
111
115
|
| `apply-threw` | `apply(ctx)` 抛 |
|
|
112
116
|
| `tool-none-registered` | 入口能加载,但一个工具都没注册 |
|
|
113
117
|
| `tool-missing-execute` / `tool-missing-output-schema` | 注册项缺 `execute` / `output.schema`(宿主用 schema 校验返回值,缺了等于没校验) |
|
|
@@ -159,7 +163,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
159
163
|
|
|
160
164
|
### 代码卫生
|
|
161
165
|
|
|
162
|
-
`lib-debugger`(error)、`lib-console-log
|
|
166
|
+
`lib-debugger`(error)、`lib-console-log`(**每个文件一条,带次数**;浏览器半边豁免——那里 `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 那一刻抛——用户看到的是「插件坏了」,不是版本不匹配)。字符串和注释里的同名文字不算。
|
|
163
167
|
|
|
164
168
|
### 依赖真实性:`lib/` 里 import 的东西,消费者那边真的会装上吗
|
|
165
169
|
|
|
@@ -169,7 +173,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
169
173
|
|---|---|
|
|
170
174
|
| `undeclared-import` | `lib/` 里 import 了某个包,`package.json` 三个依赖字段里都没有。它在你机器上能解析,只是因为别的包把它提升(hoist)到了 `node_modules` |
|
|
171
175
|
| `dep-in-dev-only` | 只在 `devDependencies` 里声明。这是最常见的一种:写代码时 `npm i -D` 装上了,测试也全绿 |
|
|
172
|
-
| `unused-dependency` | `dependencies` 里声明了,`lib/` 里从没 import。每个装你的人都白下一份(info
|
|
176
|
+
| `unused-dependency` | `dependencies` 里声明了,`lib/` 里从没 import。每个装你的人都白下一份(info,因为有些包是运行时动态加载的)。带浏览器半边的包**不查这条**:打包器已经把依赖内联进 bundle,代码就在里面,不是没用到 |
|
|
173
177
|
|
|
174
178
|
内置模块(`node:fs`、`fs/promises`)、相对路径、以及浏览器半边从宿主 `require` 拿到的 `react` 都不算。工具描述里那句 `require("react")` 是散文,也不是依赖——这条规则最早就是被它自己误报出来的。
|
|
175
179
|
|
|
@@ -235,8 +239,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
235
239
|
## 开发
|
|
236
240
|
|
|
237
241
|
```sh
|
|
238
|
-
node --test #
|
|
239
|
-
node test/integration.test.mjs #
|
|
242
|
+
node --test # 125 个引擎单测
|
|
243
|
+
node test/integration.test.mjs # 28 条集成断言(9 个真实插件作 fixture)
|
|
240
244
|
```
|
|
241
245
|
|
|
242
246
|
## License
|
package/lib/engine.js
CHANGED
|
@@ -27,16 +27,16 @@ const safeText = (p) => {
|
|
|
27
27
|
return b === null ? null : b.toString('utf8')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export const F = (rule, severity, title, detail, fix) => ({ rule, severity, title, detail, fix })
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* A finding whose severity is not error/warn/info is invisible: the counters
|
|
34
|
-
* only count those three, so the rule silently never reports. Three of them
|
|
35
|
-
* shipped that way before this existed. The source scan in the test suite
|
|
36
|
-
* catches the literal forms; this catches whatever a future refactor builds.
|
|
37
|
-
*/
|
|
38
|
-
export const SEVERITIES = new Set(['error', 'warn', 'info'])
|
|
39
|
-
export const straySeverities = (findings) =>
|
|
30
|
+
export const F = (rule, severity, title, detail, fix) => ({ rule, severity, title, detail, fix })
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A finding whose severity is not error/warn/info is invisible: the counters
|
|
34
|
+
* only count those three, so the rule silently never reports. Three of them
|
|
35
|
+
* shipped that way before this existed. The source scan in the test suite
|
|
36
|
+
* catches the literal forms; this catches whatever a future refactor builds.
|
|
37
|
+
*/
|
|
38
|
+
export const SEVERITIES = new Set(['error', 'warn', 'info'])
|
|
39
|
+
export const straySeverities = (findings) =>
|
|
40
40
|
[...new Set((findings || []).filter((f) => !SEVERITIES.has(f?.severity)).map((f) => String(f?.rule || '<no rule>')))]
|
|
41
41
|
|
|
42
42
|
/** package.json without the BOM trap. Null when it is missing or unparseable. */
|
|
@@ -191,20 +191,20 @@ export function checkPackageJson(dir) {
|
|
|
191
191
|
out.push(F('invalid-version', 'error', `version "${json.version}" is not semver`, 'npm publish rejects it.', 'Use MAJOR.MINOR.PATCH.'))
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
// npm publishes to `latest` unless told otherwise, and a prerelease version
|
|
196
|
-
// does not change that. `npm i <name>` then resolves to a beta and every user
|
|
197
|
-
// is on an unfinished build; nothing warns, and the author sees a green publish.
|
|
198
|
-
if (json.version && /^\d+\.\d+\.\d+-/.test(json.version) && (!json.publishConfig?.tag || json.publishConfig.tag === 'latest')) {
|
|
199
|
-
out.push(
|
|
200
|
-
F(
|
|
201
|
-
'prerelease-published-as-latest',
|
|
202
|
-
'warn',
|
|
203
|
-
`version "${json.version}" is a prerelease but it will be published to \`latest\``,
|
|
204
|
-
'npm publishes to `latest` by default and a `-beta`/`-rc` suffix does not change that, so `npm i <name>` starts handing everyone an unfinished build.',
|
|
205
|
-
'Publish with `npm publish --tag next`, or set publishConfig.tag to next.',
|
|
206
|
-
),
|
|
207
|
-
)
|
|
194
|
+
|
|
195
|
+
// npm publishes to `latest` unless told otherwise, and a prerelease version
|
|
196
|
+
// does not change that. `npm i <name>` then resolves to a beta and every user
|
|
197
|
+
// is on an unfinished build; nothing warns, and the author sees a green publish.
|
|
198
|
+
if (json.version && /^\d+\.\d+\.\d+-/.test(json.version) && (!json.publishConfig?.tag || json.publishConfig.tag === 'latest')) {
|
|
199
|
+
out.push(
|
|
200
|
+
F(
|
|
201
|
+
'prerelease-published-as-latest',
|
|
202
|
+
'warn',
|
|
203
|
+
`version "${json.version}" is a prerelease but it will be published to \`latest\``,
|
|
204
|
+
'npm publishes to `latest` by default and a `-beta`/`-rc` suffix does not change that, so `npm i <name>` starts handing everyone an unfinished build.',
|
|
205
|
+
'Publish with `npm publish --tag next`, or set publishConfig.tag to next.',
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
208
|
}
|
|
209
209
|
// type: module — the whole plugin surface is ESM
|
|
210
210
|
if (json.type !== 'module') {
|
|
@@ -569,29 +569,29 @@ export function checkDocs(dir) {
|
|
|
569
569
|
)
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
|
-
// A README that names files by path rots the moment one is renamed: the
|
|
573
|
-
// reader copies the command, gets ENOENT, and nothing in the repo notices.
|
|
574
|
-
// Only repo-relative paths in the directories a plugin actually owns, so a
|
|
575
|
-
// tutorial example does not turn into a finding.
|
|
576
|
-
if (text) {
|
|
577
|
-
const named = new Set()
|
|
578
|
-
for (const m of text.matchAll(/(?:^|[\s`'"(])((?:test|lib|scripts|bin|src)\/[\w./@-]+\.(?:mjs|cjs|js|json|yml|yaml|md))/g)) {
|
|
579
|
-
if (!existsSync(join(dir, m[1]))) named.add(m[1])
|
|
580
|
-
}
|
|
581
|
-
if (named.size) {
|
|
582
|
-
const list = [...named]
|
|
583
|
-
out.push(
|
|
584
|
-
F(
|
|
585
|
-
'readme-path-missing',
|
|
586
|
-
'warn',
|
|
587
|
-
`README names ${list.length} path(s) that do not exist`,
|
|
588
|
-
`Whoever copies them gets ENOENT: ${list.slice(0, 4).join(", ")}.`,
|
|
589
|
-
'Rename them in the README, or restore the files.',
|
|
590
|
-
),
|
|
591
|
-
)
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
572
|
+
// A README that names files by path rots the moment one is renamed: the
|
|
573
|
+
// reader copies the command, gets ENOENT, and nothing in the repo notices.
|
|
574
|
+
// Only repo-relative paths in the directories a plugin actually owns, so a
|
|
575
|
+
// tutorial example does not turn into a finding.
|
|
576
|
+
if (text) {
|
|
577
|
+
const named = new Set()
|
|
578
|
+
for (const m of text.matchAll(/(?:^|[\s`'"(])((?:test|lib|scripts|bin|src)\/[\w./@-]+\.(?:mjs|cjs|js|json|yml|yaml|md))/g)) {
|
|
579
|
+
if (!existsSync(join(dir, m[1]))) named.add(m[1])
|
|
580
|
+
}
|
|
581
|
+
if (named.size) {
|
|
582
|
+
const list = [...named]
|
|
583
|
+
out.push(
|
|
584
|
+
F(
|
|
585
|
+
'readme-path-missing',
|
|
586
|
+
'warn',
|
|
587
|
+
`README names ${list.length} path(s) that do not exist`,
|
|
588
|
+
`Whoever copies them gets ENOENT: ${list.slice(0, 4).join(", ")}.`,
|
|
589
|
+
'Rename them in the README, or restore the files.',
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
595
|
if (!existsSync(join(dir, 'LICENSE'))) {
|
|
596
596
|
out.push(F('no-license', 'error', 'LICENSE is missing', 'npm warns on publish and the market listing shows an unknown license.', 'Add a LICENSE file (MIT is the ecosystem norm).'))
|
|
597
597
|
} else {
|
|
@@ -782,21 +782,21 @@ export async function checkVersionTaken(name, version, { registry = 'https://reg
|
|
|
782
782
|
)
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
|
|
786
|
-
// A publish with `--tag next` leaves `latest` where it was: the new version
|
|
787
|
-
// is on the registry, `npm i <name>` still gets the old one, and nothing
|
|
788
|
-
// fails. The release looks shipped and is not what anyone installs.
|
|
789
|
-
const latestTag = json?.['dist-tags']?.latest
|
|
790
|
-
if (json?.versions?.[version] && latestTag && latestTag !== version) {
|
|
791
|
-
out.push(
|
|
792
|
-
F(
|
|
793
|
-
'publish-tag-not-latest',
|
|
794
|
-
'warn',
|
|
795
|
-
`${name}@${version} is published but \`latest\` still points at ${latestTag}`,
|
|
796
|
-
'Installing by name keeps getting the old version. This is what `--tag next` (or publishConfig.tag) does when you meant to release.',
|
|
797
|
-
`Move it: npm dist-tag add ${name}@${version} latest`,
|
|
798
|
-
),
|
|
799
|
-
)
|
|
785
|
+
|
|
786
|
+
// A publish with `--tag next` leaves `latest` where it was: the new version
|
|
787
|
+
// is on the registry, `npm i <name>` still gets the old one, and nothing
|
|
788
|
+
// fails. The release looks shipped and is not what anyone installs.
|
|
789
|
+
const latestTag = json?.['dist-tags']?.latest
|
|
790
|
+
if (json?.versions?.[version] && latestTag && latestTag !== version) {
|
|
791
|
+
out.push(
|
|
792
|
+
F(
|
|
793
|
+
'publish-tag-not-latest',
|
|
794
|
+
'warn',
|
|
795
|
+
`${name}@${version} is published but \`latest\` still points at ${latestTag}`,
|
|
796
|
+
'Installing by name keeps getting the old version. This is what `--tag next` (or publishConfig.tag) does when you meant to release.',
|
|
797
|
+
`Move it: npm dist-tag add ${name}@${version} latest`,
|
|
798
|
+
),
|
|
799
|
+
)
|
|
800
800
|
}
|
|
801
801
|
// npm accepts a publish of an OLDER version without a word. It lands in
|
|
802
802
|
// `versions`, but `dist-tags.latest` keeps pointing at the newer one, so
|
|
@@ -920,7 +920,7 @@ export async function checkMarketReadiness(dir, opts = {}) {
|
|
|
920
920
|
return out
|
|
921
921
|
}
|
|
922
922
|
const json = readPkgJson(dir)
|
|
923
|
-
const pkgName = typeof json?.name === 'string' ? json.name : null
|
|
923
|
+
const pkgName = typeof json?.name === 'string' ? json.name : null
|
|
924
924
|
const pkgVersion = typeof json?.version === 'string' ? json.version : null
|
|
925
925
|
|
|
926
926
|
let entries = Array.isArray(opts.entries) ? opts.entries : null
|
|
@@ -1036,37 +1036,37 @@ export async function checkMarketReadiness(dir, opts = {}) {
|
|
|
1036
1036
|
),
|
|
1037
1037
|
)
|
|
1038
1038
|
}
|
|
1039
|
-
}
|
|
1040
|
-
// A catalogue entry can point at a GitHub release asset instead of npm. When
|
|
1041
|
-
// the newest release is older than this directory, the entry serves code the
|
|
1042
|
-
// description no longer matches — a reviewer blocked a submission for exactly
|
|
1043
|
-
// that (the entry said 1.2.7, the asset was still 1.2.6).
|
|
1044
|
-
let releaseInfo = opts.releaseInfo ?? null
|
|
1045
|
-
if (releaseInfo === null) {
|
|
1046
|
-
try {
|
|
1047
|
-
const res = await fetch(`https://api.github.com/repos/${slug}/releases/latest`, {
|
|
1048
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
1049
|
-
headers: { 'user-agent': 'dsh-publish-gate', accept: 'application/vnd.github+json' },
|
|
1050
|
-
})
|
|
1051
|
-
if (res.ok) releaseInfo = await res.json()
|
|
1052
|
-
} catch {
|
|
1053
|
-
// no network, no releases, or a rate limit — the rule stays quiet
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
const releaseTag = typeof releaseInfo?.tag_name === 'string' ? releaseInfo.tag_name.replace(/^v/, '') : null
|
|
1057
|
-
if (releaseTag && pkgVersion && releaseTag !== pkgVersion) {
|
|
1058
|
-
const older = compareVersions(releaseTag, pkgVersion) < 0
|
|
1059
|
-
out.push(
|
|
1060
|
-
F(
|
|
1061
|
-
'market-release-behind',
|
|
1062
|
-
'warn',
|
|
1063
|
-
`the newest GitHub release is ${releaseTag}, this directory says ${pkgVersion}`,
|
|
1064
|
-
older
|
|
1065
|
-
? 'An entry that installs releases/latest/download/<asset> serves the older code while the description is written for the newer version — a reviewer reads that as a false claim.'
|
|
1066
|
-
: `The release is ahead of package.json, so what the release ships is not what this directory builds.`,
|
|
1067
|
-
`Cut a ${pkgVersion} release (asset name unchanged) or point the entry at npm.`,
|
|
1068
|
-
),
|
|
1069
|
-
)
|
|
1039
|
+
}
|
|
1040
|
+
// A catalogue entry can point at a GitHub release asset instead of npm. When
|
|
1041
|
+
// the newest release is older than this directory, the entry serves code the
|
|
1042
|
+
// description no longer matches — a reviewer blocked a submission for exactly
|
|
1043
|
+
// that (the entry said 1.2.7, the asset was still 1.2.6).
|
|
1044
|
+
let releaseInfo = opts.releaseInfo ?? null
|
|
1045
|
+
if (releaseInfo === null) {
|
|
1046
|
+
try {
|
|
1047
|
+
const res = await fetch(`https://api.github.com/repos/${slug}/releases/latest`, {
|
|
1048
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1049
|
+
headers: { 'user-agent': 'dsh-publish-gate', accept: 'application/vnd.github+json' },
|
|
1050
|
+
})
|
|
1051
|
+
if (res.ok) releaseInfo = await res.json()
|
|
1052
|
+
} catch {
|
|
1053
|
+
// no network, no releases, or a rate limit — the rule stays quiet
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const releaseTag = typeof releaseInfo?.tag_name === 'string' ? releaseInfo.tag_name.replace(/^v/, '') : null
|
|
1057
|
+
if (releaseTag && pkgVersion && releaseTag !== pkgVersion) {
|
|
1058
|
+
const older = compareVersions(releaseTag, pkgVersion) < 0
|
|
1059
|
+
out.push(
|
|
1060
|
+
F(
|
|
1061
|
+
'market-release-behind',
|
|
1062
|
+
'warn',
|
|
1063
|
+
`the newest GitHub release is ${releaseTag}, this directory says ${pkgVersion}`,
|
|
1064
|
+
older
|
|
1065
|
+
? 'An entry that installs releases/latest/download/<asset> serves the older code while the description is written for the newer version — a reviewer reads that as a false claim.'
|
|
1066
|
+
: `The release is ahead of package.json, so what the release ships is not what this directory builds.`,
|
|
1067
|
+
`Cut a ${pkgVersion} release (asset name unchanged) or point the entry at npm.`,
|
|
1068
|
+
),
|
|
1069
|
+
)
|
|
1070
1070
|
}
|
|
1071
1071
|
return out
|
|
1072
1072
|
}
|
|
@@ -1266,22 +1266,22 @@ export async function checkPack(dir, opts = {}) {
|
|
|
1266
1266
|
'Add "lib" to files.',
|
|
1267
1267
|
),
|
|
1268
1268
|
)
|
|
1269
|
-
}
|
|
1270
|
-
// npm does not force-include a changelog the way it does README and LICENSE,
|
|
1271
|
-
// so a CHANGELOG.md that is not listed in `files` never reaches the registry
|
|
1272
|
-
// page — the file exists, the author updates it, and nobody installing the
|
|
1273
|
-
// package can see it. I hit this the first time I added one.
|
|
1274
|
-
const changelogOnDisk = ['CHANGELOG.md', 'CHANGELOG', 'HISTORY.md'].find((f) => existsSync(join(dir, f)))
|
|
1275
|
-
if (changelogOnDisk && !paths.includes(changelogOnDisk)) {
|
|
1276
|
-
out.push(
|
|
1277
|
-
F(
|
|
1278
|
-
'changelog-not-shipped',
|
|
1279
|
-
'warn',
|
|
1280
|
-
`${changelogOnDisk} is not in the tarball`,
|
|
1281
|
-
'npm force-includes README and LICENSE but not the changelog, so the file you keep updating is invisible on the package page.',
|
|
1282
|
-
`Add "${changelogOnDisk}" to files.`,
|
|
1283
|
-
),
|
|
1284
|
-
)
|
|
1269
|
+
}
|
|
1270
|
+
// npm does not force-include a changelog the way it does README and LICENSE,
|
|
1271
|
+
// so a CHANGELOG.md that is not listed in `files` never reaches the registry
|
|
1272
|
+
// page — the file exists, the author updates it, and nobody installing the
|
|
1273
|
+
// package can see it. I hit this the first time I added one.
|
|
1274
|
+
const changelogOnDisk = ['CHANGELOG.md', 'CHANGELOG', 'HISTORY.md'].find((f) => existsSync(join(dir, f)))
|
|
1275
|
+
if (changelogOnDisk && !paths.includes(changelogOnDisk)) {
|
|
1276
|
+
out.push(
|
|
1277
|
+
F(
|
|
1278
|
+
'changelog-not-shipped',
|
|
1279
|
+
'warn',
|
|
1280
|
+
`${changelogOnDisk} is not in the tarball`,
|
|
1281
|
+
'npm force-includes README and LICENSE but not the changelog, so the file you keep updating is invisible on the package page.',
|
|
1282
|
+
`Add "${changelogOnDisk}" to files.`,
|
|
1283
|
+
),
|
|
1284
|
+
)
|
|
1285
1285
|
}
|
|
1286
1286
|
if (json?.dsh?.bundle?.patch && !paths.includes(String(json.dsh.bundle.patch).replace(/^\.\//, ''))) {
|
|
1287
1287
|
out.push(
|
|
@@ -1401,6 +1401,10 @@ const RUNNER = `
|
|
|
1401
1401
|
import { pathToFileURL } from 'node:url'
|
|
1402
1402
|
import { join } from 'node:path'
|
|
1403
1403
|
const dir = process.argv[1]
|
|
1404
|
+
// The entry is whatever package.json says it is. Assuming lib/index.js
|
|
1405
|
+
// reported a hard load failure for every plugin that ships index.mjs or a
|
|
1406
|
+
// dsh/ folder instead - two of the thirty-one installed plugins.
|
|
1407
|
+
const entryPath = process.argv[2] || join(dir, 'lib', 'index.js')
|
|
1404
1408
|
const registered = []
|
|
1405
1409
|
// The host renders \`parameters\` into the prompt and never validates the value
|
|
1406
1410
|
// against it, so a typo like "strin" silently removes the type hint.
|
|
@@ -1528,7 +1532,7 @@ const validateValue = (schema, value, path) => {
|
|
|
1528
1532
|
const emit = (payload) => process.stdout.write('\\n@@PG@@' + JSON.stringify(payload) + '\\n')
|
|
1529
1533
|
let stage = 'import'
|
|
1530
1534
|
try {
|
|
1531
|
-
const mod = await import(pathToFileURL(
|
|
1535
|
+
const mod = await import(pathToFileURL(entryPath).href)
|
|
1532
1536
|
stage = 'apply'
|
|
1533
1537
|
if (typeof mod.apply !== 'function') throw new Error('lib/index.js does not export apply')
|
|
1534
1538
|
await mod.apply(ctx, {})
|
|
@@ -1594,16 +1598,51 @@ try {
|
|
|
1594
1598
|
}
|
|
1595
1599
|
emit({ ok: true, tools: registered.map(({ run, ...rest }) => rest), badParamTypes, calls, fuzzIssues })
|
|
1596
1600
|
} catch (error) {
|
|
1597
|
-
emit({ ok: false, stage, error: String((error && error.message) || error) })
|
|
1601
|
+
emit({ ok: false, stage, error: String((error && error.message) || error), code: String((error && error.code) || '') })
|
|
1598
1602
|
}
|
|
1599
1603
|
process.exit(0)
|
|
1600
1604
|
`
|
|
1601
1605
|
|
|
1606
|
+
export function entryFile(dir) {
|
|
1607
|
+
const json = readPkgJson(dir)
|
|
1608
|
+
if (!json) return null
|
|
1609
|
+
const pick = (value) => {
|
|
1610
|
+
if (typeof value === 'string') return value
|
|
1611
|
+
if (value && typeof value === 'object') {
|
|
1612
|
+
for (const key of ['import', 'node', 'default', 'require']) {
|
|
1613
|
+
const got = pick(value[key])
|
|
1614
|
+
if (got) return got
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return null
|
|
1618
|
+
}
|
|
1619
|
+
const candidates = [pick(json.exports?.['.']), pick(json.exports), pick(json.main), 'lib/index.js', 'index.js']
|
|
1620
|
+
for (const rel of candidates) {
|
|
1621
|
+
if (typeof rel !== 'string' || !rel || rel.includes('*')) continue
|
|
1622
|
+
const abs = join(dir, rel.replace(/^\.\//, ''))
|
|
1623
|
+
if (existsSync(abs)) return abs
|
|
1624
|
+
}
|
|
1625
|
+
return null
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1602
1628
|
export async function checkToolRegistrations(dir, opts = {}) {
|
|
1603
1629
|
const out = []
|
|
1630
|
+
const entry = entryFile(dir)
|
|
1631
|
+
if (!entry) {
|
|
1632
|
+
out.push(
|
|
1633
|
+
F(
|
|
1634
|
+
'entry-missing',
|
|
1635
|
+
'error',
|
|
1636
|
+
'no entry file could be resolved',
|
|
1637
|
+
'package.json names no file that exists, so the host cannot import anything.',
|
|
1638
|
+
'Point main or exports["."] at the file you ship.',
|
|
1639
|
+
),
|
|
1640
|
+
)
|
|
1641
|
+
return { findings: out, tools: null }
|
|
1642
|
+
}
|
|
1604
1643
|
let payload = null
|
|
1605
1644
|
try {
|
|
1606
|
-
const { stdout } = await runFile(process.execPath, ['--input-type=module', '-e', RUNNER, dir], {
|
|
1645
|
+
const { stdout } = await runFile(process.execPath, ['--input-type=module', '-e', RUNNER, dir, entry], {
|
|
1607
1646
|
timeout: opts.timeoutMs ?? 20_000,
|
|
1608
1647
|
maxBuffer: 4 * 1024 * 1024,
|
|
1609
1648
|
windowsHide: true,
|
|
@@ -1632,6 +1671,26 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1632
1671
|
}
|
|
1633
1672
|
|
|
1634
1673
|
if (!payload.ok) {
|
|
1674
|
+
// A bare specifier that is not installed beside the plugin may still be
|
|
1675
|
+
// provided by the host: every plugin importing @deepseek-ai/* peers failed
|
|
1676
|
+
// this way, and calling that a defect makes the whole runtime layer noise.
|
|
1677
|
+
const missing = /Cannot find package '([^']+)'/.exec(String(payload.error || ''))
|
|
1678
|
+
if (payload.stage === 'import' && payload.code === 'ERR_MODULE_NOT_FOUND' && missing) {
|
|
1679
|
+
const spec = missing[1]
|
|
1680
|
+
const peers = readPkgJson(dir)?.peerDependencies || {}
|
|
1681
|
+
if (spec.startsWith('@deepseek-ai/') || Object.prototype.hasOwnProperty.call(peers, spec)) {
|
|
1682
|
+
out.push(
|
|
1683
|
+
F(
|
|
1684
|
+
'entry-import-unresolved',
|
|
1685
|
+
'warn',
|
|
1686
|
+
`imports "${spec}", which is not installed next to the plugin`,
|
|
1687
|
+
'That package is a peer the host provides, so this is not a defect — but a bare import of something the host does not provide fails at load time with the same message.',
|
|
1688
|
+
'Confirm the host really supplies it, or declare it in dependencies.',
|
|
1689
|
+
),
|
|
1690
|
+
)
|
|
1691
|
+
return { findings: out, tools: null }
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1635
1694
|
out.push(
|
|
1636
1695
|
F(
|
|
1637
1696
|
payload.stage === 'import' ? 'entry-import-failed' : 'apply-threw',
|
|
@@ -1810,18 +1869,40 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1810
1869
|
|
|
1811
1870
|
export function checkLibHygiene(dir) {
|
|
1812
1871
|
const out = []
|
|
1872
|
+
const clientRel = String(clientTarget(readPkgJson(dir)) || '').replace(/^\.\//, '')
|
|
1813
1873
|
for (const file of walkFiles(join(dir, 'lib'), /\.(?:js|mjs|cjs)$/)) {
|
|
1814
1874
|
const text = safeText(file)
|
|
1815
1875
|
if (text === null) continue
|
|
1816
1876
|
const rel = relative(dir, file).replace(/\\/g, '/')
|
|
1817
1877
|
const lines = text.split('\n')
|
|
1878
|
+
let logCount = 0
|
|
1879
|
+
let logFirst = 0
|
|
1818
1880
|
for (let i = 0; i < lines.length; i++) {
|
|
1819
1881
|
const code = stripComment(lines[i])
|
|
1820
1882
|
const at = `${rel}:${i + 1}`
|
|
1821
|
-
|
|
1883
|
+
const logSeen = code.match(/\bconsole\.log\s*\(/g)
|
|
1884
|
+
if (logSeen) {
|
|
1885
|
+
if (!logFirst) logFirst = i + 1
|
|
1886
|
+
logCount += logSeen.length
|
|
1887
|
+
}
|
|
1822
1888
|
if (/(?:^|[;{}\s])debugger\s*;?\s*$/.test(code)) out.push(F('lib-debugger', 'error', `${at} has a debugger statement`, 'In a published package a debugger statement stops every consumer who reaches that path.', 'Remove it.'))
|
|
1823
1889
|
if (/(?:[A-Za-z]:[\\/]{1,2}Users[\\/]|\/Users\/)/.test(code)) out.push(F('lib-hardcoded-path', 'warn', `${at} contains an absolute path from this machine`, "It does not exist on the consumer's machine, so that code path silently fails there.", 'Resolve it from homedir() or ctx instead.'))
|
|
1824
1890
|
}
|
|
1891
|
+
// One line per file, not per call. A bundled client carried 459
|
|
1892
|
+
// console.log calls and reported 459 warnings - that is how a gate gets
|
|
1893
|
+
// ignored. The client bundle is exempt: there, console.log is the debug
|
|
1894
|
+
// channel, not residue in the host log.
|
|
1895
|
+
if (logCount && rel !== clientRel) {
|
|
1896
|
+
out.push(
|
|
1897
|
+
F(
|
|
1898
|
+
'lib-console-log',
|
|
1899
|
+
'warn',
|
|
1900
|
+
`${rel} calls console.log ${logCount} time(s), first at line ${logFirst}`,
|
|
1901
|
+
'Tool output is the channel; a stray console.log lands in the host log and never reaches the model. Harmless, but it is debug residue in a published file.',
|
|
1902
|
+
'Delete it, or use console.error if it is a real warning.',
|
|
1903
|
+
),
|
|
1904
|
+
)
|
|
1905
|
+
}
|
|
1825
1906
|
if (/new\s+URL\s*\([\s\S]{0,240}?\)\s*\.pathname/.test(text)) {
|
|
1826
1907
|
out.push(
|
|
1827
1908
|
F(
|
|
@@ -2403,6 +2484,11 @@ export function checkDependencies(dir) {
|
|
|
2403
2484
|
)
|
|
2404
2485
|
}
|
|
2405
2486
|
|
|
2487
|
+
// A package that ships a client bundle inlines its dependencies at build
|
|
2488
|
+
// time, so "lib/ never imports it" is true and not actionable: the code is
|
|
2489
|
+
// already inside the bundle. Six of the thirty-one installed plugins are
|
|
2490
|
+
// built this way and produced fifty-nine of these notes between them.
|
|
2491
|
+
if (clientRel) return out
|
|
2406
2492
|
for (const name of deps) {
|
|
2407
2493
|
if (used.has(name)) continue
|
|
2408
2494
|
out.push(
|
|
@@ -2510,29 +2596,29 @@ export async function checkTraceability(dir, opts = {}) {
|
|
|
2510
2596
|
return out
|
|
2511
2597
|
}
|
|
2512
2598
|
|
|
2513
|
-
// No answer from the remote. Two very different situations hide here: a repo
|
|
2514
|
-
// with no `origin` at all, where the local tag is the only truth and a missing
|
|
2515
|
-
// one is real; and a repo whose `origin` simply could not be reached, where
|
|
2516
|
-
// local HEAD is not the shipped commit (the API pusher builds its own commit)
|
|
2517
|
-
// and the tag usually exists. A check that cannot see the truth must not claim
|
|
2518
|
-
// a defect — this reported six false positives on a day GitHub was down.
|
|
2519
|
-
const remotes = opts.remotes === undefined ? await gitOut(dir, ['remote']) : opts.remotes
|
|
2520
|
-
const hasOrigin = typeof remotes === 'string' && remotes.split('\n').map((r) => r.trim()).includes('origin')
|
|
2521
|
-
if (hasOrigin) {
|
|
2522
|
-
if (!localHasIt) {
|
|
2523
|
-
out.push(
|
|
2524
|
-
F(
|
|
2525
|
-
'git-tag-unverified',
|
|
2526
|
-
'info',
|
|
2527
|
-
`could not check whether v${version} is tagged on the remote`,
|
|
2528
|
-
'origin could not be read, and with an API-pushed commit the local HEAD is not the commit that shipped, so the local tag list proves nothing either way.',
|
|
2529
|
-
'Re-run when the network is back — the tag may well exist.',
|
|
2530
|
-
),
|
|
2531
|
-
)
|
|
2532
|
-
}
|
|
2533
|
-
return out
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2599
|
+
// No answer from the remote. Two very different situations hide here: a repo
|
|
2600
|
+
// with no `origin` at all, where the local tag is the only truth and a missing
|
|
2601
|
+
// one is real; and a repo whose `origin` simply could not be reached, where
|
|
2602
|
+
// local HEAD is not the shipped commit (the API pusher builds its own commit)
|
|
2603
|
+
// and the tag usually exists. A check that cannot see the truth must not claim
|
|
2604
|
+
// a defect — this reported six false positives on a day GitHub was down.
|
|
2605
|
+
const remotes = opts.remotes === undefined ? await gitOut(dir, ['remote']) : opts.remotes
|
|
2606
|
+
const hasOrigin = typeof remotes === 'string' && remotes.split('\n').map((r) => r.trim()).includes('origin')
|
|
2607
|
+
if (hasOrigin) {
|
|
2608
|
+
if (!localHasIt) {
|
|
2609
|
+
out.push(
|
|
2610
|
+
F(
|
|
2611
|
+
'git-tag-unverified',
|
|
2612
|
+
'info',
|
|
2613
|
+
`could not check whether v${version} is tagged on the remote`,
|
|
2614
|
+
'origin could not be read, and with an API-pushed commit the local HEAD is not the commit that shipped, so the local tag list proves nothing either way.',
|
|
2615
|
+
'Re-run when the network is back — the tag may well exist.',
|
|
2616
|
+
),
|
|
2617
|
+
)
|
|
2618
|
+
}
|
|
2619
|
+
return out
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2536
2622
|
if (typeof atHead === 'string' && !localHasIt) {
|
|
2537
2623
|
const exists = opts.tagExists === undefined ? await gitOut(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/v${version}`]) : opts.tagExists ? 'sha' : null
|
|
2538
2624
|
out.push(
|
|
@@ -2603,7 +2689,7 @@ const RULE_ORDER = [
|
|
|
2603
2689
|
'client-inject-missing-slots', 'client-inject-missing-sidebarRightTabs', 'client-unavailable',
|
|
2604
2690
|
'no-patch-file', 'no-insert-row', 'insert-missing-name', 'insert-name-mismatch',
|
|
2605
2691
|
'patch-name-unresolvable', 'tests-fail', 'tests-timeout', 'tests-not-run', 'no-name-export', 'no-apply-export', 'no-inject', 'require-in-esm',
|
|
2606
|
-
'entry-import-failed', 'apply-threw', 'tools-unavailable',
|
|
2692
|
+
'entry-missing', 'entry-import-failed', 'entry-import-unresolved', 'apply-threw', 'tools-unavailable',
|
|
2607
2693
|
'inject-missing-tools', 'inject-missing-web', 'inject-missing-webServer', 'inject-missing-timer',
|
|
2608
2694
|
'inject-unused-service',
|
|
2609
2695
|
'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
|
package/lib/index.js
CHANGED
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
compareVersions,
|
|
29
29
|
latestPublished,
|
|
30
30
|
renderReport,
|
|
31
|
-
runArtifactChecks,
|
|
32
|
-
checkTestRun,
|
|
31
|
+
runArtifactChecks,
|
|
32
|
+
checkTestRun,
|
|
33
33
|
straySeverities,
|
|
34
34
|
runStaticChecks,
|
|
35
35
|
} from './engine.js'
|
|
@@ -104,7 +104,7 @@ export function apply(ctx, config = {}) {
|
|
|
104
104
|
skipNetwork: { type: 'boolean', description: 'Skip the npm registry and GitHub checks (default false).' },
|
|
105
105
|
skipPack: { type: 'boolean', description: 'Skip `npm pack --dry-run` (default false). Faster, but says nothing about the real tarball.' },
|
|
106
106
|
skipTools: { type: 'boolean', description: 'Skip the isolated import of the plugin entry (default false).' },
|
|
107
|
-
skipClient: { type: 'boolean', description: 'Skip the isolated load of the browser bundle (default false).' },
|
|
107
|
+
skipClient: { type: 'boolean', description: 'Skip the isolated load of the browser bundle (default false).' },
|
|
108
108
|
skipTests: { type: 'boolean', description: 'Skip running the test suite (default false). The suite is the only check that reads the code rather than the paperwork.' },
|
|
109
109
|
},
|
|
110
110
|
},
|
|
@@ -114,21 +114,21 @@ export function apply(ctx, config = {}) {
|
|
|
114
114
|
const findings = runStaticChecks(dir)
|
|
115
115
|
const artifact = await runArtifactChecks(dir, { skipPack: args.skipPack, skipTools: args.skipTools, skipClient: args.skipClient })
|
|
116
116
|
findings.push(...artifact.findings)
|
|
117
|
-
const tests = args.skipTests ? null : await checkTestRun(dir, { timeoutMs: cfg.testTimeoutMs })
|
|
118
|
-
if (tests) findings.push(...tests)
|
|
117
|
+
const tests = args.skipTests ? null : await checkTestRun(dir, { timeoutMs: cfg.testTimeoutMs })
|
|
118
|
+
if (tests) findings.push(...tests)
|
|
119
119
|
if (!args.skipNetwork) findings.push(...(await networkGate(dir)))
|
|
120
|
-
// A finding with an unexpected severity is invisible to the counters
|
|
121
|
-
// below, so the rule silently never reports. Make that loud instead.
|
|
122
|
-
const stray = straySeverities(findings)
|
|
123
|
-
if (stray.length > 0) {
|
|
124
|
-
findings.push({
|
|
125
|
-
rule: 'internal-bad-severity',
|
|
126
|
-
severity: 'error',
|
|
127
|
-
title: `these rules built a finding with an unknown severity: ${stray.join(", ")}`,
|
|
128
|
-
detail: 'The gate counted them as nothing, so they never appeared in a report. This is a bug in publish-gate, not in the plugin being gated.',
|
|
129
|
-
fix: 'Report it: the severity slot of F(...) took something other than error/warn/info.',
|
|
130
|
-
})
|
|
131
|
-
}
|
|
120
|
+
// A finding with an unexpected severity is invisible to the counters
|
|
121
|
+
// below, so the rule silently never reports. Make that loud instead.
|
|
122
|
+
const stray = straySeverities(findings)
|
|
123
|
+
if (stray.length > 0) {
|
|
124
|
+
findings.push({
|
|
125
|
+
rule: 'internal-bad-severity',
|
|
126
|
+
severity: 'error',
|
|
127
|
+
title: `these rules built a finding with an unknown severity: ${stray.join(", ")}`,
|
|
128
|
+
detail: 'The gate counted them as nothing, so they never appeared in a report. This is a bug in publish-gate, not in the plugin being gated.',
|
|
129
|
+
fix: 'Report it: the severity slot of F(...) took something other than error/warn/info.',
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
132
|
const counts = {
|
|
133
133
|
error: findings.filter((f) => f.severity === 'error').length,
|
|
134
134
|
warn: findings.filter((f) => f.severity === 'warn').length,
|
|
@@ -412,6 +412,11 @@ export function apply(ctx, config = {}) {
|
|
|
412
412
|
const repo = json.repository?.url?.match(/github\.com[:/]([^/]+)\/([^/.]+)/)?.[2] || json.name.replace(/^@[^/]+\//, '')
|
|
413
413
|
const { existsSync } = await import('node:fs')
|
|
414
414
|
const hasGit = existsSync(join(dir, '.git'))
|
|
415
|
+
// npm publishes to `latest` whatever the version suffix says, so a
|
|
416
|
+
// prerelease needs an explicit tag or it goes out to everyone.
|
|
417
|
+
const preTag = /^\d+\.\d+\.\d+-/.test(json.version || '')
|
|
418
|
+
const tag = preTag ? (json.publishConfig?.tag && json.publishConfig.tag !== 'latest' ? json.publishConfig.tag : 'next') : null
|
|
419
|
+
const publishCmd = `npm publish --access public${tag ? ` --tag ${tag}` : ''}`
|
|
415
420
|
const steps = [
|
|
416
421
|
{
|
|
417
422
|
name: 'git init and commit',
|
|
@@ -433,7 +438,7 @@ export function apply(ctx, config = {}) {
|
|
|
433
438
|
},
|
|
434
439
|
{
|
|
435
440
|
name: 'npm publish',
|
|
436
|
-
command: process.platform === 'win32' ?
|
|
441
|
+
command: process.platform === 'win32' ? `cmd /c "${publishCmd}"` : publishCmd,
|
|
437
442
|
done: false,
|
|
438
443
|
note:
|
|
439
444
|
`a bare (unscoped) name can return 403 with a granular token — "${json.name}" is ${json.name.startsWith('@') ? 'scoped, fine' : 'UNSCOPED, rename it'}` +
|
|
@@ -447,7 +452,14 @@ export function apply(ctx, config = {}) {
|
|
|
447
452
|
command: `publish_gate_verify({ dir: "${dir.replace(/\\/g, '\\\\')}" })`,
|
|
448
453
|
done: false,
|
|
449
454
|
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',
|
|
450
|
-
},
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'tag the shipped commit',
|
|
458
|
+
command: `git tag v${json.version} && git push origin v${json.version}`,
|
|
459
|
+
done: false,
|
|
460
|
+
note: 'a GitHub release, a changelog entry and the market listing all point at a commit — none of them can point at one that was never tagged',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
451
463
|
name: 'market entry',
|
|
452
464
|
command: `write data/plugins/${owner}__${repo}.yml with url / name / category / description{en,zh}`,
|
|
453
465
|
done: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness — plus an input fuzzer that drives every declared parameter with the wrong type. 发布前的七层门禁 + 入参模糊测试。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|