dsh-standards 0.0.3 → 1.0.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/40-checker/README.md +251 -0
- package/40-checker/bin/dsh-levels.mjs +15 -0
- package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
- package/40-checker/fixtures/bad/index.mjs +12 -0
- package/40-checker/fixtures/bad/package.json +15 -0
- package/40-checker/fixtures/bad-manifest/index.js +1 -0
- package/40-checker/fixtures/bad-manifest/package.json +5 -0
- package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle/package.json +15 -0
- package/40-checker/fixtures/bundle/server.mjs +7 -0
- package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-v0/package.json +21 -0
- package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
- package/40-checker/fixtures/client-missing/client.js +1 -0
- package/40-checker/fixtures/client-missing/index.js +1 -0
- package/40-checker/fixtures/client-missing/package.json +6 -0
- package/40-checker/fixtures/flow-config/index.js +9 -0
- package/40-checker/fixtures/flow-config/package.json +10 -0
- package/40-checker/fixtures/fs-write/index.mjs +7 -0
- package/40-checker/fixtures/fs-write/package.json +10 -0
- package/40-checker/fixtures/good/index.mjs +10 -0
- package/40-checker/fixtures/good/package.json +19 -0
- package/40-checker/fixtures/hang/index.mjs +5 -0
- package/40-checker/fixtures/hang/package.json +10 -0
- package/40-checker/fixtures/known-answer/expected.json +18 -0
- package/40-checker/fixtures/market-index-real.json +18 -0
- package/40-checker/fixtures/market-index.json +18 -0
- package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-patch/package.json +1 -0
- package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
- package/40-checker/fixtures/negative/root-missing/package.json +1 -0
- package/40-checker/fixtures/negative/workspace/package.json +1 -0
- package/40-checker/package.json +22 -0
- package/40-checker/samples/README.md +8 -0
- package/40-checker/samples/entry18.json +139 -0
- package/40-checker/samples/frame.json +485 -0
- package/40-checker/scripts/apply-overlay.mjs +59 -0
- package/40-checker/scripts/attestation-check.mjs +151 -0
- package/40-checker/scripts/bundle-smoke.mjs +126 -0
- package/40-checker/scripts/claim-check.mjs +145 -0
- package/40-checker/scripts/clause-map.mjs +94 -0
- package/40-checker/scripts/collect-fixtures.mjs +44 -0
- package/40-checker/scripts/dashboard.mjs +85 -0
- package/40-checker/scripts/dco-check.mjs +147 -0
- package/40-checker/scripts/dependabot-check.mjs +40 -0
- package/40-checker/scripts/doc-commands-check.mjs +92 -0
- package/40-checker/scripts/entry-scan.mjs +190 -0
- package/40-checker/scripts/entry-validate.mjs +291 -0
- package/40-checker/scripts/entry18-attribution.mjs +66 -0
- package/40-checker/scripts/entry18-ci.mjs +685 -0
- package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
- package/40-checker/scripts/entry18-web.mjs +307 -0
- package/40-checker/scripts/evidence-chain.mjs +100 -0
- package/40-checker/scripts/flow-index.mjs +82 -0
- package/40-checker/scripts/flow-sample-ci.mjs +529 -0
- package/40-checker/scripts/governance-check.mjs +210 -0
- package/40-checker/scripts/host-farm.mjs +165 -0
- package/40-checker/scripts/journey-config-observe.mjs +109 -0
- package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
- package/40-checker/scripts/journey-runtime.mjs +178 -0
- package/40-checker/scripts/journey.mjs +76 -0
- package/40-checker/scripts/label-drift.mjs +74 -0
- package/40-checker/scripts/label-sync.mjs +87 -0
- package/40-checker/scripts/label-verify.mjs +90 -0
- package/40-checker/scripts/landlock-exec.c +101 -0
- package/40-checker/scripts/level-audit.mjs +336 -0
- package/40-checker/scripts/m1-sidecar.mjs +126 -0
- package/40-checker/scripts/m2-runner.mjs +213 -0
- package/40-checker/scripts/round-robin.mjs +88 -0
- package/40-checker/scripts/sample-frame.mjs +72 -0
- package/40-checker/scripts/sample20-ci.mjs +25 -0
- package/40-checker/scripts/sample60-ci.mjs +25 -0
- package/40-checker/scripts/stale-check.mjs +59 -0
- package/40-checker/scripts/surveillance.mjs +88 -0
- package/40-checker/scripts/web-probe.mjs +504 -0
- package/40-checker/src/cli.mjs +300 -0
- package/40-checker/src/client-shape.mjs +37 -0
- package/40-checker/src/declarations.mjs +242 -0
- package/40-checker/src/index.mjs +8 -0
- package/40-checker/src/install-spec.mjs +67 -0
- package/40-checker/src/label.mjs +130 -0
- package/40-checker/src/manifest.mjs +381 -0
- package/40-checker/src/permissions.mjs +60 -0
- package/40-checker/src/report.mjs +204 -0
- package/40-checker/src/result.mjs +60 -0
- package/40-checker/src/runtime.mjs +299 -0
- package/40-checker/src/semver.mjs +194 -0
- package/40-checker/src/util.mjs +148 -0
- package/40-checker/test/attestation.test.mjs +59 -0
- package/40-checker/test/checks.test.mjs +172 -0
- package/40-checker/test/claim.test.mjs +28 -0
- package/40-checker/test/client-shape.test.mjs +35 -0
- package/40-checker/test/dco.test.mjs +51 -0
- package/40-checker/test/entry-validate.test.mjs +172 -0
- package/40-checker/test/governance.test.mjs +76 -0
- package/40-checker/test/label.test.mjs +97 -0
- package/40-checker/test/level-audit.test.mjs +99 -0
- package/40-checker/test/levels.test.mjs +31 -0
- package/40-checker/test/ops.test.mjs +117 -0
- package/40-checker/test/permissions.test.mjs +40 -0
- package/40-checker/test/semver.test.mjs +32 -0
- package/40-checker/test/verify.test.mjs +57 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +28 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-CC-BY +23 -0
- package/NOTICE +14 -0
- package/NOTICE.md +11 -0
- package/README.md +146 -3
- package/bin/dsh-levels.mjs +4 -0
- package/bin/dsh-standards.mjs +19 -0
- package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
- package/docs/DEVELOPER-JOURNEY.md +40 -0
- package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
- package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
- package/docs/NAMING-FALLBACK.md +11 -0
- package/docs/fork-policy.md +12 -0
- package/docs/full-test-plan-2026-11-02.md +207 -0
- package/docs/incident.md +37 -0
- package/docs/onboarding.md +27 -0
- package/docs/privacy.md +12 -0
- package/docs/release.md +16 -0
- package/docs/scope-security.md +62 -0
- package/docs/status-taxonomy.md +54 -0
- package/docs/sunset.md +44 -0
- package/docs/translating.md +12 -0
- package/package.json +58 -12
- package/spec/README.md +27 -0
- package/spec/boost/README.md +8 -0
- package/spec/clauses.json +253 -0
- package/spec/entry/README.md +9 -0
- package/spec/flow/README.md +8 -0
- package/spec/levels.json +371 -0
- package/spec/levels.schema.json +45 -0
- package/spec/reference-suite.json +35 -0
- package/spec/schemas/attestation.schema.json +284 -0
- package/spec/schemas/evidence.schema.json +331 -0
- package/spec/schemas/report.schema.json +64 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SemVer range matcher for the checker.
|
|
3
|
+
*
|
|
4
|
+
* It is intentionally small: the host evaluates plugin requirements with
|
|
5
|
+
* `semver.satisfies(runtime, range, { includePrerelease: true })`
|
|
6
|
+
* (deepseek-harness/packages/boot/app-boot/src/plugin-compatibility.ts:52).
|
|
7
|
+
* This module implements the subset that appears in package manifests:
|
|
8
|
+
* exact versions, `>= > <= <`, `^`, `~`, `*`/`x`, space-separated AND,
|
|
9
|
+
* and `||` alternatives. It does NOT implement hyphen ranges, `!=`,
|
|
10
|
+
* or npm's "prerelease only matches same tuple" exclusion; the host uses
|
|
11
|
+
* includePrerelease, so we include prereleases everywhere.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/
|
|
15
|
+
|
|
16
|
+
export function parseVersion(value) {
|
|
17
|
+
if (typeof value !== 'string') return null
|
|
18
|
+
const match = VERSION_RE.exec(value.trim())
|
|
19
|
+
if (!match) return null
|
|
20
|
+
return {
|
|
21
|
+
major: Number(match[1]),
|
|
22
|
+
minor: Number(match[2]),
|
|
23
|
+
patch: Number(match[3]),
|
|
24
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareIdentifier(left, right) {
|
|
29
|
+
const leftNumeric = /^\d+$/.test(left)
|
|
30
|
+
const rightNumeric = /^\d+$/.test(right)
|
|
31
|
+
if (leftNumeric && rightNumeric) return Number(left) - Number(right)
|
|
32
|
+
if (leftNumeric) return -1
|
|
33
|
+
if (rightNumeric) return 1
|
|
34
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compareVersions(left, right) {
|
|
38
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
39
|
+
if (left[key] !== right[key]) return left[key] - right[key]
|
|
40
|
+
}
|
|
41
|
+
const a = left.prerelease
|
|
42
|
+
const b = right.prerelease
|
|
43
|
+
if (a.length === 0 && b.length === 0) return 0
|
|
44
|
+
if (a.length === 0) return 1
|
|
45
|
+
if (b.length === 0) return -1
|
|
46
|
+
const length = Math.max(a.length, b.length)
|
|
47
|
+
for (let index = 0; index < length; index += 1) {
|
|
48
|
+
if (a[index] === undefined) return -1
|
|
49
|
+
if (b[index] === undefined) return 1
|
|
50
|
+
const result = compareIdentifier(a[index], b[index])
|
|
51
|
+
if (result !== 0) return result
|
|
52
|
+
}
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function upperForCaret(version) {
|
|
57
|
+
if (version.major > 0) return { ...version, major: version.major + 1, minor: 0, patch: 0, prerelease: [] }
|
|
58
|
+
if (version.minor > 0) return { ...version, minor: version.minor + 1, patch: 0, prerelease: [] }
|
|
59
|
+
return { ...version, patch: version.patch + 1, prerelease: [] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function upperForTilde(version) {
|
|
63
|
+
return { ...version, minor: version.minor + 1, patch: 0, prerelease: [] }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parsePartial(text) {
|
|
67
|
+
const match = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(text.trim())
|
|
68
|
+
if (!match) return null
|
|
69
|
+
return {
|
|
70
|
+
major: Number(match[1]),
|
|
71
|
+
minor: match[2] === undefined || /[xX*]/.test(match[2]) ? null : Number(match[2]),
|
|
72
|
+
patch: match[3] === undefined || /[xX*]/.test(match[3]) ? null : Number(match[3]),
|
|
73
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fullVersion(partial) {
|
|
78
|
+
return { major: partial.major, minor: partial.minor ?? 0, patch: partial.patch ?? 0, prerelease: partial.prerelease }
|
|
79
|
+
}
|
|
80
|
+
function nextMinor(partial) { return { major: partial.major, minor: (partial.minor ?? 0) + 1, patch: 0, prerelease: [] } }
|
|
81
|
+
function nextMajor(partial) { return { major: partial.major + 1, minor: 0, patch: 0, prerelease: [] } }
|
|
82
|
+
function fmt(version) {
|
|
83
|
+
const base = `${version.major}.${version.minor}.${version.patch}`
|
|
84
|
+
return version.prerelease && version.prerelease.length ? `${base}-${version.prerelease.join('.')}` : base
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function expandToken(raw) {
|
|
88
|
+
if (raw === '*' || raw === 'x' || raw === 'X') return '*'
|
|
89
|
+
const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(raw)
|
|
90
|
+
if (!match) return null
|
|
91
|
+
const op = match[1] ?? '='
|
|
92
|
+
const partial = {
|
|
93
|
+
major: Number(match[2]),
|
|
94
|
+
minor: match[3] === undefined || /[xX*]/.test(match[3]) ? null : Number(match[3]),
|
|
95
|
+
patch: match[4] === undefined || /[xX*]/.test(match[4]) ? null : Number(match[4]),
|
|
96
|
+
prerelease: match[5] ? match[5].split('.') : [],
|
|
97
|
+
}
|
|
98
|
+
const low = fullVersion(partial)
|
|
99
|
+
const noMinor = partial.minor === null
|
|
100
|
+
const noPatch = partial.patch === null
|
|
101
|
+
if (!noMinor && !noPatch) return `${op}${fmt(low)}`
|
|
102
|
+
switch (op) {
|
|
103
|
+
case '^':
|
|
104
|
+
case '~':
|
|
105
|
+
case '=':
|
|
106
|
+
return noMinor ? `>=${fmt(low)} <${fmt(nextMajor(partial))}` : `>=${fmt(low)} <${fmt(nextMinor(partial))}`
|
|
107
|
+
case '>=': return `>=${fmt(low)}`
|
|
108
|
+
case '>': return noMinor ? `>=${fmt(nextMajor(partial))}` : `>=${fmt(nextMinor(partial))}`
|
|
109
|
+
case '<': return `<${fmt(low)}`
|
|
110
|
+
case '<=': return noMinor ? `<${fmt(nextMajor(partial))}` : `<${fmt(nextMinor(partial))}`
|
|
111
|
+
default: return null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Hyphen ranges and x-ranges are normalized before tokenizing. */
|
|
116
|
+
function expandAlternative(alternative) {
|
|
117
|
+
const text = alternative.trim()
|
|
118
|
+
if (text === '' || text === '*' || text === 'x' || text === 'X') return '*'
|
|
119
|
+
const hyphen = /^(.+?)\s+-\s+(.+)$/.exec(text)
|
|
120
|
+
if (hyphen) {
|
|
121
|
+
const left = parsePartial(hyphen[1])
|
|
122
|
+
const right = parsePartial(hyphen[2])
|
|
123
|
+
if (!left || !right) return null
|
|
124
|
+
const low = `>=${fmt(fullVersion(left))}`
|
|
125
|
+
if (right.minor === null) return `${low} <${fmt(nextMajor(right))}`
|
|
126
|
+
if (right.patch === null) return `${low} <${fmt(nextMinor(right))}`
|
|
127
|
+
return `${low} <=${fmt(fullVersion(right))}`
|
|
128
|
+
}
|
|
129
|
+
const parts = []
|
|
130
|
+
for (const raw of text.split(/\s+/)) {
|
|
131
|
+
const expanded = expandToken(raw)
|
|
132
|
+
if (expanded === null) return null
|
|
133
|
+
parts.push(expanded)
|
|
134
|
+
}
|
|
135
|
+
return parts.join(' ')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function tokenizeAlternative(alternative) {
|
|
139
|
+
const expanded = expandAlternative(alternative)
|
|
140
|
+
if (expanded === null) return null
|
|
141
|
+
if (expanded === '*') return [{ op: '*', version: null }]
|
|
142
|
+
const tokens = []
|
|
143
|
+
for (const raw of expanded.split(/\s+/)) {
|
|
144
|
+
const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(raw)
|
|
145
|
+
if (!match) return null
|
|
146
|
+
const op = match[1] ?? '='
|
|
147
|
+
const version = parseVersion(match[2])
|
|
148
|
+
if (!version) return null
|
|
149
|
+
tokens.push({ op, version })
|
|
150
|
+
}
|
|
151
|
+
return tokens
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function satisfiesToken(version, token) {
|
|
155
|
+
if (token.op === '*') return true
|
|
156
|
+
const cmp = compareVersions(version, token.version)
|
|
157
|
+
switch (token.op) {
|
|
158
|
+
case '=': return cmp === 0
|
|
159
|
+
case '>': return cmp > 0
|
|
160
|
+
case '>=': return cmp >= 0
|
|
161
|
+
case '<': return cmp < 0
|
|
162
|
+
case '<=': return cmp <= 0
|
|
163
|
+
case '^': return cmp >= 0 && compareVersions(version, upperForCaret(token.version)) < 0
|
|
164
|
+
case '~': return cmp >= 0 && compareVersions(version, upperForTilde(token.version)) < 0
|
|
165
|
+
default: return false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {string} version exact version to test
|
|
171
|
+
* @param {string} range SemVer range
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
export function satisfies(version, range) {
|
|
175
|
+
const parsed = parseVersion(version)
|
|
176
|
+
if (!parsed || typeof range !== 'string') return false
|
|
177
|
+
if (range.trim() === '') return false
|
|
178
|
+
const alternatives = range.split('||')
|
|
179
|
+
for (const alternative of alternatives) {
|
|
180
|
+
const tokens = tokenizeAlternative(alternative)
|
|
181
|
+
if (!tokens || tokens.length === 0) continue
|
|
182
|
+
if (tokens.every(token => satisfiesToken(parsed, token))) return true
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @returns {boolean} whether `range` is a syntactically usable range for this checker. */
|
|
188
|
+
export function validRange(range) {
|
|
189
|
+
if (typeof range !== 'string' || range.trim() === '') return false
|
|
190
|
+
for (const alternative of range.split('||')) {
|
|
191
|
+
if (!tokenizeAlternative(alternative)) return false
|
|
192
|
+
}
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'])
|
|
5
|
+
|
|
6
|
+
export function exists(target) {
|
|
7
|
+
try { fs.accessSync(target); return true } catch { return false }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isFile(target) {
|
|
11
|
+
try { return fs.statSync(target).isFile() } catch { return false }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isDirectory(target) {
|
|
15
|
+
try { return fs.statSync(target).isDirectory() } catch { return false }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readText(target) {
|
|
19
|
+
return fs.readFileSync(target, 'utf8')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readJson(target) {
|
|
23
|
+
return JSON.parse(readText(target))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readJsonMaybe(target) {
|
|
27
|
+
try { return { value: readJson(target), error: null } } catch (error) {
|
|
28
|
+
return { value: null, error }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isObject(value) {
|
|
33
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function asArray(value) {
|
|
37
|
+
if (value === undefined || value === null) return []
|
|
38
|
+
return Array.isArray(value) ? value : [value]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function escapeRegExp(value) {
|
|
42
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function posixRelative(from, to) {
|
|
46
|
+
return path.relative(from, to).split(path.sep).join('/')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SKIP_DIRECTORIES = new Set([
|
|
50
|
+
'node_modules', '.git', '.hg', '.svn', 'test', 'tests', '__tests__', 'docs', 'doc',
|
|
51
|
+
'assets', 'images', 'screenshots', 'coverage', '.github', '.agents', '.claude',
|
|
52
|
+
'tools', 'scripts', 'examples', 'benchmarks', 'fixtures', 'demo', 'dev', 'deploy', 'quality', 'bench',
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const BUILD_FILE = /(?:^|\/)(?:build|esbuild|vite|rollup|webpack)\.config\.[cm]?[jt]s$|(?:^|\/)(?:build|esbuild)\.mjs$|(?:^|\/)(?:make|render|verify|check)-[^/]+\.mjs$/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collect source files without following symlinks. Deterministic order, bounded.
|
|
59
|
+
* @param {string} root
|
|
60
|
+
* @param {{maxFiles?: number, maxBytes?: number}} [options]
|
|
61
|
+
*/
|
|
62
|
+
export function listSourceFiles(root, options = {}) {
|
|
63
|
+
const maxFiles = options.maxFiles ?? 400
|
|
64
|
+
const maxBytes = options.maxBytes ?? 2_000_000
|
|
65
|
+
const files = []
|
|
66
|
+
let bytes = 0
|
|
67
|
+
const walk = (directory) => {
|
|
68
|
+
if (files.length >= maxFiles || bytes >= maxBytes) return
|
|
69
|
+
let entries
|
|
70
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch { return }
|
|
71
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (files.length >= maxFiles || bytes >= maxBytes) return
|
|
74
|
+
const absolute = path.join(directory, entry.name)
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
if (SKIP_DIRECTORIES.has(entry.name)) continue
|
|
77
|
+
if (entry.name.startsWith('.')) continue
|
|
78
|
+
walk(absolute)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (!entry.isFile()) continue
|
|
82
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue
|
|
83
|
+
const relative = path.relative(root, absolute).split(path.sep).join('/')
|
|
84
|
+
if (BUILD_FILE.test(relative)) continue
|
|
85
|
+
let size = 0
|
|
86
|
+
try { size = fs.statSync(absolute).size } catch { continue }
|
|
87
|
+
if (bytes + size > maxBytes) continue
|
|
88
|
+
bytes += size
|
|
89
|
+
files.push(absolute)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
walk(root)
|
|
93
|
+
return files
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Read all source files once, returning {file, relative, text} records. */
|
|
97
|
+
export function readSources(root, options) {
|
|
98
|
+
return listSourceFiles(root, options).map(file => ({
|
|
99
|
+
file,
|
|
100
|
+
relative: posixRelative(root, file),
|
|
101
|
+
text: readText(file),
|
|
102
|
+
}))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function regexHits(sources, pattern) {
|
|
106
|
+
const hits = []
|
|
107
|
+
for (const source of sources) {
|
|
108
|
+
const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)
|
|
109
|
+
let match
|
|
110
|
+
while ((match = re.exec(source.text))) {
|
|
111
|
+
const before = source.text.slice(0, match.index)
|
|
112
|
+
const line = before.split('\n').length
|
|
113
|
+
hits.push({ file: source.relative, line, text: match[0] })
|
|
114
|
+
if (match[0] === '') re.lastIndex += 1
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return hits
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Resolve an exports target for the root subpath. Returns a relative path or null. */
|
|
121
|
+
export function rootExportTarget(manifest) {
|
|
122
|
+
const exportsField = manifest.exports
|
|
123
|
+
if (exportsField === undefined) return null
|
|
124
|
+
const pick = (value) => {
|
|
125
|
+
if (typeof value === 'string') return value
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
for (const item of value) {
|
|
128
|
+
const picked = pick(item)
|
|
129
|
+
if (picked) return picked
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
if (!isObject(value)) return null
|
|
134
|
+
for (const key of ['import', 'default', 'node', 'require', 'module-sync', 'browser']) {
|
|
135
|
+
if (Object.hasOwn(value, key)) {
|
|
136
|
+
const picked = pick(value[key])
|
|
137
|
+
if (picked) return picked
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
if (typeof exportsField === 'string' || Array.isArray(exportsField)) return pick(exportsField)
|
|
143
|
+
if (!isObject(exportsField)) return null
|
|
144
|
+
if (Object.hasOwn(exportsField, '.')) return pick(exportsField['.'])
|
|
145
|
+
// A conditional object without subpath keys is the root export.
|
|
146
|
+
if (Object.hasOwn(exportsField, 'import') || Object.hasOwn(exportsField, 'default') || Object.hasOwn(exportsField, 'require')) return pick(exportsField)
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { validateAttestation } from '../scripts/attestation-check.mjs'
|
|
7
|
+
|
|
8
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const ROOT = path.resolve(HERE, '..', '..')
|
|
10
|
+
const base = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'publish/attestations/DSH-ATT-2026-001.json'), 'utf8'))
|
|
11
|
+
|
|
12
|
+
test('attestation: the checked-in example passes', () => {
|
|
13
|
+
const result = validateAttestation(base())
|
|
14
|
+
assert.deepEqual(result.errors, [])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('attestation: a level field is forbidden', () => {
|
|
18
|
+
const cert = base()
|
|
19
|
+
cert.level = 'entry'
|
|
20
|
+
const result = validateAttestation(cert)
|
|
21
|
+
assert.ok(result.errors.some((e) => /level field/.test(e)))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('attestation: selfAssessed cannot be verified', () => {
|
|
25
|
+
const cert = base()
|
|
26
|
+
cert.claimLevel = 'verified'
|
|
27
|
+
const result = validateAttestation(cert)
|
|
28
|
+
assert.ok(result.errors.some((e) => /cannot claim verified/.test(e)))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('attestation: an expired active attestation fails', () => {
|
|
32
|
+
const cert = base()
|
|
33
|
+
cert.status = 'active'
|
|
34
|
+
cert.issuedAt = '2024-01-01'
|
|
35
|
+
cert.expiresAt = '2025-01-01'
|
|
36
|
+
const result = validateAttestation(cert)
|
|
37
|
+
assert.ok(result.errors.some((e) => /cannot be active/.test(e)))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('attestation: excluded scope is required', () => {
|
|
41
|
+
const cert = base()
|
|
42
|
+
cert.scope.excluded = []
|
|
43
|
+
const result = validateAttestation(cert)
|
|
44
|
+
assert.ok(result.errors.some((e) => /scope\.excluded/.test(e)))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('attestation: evidence needs a URL or hash', () => {
|
|
48
|
+
const cert = base()
|
|
49
|
+
cert.evidence[0] = { id: 'A', result: 'PASS' }
|
|
50
|
+
const result = validateAttestation(cert)
|
|
51
|
+
assert.ok(result.errors.some((e) => /needs a url or sha256/.test(e)))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('attestation: disclaimer must deny official/certification', () => {
|
|
55
|
+
const cert = base()
|
|
56
|
+
cert.disclaimer = 'A great plugin, approved.'
|
|
57
|
+
const result = validateAttestation(cert)
|
|
58
|
+
assert.ok(result.errors.some((e) => /non-official/.test(e)))
|
|
59
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { runManifestChecks } from '../src/manifest.mjs'
|
|
6
|
+
import { checkCapabilities, checkCredentials, checkLabels } from '../src/declarations.mjs'
|
|
7
|
+
import { readSources } from '../src/util.mjs'
|
|
8
|
+
import { buildReport } from '../src/report.mjs'
|
|
9
|
+
import { parseInstallSpec } from '../src/install-spec.mjs'
|
|
10
|
+
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const good = path.join(here, '..', 'fixtures', 'good')
|
|
13
|
+
const bad = path.join(here, '..', 'fixtures', 'bad')
|
|
14
|
+
|
|
15
|
+
test('good fixture passes manifest and version checks', () => {
|
|
16
|
+
const result = runManifestChecks(good)
|
|
17
|
+
assert.equal(result.kind, 'plugin')
|
|
18
|
+
assert.equal(result.checks[0].id, 'DSC-01')
|
|
19
|
+
assert.equal(result.checks[0].result, 'PASS')
|
|
20
|
+
assert.equal(result.checks[1].result, 'PASS')
|
|
21
|
+
assert.equal(result.checks[2].result, 'NA')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('bad fixture fails version and warns licence', () => {
|
|
25
|
+
const result = runManifestChecks(bad)
|
|
26
|
+
assert.equal(result.checks[0].result, 'WARN')
|
|
27
|
+
assert.equal(result.checks[1].result, 'FAIL')
|
|
28
|
+
assert.match(result.checks[1].reason, /engines\.dsh|peer/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('good fixture declarations are consistent', () => {
|
|
32
|
+
const result = runManifestChecks(good)
|
|
33
|
+
const sources = readSources(good)
|
|
34
|
+
const caps = checkCapabilities(result.manifest, sources, result.patch)
|
|
35
|
+
const creds = checkCredentials(result.manifest, sources)
|
|
36
|
+
const labels = checkLabels(result.manifest, sources)
|
|
37
|
+
assert.equal(caps.result, 'PASS', caps.reason)
|
|
38
|
+
assert.equal(creds.result, 'PASS', creds.reason)
|
|
39
|
+
assert.equal(labels.result, 'PASS', labels.reason)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('bad fixture rejects official label and unknown capability', () => {
|
|
43
|
+
const result = runManifestChecks(bad)
|
|
44
|
+
const sources = readSources(bad)
|
|
45
|
+
const caps = checkCapabilities(result.manifest, sources, result.patch)
|
|
46
|
+
const labels = checkLabels(result.manifest, sources)
|
|
47
|
+
assert.equal(labels.result, 'FAIL')
|
|
48
|
+
assert.match(labels.reason, /official|forbidden/)
|
|
49
|
+
assert.equal(caps.result, 'WARN')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('parseInstallSpec pins git and rejects floating git', () => {
|
|
53
|
+
const pinned = parseInstallSpec('github:acme/dsh-greet#0123456789abcdef0123456789abcdef01234567')
|
|
54
|
+
assert.equal(pinned.kind, 'git')
|
|
55
|
+
assert.equal(pinned.pinned, true)
|
|
56
|
+
assert.throws(() => parseInstallSpec('github:acme/dsh-greet'), /pin/)
|
|
57
|
+
assert.equal(parseInstallSpec('@acme/dsh-greet@1.2.3').pinned, true)
|
|
58
|
+
assert.equal(parseInstallSpec('@acme/dsh-greet').pinned, false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('report marks missing mandatory checks as incomplete', () => {
|
|
62
|
+
const checks = [
|
|
63
|
+
{ id: 'DSC-01', clause: 'C-1', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
|
|
64
|
+
{ id: 'DSC-02', clause: 'C-2', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
|
|
65
|
+
]
|
|
66
|
+
const report = buildReport({
|
|
67
|
+
subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
68
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
69
|
+
checks,
|
|
70
|
+
scope: 'ds-c',
|
|
71
|
+
profile: 'package',
|
|
72
|
+
kind: 'plugin',
|
|
73
|
+
})
|
|
74
|
+
assert.equal(report.conclusion.status, 'incomplete')
|
|
75
|
+
assert.equal(report.conclusion.exitCode, 2)
|
|
76
|
+
assert.equal(report.conclusion.level, null)
|
|
77
|
+
assert.ok(report.coverage.notRun > 0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('report passes when all mandatory checks are full and clean', () => {
|
|
81
|
+
const checks = ['DSC-01', 'DSC-02', 'DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09', 'DSC-11', 'DSC-12', 'DSC-13']
|
|
82
|
+
.map(id => ({ id, clause: id.replace('DSC', 'C'), layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] }))
|
|
83
|
+
const report = buildReport({
|
|
84
|
+
subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
85
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
86
|
+
checks,
|
|
87
|
+
scope: 'ds-c',
|
|
88
|
+
profile: 'package',
|
|
89
|
+
kind: 'plugin',
|
|
90
|
+
})
|
|
91
|
+
assert.equal(report.conclusion.status, 'pass')
|
|
92
|
+
assert.equal(report.conclusion.exitCode, 0)
|
|
93
|
+
assert.equal(report.conclusion.level, 'DS-C (package)')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('static-only M1 report maps L1-L3 to NA and issues DS-C static', () => {
|
|
97
|
+
const checks = ['DSC-01', 'DSC-02', 'DSC-03', 'DSC-11', 'DSC-12', 'DSC-13']
|
|
98
|
+
.map(id => ({ id, clause: id.replace('DSC', 'C'), layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] }))
|
|
99
|
+
.concat(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'].map(id => ({ id, clause: id.replace('DSC', 'C'), layer: 'L1', result: 'ERROR', coverage: 'not-run', reason: 'no runtime', details: [], evidence: [] })))
|
|
100
|
+
const report = buildReport({
|
|
101
|
+
subject: { mode: 'listing', profile: 'listing', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
102
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
103
|
+
checks,
|
|
104
|
+
scope: 'ds-c',
|
|
105
|
+
profile: 'listing',
|
|
106
|
+
kind: 'plugin',
|
|
107
|
+
staticOnly: true,
|
|
108
|
+
})
|
|
109
|
+
assert.equal(report.conclusion.status, 'pass')
|
|
110
|
+
assert.equal(report.conclusion.level, 'DS-C static')
|
|
111
|
+
assert.equal(report.conclusion.staticOnly, true)
|
|
112
|
+
assert.equal(report.checks.find(item => item.id === 'DSC-04').result, 'NA')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
function entryChecks(overrides = {}) {
|
|
116
|
+
const make = (id, clause, result = 'PASS', details = []) => ({ id, clause, layer: 'L0', result, coverage: result === 'ERROR' ? 'not-run' : 'full', reason: '', details, evidence: [] })
|
|
117
|
+
return [
|
|
118
|
+
make('DSC-01', 'C-1'),
|
|
119
|
+
make('DSC-02', 'C-2', overrides.c2Result ?? 'PASS', overrides.c2Details ?? []),
|
|
120
|
+
make('DSC-11', 'C-11'),
|
|
121
|
+
make('DSC-12', 'C-12'),
|
|
122
|
+
make('DSC-13', 'C-13'),
|
|
123
|
+
make('DSC-14', 'C-14', overrides.runResult ?? 'PASS'),
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
test('entry: C-2.1 only is WARN, runtime level', () => {
|
|
128
|
+
const report = buildReport({
|
|
129
|
+
subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
130
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
131
|
+
checks: entryChecks({ c2Result: 'FAIL', c2Details: [{ item: 'C-2.1', result: 'FAIL', reason: 'no engines' }] }),
|
|
132
|
+
scope: 'entry', profile: 'package', kind: 'plugin',
|
|
133
|
+
})
|
|
134
|
+
assert.equal(report.conclusion.status, 'warn')
|
|
135
|
+
assert.equal(report.conclusion.level, 'DSH-ENTRY runtime (warnings)')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('entry: C-2.2 remains blocking', () => {
|
|
139
|
+
const report = buildReport({
|
|
140
|
+
subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
141
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
142
|
+
checks: entryChecks({ c2Result: 'FAIL', c2Details: [{ item: 'C-2.2', result: 'FAIL', reason: 'peer mismatch' }] }),
|
|
143
|
+
scope: 'entry', profile: 'package', kind: 'plugin',
|
|
144
|
+
})
|
|
145
|
+
assert.equal(report.conclusion.status, 'fail')
|
|
146
|
+
assert.ok(report.coverage.entryBlocking.failed.includes('C-2.2'))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('entry: no runtime is incomplete, not static pass', () => {
|
|
150
|
+
const report = buildReport({
|
|
151
|
+
subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
152
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
153
|
+
checks: entryChecks({ runResult: 'ERROR' }),
|
|
154
|
+
scope: 'entry', profile: 'package', kind: 'plugin',
|
|
155
|
+
})
|
|
156
|
+
assert.equal(report.conclusion.status, 'incomplete')
|
|
157
|
+
assert.equal(report.conclusion.exitCode, 2)
|
|
158
|
+
assert.equal(report.conclusion.level, null)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('entry: static claim recognizes without runtime', () => {
|
|
162
|
+
const checks = entryChecks().filter(c => c.id !== 'DSC-14')
|
|
163
|
+
const report = buildReport({
|
|
164
|
+
subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
|
|
165
|
+
anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
|
|
166
|
+
checks,
|
|
167
|
+
scope: 'entry', profile: 'package', kind: 'plugin', entryClaim: 'static',
|
|
168
|
+
})
|
|
169
|
+
assert.equal(report.conclusion.status, 'pass')
|
|
170
|
+
assert.equal(report.conclusion.level, 'DSH-ENTRY recognized')
|
|
171
|
+
assert.equal(report.conclusion.exitCode, 0)
|
|
172
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { scanText } from '../scripts/claim-check.mjs'
|
|
4
|
+
|
|
5
|
+
test('claim: an official claim fails', () => {
|
|
6
|
+
const r = scanText('DSH-ENTRY is an official certification.', 'x.md')
|
|
7
|
+
assert.equal(r.errors.length, 1)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('claim: a negated claim passes', () => {
|
|
11
|
+
const r = scanText('DSH-ENTRY is not an official certification.', 'x.md')
|
|
12
|
+
assert.deepEqual(r.errors, [])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('claim: Chinese negation passes', () => {
|
|
16
|
+
const r = scanText('DSH-ENTRY 不是官方认证,也不做安全审计。', 'x.md')
|
|
17
|
+
assert.deepEqual(r.errors, [])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('claim: a mark without a disclaimer warns', () => {
|
|
21
|
+
const r = scanText('DSH-FLOW manual', 'x.md')
|
|
22
|
+
assert.equal(r.warnings.length, 1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('claim: a mark with a disclaimer does not warn', () => {
|
|
26
|
+
const r = scanText('DSH-FLOW manual\n非官方 · 非认证 · 非安全审计', 'x.md')
|
|
27
|
+
assert.deepEqual(r.warnings, [])
|
|
28
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { runManifestChecks } from '../src/manifest.mjs'
|
|
7
|
+
|
|
8
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const ROOT = path.resolve(HERE, '..')
|
|
10
|
+
|
|
11
|
+
test('G4: no dsh.client is not-applicable, not failed', () => {
|
|
12
|
+
const { g4 } = runManifestChecks(path.join(ROOT, 'fixtures/good'))
|
|
13
|
+
assert.equal(g4.hasClient, false)
|
|
14
|
+
assert.equal(g4.ok, true)
|
|
15
|
+
assert.equal(g4.result, 'NA')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('G4: dsh.client without exports["./client"] fails statically', () => {
|
|
19
|
+
const { g4 } = runManifestChecks(path.join(ROOT, 'fixtures/client-missing'))
|
|
20
|
+
assert.equal(g4.hasClient, true)
|
|
21
|
+
assert.equal(g4.ok, false)
|
|
22
|
+
assert.equal(g4.result, 'FAIL')
|
|
23
|
+
assert.match(g4.reason, /exports\["\.\/client"\]/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('G4: a client package with the export passes statically', () => {
|
|
27
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/client-missing/package.json'), 'utf8'))
|
|
28
|
+
manifest.exports['./client'] = './client.js'
|
|
29
|
+
const dir = fs.mkdtempSync(path.join(ROOT, 'fixtures', 'client-ok-'))
|
|
30
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(manifest))
|
|
31
|
+
const { g4 } = runManifestChecks(dir)
|
|
32
|
+
assert.equal(g4.ok, true)
|
|
33
|
+
assert.equal(g4.result, 'PASS')
|
|
34
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
35
|
+
})
|