@wendoo/bridge-app 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/app-bridge.d.ts +77 -0
- package/dist/app-bridge.d.ts.map +1 -0
- package/dist/app-bridge.js +205 -0
- package/dist/app-environment-host.d.ts +524 -0
- package/dist/app-environment-host.d.ts.map +1 -0
- package/dist/app-environment-host.js +1560 -0
- package/dist/brain-diagnostics.d.ts +47 -0
- package/dist/brain-diagnostics.d.ts.map +1 -0
- package/dist/brain-diagnostics.js +100 -0
- package/dist/bridge-project.d.ts +32 -0
- package/dist/bridge-project.d.ts.map +1 -0
- package/dist/bridge-project.js +69 -0
- package/dist/compilation.d.ts +145 -0
- package/dist/compilation.d.ts.map +1 -0
- package/dist/compilation.js +404 -0
- package/dist/core-extension.d.ts +10 -0
- package/dist/core-extension.d.ts.map +1 -0
- package/dist/core-extension.js +9 -0
- package/dist/embedded-extension-id-gate.d.ts +38 -0
- package/dist/embedded-extension-id-gate.d.ts.map +1 -0
- package/dist/embedded-extension-id-gate.js +55 -0
- package/dist/embedded-extension-loader.d.ts +33 -0
- package/dist/embedded-extension-loader.d.ts.map +1 -0
- package/dist/embedded-extension-loader.js +90 -0
- package/dist/embedded-extension-vite-plugin.d.ts +34 -0
- package/dist/embedded-extension-vite-plugin.d.ts.map +1 -0
- package/dist/embedded-extension-vite-plugin.js +39 -0
- package/dist/embedded-extensions.d.ts +293 -0
- package/dist/embedded-extensions.d.ts.map +1 -0
- package/dist/embedded-extensions.js +526 -0
- package/dist/extension-catalog.d.ts +243 -0
- package/dist/extension-catalog.d.ts.map +1 -0
- package/dist/extension-catalog.js +408 -0
- package/dist/extension-install-log.d.ts +54 -0
- package/dist/extension-install-log.d.ts.map +1 -0
- package/dist/extension-install-log.js +22 -0
- package/dist/extension-install.d.ts +162 -0
- package/dist/extension-install.d.ts.map +1 -0
- package/dist/extension-install.js +412 -0
- package/dist/extension-report-presenter.d.ts +40 -0
- package/dist/extension-report-presenter.d.ts.map +1 -0
- package/dist/extension-report-presenter.js +36 -0
- package/dist/fetched-extension-snapshots.d.ts +66 -0
- package/dist/fetched-extension-snapshots.d.ts.map +1 -0
- package/dist/fetched-extension-snapshots.js +137 -0
- package/dist/folder-host-session.d.ts +85 -0
- package/dist/folder-host-session.d.ts.map +1 -0
- package/dist/folder-host-session.js +210 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/library-offer.d.ts +60 -0
- package/dist/library-offer.d.ts.map +1 -0
- package/dist/library-offer.js +47 -0
- package/dist/library-uninstall-guard.d.ts +66 -0
- package/dist/library-uninstall-guard.d.ts.map +1 -0
- package/dist/library-uninstall-guard.js +103 -0
- package/dist/manifest-files.d.ts +53 -0
- package/dist/manifest-files.d.ts.map +1 -0
- package/dist/manifest-files.js +242 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +2 -0
- package/dist/project-file-bridge.d.ts +10 -0
- package/dist/project-file-bridge.d.ts.map +1 -0
- package/dist/project-file-bridge.js +87 -0
- package/dist/user-tile-registration.d.ts +89 -0
- package/dist/user-tile-registration.d.ts.map +1 -0
- package/dist/user-tile-registration.js +100 -0
- package/dist/vfs-asset-url-provider.d.ts +22 -0
- package/dist/vfs-asset-url-provider.d.ts.map +1 -0
- package/dist/vfs-asset-url-provider.js +52 -0
- package/dist/workspace-folder-project-store.d.ts +125 -0
- package/dist/workspace-folder-project-store.d.ts.map +1 -0
- package/dist/workspace-folder-project-store.js +400 -0
- package/package.json +103 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { applyCatalogMove, CatalogMoveApplyErrorCode, fileContentText, isAbbreviatedCommitPin, LOWEST_CONTENT_VERSION, parseCatalogMoveReference, parseExtensionReference, parseProjectContentManifest, WENDOO_JSON_PATH, } from "@wendoo/app-host";
|
|
2
|
+
/** Stable identifiers for non-fatal catalog-move findings surfaced during resolution and closure walks. */
|
|
3
|
+
export const CatalogMoveWarningCode = {
|
|
4
|
+
/** More than one move entry captures the reference; the move is not applied. */
|
|
5
|
+
AMBIGUOUS_CAPTURE: CatalogMoveApplyErrorCode.AMBIGUOUS_CAPTURE,
|
|
6
|
+
/** The move application revisited a reference shape; the move is not applied. */
|
|
7
|
+
CYCLE: CatalogMoveApplyErrorCode.CYCLE,
|
|
8
|
+
/** A floating destination could not be resolved to a pinned version. */
|
|
9
|
+
FLOATING_UNRESOLVED: "CATALOG_MOVE_FLOATING_UNRESOLVED",
|
|
10
|
+
/** A version-range selector could not be evaluated: the reference's version is undeterminable. */
|
|
11
|
+
VERSION_UNKNOWN: "CATALOG_MOVE_VERSION_UNKNOWN",
|
|
12
|
+
/** The moved destination's content is not available; the move is not applied. */
|
|
13
|
+
FETCH_FAILED: "CATALOG_MOVE_FETCH_FAILED",
|
|
14
|
+
/** The project's stored brains could not be read, so a rename cannot rewrite their namespaces; the move is not applied. */
|
|
15
|
+
BRAINS_UNREADABLE: "CATALOG_MOVE_BRAINS_UNREADABLE",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A dependency cycle in a project's extension graph. Resolution fails with this
|
|
19
|
+
* error; the graph names the origins on the cycle in traversal order.
|
|
20
|
+
*/
|
|
21
|
+
export class ExtensionResolutionCycleError extends Error {
|
|
22
|
+
/** The origins on the cycle, in the order traversed, with the first origin repeated at the end. */
|
|
23
|
+
cycle;
|
|
24
|
+
constructor(cycle) {
|
|
25
|
+
super(`Extension dependency cycle: ${cycle.join(" -> ")}`);
|
|
26
|
+
this.name = "ExtensionResolutionCycleError";
|
|
27
|
+
this.cycle = cycle;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Compare two semver strings by their release triple; a higher triple compares greater. */
|
|
31
|
+
function compareSemver(a, b) {
|
|
32
|
+
const pa = a.split("-")[0].split(".").map(Number);
|
|
33
|
+
const pb = b.split("-")[0].split(".").map(Number);
|
|
34
|
+
for (let i = 0; i < 3; i++) {
|
|
35
|
+
const va = pa[i] ?? 0;
|
|
36
|
+
const vb = pb[i] ?? 0;
|
|
37
|
+
if (Number.isNaN(va) || Number.isNaN(vb) || va !== vb) {
|
|
38
|
+
return (Number.isNaN(va) ? 0 : va) - (Number.isNaN(vb) ? 0 : vb);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
/** Build the origin-relative file map for an embedded extension, with leading-slash paths. */
|
|
44
|
+
function embeddedFiles(extension) {
|
|
45
|
+
const files = new Map();
|
|
46
|
+
for (const file of extension.files) {
|
|
47
|
+
files.set(file.path.startsWith("/") ? file.path : `/${file.path}`, file.content);
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read an extension's own display name, version, extensions list, declared
|
|
53
|
+
* ambient `.d.ts` paths, and declared identity from the `wendoo.json`
|
|
54
|
+
* carried in its content. An extension without a manifest, or with an
|
|
55
|
+
* unparseable one, contributes no dependencies and no ambient files, compares
|
|
56
|
+
* as `0.0.0`, and displays as its coordinate.
|
|
57
|
+
*/
|
|
58
|
+
function readOwnManifest(origin, files) {
|
|
59
|
+
const manifestEntry = files.get(`/${WENDOO_JSON_PATH}`) ?? files.get(WENDOO_JSON_PATH);
|
|
60
|
+
const manifestContent = manifestEntry === undefined ? undefined : fileContentText(manifestEntry);
|
|
61
|
+
if (manifestContent === undefined) {
|
|
62
|
+
return { name: origin, version: LOWEST_CONTENT_VERSION, extensions: {}, targets: [], ambient: [] };
|
|
63
|
+
}
|
|
64
|
+
const parsed = parseProjectContentManifest(manifestContent);
|
|
65
|
+
if (!parsed.ok) {
|
|
66
|
+
return { name: origin, version: LOWEST_CONTENT_VERSION, extensions: {}, targets: [], ambient: [] };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
name: parsed.manifest.name,
|
|
70
|
+
version: parsed.manifest.version,
|
|
71
|
+
extensions: parsed.manifest.extensions,
|
|
72
|
+
targets: Object.keys(parsed.manifest.targets ?? {}),
|
|
73
|
+
ambient: parsed.manifest.ambient ?? [],
|
|
74
|
+
...(parsed.manifest.identity !== undefined ? { identity: parsed.manifest.identity } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Build an origin candidate for `origin` from its content files at the given depth. */
|
|
78
|
+
function candidateFromFiles(origin, reference, depth, files) {
|
|
79
|
+
const own = readOwnManifest(origin, files);
|
|
80
|
+
return {
|
|
81
|
+
origin,
|
|
82
|
+
version: own.version,
|
|
83
|
+
name: own.name,
|
|
84
|
+
reference,
|
|
85
|
+
depth,
|
|
86
|
+
files,
|
|
87
|
+
extensions: own.extensions,
|
|
88
|
+
targets: own.targets,
|
|
89
|
+
ambient: own.ambient,
|
|
90
|
+
...(own.identity !== undefined ? { identity: own.identity } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build a {@link CatalogMoveVersionLookup} over an embed record and a
|
|
95
|
+
* reference-keyed content getter. An `embedded:` reference answers with its
|
|
96
|
+
* embed entry's manifest version; a `gh:` reference answers with the version
|
|
97
|
+
* of the manifest in the content the getter returns for it. A reference whose
|
|
98
|
+
* content is unavailable, or whose manifest is missing or unparseable, answers
|
|
99
|
+
* `undefined`.
|
|
100
|
+
*
|
|
101
|
+
* @param options.embedded - The host application's bundled embedded extensions.
|
|
102
|
+
* @param options.contentForReference - Origin-relative text files (leading-slash paths) for a `gh:` reference.
|
|
103
|
+
*/
|
|
104
|
+
export function createCatalogMoveVersionLookup(options) {
|
|
105
|
+
const byCoordinate = new Map(options.embedded.map((extension) => [extension.canonicalOrigin, extension]));
|
|
106
|
+
const cache = new Map();
|
|
107
|
+
const manifestVersion = (files) => {
|
|
108
|
+
const entry = files.get(`/${WENDOO_JSON_PATH}`) ?? files.get(WENDOO_JSON_PATH);
|
|
109
|
+
const manifestContent = entry === undefined ? undefined : fileContentText(entry);
|
|
110
|
+
if (manifestContent === undefined) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const parsed = parseProjectContentManifest(manifestContent);
|
|
114
|
+
return parsed.ok ? parsed.manifest.version : undefined;
|
|
115
|
+
};
|
|
116
|
+
return (reference) => {
|
|
117
|
+
const cached = cache.get(reference);
|
|
118
|
+
if (cached !== undefined) {
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
let version;
|
|
122
|
+
const parsed = parseExtensionReference(reference);
|
|
123
|
+
if (parsed?.transport === "embedded") {
|
|
124
|
+
const extension = byCoordinate.get(parsed.coordinate);
|
|
125
|
+
version = extension !== undefined ? manifestVersion(embeddedFiles(extension)) : undefined;
|
|
126
|
+
}
|
|
127
|
+
else if (parsed?.transport === "gh") {
|
|
128
|
+
const files = options.contentForReference(reference);
|
|
129
|
+
version = files !== undefined ? manifestVersion(files) : undefined;
|
|
130
|
+
}
|
|
131
|
+
// An undetermined version is recomputed on the next call: content can
|
|
132
|
+
// arrive between calls during a fetch walk.
|
|
133
|
+
if (version !== undefined) {
|
|
134
|
+
cache.set(reference, version);
|
|
135
|
+
}
|
|
136
|
+
return version;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve one reference string against the content sources into an origin
|
|
141
|
+
* candidate at the given depth. An `embedded:` reference resolves through the
|
|
142
|
+
* embed record; a `gh:` reference resolves through the fetched content map.
|
|
143
|
+
* A reference whose source has no content for it yields `undefined`.
|
|
144
|
+
*/
|
|
145
|
+
function candidateForReference(reference, depth, byCoordinate, fetched) {
|
|
146
|
+
const parsed = parseExtensionReference(reference);
|
|
147
|
+
if (parsed === undefined) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
if (parsed.transport === "embedded") {
|
|
151
|
+
const extension = byCoordinate.get(parsed.coordinate);
|
|
152
|
+
if (extension === undefined) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return candidateFromFiles(extension.canonicalOrigin, reference, depth, embeddedFiles(extension));
|
|
156
|
+
}
|
|
157
|
+
if (parsed.transport === "gh") {
|
|
158
|
+
const files = fetched?.get(reference);
|
|
159
|
+
if (files === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return candidateFromFiles(`${parsed.owner}/${parsed.repo}`, reference, depth, files);
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a declared target coordinate to the reference string its content is
|
|
168
|
+
* reached through: an `embedded:<coordinate>` reference when the embed record
|
|
169
|
+
* carries it, otherwise the registry's pinned `gh:` reference for the
|
|
170
|
+
* coordinate. Returns `undefined` when neither source names the coordinate.
|
|
171
|
+
*/
|
|
172
|
+
function referenceForTargetCoordinate(coordinate, byCoordinate, targetRegistry) {
|
|
173
|
+
if (byCoordinate.has(coordinate)) {
|
|
174
|
+
return `embedded:${coordinate}`;
|
|
175
|
+
}
|
|
176
|
+
return targetRegistry?.get(coordinate);
|
|
177
|
+
}
|
|
178
|
+
/** Build the warning for a declared target coordinate that resolves to no content. */
|
|
179
|
+
function unresolvedTargetWarning(coordinate) {
|
|
180
|
+
return {
|
|
181
|
+
kind: "unresolved-target",
|
|
182
|
+
origin: coordinate,
|
|
183
|
+
message: `Target "${coordinate}" resolves to no content: it is not in the host embed record, not pinned in the ` +
|
|
184
|
+
"target registry, or its pinned content is not available. Its platform stack does not materialize.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Unify an incoming candidate for an origin against the incumbent already
|
|
189
|
+
* resolved for it, selecting the winner and reporting any conflict. The higher
|
|
190
|
+
* semantic version wins and records a `version-conflict` warning; at an equal
|
|
191
|
+
* version, a differing reference nearest the host project (smaller depth) wins
|
|
192
|
+
* and records a `reference-tiebreak` warning; an identical version and
|
|
193
|
+
* reference unify onto the incumbent with no warning.
|
|
194
|
+
*/
|
|
195
|
+
export function unifyOriginCandidate(incoming, incumbent) {
|
|
196
|
+
const versionOrder = compareSemver(incoming.version, incumbent.version);
|
|
197
|
+
if (versionOrder > 0) {
|
|
198
|
+
return { winner: incoming, warning: versionConflict(incoming, incumbent) };
|
|
199
|
+
}
|
|
200
|
+
if (versionOrder < 0) {
|
|
201
|
+
return { winner: incumbent, warning: versionConflict(incumbent, incoming) };
|
|
202
|
+
}
|
|
203
|
+
if (incoming.reference !== incumbent.reference) {
|
|
204
|
+
// Equal versions, different references: the reference nearest the host
|
|
205
|
+
// project wins. A strictly-shallower newcomer replaces the incumbent.
|
|
206
|
+
const winner = incoming.depth < incumbent.depth ? incoming : incumbent;
|
|
207
|
+
const loser = winner === incoming ? incumbent : incoming;
|
|
208
|
+
return { winner, warning: referenceTiebreak(winner, loser) };
|
|
209
|
+
}
|
|
210
|
+
return { winner: incumbent, warning: undefined };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Resolve a project's extensions list against its content sources into
|
|
214
|
+
* compiler dependency inputs. Resolution is transitive: an extension's own
|
|
215
|
+
* `wendoo.json` extensions resolve recursively. Each origin appears once in
|
|
216
|
+
* the closure regardless of how many dependents reference it, so references to
|
|
217
|
+
* an origin's published types across a diamond unify on one registration.
|
|
218
|
+
*
|
|
219
|
+
* When the same origin is required at two different versions, the higher
|
|
220
|
+
* semantic version is selected and a `version-conflict` warning names both
|
|
221
|
+
* requesters; when the versions are equal but the references differ, the
|
|
222
|
+
* reference nearest the host project wins and a `reference-tiebreak` warning is
|
|
223
|
+
* emitted. A fetched origin whose winning pin is plausibly an abbreviated
|
|
224
|
+
* commit SHA raises an `abbreviated-pin` warning, and one whose content
|
|
225
|
+
* declares a manifest identity differing from its coordinate raises an
|
|
226
|
+
* `identity-mismatch` warning. A reference whose source carries no content for
|
|
227
|
+
* it is skipped; an unresolved import surfaces later as an ordinary compiler
|
|
228
|
+
* diagnostic.
|
|
229
|
+
*
|
|
230
|
+
* A project's declared compatibility targets are additional closure roots, and
|
|
231
|
+
* at every node the union of its `extensions` and `targets` edges is recursed.
|
|
232
|
+
* A target coordinate resolves to `embedded:<coordinate>` when the
|
|
233
|
+
* embed record carries it, otherwise to the target registry's pinned `gh:`
|
|
234
|
+
* reference; a target that resolves to no content raises an `unresolved-target`
|
|
235
|
+
* warning. A coordinate reached through both an extension edge and a target
|
|
236
|
+
* edge unifies onto one resolved origin. Every origin reached through a target
|
|
237
|
+
* edge, at any depth, is listed in `dependencies`, so the project's own files
|
|
238
|
+
* may `@lib`-import any layer of the materialized platform stack. A
|
|
239
|
+
* catalog-moved edge whose moved content is not available raises a
|
|
240
|
+
* stable-coded `catalog-move-failed` warning and stays unresolved this load.
|
|
241
|
+
*
|
|
242
|
+
* @param extensions - The project's extensions map, keyed by coordinate; each value is a reference string.
|
|
243
|
+
* @param sources - The content sources references resolve against.
|
|
244
|
+
* @param projectTargets - The project's declared compatibility targets, keyed by target coordinate; each is recursed as a closure root.
|
|
245
|
+
* @throws {ExtensionResolutionCycleError} when the extension graph contains a
|
|
246
|
+
* dependency cycle.
|
|
247
|
+
*/
|
|
248
|
+
export function resolveProjectExtensions(extensions, sources, projectTargets) {
|
|
249
|
+
const targetCoordinates = Object.keys(projectTargets ?? {});
|
|
250
|
+
if (!extensions && targetCoordinates.length === 0) {
|
|
251
|
+
return { dependencies: [], dependencyMounts: [], origins: [], warnings: [] };
|
|
252
|
+
}
|
|
253
|
+
const byCoordinate = new Map(sources.embedded.map((extension) => [extension.canonicalOrigin, extension]));
|
|
254
|
+
const fetched = sources.fetched;
|
|
255
|
+
const moves = sources.moves ?? {};
|
|
256
|
+
const targetRegistry = sources.targetRegistry;
|
|
257
|
+
const warnings = [];
|
|
258
|
+
const warnedTargets = new Set();
|
|
259
|
+
const warnedMoves = new Set();
|
|
260
|
+
const versionLookup = createCatalogMoveVersionLookup({
|
|
261
|
+
embedded: sources.embedded,
|
|
262
|
+
contentForReference: (reference) => fetched?.get(reference),
|
|
263
|
+
});
|
|
264
|
+
/** Record one catalog-move finding per (reference, code). */
|
|
265
|
+
const pushMoveWarning = (declaredReference, code, message) => {
|
|
266
|
+
const key = `${declaredReference}\u0000${code}`;
|
|
267
|
+
if (warnedMoves.has(key)) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
warnedMoves.add(key);
|
|
271
|
+
warnings.push({
|
|
272
|
+
kind: "catalog-move-failed",
|
|
273
|
+
origin: parseCatalogMoveReference(declaredReference)?.coordinate ?? declaredReference,
|
|
274
|
+
reference: declaredReference,
|
|
275
|
+
code,
|
|
276
|
+
message,
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Redirect one dependency edge through the catalog moves. A floating result
|
|
281
|
+
* resolves to its pin, and the moves re-apply from the pin, until the
|
|
282
|
+
* reference is concrete and uncaptured. Returns the reference to resolve,
|
|
283
|
+
* marked whether a move rewrote it, or `undefined` when the edge cannot
|
|
284
|
+
* resolve this load (a floating destination with no pinned content yet).
|
|
285
|
+
*/
|
|
286
|
+
const redirectEdge = (declaredReference) => {
|
|
287
|
+
let reference = declaredReference;
|
|
288
|
+
let moved = false;
|
|
289
|
+
const seenPins = new Set();
|
|
290
|
+
while (true) {
|
|
291
|
+
const applied = applyCatalogMove(reference, moves, versionLookup);
|
|
292
|
+
if (!applied.ok) {
|
|
293
|
+
pushMoveWarning(declaredReference, applied.code, applied.message);
|
|
294
|
+
return { reference: declaredReference, moved: false };
|
|
295
|
+
}
|
|
296
|
+
if (applied.moved) {
|
|
297
|
+
moved = true;
|
|
298
|
+
}
|
|
299
|
+
reference = applied.reference;
|
|
300
|
+
const parts = parseCatalogMoveReference(reference);
|
|
301
|
+
if (!parts?.floating) {
|
|
302
|
+
return { reference, moved };
|
|
303
|
+
}
|
|
304
|
+
const pin = sources.floatingPins?.get(parts.coordinate.toLowerCase());
|
|
305
|
+
if (pin === undefined) {
|
|
306
|
+
pushMoveWarning(declaredReference, CatalogMoveWarningCode.FLOATING_UNRESOLVED, `Catalog move for "${declaredReference}" resolves to the floating "${reference}" and no pinned content ` +
|
|
307
|
+
"is available yet; the dependency stays unresolved this load.");
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
if (seenPins.has(pin)) {
|
|
311
|
+
pushMoveWarning(declaredReference, CatalogMoveWarningCode.CYCLE, `Catalog moves for "${declaredReference}" revisited the pinned "${pin}".`);
|
|
312
|
+
return { reference: declaredReference, moved: false };
|
|
313
|
+
}
|
|
314
|
+
seenPins.add(pin);
|
|
315
|
+
reference = pin;
|
|
316
|
+
moved = true;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
// The coordinates the project's own files may import: each direct
|
|
320
|
+
// dependency's `<owner>/<repo>` coordinate plus every origin reached through
|
|
321
|
+
// a target edge. The coordinate is derived from identity, never from the
|
|
322
|
+
// manifest key. A coordinate reached through several edges is listed once.
|
|
323
|
+
const directDependencies = [];
|
|
324
|
+
const seenDirect = new Set();
|
|
325
|
+
const addDirect = (coordinate) => {
|
|
326
|
+
if (!seenDirect.has(coordinate)) {
|
|
327
|
+
seenDirect.add(coordinate);
|
|
328
|
+
directDependencies.push({ coordinate });
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Resolve one declared target coordinate at `depth` into a candidate,
|
|
333
|
+
* recording an `unresolved-target` warning (once per coordinate) when it
|
|
334
|
+
* resolves to no content.
|
|
335
|
+
*/
|
|
336
|
+
const resolveTargetEdge = (coordinate, depth) => {
|
|
337
|
+
const reference = referenceForTargetCoordinate(coordinate, byCoordinate, targetRegistry);
|
|
338
|
+
const child = reference !== undefined ? candidateForReference(reference, depth, byCoordinate, fetched) : undefined;
|
|
339
|
+
if (child === undefined) {
|
|
340
|
+
if (!warnedTargets.has(coordinate)) {
|
|
341
|
+
warnedTargets.add(coordinate);
|
|
342
|
+
warnings.push(unresolvedTargetWarning(coordinate));
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
return child;
|
|
347
|
+
};
|
|
348
|
+
const rootCandidates = [];
|
|
349
|
+
for (const reference of Object.values(extensions ?? {})) {
|
|
350
|
+
const candidate = candidateForReference(reference, 0, byCoordinate, fetched);
|
|
351
|
+
if (candidate === undefined) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
addDirect(candidate.origin);
|
|
355
|
+
rootCandidates.push(candidate);
|
|
356
|
+
}
|
|
357
|
+
for (const coordinate of targetCoordinates) {
|
|
358
|
+
const candidate = resolveTargetEdge(coordinate, 0);
|
|
359
|
+
if (candidate === undefined) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
addDirect(candidate.origin);
|
|
363
|
+
rootCandidates.push(candidate);
|
|
364
|
+
}
|
|
365
|
+
const resolved = new Map();
|
|
366
|
+
/** Unify a candidate into the resolved set, selecting the winner and recording any conflict warning. */
|
|
367
|
+
const unify = (candidate) => {
|
|
368
|
+
const existing = resolved.get(candidate.origin);
|
|
369
|
+
if (existing === undefined) {
|
|
370
|
+
resolved.set(candidate.origin, { candidate, dependencies: [] });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const { winner, warning } = unifyOriginCandidate(candidate, existing.candidate);
|
|
374
|
+
if (warning !== undefined) {
|
|
375
|
+
warnings.push(warning);
|
|
376
|
+
}
|
|
377
|
+
if (winner !== existing.candidate) {
|
|
378
|
+
resolved.set(candidate.origin, { candidate: winner, dependencies: existing.dependencies });
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
// Breadth-first: an origin's shallower reference unifies before any deeper
|
|
382
|
+
// reference to the same origin. The nearest-the-root tie-break reads this order.
|
|
383
|
+
for (const candidate of rootCandidates) {
|
|
384
|
+
unify(candidate);
|
|
385
|
+
}
|
|
386
|
+
const queue = [...rootCandidates];
|
|
387
|
+
const expanded = new Set();
|
|
388
|
+
while (queue.length > 0) {
|
|
389
|
+
const candidate = queue.shift();
|
|
390
|
+
const winner = resolved.get(candidate.origin);
|
|
391
|
+
// Expand an origin's own edges once, off its winning candidate: the union
|
|
392
|
+
// of its extension edges and its target edges.
|
|
393
|
+
if (winner.candidate === candidate && !expanded.has(candidate.origin)) {
|
|
394
|
+
expanded.add(candidate.origin);
|
|
395
|
+
for (const declaredReference of Object.values(candidate.extensions)) {
|
|
396
|
+
const redirected = redirectEdge(declaredReference);
|
|
397
|
+
if (redirected === undefined) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const child = candidateForReference(redirected.reference, candidate.depth + 1, byCoordinate, fetched);
|
|
401
|
+
if (child === undefined) {
|
|
402
|
+
if (redirected.moved) {
|
|
403
|
+
pushMoveWarning(declaredReference, CatalogMoveWarningCode.FETCH_FAILED, `Moved content "${redirected.reference}" is not available; the dependency stays unresolved this load.`);
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
winner.dependencies.push({ coordinate: child.origin });
|
|
408
|
+
unify(child);
|
|
409
|
+
queue.push(child);
|
|
410
|
+
}
|
|
411
|
+
for (const targetCoordinate of candidate.targets) {
|
|
412
|
+
const child = resolveTargetEdge(targetCoordinate, candidate.depth + 1);
|
|
413
|
+
if (child === undefined) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
winner.dependencies.push({ coordinate: child.origin });
|
|
417
|
+
addDirect(child.origin);
|
|
418
|
+
unify(child);
|
|
419
|
+
queue.push(child);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
detectCycle(directDependencies, resolved);
|
|
424
|
+
const dependencyMounts = [];
|
|
425
|
+
const origins = [];
|
|
426
|
+
for (const { candidate, dependencies } of resolved.values()) {
|
|
427
|
+
dependencyMounts.push({
|
|
428
|
+
namespace: candidate.origin,
|
|
429
|
+
files: candidate.files,
|
|
430
|
+
dependencies,
|
|
431
|
+
...(candidate.ambient.length > 0 ? { ambient: candidate.ambient } : {}),
|
|
432
|
+
});
|
|
433
|
+
origins.push({
|
|
434
|
+
origin: candidate.origin,
|
|
435
|
+
reference: candidate.reference,
|
|
436
|
+
version: candidate.version,
|
|
437
|
+
name: candidate.name,
|
|
438
|
+
});
|
|
439
|
+
warnings.push(...fetchedCandidateWarnings(candidate));
|
|
440
|
+
}
|
|
441
|
+
return { dependencies: directDependencies, dependencyMounts, origins, warnings };
|
|
442
|
+
}
|
|
443
|
+
/** The pin-form and identity warnings a fetched origin's winning candidate raises. */
|
|
444
|
+
function fetchedCandidateWarnings(candidate) {
|
|
445
|
+
const parsed = parseExtensionReference(candidate.reference);
|
|
446
|
+
if (parsed?.transport !== "gh") {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
const found = [];
|
|
450
|
+
if (parsed.routing.kind === "pin" && isAbbreviatedCommitPin(parsed.routing.pin)) {
|
|
451
|
+
found.push({
|
|
452
|
+
kind: "abbreviated-pin",
|
|
453
|
+
origin: candidate.origin,
|
|
454
|
+
reference: candidate.reference,
|
|
455
|
+
message: `Extension "${candidate.origin}" is pinned at "${parsed.routing.pin}", which looks like an abbreviated ` +
|
|
456
|
+
"commit SHA; the content CDN serves it with mutable branch semantics. " +
|
|
457
|
+
"Only the full 40-character SHA is an immutable pin.",
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (candidate.identity !== undefined && candidate.identity !== candidate.origin) {
|
|
461
|
+
found.push({
|
|
462
|
+
kind: "identity-mismatch",
|
|
463
|
+
origin: candidate.origin,
|
|
464
|
+
reference: candidate.reference,
|
|
465
|
+
declaredIdentity: candidate.identity,
|
|
466
|
+
message: `Extension "${candidate.origin}" (via "${candidate.reference}") declares the identity ` +
|
|
467
|
+
`"${candidate.identity}"; the content may have been forked or renamed upstream. ` +
|
|
468
|
+
`Resolution proceeds under "${candidate.origin}".`,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return found;
|
|
472
|
+
}
|
|
473
|
+
/** Build a version-conflict warning where `winner` was selected over `loser`. */
|
|
474
|
+
function versionConflict(winner, loser) {
|
|
475
|
+
return {
|
|
476
|
+
kind: "version-conflict",
|
|
477
|
+
origin: winner.origin,
|
|
478
|
+
selectedReference: winner.reference,
|
|
479
|
+
rejectedReference: loser.reference,
|
|
480
|
+
selectedVersion: winner.version,
|
|
481
|
+
rejectedVersion: loser.version,
|
|
482
|
+
message: `Extension "${winner.origin}" is required at ${winner.version} (via "${winner.reference}") ` +
|
|
483
|
+
`and ${loser.version} (via "${loser.reference}"); selecting ${winner.version}.`,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/** Build a reference tie-break warning where `winner` was selected over `loser` at an equal version. */
|
|
487
|
+
function referenceTiebreak(winner, loser) {
|
|
488
|
+
return {
|
|
489
|
+
kind: "reference-tiebreak",
|
|
490
|
+
origin: winner.origin,
|
|
491
|
+
selectedReference: winner.reference,
|
|
492
|
+
rejectedReference: loser.reference,
|
|
493
|
+
message: `Extension "${winner.origin}" is required at ${winner.version} through two references ` +
|
|
494
|
+
`("${winner.reference}" and "${loser.reference}"); selecting the one nearest the project ("${winner.reference}").`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Reject a dependency cycle among the resolved origins. Walks the graph from the
|
|
499
|
+
* project's direct dependencies; a back-edge to an origin already on the active
|
|
500
|
+
* path is a cycle.
|
|
501
|
+
*/
|
|
502
|
+
function detectCycle(directDependencies, resolved) {
|
|
503
|
+
const visited = new Set();
|
|
504
|
+
const onPath = new Set();
|
|
505
|
+
const path = [];
|
|
506
|
+
const visit = (origin) => {
|
|
507
|
+
if (onPath.has(origin)) {
|
|
508
|
+
const start = path.indexOf(origin);
|
|
509
|
+
throw new ExtensionResolutionCycleError([...path.slice(start), origin]);
|
|
510
|
+
}
|
|
511
|
+
if (visited.has(origin)) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
visited.add(origin);
|
|
515
|
+
onPath.add(origin);
|
|
516
|
+
path.push(origin);
|
|
517
|
+
for (const dependency of resolved.get(origin)?.dependencies ?? []) {
|
|
518
|
+
visit(dependency.coordinate);
|
|
519
|
+
}
|
|
520
|
+
path.pop();
|
|
521
|
+
onPath.delete(origin);
|
|
522
|
+
};
|
|
523
|
+
for (const dependency of directDependencies) {
|
|
524
|
+
visit(dependency.coordinate);
|
|
525
|
+
}
|
|
526
|
+
}
|