@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.
Files changed (77) hide show
  1. package/README.md +44 -0
  2. package/dist/app-bridge.d.ts +77 -0
  3. package/dist/app-bridge.d.ts.map +1 -0
  4. package/dist/app-bridge.js +205 -0
  5. package/dist/app-environment-host.d.ts +524 -0
  6. package/dist/app-environment-host.d.ts.map +1 -0
  7. package/dist/app-environment-host.js +1560 -0
  8. package/dist/brain-diagnostics.d.ts +47 -0
  9. package/dist/brain-diagnostics.d.ts.map +1 -0
  10. package/dist/brain-diagnostics.js +100 -0
  11. package/dist/bridge-project.d.ts +32 -0
  12. package/dist/bridge-project.d.ts.map +1 -0
  13. package/dist/bridge-project.js +69 -0
  14. package/dist/compilation.d.ts +145 -0
  15. package/dist/compilation.d.ts.map +1 -0
  16. package/dist/compilation.js +404 -0
  17. package/dist/core-extension.d.ts +10 -0
  18. package/dist/core-extension.d.ts.map +1 -0
  19. package/dist/core-extension.js +9 -0
  20. package/dist/embedded-extension-id-gate.d.ts +38 -0
  21. package/dist/embedded-extension-id-gate.d.ts.map +1 -0
  22. package/dist/embedded-extension-id-gate.js +55 -0
  23. package/dist/embedded-extension-loader.d.ts +33 -0
  24. package/dist/embedded-extension-loader.d.ts.map +1 -0
  25. package/dist/embedded-extension-loader.js +90 -0
  26. package/dist/embedded-extension-vite-plugin.d.ts +34 -0
  27. package/dist/embedded-extension-vite-plugin.d.ts.map +1 -0
  28. package/dist/embedded-extension-vite-plugin.js +39 -0
  29. package/dist/embedded-extensions.d.ts +293 -0
  30. package/dist/embedded-extensions.d.ts.map +1 -0
  31. package/dist/embedded-extensions.js +526 -0
  32. package/dist/extension-catalog.d.ts +243 -0
  33. package/dist/extension-catalog.d.ts.map +1 -0
  34. package/dist/extension-catalog.js +408 -0
  35. package/dist/extension-install-log.d.ts +54 -0
  36. package/dist/extension-install-log.d.ts.map +1 -0
  37. package/dist/extension-install-log.js +22 -0
  38. package/dist/extension-install.d.ts +162 -0
  39. package/dist/extension-install.d.ts.map +1 -0
  40. package/dist/extension-install.js +412 -0
  41. package/dist/extension-report-presenter.d.ts +40 -0
  42. package/dist/extension-report-presenter.d.ts.map +1 -0
  43. package/dist/extension-report-presenter.js +36 -0
  44. package/dist/fetched-extension-snapshots.d.ts +66 -0
  45. package/dist/fetched-extension-snapshots.d.ts.map +1 -0
  46. package/dist/fetched-extension-snapshots.js +137 -0
  47. package/dist/folder-host-session.d.ts +85 -0
  48. package/dist/folder-host-session.d.ts.map +1 -0
  49. package/dist/folder-host-session.js +210 -0
  50. package/dist/index.d.ts +38 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +19 -0
  53. package/dist/library-offer.d.ts +60 -0
  54. package/dist/library-offer.d.ts.map +1 -0
  55. package/dist/library-offer.js +47 -0
  56. package/dist/library-uninstall-guard.d.ts +66 -0
  57. package/dist/library-uninstall-guard.d.ts.map +1 -0
  58. package/dist/library-uninstall-guard.js +103 -0
  59. package/dist/manifest-files.d.ts +53 -0
  60. package/dist/manifest-files.d.ts.map +1 -0
  61. package/dist/manifest-files.js +242 -0
  62. package/dist/node.d.ts +4 -0
  63. package/dist/node.d.ts.map +1 -0
  64. package/dist/node.js +2 -0
  65. package/dist/project-file-bridge.d.ts +10 -0
  66. package/dist/project-file-bridge.d.ts.map +1 -0
  67. package/dist/project-file-bridge.js +87 -0
  68. package/dist/user-tile-registration.d.ts +89 -0
  69. package/dist/user-tile-registration.d.ts.map +1 -0
  70. package/dist/user-tile-registration.js +100 -0
  71. package/dist/vfs-asset-url-provider.d.ts +22 -0
  72. package/dist/vfs-asset-url-provider.d.ts.map +1 -0
  73. package/dist/vfs-asset-url-provider.js +52 -0
  74. package/dist/workspace-folder-project-store.d.ts +125 -0
  75. package/dist/workspace-folder-project-store.d.ts.map +1 -0
  76. package/dist/workspace-folder-project-store.js +400 -0
  77. 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
+ }