@bushizhanghan/dsh-publish-gate 0.3.3 → 0.4.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/README.md +15 -2
- package/lib/engine.js +183 -26
- package/lib/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
30
30
|
| 工具 | 作用 |
|
|
31
31
|
|---|---|
|
|
32
32
|
| `publish_gate` | 全量门禁:清单 + 产物 + 运行时 + 浏览器半边 + 网络,返回 `safeToPublish`(`skipPack` / `skipTools` / `skipClient` / `skipNetwork` 可逐层跳过) |
|
|
33
|
-
| `publish_gate_tools` | 只跑运行时层:在子进程里 `apply(ctx)`,看注册了哪些工具、有没有缺 `execute`、重名、`presentCall` 抛错、`card` 用了宿主不认识的值 | 传 `call: ["工具名"]` 可以**真的调一次**(空参数):注册永远不会执行 `execute`,所以「一调就抛 ReferenceError」这类 bug
|
|
33
|
+
| `publish_gate_tools` | 只跑运行时层:在子进程里 `apply(ctx)`,看注册了哪些工具、有没有缺 `execute`、重名、`presentCall` 抛错、`card` 用了宿主不认识的值 | 传 `call: ["工具名"]` 可以**真的调一次**(空参数):注册永远不会执行 `execute`,所以「一调就抛 ReferenceError」这类 bug 只有这一条能看见。默认不调——有些工具会开程序或写文件,你点名才算同意。超时单独算 `tool-call-slow`(warn),不跟「真的抛了」混在一起:`of_env` 这种要起 WSL 的工具本来就慢。 |
|
|
34
34
|
| `publish_gate_client` | 只跑浏览器半边:在子进程里假造 `window.__ModuleLoader__`,看 bundle 会不会加载、`apply` 会不会抛、到底往哪个服务注册了东西、`inject` 有没有漏 |
|
|
35
35
|
| `publish_gate_verify` | 发后校验:`npm pack --dry-run` 的 shasum 和 registry 上的 `dist.shasum` 逐字节比,比版本号猜准 |
|
|
36
36
|
| `publish_gate_patch` | 只查 `cordis.patch.yml`——「插件装了但工具没出现」的第一个怀疑对象 |
|
|
@@ -66,6 +66,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
66
66
|
| `require-in-esm` | ESM 里 `require()` → `ERR_AMBIGUOUS_MODULE_SYNTAX`,报错不带行号 |
|
|
67
67
|
| `no-license` | 缺 LICENSE |
|
|
68
68
|
| `version-taken` | 这个版本已经在 registry 上,再发是 403 |
|
|
69
|
+
| `version-behind-latest` | 本地版本比 registry 上的 `latest` **旧**。npm 会照收,但 `latest` 继续指向新版本——包上传了,按名字装的人永远拿不到,且没有任何报错 |
|
|
69
70
|
| `registry-unreachable` | registry 回了 500/429 —— 版本检查**没跑**,所以别把它当干净 |
|
|
70
71
|
|
|
71
72
|
### 值得在发布前知道的(warn / info)
|
|
@@ -137,6 +138,18 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
137
138
|
|
|
138
139
|
`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
140
|
|
|
141
|
+
### 依赖真实性:`lib/` 里 import 的东西,消费者那边真的会装上吗
|
|
142
|
+
|
|
143
|
+
`npm install` 装一个包时只装它的 `dependencies`,**永远不装 `devDependencies`**。所以在你自己仓库里 `import 'x'` 能跑(依赖树是完整的),到了用户那边 `ERR_MODULE_NOT_FOUND`——插件装上了,然后加载失败,用户看到的是「这个插件坏了」。
|
|
144
|
+
|
|
145
|
+
| 规则 | 抓什么 |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `undeclared-import` | `lib/` 里 import 了某个包,`package.json` 三个依赖字段里都没有。它在你机器上能解析,只是因为别的包把它提升(hoist)到了 `node_modules` |
|
|
148
|
+
| `dep-in-dev-only` | 只在 `devDependencies` 里声明。这是最常见的一种:写代码时 `npm i -D` 装上了,测试也全绿 |
|
|
149
|
+
| `unused-dependency` | `dependencies` 里声明了,`lib/` 里从没 import。每个装你的人都白下一份(info,因为有些包是运行时动态加载的) |
|
|
150
|
+
|
|
151
|
+
内置模块(`node:fs`、`fs/promises`)、相对路径、以及浏览器半边从宿主 `require` 拿到的 `react` 都不算。工具描述里那句 `require("react")` 是散文,也不是依赖——这条规则最早就是被它自己误报出来的。
|
|
152
|
+
|
|
140
153
|
### 构建产物:`src/` 和 `lib/` 谁更新
|
|
141
154
|
|
|
142
155
|
有 `src/` 的插件(TypeScript 源码 + 构建出来的 `lib/`)最容易在这件事上翻车,而且两种翻车的表现一模一样——**发出去的字节没人跑过**:
|
|
@@ -183,7 +196,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
183
196
|
## 开发
|
|
184
197
|
|
|
185
198
|
```sh
|
|
186
|
-
node --test #
|
|
199
|
+
node --test # 75 个引擎单测
|
|
187
200
|
node test/integration.test.mjs # 26 条集成断言(9 个真实插件作 fixture)
|
|
188
201
|
```
|
|
189
202
|
|
package/lib/engine.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// you a publish, so they are all checkable before you spend it.
|
|
9
9
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
10
10
|
import { join, relative } from 'node:path'
|
|
11
|
+
import { builtinModules } from 'node:module'
|
|
11
12
|
import { execFile } from 'node:child_process'
|
|
12
13
|
import { promisify } from 'node:util'
|
|
13
14
|
|
|
@@ -555,30 +556,36 @@ export function checkNpmState(dir) {
|
|
|
555
556
|
return out
|
|
556
557
|
}
|
|
557
558
|
|
|
558
|
-
/**
|
|
559
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Version-collision check: the same version already on the registry.
|
|
561
|
+
* `doc` injects a registry document so the rule is testable without the network.
|
|
562
|
+
*/
|
|
563
|
+
export async function checkVersionTaken(name, version, { registry = 'https://registry.npmjs.org', timeoutMs = 8000, doc } = {}) {
|
|
560
564
|
const out = []
|
|
561
565
|
try {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
566
|
+
let json = doc ?? null
|
|
567
|
+
if (!json) {
|
|
568
|
+
const res = await fetch(`${registry.replace(/\/$/, '')}/${name.replace('/', '%2f')}`, {
|
|
569
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
570
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
571
|
+
})
|
|
572
|
+
if (res.status === 404) return out // never published: fine
|
|
573
|
+
if (!res.ok) {
|
|
574
|
+
// A 500 or a rate limit is not proof the version is free. Saying nothing
|
|
575
|
+
// here made the gate report "clean" for a check that never ran.
|
|
576
|
+
out.push(
|
|
577
|
+
F(
|
|
578
|
+
'registry-unreachable',
|
|
579
|
+
'warn',
|
|
580
|
+
`could not read ${name} from the registry (HTTP ${res.status})`,
|
|
581
|
+
'The version-taken check did not run, so this is NOT a clean version check. npm itself will still refuse a duplicate on publish.',
|
|
582
|
+
'Re-run the gate later, or check https://www.npmjs.com/package/' + name + '?activeTab=versions by hand.',
|
|
583
|
+
),
|
|
584
|
+
)
|
|
585
|
+
return out
|
|
586
|
+
}
|
|
587
|
+
json = await res.json()
|
|
580
588
|
}
|
|
581
|
-
const json = await res.json()
|
|
582
589
|
if (json?.versions?.[version]) {
|
|
583
590
|
out.push(
|
|
584
591
|
F(
|
|
@@ -590,6 +597,23 @@ export async function checkVersionTaken(name, version, { registry = 'https://reg
|
|
|
590
597
|
),
|
|
591
598
|
)
|
|
592
599
|
}
|
|
600
|
+
|
|
601
|
+
// npm accepts a publish of an OLDER version without a word. It lands in
|
|
602
|
+
// `versions`, but `dist-tags.latest` keeps pointing at the newer one, so
|
|
603
|
+
// anyone installing by name never receives it. The tarball is uploaded and
|
|
604
|
+
// invisible — which reads exactly like "my update did not reach anyone".
|
|
605
|
+
const latest = json?.['dist-tags']?.latest
|
|
606
|
+
if (latest && compareVersions(version, latest) < 0) {
|
|
607
|
+
out.push(
|
|
608
|
+
F(
|
|
609
|
+
'version-behind-latest',
|
|
610
|
+
'error',
|
|
611
|
+
`${name}@${version} is older than the published latest ${latest}`,
|
|
612
|
+
'npm will accept this publish and leave `latest` on the newer version, so install-by-name keeps resolving to the old code. Nothing errors; the release simply never reaches anyone.',
|
|
613
|
+
`Publish ${bumpPatch(latest)} or higher instead.`,
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
}
|
|
593
617
|
} catch {}
|
|
594
618
|
return out
|
|
595
619
|
}
|
|
@@ -1079,17 +1103,21 @@ try {
|
|
|
1079
1103
|
// bug the author cannot see any other way - registration never runs execute.
|
|
1080
1104
|
stage = 'call'
|
|
1081
1105
|
const wanted = new Set(JSON.parse(process.env.PG_CALLS || '[]'))
|
|
1106
|
+
const budget = Number(process.env.PG_CALL_MS || 8000)
|
|
1082
1107
|
const calls = []
|
|
1083
1108
|
for (const entry of registered) {
|
|
1084
1109
|
if (!wanted.has(entry.name) || !entry.run) continue
|
|
1085
1110
|
try {
|
|
1086
1111
|
await Promise.race([
|
|
1087
1112
|
Promise.resolve(entry.run({}, { signal: undefined })),
|
|
1088
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('timed out
|
|
1113
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timed out')), budget)),
|
|
1089
1114
|
])
|
|
1090
1115
|
calls.push({ name: entry.name, ok: true })
|
|
1091
1116
|
} catch (error) {
|
|
1092
|
-
|
|
1117
|
+
const message = String((error && error.message) || error)
|
|
1118
|
+
// A slow tool is not a broken tool: of_env shells out to WSL and takes
|
|
1119
|
+
// seconds by design. Only a real throw is a defect.
|
|
1120
|
+
calls.push({ name: entry.name, ok: false, timedOut: message === 'timed out', error: message })
|
|
1093
1121
|
}
|
|
1094
1122
|
}
|
|
1095
1123
|
emit({ ok: true, tools: registered.map(({ run, ...rest }) => rest), badParamTypes, calls })
|
|
@@ -1107,7 +1135,11 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1107
1135
|
timeout: opts.timeoutMs ?? 20_000,
|
|
1108
1136
|
maxBuffer: 4 * 1024 * 1024,
|
|
1109
1137
|
windowsHide: true,
|
|
1110
|
-
env: {
|
|
1138
|
+
env: {
|
|
1139
|
+
...process.env,
|
|
1140
|
+
PG_CALLS: JSON.stringify(Array.isArray(opts.calls) ? opts.calls : []),
|
|
1141
|
+
PG_CALL_MS: String(opts.callTimeoutMs ?? 8000),
|
|
1142
|
+
},
|
|
1111
1143
|
})
|
|
1112
1144
|
const line = String(stdout).split('\n').filter((l) => l.startsWith('@@PG@@')).pop()
|
|
1113
1145
|
if (!line) throw new Error('the child printed no result')
|
|
@@ -1154,6 +1186,18 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1154
1186
|
const seen = new Set()
|
|
1155
1187
|
for (const c of Array.isArray(payload.calls) ? payload.calls : []) {
|
|
1156
1188
|
if (c.ok) continue
|
|
1189
|
+
if (c.timedOut) {
|
|
1190
|
+
out.push(
|
|
1191
|
+
F(
|
|
1192
|
+
'tool-call-slow',
|
|
1193
|
+
'warn',
|
|
1194
|
+
`tool "${c.name}" did not answer within the call budget`,
|
|
1195
|
+
'A slow tool is not a broken one — this may shell out or wait on a service. Reported so a hang is not mistaken for a pass, and NOT counted as a defect.',
|
|
1196
|
+
'Raise callTimeoutMs if the tool is legitimately slow, or check that it returns at all.',
|
|
1197
|
+
),
|
|
1198
|
+
)
|
|
1199
|
+
continue
|
|
1200
|
+
}
|
|
1157
1201
|
out.push(
|
|
1158
1202
|
F(
|
|
1159
1203
|
'tool-call-throws',
|
|
@@ -1642,6 +1686,117 @@ export async function runArtifactChecks(dir, opts = {}) {
|
|
|
1642
1686
|
}
|
|
1643
1687
|
}
|
|
1644
1688
|
|
|
1689
|
+
// =====================================================================
|
|
1690
|
+
// Dependency truth
|
|
1691
|
+
// =====================================================================
|
|
1692
|
+
|
|
1693
|
+
// A package specifier never contains whitespace, so the capture is whitespace-
|
|
1694
|
+
// excluding on purpose: a greedy [^'"]+ matched across a whole template block
|
|
1695
|
+
// and reported the runner's own source as a dependency name.
|
|
1696
|
+
const SPECIFIER_RE =
|
|
1697
|
+
/\b(?:import|export)\s[^'"]*?from\s*['"]([^'"\s]+)['"]|\bimport\s*\(\s*['"]([^'"\s]+)['"]\s*\)|\bimport\s*['"]([^'"\s]+)['"]|\brequire\s*\(\s*['"]([^'"\s]+)['"]\s*\)/g
|
|
1698
|
+
|
|
1699
|
+
const BUILTINS = new Set(builtinModules)
|
|
1700
|
+
|
|
1701
|
+
// The client half runs inside the host's module loader, which resolves these
|
|
1702
|
+
// itself (CLIENT_RUNNER's fakeRequire mirrors it). They are provided, not
|
|
1703
|
+
// installed by the plugin, so importing them is not a missing dependency.
|
|
1704
|
+
const HOST_PROVIDED = new Set(['react', 'react-dom'])
|
|
1705
|
+
|
|
1706
|
+
/** '@scope/pkg/sub' -> '@scope/pkg', 'pkg/sub' -> 'pkg', relative/builtin -> null. */
|
|
1707
|
+
export const packageOf = (spec) => {
|
|
1708
|
+
if (!spec || spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('node:')) return null
|
|
1709
|
+
const parts = spec.split('/')
|
|
1710
|
+
const name = spec.startsWith('@') ? (parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null) : parts[0]
|
|
1711
|
+
return name && !BUILTINS.has(name) ? name : null
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// A tool description that says `require("react")` is prose, not an import — this
|
|
1715
|
+
// rule fired on this plugin's own description until it was dropped. Only quoted
|
|
1716
|
+
// strings that CONTAIN a call are removed, so a real specifier survives.
|
|
1717
|
+
const dropProse = (code) =>
|
|
1718
|
+
code
|
|
1719
|
+
.replace(/'(?:[^'\\\n]|\\.)*'/g, (s) => (/\b(?:require|import)\s*\(/.test(s) ? "''" : s))
|
|
1720
|
+
.replace(/"(?:[^"\\\n]|\\.)*"/g, (s) => (/\b(?:require|import)\s*\(/.test(s) ? '""' : s))
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Every bare specifier lib/ imports, checked against what package.json declares.
|
|
1724
|
+
*
|
|
1725
|
+
* A published package installs its own `dependencies` — devDependencies are never
|
|
1726
|
+
* fetched by whoever installs the tarball. So an import that only devDependencies
|
|
1727
|
+
* satisfies works in your repo (where everything is installed) and throws
|
|
1728
|
+
* ERR_MODULE_NOT_FOUND in theirs, after they have already installed you.
|
|
1729
|
+
*/
|
|
1730
|
+
export function checkDependencies(dir) {
|
|
1731
|
+
const out = []
|
|
1732
|
+
const json = readPkgJson(dir)
|
|
1733
|
+
if (!json) return out
|
|
1734
|
+
const deps = new Set(Object.keys(json.dependencies || {}))
|
|
1735
|
+
const dev = new Set(Object.keys(json.devDependencies || {}))
|
|
1736
|
+
const provided = new Set([
|
|
1737
|
+
...Object.keys(json.peerDependencies || {}),
|
|
1738
|
+
...Object.keys(json.optionalDependencies || {}),
|
|
1739
|
+
])
|
|
1740
|
+
|
|
1741
|
+
const used = new Map() // package name -> the first lib/ file that imports it
|
|
1742
|
+
const clientRel = clientTarget(json)
|
|
1743
|
+
const isClientFile = (file) =>
|
|
1744
|
+
Boolean(clientRel) && relative(dir, file).replace(/\\/g, '/') === String(clientRel).replace(/^\.\//, '')
|
|
1745
|
+
for (const file of walkFiles(join(dir, 'lib'), /\.(?:js|mjs|cjs)$/)) {
|
|
1746
|
+
const text = safeText(file)
|
|
1747
|
+
if (text === null) continue
|
|
1748
|
+
// Backtick strings go first: a runner template literal embeds code that
|
|
1749
|
+
// contains the word `import`, and a greedy specifier match used to swallow
|
|
1750
|
+
// a whole block of it. A real specifier is always a plain quoted string.
|
|
1751
|
+
const code = dropProse(text.split('\n').map(stripComment).join('\n').replace(/`(?:[^`\\]|\\.)*`/g, '``'))
|
|
1752
|
+
for (const m of code.matchAll(SPECIFIER_RE)) {
|
|
1753
|
+
const name = packageOf(m[1] ?? m[2] ?? m[3] ?? m[4])
|
|
1754
|
+
if (!name) continue
|
|
1755
|
+
if (isClientFile(file) && HOST_PROVIDED.has(name)) continue
|
|
1756
|
+
if (!used.has(name)) used.set(name, relative(dir, file))
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
for (const [name, file] of used) {
|
|
1761
|
+
if (deps.has(name) || provided.has(name)) continue
|
|
1762
|
+
if (dev.has(name)) {
|
|
1763
|
+
out.push(
|
|
1764
|
+
F(
|
|
1765
|
+
'dep-in-dev-only',
|
|
1766
|
+
'error',
|
|
1767
|
+
`${file} imports "${name}", which is only in devDependencies`,
|
|
1768
|
+
'npm installs a package\'s dependencies for the consumer, never its devDependencies. The import resolves on your machine and fails with ERR_MODULE_NOT_FOUND on theirs — the plugin installs fine and then does not load.',
|
|
1769
|
+
`Move "${name}" into dependencies.`,
|
|
1770
|
+
),
|
|
1771
|
+
)
|
|
1772
|
+
continue
|
|
1773
|
+
}
|
|
1774
|
+
out.push(
|
|
1775
|
+
F(
|
|
1776
|
+
'undeclared-import',
|
|
1777
|
+
'error',
|
|
1778
|
+
`${file} imports "${name}", which package.json does not declare`,
|
|
1779
|
+
'It resolves locally only because some other package happens to hoist it into node_modules. The consumer has no such package, so the import fails at load time.',
|
|
1780
|
+
`Add "${name}" to dependencies (or drop the import).`,
|
|
1781
|
+
),
|
|
1782
|
+
)
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
for (const name of deps) {
|
|
1786
|
+
if (used.has(name)) continue
|
|
1787
|
+
out.push(
|
|
1788
|
+
F(
|
|
1789
|
+
'unused-dependency',
|
|
1790
|
+
'info',
|
|
1791
|
+
`dependencies lists "${name}" but lib/ never imports it`,
|
|
1792
|
+
'Every consumer downloads and installs it for nothing. Sometimes that is deliberate (a peer you only load dynamically), which is why this is a note rather than a defect.',
|
|
1793
|
+
`Remove "${name}", or ignore this line if it is loaded at runtime only.`,
|
|
1794
|
+
),
|
|
1795
|
+
)
|
|
1796
|
+
}
|
|
1797
|
+
return out
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1645
1800
|
// =====================================================================
|
|
1646
1801
|
// Orchestration
|
|
1647
1802
|
// =====================================================================
|
|
@@ -1655,6 +1810,7 @@ export function runStaticChecks(dir) {
|
|
|
1655
1810
|
...checkLibHygiene(dir),
|
|
1656
1811
|
...checkNodeVersion(dir),
|
|
1657
1812
|
...checkBuildFreshness(dir),
|
|
1813
|
+
...checkDependencies(dir),
|
|
1658
1814
|
...checkDocs(dir),
|
|
1659
1815
|
...checkTests(dir),
|
|
1660
1816
|
...checkNpmState(dir),
|
|
@@ -1675,11 +1831,12 @@ const RULE_ORDER = [
|
|
|
1675
1831
|
'inject-missing-tools', 'inject-missing-web', 'inject-missing-webServer', 'inject-missing-timer',
|
|
1676
1832
|
'inject-unused-service',
|
|
1677
1833
|
'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
|
|
1678
|
-
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type',
|
|
1834
|
+
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-call-throws', 'tool-call-slow',
|
|
1679
1835
|
'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
|
|
1836
|
+
'undeclared-import', 'dep-in-dev-only', 'unused-dependency',
|
|
1680
1837
|
'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
|
|
1681
1838
|
'pack-unexpected-dir', 'pack-script-build', 'pack-unavailable',
|
|
1682
|
-
'no-license', 'license-mismatch', 'version-taken', 'registry-unreachable', 'publish-not-served',
|
|
1839
|
+
'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'registry-unreachable', 'publish-not-served',
|
|
1683
1840
|
'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
1684
1841
|
'market-no-name', 'market-name-shape', 'market-no-category', 'market-bad-category', 'market-no-desc', 'market-no-en', 'market-no-zh', 'unscoped-name', 'no-git', 'no-readme', 'readme-no-install', 'readme-no-profile',
|
|
1685
1842
|
'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new',
|
package/lib/index.js
CHANGED
|
@@ -91,7 +91,7 @@ export function apply(ctx, config = {}) {
|
|
|
91
91
|
{
|
|
92
92
|
name: 'publish_gate',
|
|
93
93
|
description:
|
|
94
|
-
'Run the full pre-publish gate over a DSH plugin directory.
|
|
94
|
+
'Run the full pre-publish gate over a DSH plugin directory. Four layers. (1) Manifest: missing exports["."], files omitting lib/, no "type": "module", UTF-8 BOM, JSON/YAML parse errors, cordis.patch.yml insert rows at the wrong indent (the bug that makes a plugin install and never mount), insert name mismatches, missing entry exports, an entry path that points at no file, a cordis.patch.yml the manifest never declares (inert), missing LICENSE or README install line, missing tests, unscoped npm names (the 403 you cannot diagnose). (2) Dependencies: every bare import in lib/ resolved against package.json — an import nothing declares, an import only devDependencies satisfy (npm never installs those for whoever installs you), an unused dependency. (3) Artifact: runs `npm pack --dry-run` and reads what npm will actually ship — lib/ missing from the tarball, a dsh.bundle.patch that never gets packed, oversize tarballs (test fixtures are the usual cause), dev directories shipping, and a prepack hook that means the dry run did not build. (4) Runtime: imports the entry in an isolated child process, calls apply(ctx) with a fake ctx, and reports a throwing import, a throwing apply, tools with no execute, duplicate tool names, a presentCall that throws, and card kinds the host does not render; then, if the package declares exports["./client"], loads the browser bundle against a fake window.__ModuleLoader__ and reports a bundle that never loads, never returns apply, throws, or registers nothing. Deterministic: every rule is a file, subprocess or API check, no LLM. Returns {ok, report, findings:[{rule,severity,title,detail,fix}], counts}.',
|
|
95
95
|
parameters: {
|
|
96
96
|
type: 'object',
|
|
97
97
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Pre-publish gate for DSH plugins, in five layers: manifest, dependencies, artifact, runtime and the browser half. 发布前的五层门禁:清单、依赖、产物、运行时、浏览器半边。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|