@avantf/dsh-plugin-base 0.3.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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/artifact.d.ts +29 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +60 -0
- package/dist/artifact.js.map +1 -0
- package/dist/bootstrap.d.ts +54 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +334 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/compat.d.ts +413 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +742 -0
- package/dist/compat.js.map +1 -0
- package/dist/conformance.d.ts +55 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +702 -0
- package/dist/conformance.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs.d.ts +18 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +152 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +5 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +33 -0
- package/dist/integrity.js.map +1 -0
- package/dist/interface.d.ts +262 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +136 -0
- package/dist/interface.js.map +1 -0
- package/dist/interface_gate.d.ts +61 -0
- package/dist/interface_gate.d.ts.map +1 -0
- package/dist/interface_gate.js +103 -0
- package/dist/interface_gate.js.map +1 -0
- package/dist/internal.d.ts +23 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +18 -0
- package/dist/internal.js.map +1 -0
- package/dist/kit/family.d.ts +52 -0
- package/dist/kit/family.d.ts.map +1 -0
- package/dist/kit/family.js +65 -0
- package/dist/kit/family.js.map +1 -0
- package/dist/kit/index.d.ts +26 -0
- package/dist/kit/index.d.ts.map +1 -0
- package/dist/kit/index.js +26 -0
- package/dist/kit/index.js.map +1 -0
- package/dist/kit/logger.d.ts +41 -0
- package/dist/kit/logger.d.ts.map +1 -0
- package/dist/kit/logger.js +42 -0
- package/dist/kit/logger.js.map +1 -0
- package/dist/kit/prompt_files.d.ts +90 -0
- package/dist/kit/prompt_files.d.ts.map +1 -0
- package/dist/kit/prompt_files.js +121 -0
- package/dist/kit/prompt_files.js.map +1 -0
- package/dist/kit/typert.d.ts +40 -0
- package/dist/kit/typert.d.ts.map +1 -0
- package/dist/kit/typert.js +17 -0
- package/dist/kit/typert.js.map +1 -0
- package/dist/layout.d.ts +28 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +84 -0
- package/dist/layout.js.map +1 -0
- package/dist/lint.d.ts +33 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +271 -0
- package/dist/lint.js.map +1 -0
- package/dist/lock.d.ts +12 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +93 -0
- package/dist/lock.js.map +1 -0
- package/dist/manifest.d.ts +12 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +75 -0
- package/dist/manifest.js.map +1 -0
- package/dist/net.d.ts +23 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +112 -0
- package/dist/net.js.map +1 -0
- package/dist/package-name.d.ts +3 -0
- package/dist/package-name.d.ts.map +1 -0
- package/dist/package-name.js +13 -0
- package/dist/package-name.js.map +1 -0
- package/dist/preset.d.ts +46 -0
- package/dist/preset.d.ts.map +1 -0
- package/dist/preset.js +230 -0
- package/dist/preset.js.map +1 -0
- package/dist/providers/archive.d.ts +32 -0
- package/dist/providers/archive.d.ts.map +1 -0
- package/dist/providers/archive.js +248 -0
- package/dist/providers/archive.js.map +1 -0
- package/dist/providers/model.d.ts +25 -0
- package/dist/providers/model.d.ts.map +1 -0
- package/dist/providers/model.js +550 -0
- package/dist/providers/model.js.map +1 -0
- package/dist/providers/npm.d.ts +21 -0
- package/dist/providers/npm.d.ts.map +1 -0
- package/dist/providers/npm.js +458 -0
- package/dist/providers/npm.js.map +1 -0
- package/dist/provisioner.d.ts +21 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +962 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/semver.d.ts +31 -0
- package/dist/semver.d.ts.map +1 -0
- package/dist/semver.js +244 -0
- package/dist/semver.js.map +1 -0
- package/dist/state.d.ts +71 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +225 -0
- package/dist/state.js.map +1 -0
- package/dist/tar.d.ts +10 -0
- package/dist/tar.d.ts.map +1 -0
- package/dist/tar.js +178 -0
- package/dist/tar.js.map +1 -0
- package/dist/types.d.ts +431 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/zip.d.ts +8 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +109 -0
- package/dist/zip.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider conformance kit run against one provider.
|
|
3
|
+
* @module conformance
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
10
|
+
import { findFrameworkImports } from './artifact.js';
|
|
11
|
+
import { ProvisionError } from './errors.js';
|
|
12
|
+
import { defaultFs } from './fs.js';
|
|
13
|
+
import { isInside, lockPath, tempRoot } from './layout.js';
|
|
14
|
+
import { INSTALL_MANIFEST_FILE } from './manifest.js';
|
|
15
|
+
import { BUILTIN_KINDS, isNamespacedKind } from './lint.js';
|
|
16
|
+
import { defaultLock } from './lock.js';
|
|
17
|
+
import { createProvisioner } from './provisioner.js';
|
|
18
|
+
const SILENT_LOGGER = {
|
|
19
|
+
debug: () => undefined,
|
|
20
|
+
info: () => undefined,
|
|
21
|
+
warn: () => undefined,
|
|
22
|
+
error: () => undefined,
|
|
23
|
+
};
|
|
24
|
+
const PLAN_ACTIONS = ['present', 'install', 'skip', 'unknown'];
|
|
25
|
+
const SOURCES = ['resolved', 'managed', 'installed', 'system', 'explicit'];
|
|
26
|
+
/** The allowed resource sources per kind. */
|
|
27
|
+
const SOURCES_BY_KIND = {
|
|
28
|
+
'npm-package': ['resolved', 'managed', 'installed'],
|
|
29
|
+
'binary-archive': ['explicit', 'managed', 'system', 'installed'],
|
|
30
|
+
};
|
|
31
|
+
/** The check ids in report order; a short-circuit can still report the tail as skipped. */
|
|
32
|
+
const CHECK_ORDER = [
|
|
33
|
+
'conformance/kinds',
|
|
34
|
+
'conformance/id-is-package-name',
|
|
35
|
+
'conformance/identify-shape',
|
|
36
|
+
'conformance/probe-pure',
|
|
37
|
+
'conformance/plan-pure',
|
|
38
|
+
'conformance/target-dir-safe',
|
|
39
|
+
'conformance/verify-rejects-missing',
|
|
40
|
+
'conformance/install-landing',
|
|
41
|
+
'conformance/install-idempotent',
|
|
42
|
+
'conformance/install-half-failure',
|
|
43
|
+
'conformance/core-rejects-bad-target-dir',
|
|
44
|
+
'conformance/core-quarantines-bad-copy',
|
|
45
|
+
'conformance/report-completeness',
|
|
46
|
+
'conformance/register-conflict',
|
|
47
|
+
'conformance/unknown-provider',
|
|
48
|
+
'conformance/provider-importable',
|
|
49
|
+
'conformance/provider-artifact-type-only',
|
|
50
|
+
'conformance/client-artifact-clean',
|
|
51
|
+
'conformance/recycle-semantics',
|
|
52
|
+
];
|
|
53
|
+
function recordingFs(inner) {
|
|
54
|
+
const mutations = [];
|
|
55
|
+
// Recording is on by default.
|
|
56
|
+
let recording = true;
|
|
57
|
+
const note = (op, path, to) => {
|
|
58
|
+
if (recording)
|
|
59
|
+
mutations.push(to === undefined ? { op, path } : { op, path, to });
|
|
60
|
+
};
|
|
61
|
+
const fs = {
|
|
62
|
+
readFile: path => inner.readFile(path),
|
|
63
|
+
readdir: path => inner.readdir(path),
|
|
64
|
+
stat: path => inner.stat(path),
|
|
65
|
+
async writeFile(path, data) {
|
|
66
|
+
note('writeFile', path);
|
|
67
|
+
await inner.writeFile(path, data);
|
|
68
|
+
},
|
|
69
|
+
async atomicWrite(path, data) {
|
|
70
|
+
note('atomicWrite', path);
|
|
71
|
+
await inner.atomicWrite(path, data);
|
|
72
|
+
},
|
|
73
|
+
async rename(from, to) {
|
|
74
|
+
note('rename', from, to);
|
|
75
|
+
await inner.rename(from, to);
|
|
76
|
+
},
|
|
77
|
+
async mkdir(path) {
|
|
78
|
+
note('mkdir', path);
|
|
79
|
+
await inner.mkdir(path);
|
|
80
|
+
},
|
|
81
|
+
async rm(path, options) {
|
|
82
|
+
note('rm', path);
|
|
83
|
+
await inner.rm(path, options);
|
|
84
|
+
},
|
|
85
|
+
async symlink(target, path) {
|
|
86
|
+
note('symlink', path);
|
|
87
|
+
await inner.symlink(target, path);
|
|
88
|
+
},
|
|
89
|
+
readlink: path => inner.readlink(path),
|
|
90
|
+
async copyFile(from, to) {
|
|
91
|
+
note('copyFile', to);
|
|
92
|
+
await inner.copyFile(from, to);
|
|
93
|
+
},
|
|
94
|
+
async chmod(path, mode) {
|
|
95
|
+
note('chmod', path);
|
|
96
|
+
await inner.chmod(path, mode);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
fs,
|
|
101
|
+
mutations,
|
|
102
|
+
record: on => {
|
|
103
|
+
recording = on;
|
|
104
|
+
},
|
|
105
|
+
reset: () => {
|
|
106
|
+
mutations.length = 0;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function recordingLock(inner) {
|
|
111
|
+
const acquisitions = [];
|
|
112
|
+
return {
|
|
113
|
+
acquisitions,
|
|
114
|
+
lock: {
|
|
115
|
+
async acquire(path, options) {
|
|
116
|
+
acquisitions.push(path);
|
|
117
|
+
return inner.acquire(path, options);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function messageOf(error) {
|
|
123
|
+
return error instanceof Error ? error.message : String(error);
|
|
124
|
+
}
|
|
125
|
+
function check(id, status, message) {
|
|
126
|
+
return { id, status, message };
|
|
127
|
+
}
|
|
128
|
+
/** Does a published version directory exist anywhere under `root`? */
|
|
129
|
+
async function containsInstallManifest(fs, root) {
|
|
130
|
+
const info = await fs.stat(root).catch(() => undefined);
|
|
131
|
+
if (info === undefined || !info.isDirectory)
|
|
132
|
+
return false;
|
|
133
|
+
const entries = await fs.readdir(root).catch(() => []);
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry === INSTALL_MANIFEST_FILE)
|
|
136
|
+
return true;
|
|
137
|
+
if (await containsInstallManifest(fs, join(root, entry)))
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/** The resource name a provider assigns, or the item id when `identify` cannot answer. */
|
|
143
|
+
function safeName(provider, item) {
|
|
144
|
+
try {
|
|
145
|
+
const identity = provider.identify(item);
|
|
146
|
+
return typeof identity.name === 'string' && identity.name !== '' ? identity.name : item.id;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return item.id;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Run the whole kit against one provider; every outcome is a {@link ConformanceCheck}. */
|
|
153
|
+
export async function runProviderConformance(options) {
|
|
154
|
+
const provider = options.provider;
|
|
155
|
+
const items = [...options.items];
|
|
156
|
+
const innerFs = options.fs ?? defaultFs();
|
|
157
|
+
const logger = options.logger ?? SILENT_LOGGER;
|
|
158
|
+
const policy = options.policy ?? {};
|
|
159
|
+
const clock = options.clock ?? Date.now;
|
|
160
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
161
|
+
const recorder = recordingFs(innerFs);
|
|
162
|
+
const locks = recordingLock(options.lock ?? defaultLock({ clock }));
|
|
163
|
+
const created = options.home === undefined;
|
|
164
|
+
const home = options.home ?? (await mkdtemp(join(tmpdir(), 'envinit-conformance-')));
|
|
165
|
+
const checks = [];
|
|
166
|
+
const push = (entry) => {
|
|
167
|
+
checks.push(entry);
|
|
168
|
+
};
|
|
169
|
+
const skipRestFrom = (id, reason) => {
|
|
170
|
+
const start = CHECK_ORDER.indexOf(id);
|
|
171
|
+
for (const rest of CHECK_ORDER.slice(start + 1))
|
|
172
|
+
push(check(rest, 'skip', reason));
|
|
173
|
+
};
|
|
174
|
+
const providerContext = () => ({
|
|
175
|
+
home,
|
|
176
|
+
logger,
|
|
177
|
+
policy,
|
|
178
|
+
fs: recorder.fs,
|
|
179
|
+
...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
// ── registration contract ────────────────────────────────────────────────
|
|
183
|
+
const kindProblems = [];
|
|
184
|
+
if (typeof provider.id !== 'string' || provider.id === '')
|
|
185
|
+
kindProblems.push('provider.id 为空');
|
|
186
|
+
if (!Array.isArray(provider.kinds) || provider.kinds.length === 0) {
|
|
187
|
+
kindProblems.push('provider.kinds 为空');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
for (const kind of provider.kinds) {
|
|
192
|
+
if (typeof kind !== 'string' || kind === '') {
|
|
193
|
+
kindProblems.push('kinds 含非字符串项');
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (seen.has(kind))
|
|
197
|
+
kindProblems.push(`kind 重复:${kind}`);
|
|
198
|
+
seen.add(kind);
|
|
199
|
+
if (!BUILTIN_KINDS.includes(kind) && !isNamespacedKind(kind)) {
|
|
200
|
+
kindProblems.push(`第三方 kind 必须带 @scope/ 或 plugin: 前缀:${kind}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
push(check('conformance/kinds', kindProblems.length === 0 ? 'pass' : 'fail', kindProblems.length === 0 ? `id=${provider.id};kinds=${provider.kinds.join(', ')}` : kindProblems.join(';')));
|
|
205
|
+
if (kindProblems.length > 0) {
|
|
206
|
+
skipRestFrom('conformance/kinds', 'conformance/kinds 未通过,后续检查跳过');
|
|
207
|
+
return finish();
|
|
208
|
+
}
|
|
209
|
+
push(options.packageName === undefined
|
|
210
|
+
? check('conformance/id-is-package-name', 'skip', '未提供 packageName')
|
|
211
|
+
: options.packageName === provider.id
|
|
212
|
+
? check('conformance/id-is-package-name', 'pass', `id = ${provider.id}`)
|
|
213
|
+
: check('conformance/id-is-package-name', 'fail', `provider.id (${provider.id}) 必须等于它的 npm 包名 (${options.packageName})`));
|
|
214
|
+
const identityProblems = [];
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
try {
|
|
217
|
+
const identity = provider.identify(item);
|
|
218
|
+
if (typeof identity.name !== 'string' || identity.name === '') {
|
|
219
|
+
identityProblems.push(`${item.id}: identify().name 为空`);
|
|
220
|
+
}
|
|
221
|
+
if (identity.range !== undefined && typeof identity.range !== 'string') {
|
|
222
|
+
identityProblems.push(`${item.id}: identify().range 必须是字符串`);
|
|
223
|
+
}
|
|
224
|
+
if ('key' in identity) {
|
|
225
|
+
identityProblems.push(`${item.id}: identify() 不得自带 key(键归核心)`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
identityProblems.push(`${item.id}: identify() 抛错:${messageOf(error)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
push(check('conformance/identify-shape', identityProblems.length === 0 ? 'pass' : 'fail', identityProblems.length === 0 ? `${String(items.length)} 个 item 的资源身份可用` : identityProblems.join(';')));
|
|
233
|
+
// ── probe / plan purity ──────────────────────────────────────────────────
|
|
234
|
+
let networkCalls = 0;
|
|
235
|
+
const purityFetch = (async () => {
|
|
236
|
+
networkCalls += 1;
|
|
237
|
+
throw new Error('conformance: purity checks forbid the network');
|
|
238
|
+
});
|
|
239
|
+
const purityContext = { home, logger, policy, fs: recorder.fs, fetch: purityFetch };
|
|
240
|
+
const probeProblems = [];
|
|
241
|
+
for (const item of items) {
|
|
242
|
+
recorder.reset();
|
|
243
|
+
networkCalls = 0;
|
|
244
|
+
try {
|
|
245
|
+
const first = await provider.probe(item, purityContext);
|
|
246
|
+
const second = await provider.probe(item, purityContext);
|
|
247
|
+
if (recorder.mutations.length > 0)
|
|
248
|
+
probeProblems.push(`${item.id}: probe 改动了磁盘(${recorder.mutations[0]?.op ?? '?'})`);
|
|
249
|
+
if (networkCalls > 0)
|
|
250
|
+
probeProblems.push(`${item.id}: probe 尝试联网`);
|
|
251
|
+
if (!isDeepStrictEqual(first, second))
|
|
252
|
+
probeProblems.push(`${item.id}: 两次 probe 结果不一致`);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
probeProblems.push(`${item.id}: probe 抛错:${messageOf(error)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
push(check('conformance/probe-pure', probeProblems.length === 0 ? 'pass' : 'fail', probeProblems.length === 0 ? 'probe 不改盘、不联网、可重复' : probeProblems.join(';')));
|
|
259
|
+
const planProblems = [];
|
|
260
|
+
for (const item of items) {
|
|
261
|
+
recorder.reset();
|
|
262
|
+
networkCalls = 0;
|
|
263
|
+
try {
|
|
264
|
+
const first = await provider.plan(item, purityContext);
|
|
265
|
+
const second = await provider.plan(item, purityContext);
|
|
266
|
+
if (!PLAN_ACTIONS.includes(first.action))
|
|
267
|
+
planProblems.push(`${item.id}: plan().action 非法:${String(first.action)}`);
|
|
268
|
+
if (recorder.mutations.length > 0)
|
|
269
|
+
planProblems.push(`${item.id}: plan 改动了磁盘(${recorder.mutations[0]?.op ?? '?'})`);
|
|
270
|
+
if (networkCalls > 0)
|
|
271
|
+
planProblems.push(`${item.id}: plan 尝试联网`);
|
|
272
|
+
if (!isDeepStrictEqual(first, second))
|
|
273
|
+
planProblems.push(`${item.id}: 两次 plan 结果不一致`);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
planProblems.push(`${item.id}: plan 抛错:${messageOf(error)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
push(check('conformance/plan-pure', planProblems.length === 0 ? 'pass' : 'fail', planProblems.length === 0 ? 'plan 是纯函数且可重复' : planProblems.join(';')));
|
|
280
|
+
// ── targetDir safety ─────────────────────────────────────────────────────
|
|
281
|
+
const targetProblems = [];
|
|
282
|
+
for (const item of items) {
|
|
283
|
+
const ref = {
|
|
284
|
+
name: options.ref?.name ?? safeName(provider, item),
|
|
285
|
+
version: options.ref?.version ?? '0.0.0',
|
|
286
|
+
segment: options.ref?.segment ?? '0.0.0',
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
const raw = provider.targetDir(item, ref);
|
|
290
|
+
if (typeof raw !== 'string' || raw === '') {
|
|
291
|
+
targetProblems.push(`${item.id}: targetDir() 必须返回非空字符串`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const normalized = raw.replaceAll('\\', '/');
|
|
295
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) {
|
|
296
|
+
targetProblems.push(`${item.id}: targetDir() 不得是绝对路径:${raw}`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const segments = normalized.split('/');
|
|
300
|
+
if (segments.includes('..')) {
|
|
301
|
+
targetProblems.push(`${item.id}: targetDir() 含 ..:${raw}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const root = resolve(home, item.target.root);
|
|
305
|
+
if (!isInside(root, resolve(root, ...segments))) {
|
|
306
|
+
targetProblems.push(`${item.id}: targetDir() 跳出 target.root:${raw}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
targetProblems.push(`${item.id}: targetDir() 抛错:${messageOf(error)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
push(check('conformance/target-dir-safe', targetProblems.length === 0 ? 'pass' : 'fail', targetProblems.length === 0 ? 'targetDir() 是 root 下的安全相对路径' : targetProblems.join(';')));
|
|
314
|
+
// ── verify on a missing directory ────────────────────────────────────────
|
|
315
|
+
const verifyProblems = [];
|
|
316
|
+
for (const [index, item] of items.entries()) {
|
|
317
|
+
recorder.reset();
|
|
318
|
+
const name = safeName(provider, item);
|
|
319
|
+
const missing = join(home, 'conformance-missing', `${String(index)}-${item.kind.replace(/[^a-zA-Z0-9._-]/g, '_')}`);
|
|
320
|
+
const resolved = {
|
|
321
|
+
key: `${item.kind}+${name}`,
|
|
322
|
+
name,
|
|
323
|
+
version: '0.0.0',
|
|
324
|
+
dir: missing,
|
|
325
|
+
entryDir: join(missing, 'entry'),
|
|
326
|
+
source: 'installed',
|
|
327
|
+
};
|
|
328
|
+
try {
|
|
329
|
+
await provider.verify(item, resolved, providerContext());
|
|
330
|
+
verifyProblems.push(`${item.id}: verify 接受了不存在的目录 ${missing}`);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
const first = recorder.mutations[0];
|
|
334
|
+
if (first !== undefined)
|
|
335
|
+
verifyProblems.push(`${item.id}: verify 改动了磁盘(${first.op} ${first.path})`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
push(check('conformance/verify-rejects-missing', verifyProblems.length === 0 ? 'pass' : 'fail', verifyProblems.length === 0 ? 'verify 拒绝坏副本且只读' : verifyProblems.join(';')));
|
|
339
|
+
// ── install round-trip ───────────────────────────────────────────────────
|
|
340
|
+
if (options.install === false) {
|
|
341
|
+
push(check('conformance/install-landing', 'skip', 'install === false:跳过落盘检查'));
|
|
342
|
+
push(check('conformance/install-idempotent', 'skip', 'install === false:跳过幂等检查'));
|
|
343
|
+
push(check('conformance/install-half-failure', 'skip', 'install === false:跳过故障注入'));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const targetHome = join(home, 'round-trip');
|
|
347
|
+
await innerFs.mkdir(targetHome);
|
|
348
|
+
const roundTrip = await installRoundTrip(targetHome);
|
|
349
|
+
push(roundTrip.landing);
|
|
350
|
+
push(roundTrip.idempotent);
|
|
351
|
+
push(await halfFailure());
|
|
352
|
+
}
|
|
353
|
+
push(await rejectsHostileTargetDir());
|
|
354
|
+
push(await quarantinesBadCopy());
|
|
355
|
+
push(await reportCompleteness());
|
|
356
|
+
push(await conflictOutcome());
|
|
357
|
+
push(await unknownOutcome());
|
|
358
|
+
push(await providerImportable());
|
|
359
|
+
push(await artifactCheck(options.artifacts, false));
|
|
360
|
+
push(await artifactCheck(options.clientArtifacts, true));
|
|
361
|
+
push(await recycleSemantics());
|
|
362
|
+
return finish();
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
if (created && options.keepHome !== true)
|
|
366
|
+
await rm(home, { recursive: true, force: true }).catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
// ── run-scoped helpers ─────────────────────────────────────────────────────
|
|
369
|
+
function finish() {
|
|
370
|
+
return { provider: provider.id, ok: checks.every(entry => entry.status !== 'fail'), checks };
|
|
371
|
+
}
|
|
372
|
+
function provisionerOptions(targetHome) {
|
|
373
|
+
return {
|
|
374
|
+
home: targetHome,
|
|
375
|
+
logger,
|
|
376
|
+
fs: recorder.fs,
|
|
377
|
+
lock: locks.lock,
|
|
378
|
+
clock,
|
|
379
|
+
policy,
|
|
380
|
+
...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function pluginName() {
|
|
384
|
+
const first = items[0];
|
|
385
|
+
if (first === undefined)
|
|
386
|
+
return 'conformance';
|
|
387
|
+
const colon = first.id.indexOf(':');
|
|
388
|
+
return colon > 0 ? first.id.slice(0, colon) : 'conformance';
|
|
389
|
+
}
|
|
390
|
+
function declareAll(instance) {
|
|
391
|
+
const manifest = { plugin: pluginName(), items };
|
|
392
|
+
instance.declare(manifest);
|
|
393
|
+
}
|
|
394
|
+
async function installRoundTrip(targetHome) {
|
|
395
|
+
locks.acquisitions.length = 0;
|
|
396
|
+
recorder.reset();
|
|
397
|
+
const violations = [];
|
|
398
|
+
const wrapped = {
|
|
399
|
+
...provider,
|
|
400
|
+
async install(item, ctx) {
|
|
401
|
+
recorder.reset();
|
|
402
|
+
recorder.record(true);
|
|
403
|
+
try {
|
|
404
|
+
return await provider.install(item, ctx);
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
recorder.record(false);
|
|
408
|
+
const root = resolve(targetHome, item.target.root);
|
|
409
|
+
const staging = resolve(tempRoot(targetHome));
|
|
410
|
+
for (const mutation of recorder.mutations) {
|
|
411
|
+
const inStaging = isInside(staging, mutation.path);
|
|
412
|
+
const inTarget = isInside(root, mutation.path);
|
|
413
|
+
const renamedInto = mutation.op === 'rename' && mutation.to !== undefined && isInside(staging, mutation.path) && isInside(root, resolve(mutation.to));
|
|
414
|
+
if (!inStaging && !inTarget && !renamedInto) {
|
|
415
|
+
violations.push(`${item.id}: install 在 ${mutation.op} 时写到 ${mutation.path}`);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
const instance = createProvisioner(provisionerOptions(targetHome));
|
|
423
|
+
instance.register(wrapped);
|
|
424
|
+
declareAll(instance);
|
|
425
|
+
let first;
|
|
426
|
+
try {
|
|
427
|
+
first = await instance.ensure();
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const reason = `ensure 抛错:${messageOf(error)}`;
|
|
431
|
+
return { landing: check('conformance/install-landing', 'fail', reason), idempotent: check('conformance/install-idempotent', 'fail', reason) };
|
|
432
|
+
}
|
|
433
|
+
const ready = first.entries.filter(entry => entry.action === 'installed' || entry.action === 'present');
|
|
434
|
+
if (ready.length !== items.length) {
|
|
435
|
+
const detail = first.entries.map(entry => `${entry.id}=${entry.action}${entry.code === undefined ? '' : `(${entry.code})`}`).join(', ');
|
|
436
|
+
violations.push(`ensure 未能就绪全部 item:${detail}`);
|
|
437
|
+
}
|
|
438
|
+
if (!first.entries.some(entry => entry.action === 'installed')) {
|
|
439
|
+
violations.push('ensure 没有发生任何安装(item 在空 home 里就被判定为已就绪);落盘路径无法验证');
|
|
440
|
+
}
|
|
441
|
+
if (options.bypassPublish !== true && !locks.acquisitions.includes(lockPath(targetHome))) {
|
|
442
|
+
violations.push('install 未经过 ctx.publish():整个安装期没有取得发布锁');
|
|
443
|
+
}
|
|
444
|
+
const landing = check('conformance/install-landing', violations.length === 0 ? 'pass' : 'fail', violations.length === 0 ? 'install 只经 staging / target.root 落盘,且经 ctx.publish() 取锁' : violations.join(';'));
|
|
445
|
+
const idemProblems = [];
|
|
446
|
+
try {
|
|
447
|
+
const second = await instance.ensure();
|
|
448
|
+
for (const entry of second.entries) {
|
|
449
|
+
if (entry.action !== 'present') {
|
|
450
|
+
idemProblems.push(`${entry.id}: 第二次 ensure 是 ${entry.action}${entry.code === undefined ? '' : `(${entry.code})`}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const item of items) {
|
|
454
|
+
const state = instance.resolve(item.id);
|
|
455
|
+
if (state.state !== 'ready') {
|
|
456
|
+
idemProblems.push(`${item.id}: resolve() 是 ${state.state}`);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const allowed = SOURCES_BY_KIND[item.kind] ?? SOURCES;
|
|
460
|
+
if (!allowed.includes(state.handle.source)) {
|
|
461
|
+
idemProblems.push(`${item.id}: kind ${item.kind} 的 source 只能是 ${allowed.join(' / ')},得到 ${String(state.handle.source)}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
idemProblems.push(`第二次 ensure 抛错:${messageOf(error)}`);
|
|
468
|
+
}
|
|
469
|
+
const idempotent = check('conformance/install-idempotent', idemProblems.length === 0 ? 'pass' : 'fail', idemProblems.length === 0 ? '第二次 ensure 全部 present,resolve() 就绪' : idemProblems.join(';'));
|
|
470
|
+
return { landing, idempotent };
|
|
471
|
+
}
|
|
472
|
+
/** Injects a corrupt download body and asserts no usable directory is left behind. */
|
|
473
|
+
async function halfFailure() {
|
|
474
|
+
const targetHome = join(home, 'half-failure');
|
|
475
|
+
await innerFs.mkdir(targetHome);
|
|
476
|
+
const corrupt = (async () => new Response(new Uint8Array([0x1f, 0x8b, 0x08, 0x00, 0xde, 0xad, 0xbe, 0xef]), { status: 200 }));
|
|
477
|
+
const instance = createProvisioner({
|
|
478
|
+
home: targetHome,
|
|
479
|
+
logger,
|
|
480
|
+
fs: innerFs,
|
|
481
|
+
lock: locks.lock,
|
|
482
|
+
clock,
|
|
483
|
+
policy,
|
|
484
|
+
fetch: corrupt,
|
|
485
|
+
});
|
|
486
|
+
instance.register(provider);
|
|
487
|
+
declareAll(instance);
|
|
488
|
+
let report;
|
|
489
|
+
try {
|
|
490
|
+
report = await instance.ensure();
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
return check('conformance/install-half-failure', 'fail', `坏体注入时 ensure 抛错:${messageOf(error)}`);
|
|
494
|
+
}
|
|
495
|
+
const failed = report.entries.filter(entry => entry.action === 'failed');
|
|
496
|
+
if (failed.length === 0) {
|
|
497
|
+
return check('conformance/install-half-failure', 'skip', '坏体注入后没有失败项:该 provider 不依赖下载,注入不适用');
|
|
498
|
+
}
|
|
499
|
+
const problems = [];
|
|
500
|
+
for (const entry of failed) {
|
|
501
|
+
const item = items.find(candidate => candidate.id === entry.id);
|
|
502
|
+
if (item === undefined)
|
|
503
|
+
continue;
|
|
504
|
+
const state = instance.resolve(item.id);
|
|
505
|
+
if (state.state === 'ready')
|
|
506
|
+
problems.push(`${item.id}: 半截失败后 resolve() 仍报 ready`);
|
|
507
|
+
if (options.bypassPublish !== true && (await containsInstallManifest(innerFs, join(targetHome, item.target.root)))) {
|
|
508
|
+
problems.push(`${item.id}: 半截失败后 target.root 里留下了已发布的目录`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return check('conformance/install-half-failure', problems.length === 0 ? 'pass' : 'fail', problems.length === 0 ? `${String(failed.length)} 项坏体失败后没有留下可用目录` : problems.join(';'));
|
|
512
|
+
}
|
|
513
|
+
async function conflictOutcome() {
|
|
514
|
+
const clone = { ...provider, id: `${provider.id}-conformance-clone` };
|
|
515
|
+
const orders = [
|
|
516
|
+
[provider, clone],
|
|
517
|
+
[clone, provider],
|
|
518
|
+
];
|
|
519
|
+
const { kindProviders: _lifted, ...rest } = policy;
|
|
520
|
+
const problems = [];
|
|
521
|
+
for (const [index, order] of orders.entries()) {
|
|
522
|
+
const targetHome = join(home, `conflict-${String(index)}`);
|
|
523
|
+
await innerFs.mkdir(targetHome);
|
|
524
|
+
const instance = createProvisioner({
|
|
525
|
+
home: targetHome,
|
|
526
|
+
logger,
|
|
527
|
+
fs: recorder.fs,
|
|
528
|
+
lock: locks.lock,
|
|
529
|
+
clock,
|
|
530
|
+
policy: rest,
|
|
531
|
+
...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),
|
|
532
|
+
});
|
|
533
|
+
for (const candidate of order)
|
|
534
|
+
instance.register(candidate);
|
|
535
|
+
declareAll(instance);
|
|
536
|
+
try {
|
|
537
|
+
const report = await instance.ensure();
|
|
538
|
+
for (const entry of report.entries) {
|
|
539
|
+
if (entry.code !== 'provider/conflict') {
|
|
540
|
+
problems.push(`注册顺序 ${String(index)}:${entry.id} 得到 ${entry.action}/${String(entry.code)},应为 provider/conflict`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
problems.push(`注册顺序 ${String(index)}:ensure 抛错:${messageOf(error)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return check('conformance/register-conflict', problems.length === 0 ? 'pass' : 'fail', problems.length === 0 ? '同 kind 冲突 fail-closed,且与注册顺序无关' : problems.join(';'));
|
|
549
|
+
}
|
|
550
|
+
/** A shim provider that publishes whatever the core asks, for the core-level checks. */
|
|
551
|
+
function shimPipe(overrides) {
|
|
552
|
+
return {
|
|
553
|
+
...provider,
|
|
554
|
+
probe: async () => ({ found: false }),
|
|
555
|
+
plan: () => ({ action: 'install' }),
|
|
556
|
+
install: async (_item, ctx) => ctx.publish(await ctx.stage(), { name: 'shim', version: '1.0.0' }),
|
|
557
|
+
verify: async () => undefined,
|
|
558
|
+
...overrides,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/** Any `install.json` left under `root` within `depth` levels — i.e. a published version directory. */
|
|
562
|
+
async function findManifests(fs_, root, depth) {
|
|
563
|
+
if (depth < 0)
|
|
564
|
+
return [];
|
|
565
|
+
const found = [];
|
|
566
|
+
const entries = await fs_.readdir(root).catch(() => []);
|
|
567
|
+
for (const entry of entries) {
|
|
568
|
+
const path = join(root, entry);
|
|
569
|
+
if (await fs_.stat(join(path, INSTALL_MANIFEST_FILE)).then(info => info !== undefined).catch(() => false)) {
|
|
570
|
+
found.push(path);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
found.push(...(await findManifests(fs_, path, depth - 1)));
|
|
574
|
+
}
|
|
575
|
+
return found;
|
|
576
|
+
}
|
|
577
|
+
async function withShim(shim, home_) {
|
|
578
|
+
const instance = createProvisioner({ home: home_, logger, fs: recorder.fs, lock: locks.lock, clock });
|
|
579
|
+
instance.register(shim);
|
|
580
|
+
for (const item of items)
|
|
581
|
+
instance.declare({ plugin: 'conformance', items: [item] });
|
|
582
|
+
const report = await instance.ensure();
|
|
583
|
+
return report.entries.find(entry => entry.plugin === 'conformance');
|
|
584
|
+
}
|
|
585
|
+
async function rejectsHostileTargetDir() {
|
|
586
|
+
const targetHome = join(home, 'hostile-target');
|
|
587
|
+
await innerFs.mkdir(targetHome);
|
|
588
|
+
try {
|
|
589
|
+
const entry = await withShim(shimPipe({ targetDir: () => '../../escape' }), targetHome);
|
|
590
|
+
const ok = entry?.code === 'invalid-option';
|
|
591
|
+
return check('conformance/core-rejects-bad-target-dir', ok ? 'pass' : 'fail', ok ? '核心拒绝跳出 target.root 的 targetDir' : `targetDir 返回 ../../escape 时得到 ${String(entry?.action)}/${String(entry?.code)}`);
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
return check('conformance/core-rejects-bad-target-dir', 'fail', `ensure 抛错:${messageOf(error)}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async function quarantinesBadCopy() {
|
|
598
|
+
const targetHome = join(home, 'quarantine');
|
|
599
|
+
await innerFs.mkdir(targetHome);
|
|
600
|
+
try {
|
|
601
|
+
const entry = await withShim(shimPipe({ verify: async () => Promise.reject(new ProvisionError('verify/failed', 'kit: verify 必定失败')) }), targetHome);
|
|
602
|
+
const failed = entry?.code === 'verify/failed';
|
|
603
|
+
const quarantined = await innerFs.readdir(join(targetHome, '.envinit', '.quarantine')).catch(() => []);
|
|
604
|
+
// No version directory with a manifest may remain under the item's target root.
|
|
605
|
+
const left = await findManifests(innerFs, join(targetHome, items[0]?.target.root ?? 'runtime'), 2);
|
|
606
|
+
const ok = failed && quarantined.length > 0 && left.length === 0;
|
|
607
|
+
return check('conformance/core-quarantines-bad-copy', ok ? 'pass' : 'fail', ok ? 'verify 失败 ⇒ 核心把已发布目录改名进 .quarantine' : `verify 失败后:code=${String(entry?.code)},隔离 ${String(quarantined.length)} 个,目标根仍留 ${String(left.length)} 个版本目录`);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
return check('conformance/core-quarantines-bad-copy', 'fail', `ensure 抛错:${messageOf(error)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function reportCompleteness() {
|
|
614
|
+
const targetHome = join(home, 'report');
|
|
615
|
+
await innerFs.mkdir(targetHome);
|
|
616
|
+
try {
|
|
617
|
+
const instance = createProvisioner({ home: targetHome, logger, fs: recorder.fs, lock: locks.lock, clock, ...(fetchImpl === undefined ? {} : { fetch: fetchImpl }) });
|
|
618
|
+
instance.register(provider);
|
|
619
|
+
for (const item of items)
|
|
620
|
+
instance.declare({ plugin: 'conformance', items: [item] });
|
|
621
|
+
const report = await instance.ensure();
|
|
622
|
+
const problems = [];
|
|
623
|
+
for (const entry of report.entries) {
|
|
624
|
+
if (entry.source === undefined)
|
|
625
|
+
problems.push(`${entry.id}: source 为空`);
|
|
626
|
+
if ((entry.action === 'failed' || entry.action === 'skipped') && entry.code === undefined) {
|
|
627
|
+
problems.push(`${entry.id}: ${entry.action} 没有 code`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return check('conformance/report-completeness', problems.length === 0 ? 'pass' : 'fail', problems.length === 0 ? '每行都带 source,失败/跳过都带 code' : problems.join(';'));
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
return check('conformance/report-completeness', 'fail', `ensure 抛错:${messageOf(error)}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function providerImportable() {
|
|
637
|
+
const entry = options.providerEntry;
|
|
638
|
+
if (entry === undefined || entry === '') {
|
|
639
|
+
return check('conformance/provider-importable', 'skip', '未提供 providerEntry:无法验证 import() 成功');
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const specifier = entry.startsWith('/') || entry.startsWith('.') ? pathToFileURL(entry).href : entry;
|
|
643
|
+
const loaded = await import(specifier);
|
|
644
|
+
const ok = typeof loaded === 'object' && loaded !== null;
|
|
645
|
+
return check('conformance/provider-importable', ok ? 'pass' : 'fail', ok ? `provider 产物可 import:${entry}` : `import 结果不是模块:${entry}`);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
return check('conformance/provider-importable', 'fail', `provider 产物 import 失败:${entry}(${messageOf(error)})`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function recycleSemantics() {
|
|
652
|
+
return check('conformance/recycle-semantics', 'skip', '回收(prune/GC/配额)尚未实现;当前 `prune` 是空实现');
|
|
653
|
+
}
|
|
654
|
+
async function unknownOutcome() {
|
|
655
|
+
const targetHome = join(home, 'unknown-provider');
|
|
656
|
+
await innerFs.mkdir(targetHome);
|
|
657
|
+
const instance = createProvisioner(provisionerOptions(targetHome));
|
|
658
|
+
declareAll(instance);
|
|
659
|
+
try {
|
|
660
|
+
const report = await instance.ensure();
|
|
661
|
+
const wrong = report.entries.filter(entry => entry.code !== 'unknown-provider');
|
|
662
|
+
return check('conformance/unknown-provider', wrong.length === 0 ? 'pass' : 'fail', wrong.length === 0 ? '无人认领的 kind 报 unknown-provider' : wrong.map(entry => `${entry.id} 得到 ${String(entry.code)}`).join(';'));
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
return check('conformance/unknown-provider', 'fail', `ensure 抛错:${messageOf(error)}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function artifactCheck(paths, client) {
|
|
669
|
+
const id = client ? 'conformance/client-artifact-clean' : 'conformance/provider-artifact-type-only';
|
|
670
|
+
if (paths === undefined || paths.length === 0) {
|
|
671
|
+
return check(id, 'skip', client ? '未提供 client 产物' : '未提供 provider 产物');
|
|
672
|
+
}
|
|
673
|
+
const offenders = [];
|
|
674
|
+
for (const path of paths) {
|
|
675
|
+
let text;
|
|
676
|
+
try {
|
|
677
|
+
text = await readFile(path, 'utf8');
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
offenders.push(`${path}: 不可读(${messageOf(error)})`);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const specifiers = [...new Set(findFrameworkImports(text).map(found => found.specifier))];
|
|
684
|
+
if (specifiers.length > 0) {
|
|
685
|
+
offenders.push(client
|
|
686
|
+
? `${path}: client 产物不得 import 框架(${specifiers.join(', ')})`
|
|
687
|
+
: `${path}: provider 产物对框架只能 import type,产物里不得留下(${specifiers.join(', ')})`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return check(id, offenders.length === 0 ? 'pass' : 'fail', offenders.length === 0
|
|
691
|
+
? `${String(paths.length)} 个${client ? 'client' : 'provider'}产物无框架 value import`
|
|
692
|
+
: offenders.join(';'));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/** Throw when any check failed; the message lists the failing rule ids. */
|
|
696
|
+
export function assertProviderConformance(report) {
|
|
697
|
+
const failed = report.checks.filter(entry => entry.status === 'fail');
|
|
698
|
+
if (failed.length === 0)
|
|
699
|
+
return;
|
|
700
|
+
throw new Error(`provider ${report.provider} 未通过 conformance:\n${failed.map(entry => ` [${entry.id}] ${entry.message}`).join('\n')}`);
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=conformance.js.map
|