dshmarket 1.66.30 → 1.66.32
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/lib/profile.js +49 -1
- package/lib/regions.js +28 -0
- package/lib/registry.js +22 -6
- package/lib/types/regions.d.ts +20 -0
- package/lib/verify.js +23 -9
- package/package.json +1 -1
- package/src/profile.ts +51 -1
- package/src/regions.ts +28 -0
- package/src/registry.ts +22 -6
- package/src/verify.ts +28 -14
package/lib/profile.js
CHANGED
|
@@ -139,7 +139,13 @@ export function readManifestDeps(profile, explicitDir) {
|
|
|
139
139
|
export function readDependencyOwners(profile, names, explicitDir) {
|
|
140
140
|
const installed = new Set(names);
|
|
141
141
|
const owners = {};
|
|
142
|
-
|
|
142
|
+
// Who may be an OWNER is not the same question as who gets a card (#892).
|
|
143
|
+
// pnpm hoists a transitive dependency's directory to `node_modules/<name>`
|
|
144
|
+
// as well, so `ws` — a dependency of a plugin, never a profile dependency —
|
|
145
|
+
// is where `bufferutil` and `utf-8-validate` are declared. Scanning only the
|
|
146
|
+
// profile's own entries could therefore never answer "who asked for these",
|
|
147
|
+
// and the two native peers stayed on the list as plugins nobody chose.
|
|
148
|
+
for (const owner of topLevelPackageNames(profile, names, explicitDir)) {
|
|
143
149
|
const manifest = readInstalledManifest(profile, owner, explicitDir);
|
|
144
150
|
if (typeof manifest !== 'object' || manifest === null)
|
|
145
151
|
continue;
|
|
@@ -156,6 +162,48 @@ export function readDependencyOwners(profile, names, explicitDir) {
|
|
|
156
162
|
}
|
|
157
163
|
return owners;
|
|
158
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Every package that could be an owner, sorted: the profile's own entries
|
|
167
|
+
* plus every package sitting at the profile's top level.
|
|
168
|
+
*
|
|
169
|
+
* The manifest's keys are the floor, not the ceiling — a missing or
|
|
170
|
+
* unreadable `node_modules` leaves the answer exactly as it was before the
|
|
171
|
+
* top level was scanned, so this can only ever find MORE owners.
|
|
172
|
+
*
|
|
173
|
+
* One manifest read per top-level package, measured at ~30 ms for 300
|
|
174
|
+
* packages on a warm cache, once per `/installed` request — which already
|
|
175
|
+
* reads a manifest, a patch file and an entry artifact for every card.
|
|
176
|
+
*/
|
|
177
|
+
function topLevelPackageNames(profile, names, explicitDir) {
|
|
178
|
+
const found = new Set(names);
|
|
179
|
+
const root = join(profileDir(profile, explicitDir), 'node_modules');
|
|
180
|
+
let entries = [];
|
|
181
|
+
try {
|
|
182
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
183
|
+
// A scope directory is named `@scope`, so the leading `@` belongs here;
|
|
184
|
+
// the package's own namespace is added below.
|
|
185
|
+
.filter(dirent => dirent.isDirectory() && /^@?[A-Za-z0-9._~-]+$/.test(dirent.name))
|
|
186
|
+
.map(dirent => dirent.name);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return [...found].sort();
|
|
190
|
+
}
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (!entry.startsWith('@')) {
|
|
193
|
+
found.add(entry);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// A scope directory holds the packages themselves.
|
|
197
|
+
try {
|
|
198
|
+
for (const inner of readdirSync(join(root, entry), { withFileTypes: true })) {
|
|
199
|
+
if (inner.isDirectory() && /^[A-Za-z0-9._~-]+$/.test(inner.name))
|
|
200
|
+
found.add(`${entry}/${inner.name}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch { /* unreadable scope — its packages simply cannot own anything */ }
|
|
204
|
+
}
|
|
205
|
+
return [...found].sort();
|
|
206
|
+
}
|
|
159
207
|
function objectRecord(value) {
|
|
160
208
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
161
209
|
? value
|
package/lib/regions.js
CHANGED
|
@@ -107,6 +107,34 @@ const ROUTES = {
|
|
|
107
107
|
],
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Every catalog source worth trying for a region: its own list, plus the
|
|
112
|
+
* catalog's published package on that region's own registry when the list
|
|
113
|
+
* does not already name it.
|
|
114
|
+
*
|
|
115
|
+
* The catalog hangs off a SECOND, independently blockable host (#891), and
|
|
116
|
+
* the probe that picks a region measures only the npm registry. A network
|
|
117
|
+
* where npm answers fast and GitHub Pages is blocked therefore measures
|
|
118
|
+
* `global` — whose one source is that very host — and since the region is
|
|
119
|
+
* decided once and persisted, nothing ever reconsiders: "catalog fetch
|
|
120
|
+
* failed" forever, on a machine whose registry is serving the catalog as a
|
|
121
|
+
* package all along.
|
|
122
|
+
*
|
|
123
|
+
* Appending the package closes that blind spot without asking the probe to
|
|
124
|
+
* learn about the second host, without revisiting the decision, and without
|
|
125
|
+
* sending anyone to another region's mirror: it rides the registry the probe
|
|
126
|
+
* already proved reachable. Last, not first — for everyone the official
|
|
127
|
+
* domain serves, this stays a source that is never contacted.
|
|
128
|
+
*/
|
|
129
|
+
export function catalogSourcesFor(region) {
|
|
130
|
+
const routes = routesFor(region);
|
|
131
|
+
const sources = [...routes.catalog];
|
|
132
|
+
const served = sources.some(source => source.kind === 'npm'
|
|
133
|
+
&& source.registry === routes.npmRegistry && source.pkg === CATALOG_PACKAGE);
|
|
134
|
+
if (!served)
|
|
135
|
+
sources.push({ kind: 'npm', registry: routes.npmRegistry, pkg: CATALOG_PACKAGE });
|
|
136
|
+
return sources;
|
|
137
|
+
}
|
|
110
138
|
/** Read an environment override, treating blank as unset. */
|
|
111
139
|
function override(env, name) {
|
|
112
140
|
const raw = env[name];
|
package/lib/registry.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* fresh on every request. See `loadRegistry` for why there is nothing
|
|
4
4
|
* behind it any more.
|
|
5
5
|
*/
|
|
6
|
+
import { logEvent } from './log.js';
|
|
6
7
|
import { configuredProxy, marketFetch } from './net.js';
|
|
7
8
|
import { catalogFromPackage } from './catalog-npm.js';
|
|
8
|
-
import { activeRegion, routesFor } from './regions.js';
|
|
9
|
+
import { activeRegion, catalogSourcesFor, routesFor } from './regions.js';
|
|
9
10
|
/**
|
|
10
11
|
* Category ids for one catalog entry, de-duplicated in declaration order.
|
|
11
12
|
*
|
|
@@ -166,8 +167,23 @@ export async function loadRegistry(region = activeRegion()) {
|
|
|
166
167
|
// the FIRST request the market makes, so a mirror that has gone down must
|
|
167
168
|
// mean a slow market rather than an empty one — the list ends at the
|
|
168
169
|
// address that has always worked.
|
|
169
|
-
|
|
170
|
+
//
|
|
171
|
+
// The list runs one entry past the region's own sources on purpose (#891):
|
|
172
|
+
// the region probe measures the npm registry, so it cannot see a network
|
|
173
|
+
// where npm is fast and the catalog's host is blocked — and the region is
|
|
174
|
+
// decided once and persisted. The catalog's published package, on the
|
|
175
|
+
// registry the probe just proved reachable, is the repair that needs
|
|
176
|
+
// neither the probe nor the decision to change.
|
|
177
|
+
const ownSources = routesFor(region).catalog.length;
|
|
178
|
+
const catalogSources = catalogSourcesFor(region);
|
|
179
|
+
for (const [sourceIndex, source] of catalogSources.entries()) {
|
|
170
180
|
const key = sourceKey(source);
|
|
181
|
+
const servedBy = (data) => {
|
|
182
|
+
if (sourceIndex >= ownSources) {
|
|
183
|
+
logEvent('warn', 'registry', `catalog: the ${region} sources did not answer; read the catalog package from ${key} instead (#891)`);
|
|
184
|
+
}
|
|
185
|
+
return data;
|
|
186
|
+
};
|
|
171
187
|
// Two attempts each. A catalog fetch crossing a long, lossy path fails
|
|
172
188
|
// transiently often enough that one retry is worth more than the second
|
|
173
189
|
// or two it costs — and with nothing behind this call any more, a
|
|
@@ -183,12 +199,12 @@ export async function loadRegistry(region = activeRegion()) {
|
|
|
183
199
|
const { version, data } = await catalogFromPackage(source.registry, source.pkg, reusable?.version ?? undefined);
|
|
184
200
|
// `data === null` means the published version is the one in hand.
|
|
185
201
|
if (data === null && reusable !== null)
|
|
186
|
-
return reusable.data;
|
|
202
|
+
return servedBy(reusable.data);
|
|
187
203
|
if (data === null)
|
|
188
204
|
throw new Error('the catalog package reported no change with nothing to reuse');
|
|
189
205
|
const parsed = asRegistry(data);
|
|
190
206
|
served = { key, etag: null, modified: null, version, data: parsed };
|
|
191
|
-
return parsed;
|
|
207
|
+
return servedBy(parsed);
|
|
192
208
|
}
|
|
193
209
|
// ETag first: it is exact, while a date has one-second resolution and
|
|
194
210
|
// a catalog republished twice within the same second would validate
|
|
@@ -221,7 +237,7 @@ export async function loadRegistry(region = activeRegion()) {
|
|
|
221
237
|
// otherwise surface as a confusing parse error on an empty body.
|
|
222
238
|
if (reusable === null)
|
|
223
239
|
throw new Error('the catalog answered "not modified" with nothing to revalidate');
|
|
224
|
-
return reusable.data;
|
|
240
|
+
return servedBy(reusable.data);
|
|
225
241
|
}
|
|
226
242
|
if (!res.ok)
|
|
227
243
|
throw new Error(`HTTP ${String(res.status)}`);
|
|
@@ -229,7 +245,7 @@ export async function loadRegistry(region = activeRegion()) {
|
|
|
229
245
|
served = {
|
|
230
246
|
key, etag: res.headers.get('etag'), modified: res.headers.get('last-modified'), version: null, data,
|
|
231
247
|
};
|
|
232
|
-
return data;
|
|
248
|
+
return servedBy(data);
|
|
233
249
|
}
|
|
234
250
|
catch (error) {
|
|
235
251
|
last = error;
|
package/lib/types/regions.d.ts
CHANGED
|
@@ -71,6 +71,26 @@ export interface RegionRoutes {
|
|
|
71
71
|
*/
|
|
72
72
|
catalog: CatalogSource[];
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Every catalog source worth trying for a region: its own list, plus the
|
|
76
|
+
* catalog's published package on that region's own registry when the list
|
|
77
|
+
* does not already name it.
|
|
78
|
+
*
|
|
79
|
+
* The catalog hangs off a SECOND, independently blockable host (#891), and
|
|
80
|
+
* the probe that picks a region measures only the npm registry. A network
|
|
81
|
+
* where npm answers fast and GitHub Pages is blocked therefore measures
|
|
82
|
+
* `global` — whose one source is that very host — and since the region is
|
|
83
|
+
* decided once and persisted, nothing ever reconsiders: "catalog fetch
|
|
84
|
+
* failed" forever, on a machine whose registry is serving the catalog as a
|
|
85
|
+
* package all along.
|
|
86
|
+
*
|
|
87
|
+
* Appending the package closes that blind spot without asking the probe to
|
|
88
|
+
* learn about the second host, without revisiting the decision, and without
|
|
89
|
+
* sending anyone to another region's mirror: it rides the registry the probe
|
|
90
|
+
* already proved reachable. Last, not first — for everyone the official
|
|
91
|
+
* domain serves, this stays a source that is never contacted.
|
|
92
|
+
*/
|
|
93
|
+
export declare function catalogSourcesFor(region: Region): CatalogSource[];
|
|
74
94
|
/**
|
|
75
95
|
* Normalize a user-maintained prefix, or reject it.
|
|
76
96
|
*
|
package/lib/verify.js
CHANGED
|
@@ -33,7 +33,7 @@ import { join } from 'node:path';
|
|
|
33
33
|
import { listHotMounts, parseSimplePatch } from './hot.js';
|
|
34
34
|
import { userPatchPackageReferences } from './patch.js';
|
|
35
35
|
import { nameMatchesPackage } from './entry-identity.js';
|
|
36
|
-
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.js';
|
|
36
|
+
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, pluginSubdirs, profileDir, readInstalled } from './profile.js';
|
|
37
37
|
import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.js';
|
|
38
38
|
import { dshHostInfo } from './dsh-install.js';
|
|
39
39
|
/**
|
|
@@ -422,19 +422,33 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
|
|
|
422
422
|
hot: false,
|
|
423
423
|
};
|
|
424
424
|
}
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
if (inBundles) {
|
|
426
|
+
return {
|
|
427
427
|
state: 'restart',
|
|
428
428
|
reasons: ['由你自己的 cordis.patch.yml 按名加载,重启后生效 / loaded by name from your own cordis.patch.yml — live after a restart'],
|
|
429
429
|
bundle: true,
|
|
430
430
|
hot: false,
|
|
431
|
-
}
|
|
432
|
-
: {
|
|
433
|
-
state: 'inert',
|
|
434
|
-
reasons: ['普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
|
|
435
|
-
bundle: false,
|
|
436
|
-
hot: false,
|
|
437
431
|
};
|
|
432
|
+
}
|
|
433
|
+
// A collection repository's ROOT package says nothing about itself and
|
|
434
|
+
// holds the plugins one level down (#892). Installed whole — which the
|
|
435
|
+
// market used to do before it learned to retarget one, and which a user
|
|
436
|
+
// can still do by hand — it lands on this list as "installed, not in
|
|
437
|
+
// effect" with no way out of that sentence. Name the plugins it does
|
|
438
|
+
// carry and the way to install one: the dead end is the bug.
|
|
439
|
+
const carried = pluginSubdirs(dir);
|
|
440
|
+
const shown = carried.slice(0, 3).join('、');
|
|
441
|
+
const more = carried.length > 3 ? `等 ${carried.length} 个` : '';
|
|
442
|
+
const shownEn = carried.slice(0, 3).join(', ');
|
|
443
|
+
const moreEn = carried.length > 3 ? ` and ${carried.length - 3} more` : '';
|
|
444
|
+
return {
|
|
445
|
+
state: 'inert',
|
|
446
|
+
reasons: [carried.length > 0
|
|
447
|
+
? `这是集合仓库的根包,本身不是插件。可安装的子包:${shown}${more}。请装子包而不是它(仓库地址后加 #path:/子包目录) / this is a collection repository's root package, not a plugin itself; install one of its plugins instead — ${shownEn}${moreEn} — by adding #path:/<subdirectory> to the repository URL`
|
|
448
|
+
: '普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
|
|
449
|
+
bundle: false,
|
|
450
|
+
hot: false,
|
|
451
|
+
};
|
|
438
452
|
}
|
|
439
453
|
// Carrier bundles (#103) ship no entry of their own — what they mount is
|
|
440
454
|
// the point — so judge by "is anything loadable", not by this package's
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.66.
|
|
4
|
+
"version": "1.66.32",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/profile.ts
CHANGED
|
@@ -151,7 +151,13 @@ export function readDependencyOwners(
|
|
|
151
151
|
): Record<string, string> {
|
|
152
152
|
const installed = new Set(names)
|
|
153
153
|
const owners: Record<string, string> = {}
|
|
154
|
-
|
|
154
|
+
// Who may be an OWNER is not the same question as who gets a card (#892).
|
|
155
|
+
// pnpm hoists a transitive dependency's directory to `node_modules/<name>`
|
|
156
|
+
// as well, so `ws` — a dependency of a plugin, never a profile dependency —
|
|
157
|
+
// is where `bufferutil` and `utf-8-validate` are declared. Scanning only the
|
|
158
|
+
// profile's own entries could therefore never answer "who asked for these",
|
|
159
|
+
// and the two native peers stayed on the list as plugins nobody chose.
|
|
160
|
+
for (const owner of topLevelPackageNames(profile, names, explicitDir)) {
|
|
155
161
|
const manifest = readInstalledManifest(profile, owner, explicitDir)
|
|
156
162
|
if (typeof manifest !== 'object' || manifest === null) continue
|
|
157
163
|
const declared = manifest as { dependencies?: unknown; peerDependencies?: unknown }
|
|
@@ -166,6 +172,50 @@ export function readDependencyOwners(
|
|
|
166
172
|
return owners
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Every package that could be an owner, sorted: the profile's own entries
|
|
177
|
+
* plus every package sitting at the profile's top level.
|
|
178
|
+
*
|
|
179
|
+
* The manifest's keys are the floor, not the ceiling — a missing or
|
|
180
|
+
* unreadable `node_modules` leaves the answer exactly as it was before the
|
|
181
|
+
* top level was scanned, so this can only ever find MORE owners.
|
|
182
|
+
*
|
|
183
|
+
* One manifest read per top-level package, measured at ~30 ms for 300
|
|
184
|
+
* packages on a warm cache, once per `/installed` request — which already
|
|
185
|
+
* reads a manifest, a patch file and an entry artifact for every card.
|
|
186
|
+
*/
|
|
187
|
+
function topLevelPackageNames(
|
|
188
|
+
profile: string,
|
|
189
|
+
names: readonly string[],
|
|
190
|
+
explicitDir?: string,
|
|
191
|
+
): string[] {
|
|
192
|
+
const found = new Set(names)
|
|
193
|
+
const root = join(profileDir(profile, explicitDir), 'node_modules')
|
|
194
|
+
let entries: string[] = []
|
|
195
|
+
try {
|
|
196
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
197
|
+
// A scope directory is named `@scope`, so the leading `@` belongs here;
|
|
198
|
+
// the package's own namespace is added below.
|
|
199
|
+
.filter(dirent => dirent.isDirectory() && /^@?[A-Za-z0-9._~-]+$/.test(dirent.name))
|
|
200
|
+
.map(dirent => dirent.name)
|
|
201
|
+
} catch {
|
|
202
|
+
return [...found].sort()
|
|
203
|
+
}
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (!entry.startsWith('@')) {
|
|
206
|
+
found.add(entry)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
// A scope directory holds the packages themselves.
|
|
210
|
+
try {
|
|
211
|
+
for (const inner of readdirSync(join(root, entry), { withFileTypes: true })) {
|
|
212
|
+
if (inner.isDirectory() && /^[A-Za-z0-9._~-]+$/.test(inner.name)) found.add(`${entry}/${inner.name}`)
|
|
213
|
+
}
|
|
214
|
+
} catch { /* unreadable scope — its packages simply cannot own anything */ }
|
|
215
|
+
}
|
|
216
|
+
return [...found].sort()
|
|
217
|
+
}
|
|
218
|
+
|
|
169
219
|
/** Exact rollback state owned by one profile package operation. */
|
|
170
220
|
export interface ProfileManifestSnapshot {
|
|
171
221
|
dependencies: Record<string, string>
|
package/src/regions.ts
CHANGED
|
@@ -157,6 +157,34 @@ const ROUTES: Record<Region, RegionRoutes> = {
|
|
|
157
157
|
},
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Every catalog source worth trying for a region: its own list, plus the
|
|
162
|
+
* catalog's published package on that region's own registry when the list
|
|
163
|
+
* does not already name it.
|
|
164
|
+
*
|
|
165
|
+
* The catalog hangs off a SECOND, independently blockable host (#891), and
|
|
166
|
+
* the probe that picks a region measures only the npm registry. A network
|
|
167
|
+
* where npm answers fast and GitHub Pages is blocked therefore measures
|
|
168
|
+
* `global` — whose one source is that very host — and since the region is
|
|
169
|
+
* decided once and persisted, nothing ever reconsiders: "catalog fetch
|
|
170
|
+
* failed" forever, on a machine whose registry is serving the catalog as a
|
|
171
|
+
* package all along.
|
|
172
|
+
*
|
|
173
|
+
* Appending the package closes that blind spot without asking the probe to
|
|
174
|
+
* learn about the second host, without revisiting the decision, and without
|
|
175
|
+
* sending anyone to another region's mirror: it rides the registry the probe
|
|
176
|
+
* already proved reachable. Last, not first — for everyone the official
|
|
177
|
+
* domain serves, this stays a source that is never contacted.
|
|
178
|
+
*/
|
|
179
|
+
export function catalogSourcesFor(region: Region): CatalogSource[] {
|
|
180
|
+
const routes = routesFor(region)
|
|
181
|
+
const sources = [...routes.catalog]
|
|
182
|
+
const served = sources.some(source => source.kind === 'npm'
|
|
183
|
+
&& source.registry === routes.npmRegistry && source.pkg === CATALOG_PACKAGE)
|
|
184
|
+
if (!served) sources.push({ kind: 'npm', registry: routes.npmRegistry, pkg: CATALOG_PACKAGE })
|
|
185
|
+
return sources
|
|
186
|
+
}
|
|
187
|
+
|
|
160
188
|
/** Read an environment override, treating blank as unset. */
|
|
161
189
|
function override(env: NodeJS.ProcessEnv, name: string): string | null {
|
|
162
190
|
const raw = env[name]
|
package/src/registry.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* behind it any more.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { logEvent } from './log.ts'
|
|
7
8
|
import { configuredProxy, marketFetch } from './net.ts'
|
|
8
9
|
import { catalogFromPackage } from './catalog-npm.ts'
|
|
9
|
-
import { activeRegion, routesFor, type CatalogSource, type Region } from './regions.ts'
|
|
10
|
+
import { activeRegion, catalogSourcesFor, routesFor, type CatalogSource, type Region } from './regions.ts'
|
|
10
11
|
|
|
11
12
|
export interface RegistryPlugin {
|
|
12
13
|
name: string
|
|
@@ -245,8 +246,23 @@ export async function loadRegistry(region: Region = activeRegion()): Promise<Reg
|
|
|
245
246
|
// the FIRST request the market makes, so a mirror that has gone down must
|
|
246
247
|
// mean a slow market rather than an empty one — the list ends at the
|
|
247
248
|
// address that has always worked.
|
|
248
|
-
|
|
249
|
+
//
|
|
250
|
+
// The list runs one entry past the region's own sources on purpose (#891):
|
|
251
|
+
// the region probe measures the npm registry, so it cannot see a network
|
|
252
|
+
// where npm is fast and the catalog's host is blocked — and the region is
|
|
253
|
+
// decided once and persisted. The catalog's published package, on the
|
|
254
|
+
// registry the probe just proved reachable, is the repair that needs
|
|
255
|
+
// neither the probe nor the decision to change.
|
|
256
|
+
const ownSources = routesFor(region).catalog.length
|
|
257
|
+
const catalogSources = catalogSourcesFor(region)
|
|
258
|
+
for (const [sourceIndex, source] of catalogSources.entries()) {
|
|
249
259
|
const key = sourceKey(source)
|
|
260
|
+
const servedBy = <T>(data: T): T => {
|
|
261
|
+
if (sourceIndex >= ownSources) {
|
|
262
|
+
logEvent('warn', 'registry', `catalog: the ${region} sources did not answer; read the catalog package from ${key} instead (#891)`)
|
|
263
|
+
}
|
|
264
|
+
return data
|
|
265
|
+
}
|
|
250
266
|
// Two attempts each. A catalog fetch crossing a long, lossy path fails
|
|
251
267
|
// transiently often enough that one retry is worth more than the second
|
|
252
268
|
// or two it costs — and with nothing behind this call any more, a
|
|
@@ -263,11 +279,11 @@ export async function loadRegistry(region: Region = activeRegion()): Promise<Reg
|
|
|
263
279
|
source.registry, source.pkg, reusable?.version ?? undefined,
|
|
264
280
|
)
|
|
265
281
|
// `data === null` means the published version is the one in hand.
|
|
266
|
-
if (data === null && reusable !== null) return reusable.data
|
|
282
|
+
if (data === null && reusable !== null) return servedBy(reusable.data)
|
|
267
283
|
if (data === null) throw new Error('the catalog package reported no change with nothing to reuse')
|
|
268
284
|
const parsed = asRegistry(data)
|
|
269
285
|
served = { key, etag: null, modified: null, version, data: parsed }
|
|
270
|
-
return parsed
|
|
286
|
+
return servedBy(parsed)
|
|
271
287
|
}
|
|
272
288
|
// ETag first: it is exact, while a date has one-second resolution and
|
|
273
289
|
// a catalog republished twice within the same second would validate
|
|
@@ -300,7 +316,7 @@ export async function loadRegistry(region: Region = activeRegion()): Promise<Reg
|
|
|
300
316
|
// Guarded anyway: answering a 304 with nothing to reuse would
|
|
301
317
|
// otherwise surface as a confusing parse error on an empty body.
|
|
302
318
|
if (reusable === null) throw new Error('the catalog answered "not modified" with nothing to revalidate')
|
|
303
|
-
return reusable.data
|
|
319
|
+
return servedBy(reusable.data)
|
|
304
320
|
}
|
|
305
321
|
if (!res.ok) throw new Error(`HTTP ${String(res.status)}`)
|
|
306
322
|
const data = asRegistry(JSON.parse(new TextDecoder().decode(
|
|
@@ -309,7 +325,7 @@ export async function loadRegistry(region: Region = activeRegion()): Promise<Reg
|
|
|
309
325
|
served = {
|
|
310
326
|
key, etag: res.headers.get('etag'), modified: res.headers.get('last-modified'), version: null, data,
|
|
311
327
|
}
|
|
312
|
-
return data
|
|
328
|
+
return servedBy(data)
|
|
313
329
|
} catch (error) {
|
|
314
330
|
last = error
|
|
315
331
|
failures.push({
|
package/src/verify.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { join } from 'node:path'
|
|
|
34
34
|
import { listHotMounts, parseSimplePatch } from './hot.ts'
|
|
35
35
|
import { userPatchPackageReferences } from './patch.ts'
|
|
36
36
|
import { nameMatchesPackage } from './entry-identity.ts'
|
|
37
|
-
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.ts'
|
|
37
|
+
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, pluginSubdirs, profileDir, readInstalled } from './profile.ts'
|
|
38
38
|
import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.ts'
|
|
39
39
|
import { dshHostInfo } from './dsh-install.ts'
|
|
40
40
|
|
|
@@ -498,19 +498,33 @@ export function verifyActivation(
|
|
|
498
498
|
hot: false,
|
|
499
499
|
}
|
|
500
500
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
501
|
+
if (inBundles) {
|
|
502
|
+
return {
|
|
503
|
+
state: 'restart',
|
|
504
|
+
reasons: ['由你自己的 cordis.patch.yml 按名加载,重启后生效 / loaded by name from your own cordis.patch.yml — live after a restart'],
|
|
505
|
+
bundle: true,
|
|
506
|
+
hot: false,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// A collection repository's ROOT package says nothing about itself and
|
|
510
|
+
// holds the plugins one level down (#892). Installed whole — which the
|
|
511
|
+
// market used to do before it learned to retarget one, and which a user
|
|
512
|
+
// can still do by hand — it lands on this list as "installed, not in
|
|
513
|
+
// effect" with no way out of that sentence. Name the plugins it does
|
|
514
|
+
// carry and the way to install one: the dead end is the bug.
|
|
515
|
+
const carried = pluginSubdirs(dir)
|
|
516
|
+
const shown = carried.slice(0, 3).join('、')
|
|
517
|
+
const more = carried.length > 3 ? `等 ${carried.length} 个` : ''
|
|
518
|
+
const shownEn = carried.slice(0, 3).join(', ')
|
|
519
|
+
const moreEn = carried.length > 3 ? ` and ${carried.length - 3} more` : ''
|
|
520
|
+
return {
|
|
521
|
+
state: 'inert',
|
|
522
|
+
reasons: [carried.length > 0
|
|
523
|
+
? `这是集合仓库的根包,本身不是插件。可安装的子包:${shown}${more}。请装子包而不是它(仓库地址后加 #path:/子包目录) / this is a collection repository's root package, not a plugin itself; install one of its plugins instead — ${shownEn}${moreEn} — by adding #path:/<subdirectory> to the repository URL`
|
|
524
|
+
: '普通依赖(未声明 dsh 元数据),不是 profile 层插件;若它由某个 bundle patch 按名加载,启动后会显示为已加载 / a plain dependency with no dsh metadata — not a profile-layer plugin; if some bundle patch loads it by name it will read as live once running'],
|
|
525
|
+
bundle: false,
|
|
526
|
+
hot: false,
|
|
527
|
+
}
|
|
514
528
|
}
|
|
515
529
|
// Carrier bundles (#103) ship no entry of their own — what they mount is
|
|
516
530
|
// the point — so judge by "is anything loadable", not by this package's
|