@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,243 @@
1
+ import { type ExtensionCatalogDocument, type ExtensionCatalogDocumentEntry, type ExtensionTarget } from "@wendoo/app-host";
2
+ export { satisfiesRange } from "@wendoo/app-host";
3
+ import type { EmbeddedExtension, FetchedExtensionContentMap } from "./embedded-extensions.js";
4
+ /**
5
+ * One layer of a project's platform stack: a resolved layer library's
6
+ * `<owner>/<repo>` coordinate and the semantic version its bundled content
7
+ * declares. Compatibility filtering matches an extension's declared targets
8
+ * against these entries.
9
+ */
10
+ export interface PlatformStackLayer {
11
+ /** The layer library's `<owner>/<repo>` coordinate. */
12
+ readonly coordinate: string;
13
+ /** The layer library's declared semantic version, or `0.0.0` when it declares none. */
14
+ readonly version: string;
15
+ }
16
+ /**
17
+ * One entry of the extension catalog: an embedded extension surfaced for a
18
+ * project, annotated with its manifest identity and its lifecycle state
19
+ * relative to the project.
20
+ */
21
+ export interface ExtensionCatalogEntry {
22
+ /** The extension's `<owner>/<repo>` coordinate: its identity and manifest key. */
23
+ readonly coordinate: string;
24
+ /** Display name read from the extension's content manifest; the coordinate when it declares none. */
25
+ readonly name: string;
26
+ /** Semantic version read from the extension's content manifest, or `0.0.0` when it declares none. */
27
+ readonly version: string;
28
+ /**
29
+ * Free-form description: the extension's content manifest's when it carries
30
+ * one, else the host catalog document's entry description; absent when
31
+ * neither declares one.
32
+ */
33
+ readonly description?: string;
34
+ /** Thumbnail URL or data URI read from the extension's content manifest; absent when it declares none. */
35
+ readonly thumbnailUrl?: string;
36
+ /** True when the extension is in the project's resolved extension set. */
37
+ readonly installed: boolean;
38
+ /** GitHub repository URL `https://github.com/<coordinate>`; set only for remote (`gh:`) entries, whose coordinate is their repository. */
39
+ readonly repoUrl?: string;
40
+ /** True for a fetched (`gh:`) dependency with installed content, which carries an on-request update affordance. */
41
+ readonly updatable?: boolean;
42
+ /** Present when a declared `gh:` dependency has no installed snapshot content. */
43
+ readonly broken?: {
44
+ /** Stable failure code of the most recent recorded install attempt; absent when none is recorded. */
45
+ readonly code?: string;
46
+ /** Human-readable description of the broken state. */
47
+ readonly message: string;
48
+ };
49
+ /** Present when the installed content's manifest identity differs from the coordinate its reference names. */
50
+ readonly identityMismatch?: {
51
+ /** The `<owner>/<repo>` identity the installed content's manifest declares. */
52
+ readonly declaredIdentity: string;
53
+ };
54
+ }
55
+ /** The last recorded fetch failure per reference, keyed by the reference string as written. */
56
+ export type ExtensionFetchFailures = ReadonlyMap<string, {
57
+ readonly code: string;
58
+ readonly message: string;
59
+ }>;
60
+ /** Stable identifiers for the outcome of an install or uninstall action. */
61
+ export declare const ExtensionActionResultCode: {
62
+ /** The extension reference was added to the project's extensions map. */
63
+ readonly INSTALLED: "EXTENSION_INSTALLED";
64
+ /** The extension was already in the project's extensions map; nothing changed. */
65
+ readonly ALREADY_INSTALLED: "EXTENSION_ALREADY_INSTALLED";
66
+ /** The extension reference was removed from the project's extensions map. */
67
+ readonly UNINSTALLED: "EXTENSION_UNINSTALLED";
68
+ /** The extension was not in the project's extensions map; nothing changed. */
69
+ readonly NOT_INSTALLED: "EXTENSION_NOT_INSTALLED";
70
+ /** The coordinate is a required layer library and cannot be uninstalled; nothing changed. */
71
+ readonly LOCKED: "EXTENSION_LOCKED";
72
+ /** The coordinate names no bundled extension in the embed record; nothing changed. */
73
+ readonly UNKNOWN_COORDINATE: "EXTENSION_UNKNOWN_COORDINATE";
74
+ /** The reference string is not a well-formed remote extension reference; nothing changed. */
75
+ readonly INVALID_REFERENCE: "EXTENSION_INVALID_REFERENCE";
76
+ /** Another installed extension depends on the coordinate; it cannot be uninstalled while depended upon. Nothing changed. */
77
+ readonly REQUIRED_BY_DEPENDENT: "EXTENSION_REQUIRED_BY_DEPENDENT";
78
+ };
79
+ /** Union of all {@link ExtensionActionResultCode} values. */
80
+ export type ExtensionActionResultCode = (typeof ExtensionActionResultCode)[keyof typeof ExtensionActionResultCode];
81
+ /** Outcome of an install or uninstall action against a project's extensions map. */
82
+ export interface ExtensionActionResult {
83
+ /** True when the action changed the extensions map. */
84
+ readonly ok: boolean;
85
+ /** Stable code describing what happened. */
86
+ readonly code: ExtensionActionResultCode;
87
+ /** The extensions map after the action; unchanged from the input when `ok` is false. */
88
+ readonly extensions: Readonly<Record<string, string>>;
89
+ }
90
+ /**
91
+ * Derive a project's platform stack: the resolved layer libraries the project
92
+ * sits on, each with its declared version. The stack is the intersection of the
93
+ * project's resolved extension closure with the declared platform-layer
94
+ * coordinates; a microbit-sim project resolves `core`, `wodal`, and
95
+ * `microbit-v2`, an apps/ecosim project resolves `core` and `sim`.
96
+ *
97
+ * @param extensions - The project's extensions map, keyed by coordinate.
98
+ * @param embedRecord - The host application's bundled embedded extensions.
99
+ * @param layerCoordinates - The coordinates the host declares as platform layers.
100
+ */
101
+ export declare function deriveProjectPlatformStack(extensions: Readonly<Record<string, string>> | undefined, embedRecord: readonly EmbeddedExtension[], layerCoordinates: ReadonlySet<string>): PlatformStackLayer[];
102
+ /**
103
+ * Report whether an extension declaring the given compatibility targets is
104
+ * compatible with a platform stack. Compatible when one of the extension's
105
+ * target packages is in the stack and the stack's version for that package
106
+ * satisfies the target's `packageVersion` semver range. An extension with no
107
+ * targets, or an empty targets map, is compatible with no stack.
108
+ *
109
+ * @param targets - The extension's declared compatibility targets, keyed by target package coordinate.
110
+ * @param stack - The project's platform stack.
111
+ */
112
+ export declare function isExtensionCompatible(targets: Readonly<Record<string, ExtensionTarget>> | undefined, stack: readonly PlatformStackLayer[]): boolean;
113
+ /**
114
+ * Build the entry cards for a project's extension browser: the project's direct
115
+ * dependencies that a user manages, across both transports. An embedded entry
116
+ * is listed when the map references its coordinate directly through an
117
+ * `embedded:` reference and the coordinate is not a platform layer; a remote
118
+ * (`gh:`) entry is listed for each `gh:` reference the map names. Nothing else
119
+ * lists: a platform layer library, a non-referenced bundled add-on, and a
120
+ * transitively-resolved sub-dependency are not entry cards (a bundled add-on is
121
+ * surfaced through the catalog offers instead). A remote dependency's card
122
+ * reads its name and version from its installed snapshot content and is marked
123
+ * `updatable`; a remote reference with no content available is listed under its
124
+ * coordinate as `broken`, carrying the last recorded fetch failure when one is
125
+ * known, so it can be retried or removed. A remote dependency whose installed
126
+ * manifest declares a different identity than its coordinate carries an
127
+ * `identityMismatch` annotation.
128
+ *
129
+ * @param extensions - The project's extensions map, keyed by coordinate.
130
+ * @param embedRecord - The host application's bundled embedded extensions.
131
+ * @param layerCoordinates - The coordinates the host declares as platform layers; these are never entry cards.
132
+ * @param fetched - Installed fetched-extension content, keyed by reference.
133
+ * @param fetchFailures - The last recorded fetch failure per reference.
134
+ * @param catalog - The host's catalog document; its entry descriptions back-fill a manifest that declares none.
135
+ */
136
+ export declare function buildExtensionCatalog(extensions: Readonly<Record<string, string>> | undefined, embedRecord: readonly EmbeddedExtension[], layerCoordinates: ReadonlySet<string>, fetched?: FetchedExtensionContentMap, fetchFailures?: ExtensionFetchFailures, catalog?: ExtensionCatalogDocument): ExtensionCatalogEntry[];
137
+ /**
138
+ * One catalog document entry offered to a project: the entry's display
139
+ * metadata. Only libraries the project has not yet installed are offered.
140
+ */
141
+ export interface ExtensionCatalogOffer {
142
+ /** The extension's `<owner>/<repo>` coordinate. */
143
+ readonly coordinate: string;
144
+ /** Display name from the catalog entry. */
145
+ readonly name: string;
146
+ /** Published version from the catalog entry. */
147
+ readonly version: string;
148
+ /** Description from the catalog entry. */
149
+ readonly description: string;
150
+ /** Thumbnail URL or data URI from the catalog entry; absent when it declares none. */
151
+ readonly thumbnailUrl?: string;
152
+ /** The pinned `gh:` reference an install of this offer writes. */
153
+ readonly ref: string;
154
+ }
155
+ /**
156
+ * Adapt a validated extension catalog document into per-project offers,
157
+ * compatibility-filtered against the project's platform stack: one offer per
158
+ * compatible entry the project has not already installed, rendered from the
159
+ * entry's display metadata alone. An entry whose coordinate the project's
160
+ * extensions map already carries is dropped, since it is represented instead by
161
+ * its manageable entry card. The stack is derived from the project's
162
+ * extensions, the embed record, and the layer coordinates.
163
+ *
164
+ * An offer's compatibility is judged by its reference transport. An
165
+ * `embedded:` offer is compatible when at least one target coordinate its
166
+ * embed-record manifest declares is a layer in the stack; its declared version
167
+ * range is ignored, since an embedded library versions with the host. A `gh:`
168
+ * offer is compatible when its catalog-entry targets both name a stack layer
169
+ * and satisfy that layer's version through {@link isExtensionCompatible}; a
170
+ * `gh:` offer that declares no targets is excluded, since it cannot be
171
+ * verified compatible.
172
+ *
173
+ * @param document - The validated catalog document.
174
+ * @param extensions - The project's extensions map, keyed by coordinate.
175
+ * @param embedRecord - The host application's bundled embedded extensions.
176
+ * @param layerCoordinates - The coordinates the host declares as platform layers.
177
+ */
178
+ export declare function buildExtensionCatalogOffers(document: ExtensionCatalogDocument, extensions: Readonly<Record<string, string>> | undefined, embedRecord: readonly EmbeddedExtension[], layerCoordinates: ReadonlySet<string>): ExtensionCatalogOffer[];
179
+ /**
180
+ * One approved catalog entry as the library shelf lists it: the entry's own
181
+ * curated metadata, annotated with whether the project already holds it.
182
+ */
183
+ export type ExtensionCatalogShelfEntry = Pick<ExtensionCatalogDocumentEntry, "coordinate" | "name" | "version" | "description"> & {
184
+ /** True when the project's extensions map already carries the entry's coordinate. */
185
+ readonly installed: boolean;
186
+ /** GitHub repository URL `https://github.com/<coordinate>`; set only for entries a remote (`gh:`) reference installs. */
187
+ readonly sourceUrl?: string;
188
+ };
189
+ /**
190
+ * Adapt a validated extension catalog document into the shelf a project can be
191
+ * told about: every entry the project already holds, marked installed, plus
192
+ * every offer {@link buildExtensionCatalogOffers} finds compatible with the
193
+ * project's platform stack, marked not installed. Entries run in document
194
+ * order, and an entry that is neither installed nor compatible is left out. An
195
+ * entry a remote (`gh:`) reference installs carries the repository URL it is
196
+ * published at.
197
+ *
198
+ * @param document - The validated catalog document.
199
+ * @param extensions - The project's extensions map, keyed by coordinate.
200
+ * @param embedRecord - The host application's bundled embedded extensions.
201
+ * @param layerCoordinates - The coordinates the host declares as platform layers.
202
+ */
203
+ export declare function buildExtensionCatalogShelf(document: ExtensionCatalogDocument, extensions: Readonly<Record<string, string>> | undefined, embedRecord: readonly EmbeddedExtension[], layerCoordinates: ReadonlySet<string>): ExtensionCatalogShelfEntry[];
204
+ /**
205
+ * Add a remote extension to a project's extensions map from a `gh:` reference
206
+ * string, keyed by the reference's `<owner>/<repo>` coordinate. Rejects a
207
+ * string that is not a well-formed `gh:` reference, and reports an
208
+ * already-present coordinate as a no-op.
209
+ *
210
+ * @param extensions - The project's current extensions map, keyed by coordinate.
211
+ * @param reference - The `gh:<owner>/<repo>@<pin>` or `gh:<owner>/<repo>#<branch>` reference to add.
212
+ */
213
+ export declare function installExtensionReference(extensions: Readonly<Record<string, string>> | undefined, reference: string): ExtensionActionResult;
214
+ /**
215
+ * Install an embedded extension into a project's extensions map by adding an
216
+ * `embedded:<owner>/<repo>` reference keyed by its coordinate. The existing
217
+ * resolution and materialization pipeline includes it thereafter. Rejects a
218
+ * coordinate that names no bundled extension, and reports an already-present
219
+ * coordinate as a no-op.
220
+ *
221
+ * @param extensions - The project's current extensions map, keyed by coordinate.
222
+ * @param embedRecord - The host application's bundled embedded extensions.
223
+ * @param coordinate - The `<owner>/<repo>` coordinate to install.
224
+ */
225
+ export declare function installEmbeddedExtension(extensions: Readonly<Record<string, string>> | undefined, embedRecord: readonly EmbeddedExtension[], coordinate: string): ExtensionActionResult;
226
+ /**
227
+ * Uninstall an extension from a project's extensions map by removing the entry
228
+ * keyed by its coordinate, regardless of the entry's transport. The existing
229
+ * regenerate-on-load pipeline de-materializes it thereafter. Rejects a required
230
+ * platform layer library, reports an absent coordinate as a no-op, and blocks
231
+ * removing a coordinate that another still-installed extension depends on:
232
+ * after the removal that dependent would still pull the coordinate back into
233
+ * the resolved closure, so removing its explicit entry is refused with
234
+ * {@link ExtensionActionResultCode.REQUIRED_BY_DEPENDENT}.
235
+ *
236
+ * @param extensions - The project's current extensions map, keyed by coordinate.
237
+ * @param coordinate - The `<owner>/<repo>` coordinate to uninstall.
238
+ * @param layerCoordinates - The coordinates the host declares as platform layers; these are not uninstallable.
239
+ * @param embedRecord - The host application's bundled embedded extensions, used to resolve dependents.
240
+ * @param fetched - Installed fetched-extension content, keyed by reference, used to resolve dependents.
241
+ */
242
+ export declare function uninstallExtension(extensions: Readonly<Record<string, string>> | undefined, coordinate: string, layerCoordinates: ReadonlySet<string>, embedRecord: readonly EmbeddedExtension[], fetched?: FetchedExtensionContentMap): ExtensionActionResult;
243
+ //# sourceMappingURL=extension-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension-catalog.d.ts","sourceRoot":"","sources":["../src/extension-catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,408 @@
1
+ import { fileContentText, LOWEST_CONTENT_VERSION, parseExtensionReference, parseProjectContentManifest, satisfiesRange, WENDOO_JSON_PATH, } from "@wendoo/app-host";
2
+ export { satisfiesRange } from "@wendoo/app-host";
3
+ import { resolveProjectExtensions } from "./embedded-extensions.js";
4
+ /** Stable identifiers for the outcome of an install or uninstall action. */
5
+ export const ExtensionActionResultCode = {
6
+ /** The extension reference was added to the project's extensions map. */
7
+ INSTALLED: "EXTENSION_INSTALLED",
8
+ /** The extension was already in the project's extensions map; nothing changed. */
9
+ ALREADY_INSTALLED: "EXTENSION_ALREADY_INSTALLED",
10
+ /** The extension reference was removed from the project's extensions map. */
11
+ UNINSTALLED: "EXTENSION_UNINSTALLED",
12
+ /** The extension was not in the project's extensions map; nothing changed. */
13
+ NOT_INSTALLED: "EXTENSION_NOT_INSTALLED",
14
+ /** The coordinate is a required layer library and cannot be uninstalled; nothing changed. */
15
+ LOCKED: "EXTENSION_LOCKED",
16
+ /** The coordinate names no bundled extension in the embed record; nothing changed. */
17
+ UNKNOWN_COORDINATE: "EXTENSION_UNKNOWN_COORDINATE",
18
+ /** The reference string is not a well-formed remote extension reference; nothing changed. */
19
+ INVALID_REFERENCE: "EXTENSION_INVALID_REFERENCE",
20
+ /** Another installed extension depends on the coordinate; it cannot be uninstalled while depended upon. Nothing changed. */
21
+ REQUIRED_BY_DEPENDENT: "EXTENSION_REQUIRED_BY_DEPENDENT",
22
+ };
23
+ /** Read an embedded extension's manifest identity from its bundled `wendoo.json`. */
24
+ function readEmbeddedManifest(extension) {
25
+ const manifestFile = extension.files.find((file) => file.path === WENDOO_JSON_PATH || file.path === `/${WENDOO_JSON_PATH}`);
26
+ if (manifestFile === undefined) {
27
+ return { name: extension.canonicalOrigin, version: LOWEST_CONTENT_VERSION };
28
+ }
29
+ const parsed = parseProjectContentManifest(manifestFile.content);
30
+ if (!parsed.ok) {
31
+ return { name: extension.canonicalOrigin, version: LOWEST_CONTENT_VERSION };
32
+ }
33
+ return {
34
+ name: parsed.manifest.name,
35
+ version: parsed.manifest.version,
36
+ ...(parsed.manifest.description !== undefined ? { description: parsed.manifest.description } : {}),
37
+ ...(parsed.manifest.thumbnailUrl !== undefined ? { thumbnailUrl: parsed.manifest.thumbnailUrl } : {}),
38
+ ...(parsed.manifest.targets !== undefined ? { targets: parsed.manifest.targets } : {}),
39
+ };
40
+ }
41
+ /** The `<owner>/<repo>` coordinates a project's extensions map references directly through embedded references. */
42
+ function directEmbeddedCoordinates(extensions, byCoordinate) {
43
+ const direct = new Set();
44
+ for (const reference of Object.values(extensions ?? {})) {
45
+ const parsed = parseExtensionReference(reference);
46
+ if (parsed?.transport === "embedded" && byCoordinate.has(parsed.coordinate)) {
47
+ direct.add(parsed.coordinate);
48
+ }
49
+ }
50
+ return direct;
51
+ }
52
+ /** The coordinates of every origin in a project's resolved extension closure. */
53
+ function resolvedOrigins(extensions, embedRecord, fetched) {
54
+ const resolved = resolveProjectExtensions(extensions, { embedded: embedRecord, fetched });
55
+ return new Set(resolved.dependencyMounts.map((mount) => mount.namespace));
56
+ }
57
+ /** Read a fetched extension's manifest identity from its snapshot content. */
58
+ function readFetchedManifest(files) {
59
+ const entry = files.get(`/${WENDOO_JSON_PATH}`) ?? files.get(WENDOO_JSON_PATH);
60
+ const manifestContent = entry === undefined ? undefined : fileContentText(entry);
61
+ if (manifestContent === undefined) {
62
+ return undefined;
63
+ }
64
+ const parsed = parseProjectContentManifest(manifestContent);
65
+ if (!parsed.ok) {
66
+ return undefined;
67
+ }
68
+ return {
69
+ name: parsed.manifest.name,
70
+ version: parsed.manifest.version,
71
+ ...(parsed.manifest.description !== undefined ? { description: parsed.manifest.description } : {}),
72
+ ...(parsed.manifest.thumbnailUrl !== undefined ? { thumbnailUrl: parsed.manifest.thumbnailUrl } : {}),
73
+ ...(parsed.manifest.targets !== undefined ? { targets: parsed.manifest.targets } : {}),
74
+ ...(parsed.manifest.identity !== undefined ? { identity: parsed.manifest.identity } : {}),
75
+ };
76
+ }
77
+ /**
78
+ * Derive a project's platform stack: the resolved layer libraries the project
79
+ * sits on, each with its declared version. The stack is the intersection of the
80
+ * project's resolved extension closure with the declared platform-layer
81
+ * coordinates; a microbit-sim project resolves `core`, `wodal`, and
82
+ * `microbit-v2`, an apps/ecosim project resolves `core` and `sim`.
83
+ *
84
+ * @param extensions - The project's extensions map, keyed by coordinate.
85
+ * @param embedRecord - The host application's bundled embedded extensions.
86
+ * @param layerCoordinates - The coordinates the host declares as platform layers.
87
+ */
88
+ export function deriveProjectPlatformStack(extensions, embedRecord, layerCoordinates) {
89
+ const byCoordinate = new Map(embedRecord.map((extension) => [extension.canonicalOrigin, extension]));
90
+ const stack = [];
91
+ for (const origin of resolvedOrigins(extensions, embedRecord)) {
92
+ if (!layerCoordinates.has(origin)) {
93
+ continue;
94
+ }
95
+ const extension = byCoordinate.get(origin);
96
+ if (extension === undefined) {
97
+ continue;
98
+ }
99
+ stack.push({ coordinate: origin, version: readEmbeddedManifest(extension).version });
100
+ }
101
+ return stack;
102
+ }
103
+ /**
104
+ * Report whether an extension declaring the given compatibility targets is
105
+ * compatible with a platform stack. Compatible when one of the extension's
106
+ * target packages is in the stack and the stack's version for that package
107
+ * satisfies the target's `packageVersion` semver range. An extension with no
108
+ * targets, or an empty targets map, is compatible with no stack.
109
+ *
110
+ * @param targets - The extension's declared compatibility targets, keyed by target package coordinate.
111
+ * @param stack - The project's platform stack.
112
+ */
113
+ export function isExtensionCompatible(targets, stack) {
114
+ if (targets === undefined) {
115
+ return false;
116
+ }
117
+ for (const [coordinate, target] of Object.entries(targets)) {
118
+ const layer = stack.find((entry) => entry.coordinate === coordinate);
119
+ if (layer !== undefined && satisfiesRange(layer.version, target.packageVersion)) {
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ /**
126
+ * Build the entry cards for a project's extension browser: the project's direct
127
+ * dependencies that a user manages, across both transports. An embedded entry
128
+ * is listed when the map references its coordinate directly through an
129
+ * `embedded:` reference and the coordinate is not a platform layer; a remote
130
+ * (`gh:`) entry is listed for each `gh:` reference the map names. Nothing else
131
+ * lists: a platform layer library, a non-referenced bundled add-on, and a
132
+ * transitively-resolved sub-dependency are not entry cards (a bundled add-on is
133
+ * surfaced through the catalog offers instead). A remote dependency's card
134
+ * reads its name and version from its installed snapshot content and is marked
135
+ * `updatable`; a remote reference with no content available is listed under its
136
+ * coordinate as `broken`, carrying the last recorded fetch failure when one is
137
+ * known, so it can be retried or removed. A remote dependency whose installed
138
+ * manifest declares a different identity than its coordinate carries an
139
+ * `identityMismatch` annotation.
140
+ *
141
+ * @param extensions - The project's extensions map, keyed by coordinate.
142
+ * @param embedRecord - The host application's bundled embedded extensions.
143
+ * @param layerCoordinates - The coordinates the host declares as platform layers; these are never entry cards.
144
+ * @param fetched - Installed fetched-extension content, keyed by reference.
145
+ * @param fetchFailures - The last recorded fetch failure per reference.
146
+ * @param catalog - The host's catalog document; its entry descriptions back-fill a manifest that declares none.
147
+ */
148
+ export function buildExtensionCatalog(extensions, embedRecord, layerCoordinates, fetched, fetchFailures, catalog) {
149
+ const byCoordinate = new Map(embedRecord.map((extension) => [extension.canonicalOrigin, extension]));
150
+ const installed = resolvedOrigins(extensions, embedRecord, fetched);
151
+ const direct = directEmbeddedCoordinates(extensions, byCoordinate);
152
+ const description = (coordinate, manifest) => {
153
+ return manifest?.description ?? catalog?.entries.find((entry) => entry.coordinate === coordinate)?.description;
154
+ };
155
+ const entries = [];
156
+ for (const extension of embedRecord) {
157
+ const coordinate = extension.canonicalOrigin;
158
+ if (!direct.has(coordinate) || layerCoordinates.has(coordinate)) {
159
+ continue;
160
+ }
161
+ const manifest = readEmbeddedManifest(extension);
162
+ const described = description(coordinate, manifest);
163
+ entries.push({
164
+ coordinate,
165
+ name: manifest.name,
166
+ version: manifest.version,
167
+ ...(described !== undefined ? { description: described } : {}),
168
+ ...(manifest.thumbnailUrl !== undefined ? { thumbnailUrl: manifest.thumbnailUrl } : {}),
169
+ installed: installed.has(coordinate),
170
+ });
171
+ }
172
+ for (const [coordinate, reference] of Object.entries(extensions ?? {})) {
173
+ const parsed = parseExtensionReference(reference);
174
+ if (parsed?.transport !== "gh") {
175
+ continue;
176
+ }
177
+ const content = fetched?.get(reference);
178
+ const manifest = content !== undefined ? readFetchedManifest(content) : undefined;
179
+ const failure = fetchFailures?.get(reference);
180
+ const broken = content === undefined
181
+ ? {
182
+ ...(failure !== undefined ? { code: failure.code } : {}),
183
+ message: failure?.message ?? `No content is installed for "${reference}".`,
184
+ }
185
+ : undefined;
186
+ const identityMismatch = manifest?.identity !== undefined && manifest.identity !== coordinate
187
+ ? { declaredIdentity: manifest.identity }
188
+ : undefined;
189
+ const described = description(coordinate, manifest);
190
+ entries.push({
191
+ coordinate,
192
+ name: manifest?.name ?? coordinate,
193
+ version: manifest?.version ?? LOWEST_CONTENT_VERSION,
194
+ ...(described !== undefined ? { description: described } : {}),
195
+ ...(manifest?.thumbnailUrl !== undefined ? { thumbnailUrl: manifest.thumbnailUrl } : {}),
196
+ installed: installed.has(coordinate),
197
+ repoUrl: `https://github.com/${coordinate}`,
198
+ ...(content !== undefined ? { updatable: true } : {}),
199
+ ...(broken !== undefined ? { broken } : {}),
200
+ ...(identityMismatch !== undefined ? { identityMismatch } : {}),
201
+ });
202
+ }
203
+ return entries;
204
+ }
205
+ /**
206
+ * Report whether an offer's declared target coordinates place it on a platform
207
+ * stack, ignoring version ranges. True when at least one declared target
208
+ * coordinate is a layer in the stack. An offer with no targets is on no stack.
209
+ * Used for embedded offers, which version with the host and so declare only the
210
+ * target coordinate they belong to.
211
+ *
212
+ * @param targets - The offer's declared compatibility targets, keyed by target package coordinate.
213
+ * @param stack - The project's platform stack.
214
+ */
215
+ function targetsCoordinateInStack(targets, stack) {
216
+ if (targets === undefined) {
217
+ return false;
218
+ }
219
+ for (const coordinate of Object.keys(targets)) {
220
+ if (stack.some((layer) => layer.coordinate === coordinate)) {
221
+ return true;
222
+ }
223
+ }
224
+ return false;
225
+ }
226
+ /**
227
+ * Adapt a validated extension catalog document into per-project offers,
228
+ * compatibility-filtered against the project's platform stack: one offer per
229
+ * compatible entry the project has not already installed, rendered from the
230
+ * entry's display metadata alone. An entry whose coordinate the project's
231
+ * extensions map already carries is dropped, since it is represented instead by
232
+ * its manageable entry card. The stack is derived from the project's
233
+ * extensions, the embed record, and the layer coordinates.
234
+ *
235
+ * An offer's compatibility is judged by its reference transport. An
236
+ * `embedded:` offer is compatible when at least one target coordinate its
237
+ * embed-record manifest declares is a layer in the stack; its declared version
238
+ * range is ignored, since an embedded library versions with the host. A `gh:`
239
+ * offer is compatible when its catalog-entry targets both name a stack layer
240
+ * and satisfy that layer's version through {@link isExtensionCompatible}; a
241
+ * `gh:` offer that declares no targets is excluded, since it cannot be
242
+ * verified compatible.
243
+ *
244
+ * @param document - The validated catalog document.
245
+ * @param extensions - The project's extensions map, keyed by coordinate.
246
+ * @param embedRecord - The host application's bundled embedded extensions.
247
+ * @param layerCoordinates - The coordinates the host declares as platform layers.
248
+ */
249
+ export function buildExtensionCatalogOffers(document, extensions, embedRecord, layerCoordinates) {
250
+ const current = extensions ?? {};
251
+ const stack = deriveProjectPlatformStack(extensions, embedRecord, layerCoordinates);
252
+ const byCoordinate = new Map(embedRecord.map((extension) => [extension.canonicalOrigin, extension]));
253
+ const offers = [];
254
+ for (const entry of document.entries) {
255
+ if (entry.coordinate in current) {
256
+ continue;
257
+ }
258
+ const parsed = parseExtensionReference(entry.ref);
259
+ let compatible;
260
+ if (parsed?.transport === "embedded") {
261
+ const embedded = byCoordinate.get(entry.coordinate);
262
+ const targets = embedded !== undefined ? readEmbeddedManifest(embedded).targets : undefined;
263
+ compatible = targetsCoordinateInStack(targets, stack);
264
+ }
265
+ else {
266
+ compatible = isExtensionCompatible(entry.targets, stack);
267
+ }
268
+ if (!compatible) {
269
+ continue;
270
+ }
271
+ offers.push({
272
+ coordinate: entry.coordinate,
273
+ name: entry.name,
274
+ version: entry.version,
275
+ description: entry.description,
276
+ ...(entry.thumbnail !== undefined ? { thumbnailUrl: entry.thumbnail } : {}),
277
+ ref: entry.ref,
278
+ });
279
+ }
280
+ return offers;
281
+ }
282
+ /**
283
+ * Adapt a validated extension catalog document into the shelf a project can be
284
+ * told about: every entry the project already holds, marked installed, plus
285
+ * every offer {@link buildExtensionCatalogOffers} finds compatible with the
286
+ * project's platform stack, marked not installed. Entries run in document
287
+ * order, and an entry that is neither installed nor compatible is left out. An
288
+ * entry a remote (`gh:`) reference installs carries the repository URL it is
289
+ * published at.
290
+ *
291
+ * @param document - The validated catalog document.
292
+ * @param extensions - The project's extensions map, keyed by coordinate.
293
+ * @param embedRecord - The host application's bundled embedded extensions.
294
+ * @param layerCoordinates - The coordinates the host declares as platform layers.
295
+ */
296
+ export function buildExtensionCatalogShelf(document, extensions, embedRecord, layerCoordinates) {
297
+ const current = extensions ?? {};
298
+ const offered = new Map(buildExtensionCatalogOffers(document, extensions, embedRecord, layerCoordinates).map((offer) => [
299
+ offer.coordinate,
300
+ offer,
301
+ ]));
302
+ const shelf = [];
303
+ for (const entry of document.entries) {
304
+ const installed = entry.coordinate in current;
305
+ const listed = installed ? entry : offered.get(entry.coordinate);
306
+ if (listed === undefined) {
307
+ continue;
308
+ }
309
+ const remote = parseExtensionReference(listed.ref)?.transport === "gh";
310
+ shelf.push({
311
+ coordinate: listed.coordinate,
312
+ name: listed.name,
313
+ version: listed.version,
314
+ description: listed.description,
315
+ installed,
316
+ ...(remote ? { sourceUrl: `https://github.com/${listed.coordinate}` } : {}),
317
+ });
318
+ }
319
+ return shelf;
320
+ }
321
+ /**
322
+ * Add a remote extension to a project's extensions map from a `gh:` reference
323
+ * string, keyed by the reference's `<owner>/<repo>` coordinate. Rejects a
324
+ * string that is not a well-formed `gh:` reference, and reports an
325
+ * already-present coordinate as a no-op.
326
+ *
327
+ * @param extensions - The project's current extensions map, keyed by coordinate.
328
+ * @param reference - The `gh:<owner>/<repo>@<pin>` or `gh:<owner>/<repo>#<branch>` reference to add.
329
+ */
330
+ export function installExtensionReference(extensions, reference) {
331
+ const current = extensions ?? {};
332
+ const parsed = parseExtensionReference(reference.trim());
333
+ if (parsed?.transport !== "gh") {
334
+ return { ok: false, code: ExtensionActionResultCode.INVALID_REFERENCE, extensions: current };
335
+ }
336
+ const coordinate = `${parsed.owner}/${parsed.repo}`;
337
+ if (coordinate in current) {
338
+ return { ok: false, code: ExtensionActionResultCode.ALREADY_INSTALLED, extensions: current };
339
+ }
340
+ const next = {};
341
+ for (const [key, value] of Object.entries(current)) {
342
+ next[key] = value;
343
+ }
344
+ next[coordinate] = reference.trim();
345
+ return { ok: true, code: ExtensionActionResultCode.INSTALLED, extensions: next };
346
+ }
347
+ /**
348
+ * Install an embedded extension into a project's extensions map by adding an
349
+ * `embedded:<owner>/<repo>` reference keyed by its coordinate. The existing
350
+ * resolution and materialization pipeline includes it thereafter. Rejects a
351
+ * coordinate that names no bundled extension, and reports an already-present
352
+ * coordinate as a no-op.
353
+ *
354
+ * @param extensions - The project's current extensions map, keyed by coordinate.
355
+ * @param embedRecord - The host application's bundled embedded extensions.
356
+ * @param coordinate - The `<owner>/<repo>` coordinate to install.
357
+ */
358
+ export function installEmbeddedExtension(extensions, embedRecord, coordinate) {
359
+ const current = extensions ?? {};
360
+ const known = embedRecord.some((extension) => extension.canonicalOrigin === coordinate);
361
+ if (!known) {
362
+ return { ok: false, code: ExtensionActionResultCode.UNKNOWN_COORDINATE, extensions: current };
363
+ }
364
+ if (coordinate in current) {
365
+ return { ok: false, code: ExtensionActionResultCode.ALREADY_INSTALLED, extensions: current };
366
+ }
367
+ const next = {};
368
+ for (const [key, value] of Object.entries(current)) {
369
+ next[key] = value;
370
+ }
371
+ next[coordinate] = `embedded:${coordinate}`;
372
+ return { ok: true, code: ExtensionActionResultCode.INSTALLED, extensions: next };
373
+ }
374
+ /**
375
+ * Uninstall an extension from a project's extensions map by removing the entry
376
+ * keyed by its coordinate, regardless of the entry's transport. The existing
377
+ * regenerate-on-load pipeline de-materializes it thereafter. Rejects a required
378
+ * platform layer library, reports an absent coordinate as a no-op, and blocks
379
+ * removing a coordinate that another still-installed extension depends on:
380
+ * after the removal that dependent would still pull the coordinate back into
381
+ * the resolved closure, so removing its explicit entry is refused with
382
+ * {@link ExtensionActionResultCode.REQUIRED_BY_DEPENDENT}.
383
+ *
384
+ * @param extensions - The project's current extensions map, keyed by coordinate.
385
+ * @param coordinate - The `<owner>/<repo>` coordinate to uninstall.
386
+ * @param layerCoordinates - The coordinates the host declares as platform layers; these are not uninstallable.
387
+ * @param embedRecord - The host application's bundled embedded extensions, used to resolve dependents.
388
+ * @param fetched - Installed fetched-extension content, keyed by reference, used to resolve dependents.
389
+ */
390
+ export function uninstallExtension(extensions, coordinate, layerCoordinates, embedRecord, fetched) {
391
+ const current = extensions ?? {};
392
+ if (layerCoordinates.has(coordinate)) {
393
+ return { ok: false, code: ExtensionActionResultCode.LOCKED, extensions: current };
394
+ }
395
+ if (!(coordinate in current)) {
396
+ return { ok: false, code: ExtensionActionResultCode.NOT_INSTALLED, extensions: current };
397
+ }
398
+ const next = {};
399
+ for (const [key, value] of Object.entries(current)) {
400
+ if (key !== coordinate) {
401
+ next[key] = value;
402
+ }
403
+ }
404
+ if (resolvedOrigins(next, embedRecord, fetched).has(coordinate)) {
405
+ return { ok: false, code: ExtensionActionResultCode.REQUIRED_BY_DEPENDENT, extensions: current };
406
+ }
407
+ return { ok: true, code: ExtensionActionResultCode.UNINSTALLED, extensions: next };
408
+ }