@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,1560 @@
|
|
|
1
|
+
import { AppHostError, AppHostErrorCode, appHostError, applyCatalogMove, checkExtensionReferenceUpdate, collectUnstableDependencies, diffWendooJsonToManifest, ExtensionFetchErrorCode, fetchExtensionSnapshot, fileContentText, highestListedRelease, parseCatalogMoveReference, parseExtensionAddInput, parseProjectContentManifest, resolveExtensionAddInput, syncManifestToWendooJson, WENDOO_JSON_PATH, } from "@wendoo/app-host";
|
|
2
|
+
import { createWendooEnvironment, Dict, encodePersistedBrainJson, logger, renameBrainNamespaces, } from "@wendoo/core/app";
|
|
3
|
+
import { augmentProjectFileSystem, createBridgeProject, createProjectCompiler } from "./compilation.js";
|
|
4
|
+
import { CatalogMoveWarningCode, createCatalogMoveVersionLookup, ExtensionResolutionCycleError, resolveProjectExtensions, } from "./embedded-extensions.js";
|
|
5
|
+
import { collectExtensionFetchClosure, diffProjectDiagnostics, floatingPinsFromSnapshots, movedClosureHasMissingContent, typecheckBrainProblems, } from "./extension-install.js";
|
|
6
|
+
import { appendExtensionInstallLog, EXTENSION_INSTALL_LOG_APP_DATA_KEY, parseExtensionInstallLog, } from "./extension-install-log.js";
|
|
7
|
+
import { decodeInstalledSnapshotFiles, fetchedContentFromSnapshots, INSTALLED_EXTENSIONS_APP_DATA_KEY, installedExtensionMetadataFromSnapshots, parseInstalledExtensionSnapshots, serializeInstalledExtensionSnapshots, } from "./fetched-extension-snapshots.js";
|
|
8
|
+
import { applyCompiledUserTiles, collectTileSourceCompileErrors } from "./user-tile-registration.js";
|
|
9
|
+
// Project app-data keys.
|
|
10
|
+
const BRAINS_APP_DATA_KEY = "brains";
|
|
11
|
+
/**
|
|
12
|
+
* Build the `BRAIN_RECORD_UNREADABLE` error for a stored brain record that
|
|
13
|
+
* could not be read or parsed, carrying `cause`'s text as detail.
|
|
14
|
+
*/
|
|
15
|
+
function brainRecordUnreadable(cause) {
|
|
16
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
17
|
+
return appHostError(AppHostErrorCode.BRAIN_RECORD_UNREADABLE, `Stored brains could not be read: ${detail}`);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the `EXTENSION_RECORD_UNREADABLE` error for a stored installed-extension
|
|
21
|
+
* record that could not be read, carrying `cause`'s text as detail.
|
|
22
|
+
*/
|
|
23
|
+
function extensionRecordUnreadable(cause) {
|
|
24
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
25
|
+
return appHostError(AppHostErrorCode.EXTENSION_RECORD_UNREADABLE, `Stored installed libraries could not be read: ${detail}`);
|
|
26
|
+
}
|
|
27
|
+
const NO_COMPILE_DIAGNOSTICS = [];
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// AppEnvironmentHost
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* Glue layer that wires a {@link ProjectManager}, a {@link WendooEnvironment},
|
|
33
|
+
* the project compiler, user-tile registration, and (optionally) the bridge
|
|
34
|
+
* into a single host an app UI can drive.
|
|
35
|
+
*/
|
|
36
|
+
export class AppEnvironmentHost {
|
|
37
|
+
env;
|
|
38
|
+
projectManager;
|
|
39
|
+
mounts;
|
|
40
|
+
embeddedExtensions;
|
|
41
|
+
extensionFetchTransport;
|
|
42
|
+
catalogMoves;
|
|
43
|
+
approvedCatalogEntry;
|
|
44
|
+
onDidCompileCallback;
|
|
45
|
+
// -- Installed fetched-extension snapshots (persisted in the project store) --
|
|
46
|
+
_installedSnapshots = {};
|
|
47
|
+
_installedContent = new Map();
|
|
48
|
+
// -- Latest resolved extension closure of the active project --
|
|
49
|
+
_lastResolution;
|
|
50
|
+
_resolutionWarningsListeners = new Set();
|
|
51
|
+
// -- Last recorded fetch failure per reference (seeded from the install log) --
|
|
52
|
+
_fetchFailures = new Map();
|
|
53
|
+
// -- Latest workspace compile result --
|
|
54
|
+
_lastCompileResult;
|
|
55
|
+
// -- Per-tile error compile diagnostics and source path, keyed by ActionKey; rebuilt from the latest compile --
|
|
56
|
+
_tileCompileDiagnostics = new Map();
|
|
57
|
+
// -- Brain cache --
|
|
58
|
+
_brainCache = new Map();
|
|
59
|
+
_defaultBrainCache = new Map();
|
|
60
|
+
// -- First stored-record read failure of the active project's load --
|
|
61
|
+
_projectRecordFailure;
|
|
62
|
+
// -- Brain rebuild coordination --
|
|
63
|
+
_pendingBrainRebuild = false;
|
|
64
|
+
// -- Doc / VFS revision counters (useSyncExternalStore pattern) --
|
|
65
|
+
_docRevision = 0;
|
|
66
|
+
_vfsRevision = 0;
|
|
67
|
+
_docRevisionListeners = new Set();
|
|
68
|
+
_vfsRevisionListeners = new Set();
|
|
69
|
+
// -- Brain-diagnostics revision counter (useSyncExternalStore pattern) --
|
|
70
|
+
_brainDiagnosticsRevision = 0;
|
|
71
|
+
_brainDiagnosticsListeners = new Set();
|
|
72
|
+
// -- Latest workspace-compile diagnostics (useSyncExternalStore pattern) --
|
|
73
|
+
_compileDiagnostics = NO_COMPILE_DIAGNOSTICS;
|
|
74
|
+
_compileDiagnosticsListeners = new Set();
|
|
75
|
+
// -- Project lifecycle --
|
|
76
|
+
_projectUnloadingListeners = new Set();
|
|
77
|
+
_projectLoadedListeners = new Set();
|
|
78
|
+
// -- Bridge --
|
|
79
|
+
_bridge;
|
|
80
|
+
_bridgeUrl;
|
|
81
|
+
_bridgeStatus = "disconnected";
|
|
82
|
+
_bridgeJoinCode;
|
|
83
|
+
_bridgeStatusListeners = new Set();
|
|
84
|
+
_bridgeJoinCodeListeners = new Set();
|
|
85
|
+
_bridgeStateUnsub;
|
|
86
|
+
_remoteChangeUnsub;
|
|
87
|
+
_loadBindingToken;
|
|
88
|
+
_saveBindingToken;
|
|
89
|
+
// -- User tile metadata (last known) --
|
|
90
|
+
_lastUserTileMetadata;
|
|
91
|
+
// -- Compilation --
|
|
92
|
+
_compiler;
|
|
93
|
+
// -- Served file system (raw project files plus compiler-controlled files) --
|
|
94
|
+
_servedFileSystem;
|
|
95
|
+
// -- Compiler-controlled file set changes --
|
|
96
|
+
_compilerControlledFilesListeners = new Set();
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.projectManager = options.projectManager;
|
|
99
|
+
this.mounts = options.mounts;
|
|
100
|
+
this.embeddedExtensions = options.embeddedExtensions ?? [];
|
|
101
|
+
this.extensionFetchTransport = options.extensionFetchTransport;
|
|
102
|
+
this.catalogMoves = options.catalogMoves ?? {};
|
|
103
|
+
this.approvedCatalogEntry = options.approvedCatalogEntry;
|
|
104
|
+
this.onDidCompileCallback = options.onDidCompile;
|
|
105
|
+
this._bridgeUrl = options.bridgeUrl;
|
|
106
|
+
this._loadBindingToken = options.loadBindingToken ?? (() => undefined);
|
|
107
|
+
this._saveBindingToken = options.saveBindingToken ?? (() => { });
|
|
108
|
+
this.env = createWendooEnvironment({
|
|
109
|
+
modules: [...options.modules],
|
|
110
|
+
rng: options.rng,
|
|
111
|
+
numerics: options.numerics,
|
|
112
|
+
localizer: options.localizer,
|
|
113
|
+
});
|
|
114
|
+
this.env.onBrainsInvalidated((event) => {
|
|
115
|
+
if (event.invalidatedBrains.length > 0) {
|
|
116
|
+
this._pendingBrainRebuild = true;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Project file system
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
get projectFileSystem() {
|
|
124
|
+
return this.projectManager.activeProject.filesystem;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The file system whose exported snapshot carries both the raw project files
|
|
128
|
+
* and the compiler-controlled files (ambient declarations, `tsconfig.json`,
|
|
129
|
+
* and the installed-extensions tree), including extension-owned assets such
|
|
130
|
+
* as tile icons. Each `exportSnapshot()` reads the live compiler, so
|
|
131
|
+
* installing or uninstalling an extension is reflected without a rebuild.
|
|
132
|
+
* Falls back to the raw project file system until the compiler is wired.
|
|
133
|
+
*/
|
|
134
|
+
get servedProjectFileSystem() {
|
|
135
|
+
return this._servedFileSystem ?? this.projectFileSystem;
|
|
136
|
+
}
|
|
137
|
+
get activeProjectManifest() {
|
|
138
|
+
return this.projectManager.activeProject?.manifest;
|
|
139
|
+
}
|
|
140
|
+
/** Release bridge and project-manager resources owned by this host. */
|
|
141
|
+
dispose() {
|
|
142
|
+
this.teardownBridge();
|
|
143
|
+
this.projectManager.dispose();
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Initialize
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
async initialize(defaultProjectName) {
|
|
149
|
+
await this.projectManager.init();
|
|
150
|
+
const state = await this.projectManager.getProjectCollectionState();
|
|
151
|
+
if (state.access === "locked") {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await this.projectManager.ensureDefaultProject(defaultProjectName);
|
|
155
|
+
await this.initCompiler();
|
|
156
|
+
this.absorbCatalogMovesOutcome(await this.applyCatalogMoves());
|
|
157
|
+
await this.loadBrainsFromProject();
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Compilation (always available, independent of bridge)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the active project's extension dependency graph against the host's
|
|
164
|
+
* embed record and the project's stored fetched snapshots. Logs any conflict
|
|
165
|
+
* warnings. On this load path a dependency cycle is logged and resolution
|
|
166
|
+
* falls back to no extension dependencies, so the project still loads and
|
|
167
|
+
* unresolved imports surface as ordinary compiler diagnostics; an install
|
|
168
|
+
* transaction refuses on a cycle instead.
|
|
169
|
+
*/
|
|
170
|
+
resolveExtensions() {
|
|
171
|
+
try {
|
|
172
|
+
const resolved = resolveProjectExtensions(this.projectManager.activeProject.manifest.extensions, {
|
|
173
|
+
embedded: this.embeddedExtensions,
|
|
174
|
+
fetched: this._installedContent,
|
|
175
|
+
moves: this.catalogMoves,
|
|
176
|
+
floatingPins: floatingPinsFromSnapshots(this._installedSnapshots),
|
|
177
|
+
}, this.projectManager.activeProject.manifest.targets);
|
|
178
|
+
for (const warning of resolved.warnings) {
|
|
179
|
+
logger.warn(`[extension-resolution] ${warning.message}`);
|
|
180
|
+
}
|
|
181
|
+
return resolved;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (err instanceof ExtensionResolutionCycleError) {
|
|
185
|
+
logger.warn(`[extension-resolution] ${err.message}`);
|
|
186
|
+
return { dependencies: [], dependencyMounts: [], origins: [], warnings: [] };
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Replace the project's installed snapshot records, in memory and in the project store. */
|
|
192
|
+
async persistInstalledSnapshots(snapshots) {
|
|
193
|
+
this._installedSnapshots = snapshots;
|
|
194
|
+
this._installedContent = fetchedContentFromSnapshots(snapshots);
|
|
195
|
+
await this.projectManager.saveAppData(INSTALLED_EXTENSIONS_APP_DATA_KEY, serializeInstalledExtensionSnapshots(snapshots));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Read the active project's stored extension records: the installed
|
|
199
|
+
* snapshots and the install log. Both are empty when the project has stored
|
|
200
|
+
* neither. Returns undefined when the store could not serve them, recording
|
|
201
|
+
* the failure on {@link AppEnvironmentHost.projectRecordFailure}. On
|
|
202
|
+
* undefined the caller must leave the stored records untouched.
|
|
203
|
+
*/
|
|
204
|
+
async readStoredExtensionRecordsForLoad() {
|
|
205
|
+
try {
|
|
206
|
+
const snapshots = parseInstalledExtensionSnapshots(await this.projectManager.loadAppData(INSTALLED_EXTENSIONS_APP_DATA_KEY));
|
|
207
|
+
const log = parseExtensionInstallLog(await this.projectManager.loadAppData(EXTENSION_INSTALL_LOG_APP_DATA_KEY));
|
|
208
|
+
return { snapshots, log };
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const failure = extensionRecordUnreadable(err);
|
|
212
|
+
logger.warn("Failed to load installed-library records:", failure);
|
|
213
|
+
this._projectRecordFailure ??= failure;
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async initCompiler() {
|
|
218
|
+
// The installed-extensions tree regenerates from the stored snapshots;
|
|
219
|
+
// loading a project never reaches the network.
|
|
220
|
+
const stored = await this.readStoredExtensionRecordsForLoad();
|
|
221
|
+
this._installedSnapshots = stored?.snapshots ?? {};
|
|
222
|
+
this._installedContent = fetchedContentFromSnapshots(this._installedSnapshots);
|
|
223
|
+
this._fetchFailures = new Map();
|
|
224
|
+
for (const event of stored?.log ?? []) {
|
|
225
|
+
if (event.kind === "fetch-refusal") {
|
|
226
|
+
this._fetchFailures.set(event.reference, { code: event.code, message: event.message });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const resolution = this.resolveExtensions();
|
|
230
|
+
this.setLastResolution(resolution);
|
|
231
|
+
const { dependencies, dependencyMounts } = resolution;
|
|
232
|
+
this._compiler = createProjectCompiler({
|
|
233
|
+
environment: this.env,
|
|
234
|
+
filesystem: this.projectFileSystem,
|
|
235
|
+
projectNamespace: this.projectManager.activeProject.manifest.id,
|
|
236
|
+
mounts: this.mounts,
|
|
237
|
+
dependencies,
|
|
238
|
+
dependencyMounts,
|
|
239
|
+
onDidCompile: (result) => {
|
|
240
|
+
this._lastCompileResult = result;
|
|
241
|
+
this.setCompileDiagnostics(result);
|
|
242
|
+
this.persistMintedActionIds(result.projectResult.sourceRewrites);
|
|
243
|
+
logWorkspaceCompile(result);
|
|
244
|
+
const tileResult = applyCompiledUserTiles(this.env, result);
|
|
245
|
+
if (tileResult) {
|
|
246
|
+
this._lastUserTileMetadata = tileResult.metadata;
|
|
247
|
+
this.bumpDocRevision();
|
|
248
|
+
if (tileResult.changedActionKeys.length > 0) {
|
|
249
|
+
// A changed action bundle can make a previously unbuildable brain
|
|
250
|
+
// buildable, including one born invalidated because its action was
|
|
251
|
+
// missing at creation. Schedule the rebuild flush to retry the
|
|
252
|
+
// invalidated set even when this compile invalidated no live brain.
|
|
253
|
+
this._pendingBrainRebuild = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.onDidCompileCallback?.(result, tileResult);
|
|
257
|
+
// A compile can change a tile's broken/clean status, which the per-brain
|
|
258
|
+
// diagnostics feed reads through the tile-compile map. Refresh the feed's
|
|
259
|
+
// subscribers so the brain-list badge tracks the newest compile.
|
|
260
|
+
this.bumpBrainDiagnosticsRevision();
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
this._servedFileSystem = augmentProjectFileSystem(this.projectFileSystem, this._compiler.compiler, {
|
|
264
|
+
onCompilerControlledFilesChanged: (files) => {
|
|
265
|
+
for (const listener of this._compilerControlledFilesListeners) {
|
|
266
|
+
listener(files);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
syncManifestToWendooJson(this.projectFileSystem, this.projectManager.activeProject.manifest);
|
|
271
|
+
this._compiler.initialize();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Persist source files whose user-action declaration had a stable `id` minted
|
|
275
|
+
* during compilation. Writes the updated text to the project file system and
|
|
276
|
+
* to the compiler's in-memory view.
|
|
277
|
+
*/
|
|
278
|
+
persistMintedActionIds(sourceRewrites) {
|
|
279
|
+
if (sourceRewrites.size === 0) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
for (const [path, content] of sourceRewrites) {
|
|
283
|
+
const newEtag = `idgen-${Date.now()}`;
|
|
284
|
+
this.projectFileSystem.applyLocalChange({ action: "write", path, content, newEtag });
|
|
285
|
+
this._compiler?.compiler.applyWorkspaceChange({ action: "write", path, content, newEtag });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Brain persistence (keyed by app-defined string)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
async saveBrainForKey(key, brainDef) {
|
|
292
|
+
this._brainCache.set(key, brainDef);
|
|
293
|
+
const record = await this.loadBrainRecord();
|
|
294
|
+
record[key] = this.serializeBrainForStorage(brainDef);
|
|
295
|
+
await this.projectManager.saveAppData(BRAINS_APP_DATA_KEY, JSON.stringify(record));
|
|
296
|
+
this.bumpBrainDiagnosticsRevision();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Serialize a brain into its persisted form: identifiers qualified by the
|
|
300
|
+
* active project's namespace are stored with the namespace absent.
|
|
301
|
+
*/
|
|
302
|
+
serializeBrainForStorage(brainDef) {
|
|
303
|
+
return encodePersistedBrainJson(brainDef, this.projectManager.activeProject.manifest.id);
|
|
304
|
+
}
|
|
305
|
+
async removeBrain(key) {
|
|
306
|
+
this._brainCache.delete(key);
|
|
307
|
+
const record = await this.loadBrainRecord();
|
|
308
|
+
delete record[key];
|
|
309
|
+
await this.projectManager.saveAppData(BRAINS_APP_DATA_KEY, JSON.stringify(record));
|
|
310
|
+
}
|
|
311
|
+
async loadBrainFromProject(key) {
|
|
312
|
+
const record = await this.loadBrainRecord();
|
|
313
|
+
const json = record[key];
|
|
314
|
+
if (!json)
|
|
315
|
+
return undefined;
|
|
316
|
+
return this.deserializeBrainForKey(key, json);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Returns the active project's in-memory brain for `key`, or undefined when none is cached. The
|
|
320
|
+
* cache is loaded on project load and updated by `saveBrainForKey` and `removeBrain`.
|
|
321
|
+
*/
|
|
322
|
+
getCachedBrain(key) {
|
|
323
|
+
return this._brainCache.get(key);
|
|
324
|
+
}
|
|
325
|
+
/** Returns the keys of the active project's cached brains, in cache order. */
|
|
326
|
+
getCachedBrainKeys() {
|
|
327
|
+
return [...this._brainCache.keys()];
|
|
328
|
+
}
|
|
329
|
+
setDefaultBrain(key, brainDef) {
|
|
330
|
+
this._defaultBrainCache.set(key, brainDef);
|
|
331
|
+
}
|
|
332
|
+
getDefaultBrain(key) {
|
|
333
|
+
return this._defaultBrainCache.get(key);
|
|
334
|
+
}
|
|
335
|
+
async saveAllBrains() {
|
|
336
|
+
// Overlay the cache onto the stored record: an entry that failed to
|
|
337
|
+
// deserialize on load has no cache slot, and its stored bytes must survive.
|
|
338
|
+
const record = await this.loadBrainRecord();
|
|
339
|
+
for (const [key, def] of this._brainCache) {
|
|
340
|
+
record[key] = this.serializeBrainForStorage(def);
|
|
341
|
+
}
|
|
342
|
+
await this.projectManager.saveAppData(BRAINS_APP_DATA_KEY, JSON.stringify(record));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Read the active project's stored brain record together with the raw stored
|
|
346
|
+
* text it was parsed from. Both are empty -- `raw` undefined, `record` `{}` --
|
|
347
|
+
* when the project has never stored a record. Throws {@link AppHostError} with
|
|
348
|
+
* code `BRAIN_RECORD_UNREADABLE` -- and no other error -- when a stored record
|
|
349
|
+
* exists but cannot be read or parsed.
|
|
350
|
+
*/
|
|
351
|
+
async readStoredBrains() {
|
|
352
|
+
let raw;
|
|
353
|
+
try {
|
|
354
|
+
raw = await this.projectManager.loadAppData(BRAINS_APP_DATA_KEY);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
throw brainRecordUnreadable(err);
|
|
358
|
+
}
|
|
359
|
+
if (!raw)
|
|
360
|
+
return { raw, record: {} };
|
|
361
|
+
try {
|
|
362
|
+
return { raw, record: JSON.parse(raw) };
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
throw brainRecordUnreadable(err);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Read the active project's stored brain record. Returns an empty record when
|
|
370
|
+
* the project has never stored one. Throws {@link AppHostError} with code
|
|
371
|
+
* `BRAIN_RECORD_UNREADABLE` -- and no other error -- when a stored record
|
|
372
|
+
* exists but cannot be read or parsed.
|
|
373
|
+
*/
|
|
374
|
+
async loadBrainRecord() {
|
|
375
|
+
return (await this.readStoredBrains()).record;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Read the stored brains on a project-load path. Returns the record and the
|
|
379
|
+
* raw stored text, or undefined when they could not be read, recording the
|
|
380
|
+
* failure on {@link AppEnvironmentHost.projectRecordFailure}. On undefined the
|
|
381
|
+
* caller must leave the stored brains untouched.
|
|
382
|
+
*/
|
|
383
|
+
async readStoredBrainsForLoad() {
|
|
384
|
+
try {
|
|
385
|
+
return await this.readStoredBrains();
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
if (!(err instanceof AppHostError)) {
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
logger.warn("Failed to load brain record:", err);
|
|
392
|
+
this._projectRecordFailure ??= err;
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The first failure that stopped one of the active project's stored records
|
|
398
|
+
* from loading, or undefined when they all loaded. Its `code` names the
|
|
399
|
+
* record: `BRAIN_RECORD_UNREADABLE` for the saved brains,
|
|
400
|
+
* `EXTENSION_RECORD_UNREADABLE` for the installed-library snapshots and the
|
|
401
|
+
* install log. While it is set the brain cache is empty because a record
|
|
402
|
+
* could not be read, not because the project has no brains, every brain
|
|
403
|
+
* write refuses, and no write replaces a record that could not be read.
|
|
404
|
+
* Cleared when the project unloads.
|
|
405
|
+
*/
|
|
406
|
+
get projectRecordFailure() {
|
|
407
|
+
return this._projectRecordFailure;
|
|
408
|
+
}
|
|
409
|
+
/** True while the active project's stored extension records could not be read. */
|
|
410
|
+
get extensionRecordUnreadable() {
|
|
411
|
+
return this._projectRecordFailure?.code === AppHostErrorCode.EXTENSION_RECORD_UNREADABLE;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Deserialize the project's stored brains into the cache. Call after the
|
|
415
|
+
* load's catalog moves have applied: deserialization resolves saved tile
|
|
416
|
+
* references against the current action bundle. Caches nothing when the load
|
|
417
|
+
* already recorded a stored-record failure: the extension closure is then
|
|
418
|
+
* unknown, so no saved tile reference resolves.
|
|
419
|
+
*/
|
|
420
|
+
async loadBrainsFromProject() {
|
|
421
|
+
if (this._projectRecordFailure) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const stored = await this.readStoredBrainsForLoad();
|
|
425
|
+
if (!stored) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
for (const [key, json] of Object.entries(stored.record)) {
|
|
429
|
+
const def = this.deserializeBrainForKey(key, json);
|
|
430
|
+
if (def) {
|
|
431
|
+
this._brainCache.set(key, def);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Reconcile the in-memory brain cache against the project store's current
|
|
437
|
+
* brain record: cached brains absent from the record are dropped, and
|
|
438
|
+
* record entries whose stored form differs from the cached brain are
|
|
439
|
+
* deserialized into the cache. Returns the changed and removed brain keys.
|
|
440
|
+
*/
|
|
441
|
+
async reconcileBrainsFromStore() {
|
|
442
|
+
const record = await this.loadBrainRecord();
|
|
443
|
+
const changed = [];
|
|
444
|
+
const removed = [];
|
|
445
|
+
for (const key of [...this._brainCache.keys()]) {
|
|
446
|
+
if (!(key in record)) {
|
|
447
|
+
this._brainCache.delete(key);
|
|
448
|
+
removed.push(key);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const [key, json] of Object.entries(record)) {
|
|
452
|
+
const cached = this._brainCache.get(key);
|
|
453
|
+
if (cached && JSON.stringify(json) === JSON.stringify(this.serializeBrainForStorage(cached))) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const def = this.deserializeBrainForKey(key, json);
|
|
457
|
+
if (!def) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
this._brainCache.set(key, def);
|
|
461
|
+
changed.push(key);
|
|
462
|
+
}
|
|
463
|
+
return { changed, removed };
|
|
464
|
+
}
|
|
465
|
+
deserializeBrainForKey(key, json) {
|
|
466
|
+
try {
|
|
467
|
+
const brainDef = this.env.deserializeBrainJsonFromPlain(json, this.projectManager.activeProject.manifest.id);
|
|
468
|
+
if (brainDef.pages().size() === 0) {
|
|
469
|
+
brainDef.appendNewPage();
|
|
470
|
+
}
|
|
471
|
+
// Establish the stored typecheck state consumers of the loaded brain
|
|
472
|
+
// read (badges, diagnostics baselines).
|
|
473
|
+
typecheckBrainProblems(brainDef);
|
|
474
|
+
return brainDef;
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
logger.warn(`Failed to load brain "${key}":`, err);
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Re-resolve every cached brain's tile references against the current
|
|
483
|
+
* catalogs by round-tripping it through its persisted form -- the same
|
|
484
|
+
* resolution a project load performs, so a tile whose library left the
|
|
485
|
+
* closure becomes a placeholder and a placeholder whose library returned
|
|
486
|
+
* becomes the real tile again. A brain whose round-trip fails keeps its
|
|
487
|
+
* current definition.
|
|
488
|
+
*/
|
|
489
|
+
refreshBrainCache() {
|
|
490
|
+
for (const [key, brainDef] of [...this._brainCache]) {
|
|
491
|
+
const refreshed = this.deserializeBrainForKey(key, this.serializeBrainForStorage(brainDef));
|
|
492
|
+
if (refreshed) {
|
|
493
|
+
this._brainCache.set(key, refreshed);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Project metadata
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
async updateProjectMetadata(updates) {
|
|
501
|
+
await this.projectManager.updateActive(updates);
|
|
502
|
+
syncManifestToWendooJson(this.projectFileSystem, this.projectManager.activeProject.manifest);
|
|
503
|
+
}
|
|
504
|
+
/** The installed fetched-extension content available to the active project, keyed by reference. */
|
|
505
|
+
get installedExtensionContent() {
|
|
506
|
+
return this._installedContent;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* The compiler-controlled file set of the active project's workspace: the
|
|
510
|
+
* generated `tsconfig.json`, ambient declarations, and the materialized
|
|
511
|
+
* installed-extensions tree. Undefined until the compiler is wired.
|
|
512
|
+
*/
|
|
513
|
+
getCompilerControlledFiles() {
|
|
514
|
+
return this._compiler?.compiler.getCompilerControlledFiles();
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Subscribe to compiler-controlled file set changes. The listener receives
|
|
518
|
+
* the full new set after each compile that changed it. Returns an
|
|
519
|
+
* unsubscribe function.
|
|
520
|
+
*/
|
|
521
|
+
onCompilerControlledFilesChanged(listener) {
|
|
522
|
+
this._compilerControlledFilesListeners.add(listener);
|
|
523
|
+
return () => {
|
|
524
|
+
this._compilerControlledFilesListeners.delete(listener);
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** Install provenance of every installed fetched dependency, keyed by `<owner>/<repo>` origin. */
|
|
528
|
+
getInstalledExtensionMetadata() {
|
|
529
|
+
return installedExtensionMetadataFromSnapshots(this._installedSnapshots);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The installed libraries of the active project's resolved extension
|
|
533
|
+
* closure: one record per resolved origin, carrying the coordinate a
|
|
534
|
+
* compiled tile's identity namespace names and the display name the
|
|
535
|
+
* library's own `wendoo.json` declares. Empty until the compiler is
|
|
536
|
+
* wired.
|
|
537
|
+
*/
|
|
538
|
+
get installedLibraries() {
|
|
539
|
+
const origins = this._lastResolution?.origins ?? [];
|
|
540
|
+
return origins.map((origin) => ({ coordinate: origin.origin, name: origin.name }));
|
|
541
|
+
}
|
|
542
|
+
/** The last recorded fetch failure per reference, keyed by the reference string as written. */
|
|
543
|
+
get extensionFetchFailures() {
|
|
544
|
+
return this._fetchFailures;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Non-fatal warnings of the active project's latest extension resolution,
|
|
548
|
+
* including the stable-coded catalog-move findings the load's move
|
|
549
|
+
* application recorded. Empty until the compiler is wired.
|
|
550
|
+
*/
|
|
551
|
+
get resolutionWarnings() {
|
|
552
|
+
return this._lastResolution?.warnings ?? NO_RESOLUTION_WARNINGS;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Subscribe to resolution-warning changes for `useSyncExternalStore`. The
|
|
556
|
+
* listener fires whenever the active project's latest resolution is
|
|
557
|
+
* replaced: on project load and switch, and after each install transaction.
|
|
558
|
+
* Returns an unsubscribe function.
|
|
559
|
+
*/
|
|
560
|
+
subscribeToResolutionWarnings = (listener) => {
|
|
561
|
+
this._resolutionWarningsListeners.add(listener);
|
|
562
|
+
return () => this._resolutionWarningsListeners.delete(listener);
|
|
563
|
+
};
|
|
564
|
+
/** Snapshot of {@link resolutionWarnings} for `useSyncExternalStore`. */
|
|
565
|
+
getResolutionWarningsSnapshot = () => {
|
|
566
|
+
return this.resolutionWarnings;
|
|
567
|
+
};
|
|
568
|
+
/** Record the latest resolution and notify resolution-warning subscribers. */
|
|
569
|
+
setLastResolution(resolution) {
|
|
570
|
+
this._lastResolution = resolution;
|
|
571
|
+
for (const listener of this._resolutionWarningsListeners) {
|
|
572
|
+
listener();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Merge a load's catalog-move findings into the latest resolution's warnings, skipping duplicates. */
|
|
576
|
+
absorbCatalogMovesOutcome(outcome) {
|
|
577
|
+
if (outcome === undefined || outcome.warnings.length === 0 || this._lastResolution === undefined) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const seen = new Set(this._lastResolution.warnings.map(resolutionWarningKey));
|
|
581
|
+
const added = outcome.warnings.filter((warning) => !seen.has(resolutionWarningKey(warning)));
|
|
582
|
+
if (added.length === 0) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.setLastResolution({
|
|
586
|
+
...this._lastResolution,
|
|
587
|
+
warnings: [...this._lastResolution.warnings, ...added],
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** Fetch one `gh:` reference's snapshot through the host's transport. */
|
|
591
|
+
fetchSnapshot(reference) {
|
|
592
|
+
if (!this.extensionFetchTransport) {
|
|
593
|
+
return Promise.resolve({
|
|
594
|
+
ok: false,
|
|
595
|
+
error: {
|
|
596
|
+
code: ExtensionFetchErrorCode.UNREACHABLE,
|
|
597
|
+
reference,
|
|
598
|
+
message: "The host application provides no extension fetch transport.",
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return fetchExtensionSnapshot(reference, this.extensionFetchTransport);
|
|
603
|
+
}
|
|
604
|
+
/** Materialize a resolution: dependencies, `wendoo.json`, and a recompile of the whole workspace. */
|
|
605
|
+
applyResolution(resolution) {
|
|
606
|
+
if (!this._compiler) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
this.setLastResolution(resolution);
|
|
610
|
+
this._compiler.compiler.setDependencies(resolution.dependencies, resolution.dependencyMounts);
|
|
611
|
+
syncManifestToWendooJson(this.projectFileSystem, this.projectManager.activeProject.manifest);
|
|
612
|
+
this._compiler.replaceProjectFiles();
|
|
613
|
+
}
|
|
614
|
+
/** Rewrite `wendoo.json` from the store manifest, restoring the file after a refused transaction. */
|
|
615
|
+
resyncManifestFile() {
|
|
616
|
+
const active = this.projectManager.activeProject;
|
|
617
|
+
if (active) {
|
|
618
|
+
syncManifestToWendooJson(this.projectFileSystem, active.manifest);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* The active project's diagnostic state: the latest workspace compile's
|
|
623
|
+
* per-file diagnostics plus a fresh typecheck of every cached brain.
|
|
624
|
+
*/
|
|
625
|
+
captureProjectDiagnostics() {
|
|
626
|
+
const files = this._lastCompileResult?.files ?? new Map();
|
|
627
|
+
const brains = new Map();
|
|
628
|
+
for (const [key, brain] of this._brainCache) {
|
|
629
|
+
brains.set(key, typecheckBrainProblems(brain));
|
|
630
|
+
}
|
|
631
|
+
return { files, brains };
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Append the transaction's events to the project's install log. Appends
|
|
635
|
+
* nothing while the stored extension records are unreadable: the log's
|
|
636
|
+
* existing entries are unknown, so a write would replace them with these
|
|
637
|
+
* events alone.
|
|
638
|
+
*/
|
|
639
|
+
async appendInstallLogEvents(events) {
|
|
640
|
+
if (events.length === 0 || this.extensionRecordUnreadable) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const raw = await this.projectManager.loadAppData(EXTENSION_INSTALL_LOG_APP_DATA_KEY);
|
|
644
|
+
await this.projectManager.saveAppData(EXTENSION_INSTALL_LOG_APP_DATA_KEY, appendExtensionInstallLog(raw, events));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* The install and remove events between two resolved closures, plus the new
|
|
648
|
+
* resolution's warnings. An origin present in both closures records an
|
|
649
|
+
* install event when its winning reference or installed specifier changed
|
|
650
|
+
* (an update).
|
|
651
|
+
*/
|
|
652
|
+
installLogEventsForChange(previous, next, previousSnapshots, nextSnapshots) {
|
|
653
|
+
const at = Date.now();
|
|
654
|
+
const previousByOrigin = new Map(previous.origins.map((origin) => [origin.origin, origin]));
|
|
655
|
+
const nextOrigins = new Set(next.origins.map((origin) => origin.origin));
|
|
656
|
+
const events = [];
|
|
657
|
+
for (const origin of next.origins) {
|
|
658
|
+
const specifier = nextSnapshots[origin.origin]?.specifier;
|
|
659
|
+
const before = previousByOrigin.get(origin.origin);
|
|
660
|
+
if (before !== undefined &&
|
|
661
|
+
before.reference === origin.reference &&
|
|
662
|
+
previousSnapshots[origin.origin]?.specifier === specifier) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
events.push({
|
|
666
|
+
kind: "install",
|
|
667
|
+
at,
|
|
668
|
+
origin: origin.origin,
|
|
669
|
+
reference: origin.reference,
|
|
670
|
+
...(specifier !== undefined ? { specifier } : {}),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
for (const origin of previous.origins) {
|
|
674
|
+
if (!nextOrigins.has(origin.origin)) {
|
|
675
|
+
events.push({ kind: "remove", at, origin: origin.origin });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
for (const warning of next.warnings) {
|
|
679
|
+
events.push({ kind: "resolution-warning", at, warning });
|
|
680
|
+
}
|
|
681
|
+
return events;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Apply an extensions-map change to the active project as one transaction:
|
|
685
|
+
* fetch content for every newly reachable `gh:` reference, resolve the
|
|
686
|
+
* transitive dependency graph, then commit -- persist the manifest entries
|
|
687
|
+
* and the fetched snapshots, re-materialize the installed-extensions tree,
|
|
688
|
+
* recompile the workspace, and re-typecheck the project's brains -- and
|
|
689
|
+
* report the diagnostic difference against the pre-change baseline.
|
|
690
|
+
*
|
|
691
|
+
* Improved, unchanged, and worsened outcomes all commit. The transaction
|
|
692
|
+
* refuses outright only on mechanics failures -- an unreachable source, a
|
|
693
|
+
* missing or unparseable manifest, a missing listed file, a dependency
|
|
694
|
+
* cycle, or stored extension records the store could not serve -- leaving
|
|
695
|
+
* the project unchanged. Every commit appends its install, remove, and
|
|
696
|
+
* resolution-warning events to the project's install log.
|
|
697
|
+
*
|
|
698
|
+
* @param extensions - The active project's next extensions map, keyed by coordinate.
|
|
699
|
+
* @param options.refetchReferences - References fetched fresh even when a stored snapshot matches.
|
|
700
|
+
*/
|
|
701
|
+
async updateProjectExtensions(extensions, options) {
|
|
702
|
+
if (this.extensionRecordUnreadable) {
|
|
703
|
+
const failure = this._projectRecordFailure;
|
|
704
|
+
return { committed: false, refusal: { kind: "store", code: failure.code, message: failure.message } };
|
|
705
|
+
}
|
|
706
|
+
const previousSnapshots = this._installedSnapshots;
|
|
707
|
+
const previousResolution = this.resolveExtensions();
|
|
708
|
+
const baseline = this.captureProjectDiagnostics();
|
|
709
|
+
const closure = await collectExtensionFetchClosure({
|
|
710
|
+
extensions,
|
|
711
|
+
embedded: this.embeddedExtensions,
|
|
712
|
+
stored: previousSnapshots,
|
|
713
|
+
refetch: options?.refetchReferences,
|
|
714
|
+
moves: this.catalogMoves,
|
|
715
|
+
fetchSnapshot: (reference) => this.fetchSnapshot(reference),
|
|
716
|
+
...(this.extensionFetchTransport !== undefined
|
|
717
|
+
? {
|
|
718
|
+
listVersions: (owner, repo) => this.extensionFetchTransport.listVersionTags(owner, repo),
|
|
719
|
+
}
|
|
720
|
+
: {}),
|
|
721
|
+
});
|
|
722
|
+
if (!closure.ok) {
|
|
723
|
+
this.resyncManifestFile();
|
|
724
|
+
this._fetchFailures.set(closure.error.reference, {
|
|
725
|
+
code: closure.error.code,
|
|
726
|
+
message: closure.error.message,
|
|
727
|
+
});
|
|
728
|
+
await this.appendInstallLogEvents([
|
|
729
|
+
{
|
|
730
|
+
kind: "fetch-refusal",
|
|
731
|
+
at: Date.now(),
|
|
732
|
+
reference: closure.error.reference,
|
|
733
|
+
code: closure.error.code,
|
|
734
|
+
message: closure.error.message,
|
|
735
|
+
},
|
|
736
|
+
]);
|
|
737
|
+
return { committed: false, refusal: { kind: "fetch", error: closure.error } };
|
|
738
|
+
}
|
|
739
|
+
const fetchedContent = new Map();
|
|
740
|
+
for (const [reference, record] of closure.snapshotsByReference) {
|
|
741
|
+
fetchedContent.set(reference, decodeInstalledSnapshotFiles(record));
|
|
742
|
+
}
|
|
743
|
+
let resolution;
|
|
744
|
+
try {
|
|
745
|
+
resolution = resolveProjectExtensions(extensions, {
|
|
746
|
+
embedded: this.embeddedExtensions,
|
|
747
|
+
fetched: fetchedContent,
|
|
748
|
+
moves: this.catalogMoves,
|
|
749
|
+
floatingPins: closure.floatingPins,
|
|
750
|
+
}, this.projectManager.activeProject.manifest.targets);
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
if (err instanceof ExtensionResolutionCycleError) {
|
|
754
|
+
this.resyncManifestFile();
|
|
755
|
+
return { committed: false, refusal: { kind: "cycle", cycle: err.cycle, message: err.message } };
|
|
756
|
+
}
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
759
|
+
// Commit. Snapshot records persist for exactly the fetched origins in the
|
|
760
|
+
// resolved closure; records for origins the change dropped are pruned.
|
|
761
|
+
// Invariant: the installed content swaps in before the manifest commit,
|
|
762
|
+
// and the commit's active-project notification observes the new closure's
|
|
763
|
+
// content. A manifest commit failure restores the previous snapshots.
|
|
764
|
+
const nextSnapshots = {};
|
|
765
|
+
for (const origin of resolution.origins) {
|
|
766
|
+
const record = closure.snapshotsByReference.get(origin.reference);
|
|
767
|
+
if (record) {
|
|
768
|
+
nextSnapshots[origin.origin] = record;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
await this.persistInstalledSnapshots(nextSnapshots);
|
|
772
|
+
try {
|
|
773
|
+
await this.projectManager.updateActive({ extensions });
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
await this.persistInstalledSnapshots(previousSnapshots);
|
|
777
|
+
throw err;
|
|
778
|
+
}
|
|
779
|
+
for (const reference of closure.snapshotsByReference.keys()) {
|
|
780
|
+
this._fetchFailures.delete(reference);
|
|
781
|
+
}
|
|
782
|
+
this.applyResolution(resolution);
|
|
783
|
+
this.refreshBrainCache();
|
|
784
|
+
const outcome = diffProjectDiagnostics(baseline, this.captureProjectDiagnostics());
|
|
785
|
+
this.bumpBrainDiagnosticsRevision();
|
|
786
|
+
await this.appendInstallLogEvents(this.installLogEventsForChange(previousResolution, resolution, previousSnapshots, nextSnapshots));
|
|
787
|
+
for (const warning of closure.warnings) {
|
|
788
|
+
logger.warn(`[catalog-moves] ${warning.kind === "catalog-move-failed" ? warning.code : warning.kind}: ${warning.message}`);
|
|
789
|
+
}
|
|
790
|
+
const warnings = [...resolution.warnings, ...closure.warnings];
|
|
791
|
+
return { committed: true, outcome, warnings };
|
|
792
|
+
}
|
|
793
|
+
/** Version lookup over the embed record and the project's persisted snapshot content. */
|
|
794
|
+
moveVersionLookup() {
|
|
795
|
+
return createCatalogMoveVersionLookup({
|
|
796
|
+
embedded: this.embeddedExtensions,
|
|
797
|
+
contentForReference: (reference) => this._installedContent.get(reference),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Resolve a floating catalog-move destination for a coordinate to a pinned
|
|
802
|
+
* `gh:` reference: the coordinate's persisted snapshot pin when one exists,
|
|
803
|
+
* otherwise the highest stable published version listed by the host's
|
|
804
|
+
* transport. Returns undefined when no pin can be produced.
|
|
805
|
+
*/
|
|
806
|
+
async resolveFloatingMoveReference(coordinate) {
|
|
807
|
+
const known = floatingPinsFromSnapshots(this._installedSnapshots).get(coordinate.toLowerCase());
|
|
808
|
+
if (known !== undefined) {
|
|
809
|
+
return known;
|
|
810
|
+
}
|
|
811
|
+
if (!this.extensionFetchTransport) {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
const slash = coordinate.indexOf("/");
|
|
815
|
+
const listed = await this.extensionFetchTransport.listVersionTags(coordinate.slice(0, slash), coordinate.slice(slash + 1));
|
|
816
|
+
const version = listed.ok ? highestListedRelease(listed.versions) : undefined;
|
|
817
|
+
return version === undefined ? undefined : `gh:${coordinate}@${version}`;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Apply the host's curated catalog moves to the active project. Each
|
|
821
|
+
* captured top-level manifest entry is one move-application unit: its
|
|
822
|
+
* manifest rewrite, its saved-brain namespace rewrite (when the entry's
|
|
823
|
+
* final coordinate differs), and the fetches its chain requires run as one
|
|
824
|
+
* install transaction, rolled back together when the transaction refuses. A
|
|
825
|
+
* failed unit writes nothing, surfaces a stable-coded warning, and is
|
|
826
|
+
* retried on the next load, while other units proceed independently.
|
|
827
|
+
*
|
|
828
|
+
* Application interleaves apply, fetch, and re-apply: after each committed
|
|
829
|
+
* transaction the entries are re-applied with the versions the new content
|
|
830
|
+
* determines, and a transitive moved dependency whose content the project
|
|
831
|
+
* never persisted is healed by a fetch transaction, until nothing changes.
|
|
832
|
+
* A floating destination is resolved to a pin before its transaction runs;
|
|
833
|
+
* the pin is what the manifest receives. Returns undefined when the host
|
|
834
|
+
* declares no moves or the project is a true no-op; otherwise the
|
|
835
|
+
* transactions' reports and the load's stable-coded move warnings. Returns
|
|
836
|
+
* undefined without applying anything while the stored extension records are
|
|
837
|
+
* unreadable, so no move writes over a record the store could not serve.
|
|
838
|
+
*/
|
|
839
|
+
async applyCatalogMoves() {
|
|
840
|
+
if (Object.keys(this.catalogMoves).length === 0 || this.extensionRecordUnreadable) {
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
const reports = [];
|
|
844
|
+
const warnings = [];
|
|
845
|
+
const warned = new Set();
|
|
846
|
+
const warn = (reference, code, message) => {
|
|
847
|
+
const key = `${reference} ${code}`;
|
|
848
|
+
if (warned.has(key)) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
warned.add(key);
|
|
852
|
+
warnings.push({
|
|
853
|
+
kind: "catalog-move-failed",
|
|
854
|
+
origin: parseCatalogMoveReference(reference)?.coordinate ?? reference,
|
|
855
|
+
reference,
|
|
856
|
+
code,
|
|
857
|
+
message,
|
|
858
|
+
});
|
|
859
|
+
};
|
|
860
|
+
// Entries whose unit failed this load; they are not retried until the next load.
|
|
861
|
+
const failedEntries = new Set();
|
|
862
|
+
let progressed = true;
|
|
863
|
+
while (progressed) {
|
|
864
|
+
progressed = false;
|
|
865
|
+
// Top-level entries: each captured entry is its own transaction.
|
|
866
|
+
const lookup = this.moveVersionLookup();
|
|
867
|
+
const current = this.projectManager.activeProject?.manifest.extensions ?? {};
|
|
868
|
+
for (const [coordinate, reference] of Object.entries(current)) {
|
|
869
|
+
if (failedEntries.has(coordinate)) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const applied = applyCatalogMove(reference, this.catalogMoves, lookup);
|
|
873
|
+
if (!applied.ok) {
|
|
874
|
+
warn(reference, applied.code, applied.message);
|
|
875
|
+
failedEntries.add(coordinate);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (!applied.moved) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
let finalReference = applied.reference;
|
|
882
|
+
const parts = parseCatalogMoveReference(finalReference);
|
|
883
|
+
if (parts === undefined) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (parts.floating) {
|
|
887
|
+
const pin = await this.resolveFloatingMoveReference(parts.coordinate);
|
|
888
|
+
if (pin === undefined) {
|
|
889
|
+
warn(reference, CatalogMoveWarningCode.FLOATING_UNRESOLVED, `Catalog move for "${reference}" resolves to the floating "${finalReference}" and no stable ` +
|
|
890
|
+
"published version could be pinned; the move is skipped this load.");
|
|
891
|
+
failedEntries.add(coordinate);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
finalReference = pin;
|
|
895
|
+
}
|
|
896
|
+
const finalCoordinate = parts.coordinate;
|
|
897
|
+
const next = {};
|
|
898
|
+
for (const [key, value] of Object.entries(current)) {
|
|
899
|
+
if (key !== coordinate) {
|
|
900
|
+
next[key] = value;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
next[finalCoordinate] = finalReference;
|
|
904
|
+
// A rename rewrites saved-brain namespaces in the same unit, restored
|
|
905
|
+
// together when the transaction refuses. A brain record the store
|
|
906
|
+
// cannot serve fails the unit before any write.
|
|
907
|
+
let brainsBeforeRename;
|
|
908
|
+
let brainsRewritten = false;
|
|
909
|
+
if (finalCoordinate !== coordinate) {
|
|
910
|
+
const stored = await this.readStoredBrainsForLoad();
|
|
911
|
+
if (stored === undefined) {
|
|
912
|
+
warn(reference, CatalogMoveWarningCode.BRAINS_UNREADABLE, `Catalog move for "${reference}" renames the coordinate to "${finalCoordinate}", and the project's ` +
|
|
913
|
+
"stored brains could not be read to rewrite their references; the move is skipped this load.");
|
|
914
|
+
failedEntries.add(coordinate);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
brainsBeforeRename = stored.raw;
|
|
918
|
+
brainsRewritten = await this.renameBrainNamespaces(stored.record, [
|
|
919
|
+
{ from: coordinate, to: finalCoordinate },
|
|
920
|
+
]);
|
|
921
|
+
}
|
|
922
|
+
const report = await this.updateProjectExtensions(next);
|
|
923
|
+
reports.push(report);
|
|
924
|
+
if (!report.committed) {
|
|
925
|
+
if (brainsRewritten) {
|
|
926
|
+
await this.restoreBrains(brainsBeforeRename);
|
|
927
|
+
}
|
|
928
|
+
const detail = report.refusal.kind === "fetch"
|
|
929
|
+
? `${report.refusal.error.code}: ${report.refusal.error.message}`
|
|
930
|
+
: report.refusal.message;
|
|
931
|
+
warn(reference, CatalogMoveWarningCode.FETCH_FAILED, `Catalog move for "${reference}" to "${finalReference}" was refused (${detail}); the move is ` +
|
|
932
|
+
"skipped this load.");
|
|
933
|
+
failedEntries.add(coordinate);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
progressed = true;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
if (progressed) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
// Transitive heal: a moved dependency at any depth may lack content on
|
|
943
|
+
// this load -- a dependency installed while the moved library was
|
|
944
|
+
// bundled has no persisted snapshot, and a floating destination may not
|
|
945
|
+
// be pinned yet. The fetch transaction fetches and persists exactly the
|
|
946
|
+
// missing content; content it learns can enable further captures.
|
|
947
|
+
const extensions = this.projectManager.activeProject?.manifest.extensions ?? {};
|
|
948
|
+
const needsHeal = await movedClosureHasMissingContent({
|
|
949
|
+
extensions,
|
|
950
|
+
embedded: this.embeddedExtensions,
|
|
951
|
+
stored: this._installedSnapshots,
|
|
952
|
+
moves: this.catalogMoves,
|
|
953
|
+
});
|
|
954
|
+
if (needsHeal) {
|
|
955
|
+
const before = new Set(Object.values(this._installedSnapshots).map((record) => record.reference));
|
|
956
|
+
const report = await this.updateProjectExtensions(extensions);
|
|
957
|
+
reports.push(report);
|
|
958
|
+
if (report.committed) {
|
|
959
|
+
if (Object.values(this._installedSnapshots).some((record) => !before.has(record.reference))) {
|
|
960
|
+
progressed = true;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else if (report.refusal.kind === "fetch") {
|
|
964
|
+
warn(report.refusal.error.reference, CatalogMoveWarningCode.FETCH_FAILED, report.refusal.error.message);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// A version-scoped capture still unevaluable at the end of the loop is a
|
|
969
|
+
// loud skip, retried on the next load.
|
|
970
|
+
const lookup = this.moveVersionLookup();
|
|
971
|
+
for (const reference of Object.values(this.projectManager.activeProject?.manifest.extensions ?? {})) {
|
|
972
|
+
const applied = applyCatalogMove(reference, this.catalogMoves, lookup);
|
|
973
|
+
if (applied.ok && applied.pendingVersion) {
|
|
974
|
+
warn(reference, CatalogMoveWarningCode.VERSION_UNKNOWN, `The version of "${reference}" could not be determined, so its version-scoped catalog move cannot be ` +
|
|
975
|
+
"evaluated; the move is skipped this load.");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
for (const warning of warnings) {
|
|
979
|
+
if (warning.kind === "catalog-move-failed") {
|
|
980
|
+
logger.warn(`[catalog-moves] ${warning.code}: ${warning.message}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (reports.length === 0 && warnings.length === 0) {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
return { reports, warnings };
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Rewrite every saved brain's foreign-namespace references from each rename's
|
|
990
|
+
* source coordinate to its target, persisting the rewritten brains record and
|
|
991
|
+
* re-deserializing the changed brains into the cache. Returns true when at
|
|
992
|
+
* least one brain changed.
|
|
993
|
+
*
|
|
994
|
+
* @param record - The stored brain record to rewrite, read by the caller; mutated in place.
|
|
995
|
+
* @param renames - The coordinate renames to apply, each a source and its target.
|
|
996
|
+
*/
|
|
997
|
+
async renameBrainNamespaces(record, renames) {
|
|
998
|
+
const rewrite = (namespace) => {
|
|
999
|
+
for (const rename of renames) {
|
|
1000
|
+
if (rename.from === namespace) {
|
|
1001
|
+
return rename.to;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return namespace;
|
|
1005
|
+
};
|
|
1006
|
+
const changedKeys = [];
|
|
1007
|
+
for (const [key, json] of Object.entries(record)) {
|
|
1008
|
+
const result = renameBrainNamespaces(json, rewrite);
|
|
1009
|
+
if (result.changed) {
|
|
1010
|
+
record[key] = result.brain;
|
|
1011
|
+
changedKeys.push(key);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (changedKeys.length === 0) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
await this.projectManager.saveAppData(BRAINS_APP_DATA_KEY, JSON.stringify(record));
|
|
1018
|
+
for (const key of changedKeys) {
|
|
1019
|
+
const def = this.deserializeBrainForKey(key, record[key]);
|
|
1020
|
+
if (def) {
|
|
1021
|
+
this._brainCache.set(key, def);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
/** Restore the brains record and cache to a pre-rewrite snapshot after a refused migration. */
|
|
1027
|
+
async restoreBrains(raw) {
|
|
1028
|
+
if (raw === undefined) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
await this.projectManager.saveAppData(BRAINS_APP_DATA_KEY, raw);
|
|
1032
|
+
const record = JSON.parse(raw);
|
|
1033
|
+
for (const [key, json] of Object.entries(record)) {
|
|
1034
|
+
const def = this.deserializeBrainForKey(key, json);
|
|
1035
|
+
if (def) {
|
|
1036
|
+
this._brainCache.set(key, def);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Normalize add-by-reference input to an installable extension reference: a
|
|
1042
|
+
* complete reference, an `<owner>/<repo>` coordinate, or a GitHub repository
|
|
1043
|
+
* URL. Input naming a repository without a version resolves to the
|
|
1044
|
+
* repository's latest published version through the host's transport. Fails
|
|
1045
|
+
* with a stable code when the input is unrecognized, no version can be
|
|
1046
|
+
* resolved, or the host has no transport to resolve one through.
|
|
1047
|
+
*
|
|
1048
|
+
* @param input - The pasted add-field text.
|
|
1049
|
+
*/
|
|
1050
|
+
async resolveExtensionInstallInput(input) {
|
|
1051
|
+
if (this.extensionFetchTransport) {
|
|
1052
|
+
return resolveExtensionAddInput(input, this.extensionFetchTransport);
|
|
1053
|
+
}
|
|
1054
|
+
const parsed = parseExtensionAddInput(input);
|
|
1055
|
+
switch (parsed.kind) {
|
|
1056
|
+
case "reference":
|
|
1057
|
+
return { ok: true, reference: parsed.reference };
|
|
1058
|
+
case "coordinate":
|
|
1059
|
+
return {
|
|
1060
|
+
ok: false,
|
|
1061
|
+
code: ExtensionFetchErrorCode.UNREACHABLE,
|
|
1062
|
+
message: "The host application provides no extension fetch transport.",
|
|
1063
|
+
};
|
|
1064
|
+
case "invalid":
|
|
1065
|
+
return { ok: false, code: parsed.code, message: parsed.message };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Check one installed fetched dependency of the active project for newer
|
|
1070
|
+
* content: a `@<pin>` reference against the host catalog's approved pin when
|
|
1071
|
+
* the catalog lists the coordinate and against the source's published
|
|
1072
|
+
* versions when it does not, a `#<branch>` reference against the branch's
|
|
1073
|
+
* head commit. The check runs on request, reaches the source through the
|
|
1074
|
+
* host's transport, and changes nothing.
|
|
1075
|
+
*
|
|
1076
|
+
* @param coordinate - The dependency's `<owner>/<repo>` coordinate in the project's extensions map.
|
|
1077
|
+
*/
|
|
1078
|
+
async checkExtensionUpdate(coordinate) {
|
|
1079
|
+
const reference = this.projectManager.activeProject.manifest.extensions?.[coordinate];
|
|
1080
|
+
const record = this._installedSnapshots[coordinate];
|
|
1081
|
+
if (reference === undefined || record === undefined || record.reference !== reference) {
|
|
1082
|
+
return {
|
|
1083
|
+
ok: false,
|
|
1084
|
+
error: {
|
|
1085
|
+
code: ExtensionFetchErrorCode.INVALID_REFERENCE,
|
|
1086
|
+
reference: reference ?? coordinate,
|
|
1087
|
+
message: `"${coordinate}" is not an installed fetched dependency of the active project.`,
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (!this.extensionFetchTransport) {
|
|
1092
|
+
return {
|
|
1093
|
+
ok: false,
|
|
1094
|
+
error: {
|
|
1095
|
+
code: ExtensionFetchErrorCode.UNREACHABLE,
|
|
1096
|
+
reference,
|
|
1097
|
+
message: "The host application provides no extension fetch transport.",
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const manifestEntry = decodeInstalledSnapshotFiles(record).get(`/${WENDOO_JSON_PATH}`);
|
|
1102
|
+
const manifestContent = manifestEntry === undefined ? undefined : fileContentText(manifestEntry);
|
|
1103
|
+
const parsed = manifestContent !== undefined ? parseProjectContentManifest(manifestContent) : undefined;
|
|
1104
|
+
return checkExtensionReferenceUpdate({
|
|
1105
|
+
reference,
|
|
1106
|
+
installedSpecifier: record.specifier,
|
|
1107
|
+
installedVersion: parsed?.ok ? parsed.manifest.version : "0.0.0",
|
|
1108
|
+
transport: this.extensionFetchTransport,
|
|
1109
|
+
...(this.approvedCatalogEntry !== undefined ? { approvedEntry: this.approvedCatalogEntry } : {}),
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Apply one or more dependency updates to the active project as a single
|
|
1114
|
+
* install transaction with one outcome: each update's coordinate takes its
|
|
1115
|
+
* new reference in the extensions map, and each updated reference is fetched
|
|
1116
|
+
* fresh, never satisfied from its stored snapshot. The report and the
|
|
1117
|
+
* install log are those of {@link updateProjectExtensions}.
|
|
1118
|
+
*
|
|
1119
|
+
* @param updates - The updates to apply, as returned by update checks.
|
|
1120
|
+
*/
|
|
1121
|
+
async applyExtensionUpdates(updates) {
|
|
1122
|
+
const next = { ...(this.projectManager.activeProject.manifest.extensions ?? {}) };
|
|
1123
|
+
const refetchReferences = new Set();
|
|
1124
|
+
for (const update of updates) {
|
|
1125
|
+
next[update.coordinate] = update.reference;
|
|
1126
|
+
refetchReferences.add(update.reference);
|
|
1127
|
+
}
|
|
1128
|
+
return this.updateProjectExtensions(next, { refetchReferences });
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Collect the active project's dependencies that are not stable for
|
|
1132
|
+
* consumers: branch references, and pinned references whose content the
|
|
1133
|
+
* project has no installed snapshot for. Exporting or publishing such a
|
|
1134
|
+
* project should ask for confirmation first.
|
|
1135
|
+
*/
|
|
1136
|
+
async collectUnstableProjectDependencies() {
|
|
1137
|
+
const extensions = this.projectManager.activeProject.manifest.extensions ?? {};
|
|
1138
|
+
return collectUnstableDependencies(extensions, async (owner, repo, pin) => this._installedContent.has(`gh:${owner}/${repo}@${pin}`));
|
|
1139
|
+
}
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
// Project lifecycle events
|
|
1142
|
+
// ---------------------------------------------------------------------------
|
|
1143
|
+
onProjectUnloading(listener) {
|
|
1144
|
+
this._projectUnloadingListeners.add(listener);
|
|
1145
|
+
return () => this._projectUnloadingListeners.delete(listener);
|
|
1146
|
+
}
|
|
1147
|
+
onProjectLoaded(listener) {
|
|
1148
|
+
this._projectLoadedListeners.add(listener);
|
|
1149
|
+
return () => this._projectLoadedListeners.delete(listener);
|
|
1150
|
+
}
|
|
1151
|
+
// ---------------------------------------------------------------------------
|
|
1152
|
+
// Project switching / creation
|
|
1153
|
+
// ---------------------------------------------------------------------------
|
|
1154
|
+
async createProject(name) {
|
|
1155
|
+
await this.prepareProjectTransition();
|
|
1156
|
+
const manifest = await this.projectManager.create(name, {
|
|
1157
|
+
beforeActiveProjectChange: () => this.notifyProjectUnloading(),
|
|
1158
|
+
});
|
|
1159
|
+
await this.completeProjectTransition();
|
|
1160
|
+
return manifest;
|
|
1161
|
+
}
|
|
1162
|
+
async switchProject(id) {
|
|
1163
|
+
if (this.projectManager.activeProject?.manifest.id === id) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
await this.prepareProjectTransition();
|
|
1167
|
+
await this.projectManager.open(id, {
|
|
1168
|
+
beforeActiveProjectChange: () => this.notifyProjectUnloading(),
|
|
1169
|
+
});
|
|
1170
|
+
await this.completeProjectTransition();
|
|
1171
|
+
}
|
|
1172
|
+
async switchProjectCollectionAndOpenProject(projectCollectionId, projectId) {
|
|
1173
|
+
if (this.projectManager.activeProjectCollection?.projectCollectionId === projectCollectionId &&
|
|
1174
|
+
this.projectManager.activeProject?.manifest.id === projectId) {
|
|
1175
|
+
return {
|
|
1176
|
+
collection: this.projectManager.activeProjectCollection,
|
|
1177
|
+
project: this.projectManager.activeProject.manifest,
|
|
1178
|
+
access: "ready",
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
await this.prepareProjectTransition();
|
|
1182
|
+
const result = await this.projectManager.switchProjectCollectionAndOpenProject(projectCollectionId, projectId, {
|
|
1183
|
+
beforeActiveProjectChange: () => this.notifyProjectUnloading(),
|
|
1184
|
+
});
|
|
1185
|
+
await this.completeProjectTransition();
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
async switchProjectCollectionAndCreateProject(projectCollectionId, name) {
|
|
1189
|
+
await this.prepareProjectTransition();
|
|
1190
|
+
const result = await this.projectManager.switchProjectCollectionAndCreateProject(projectCollectionId, name, {
|
|
1191
|
+
beforeActiveProjectChange: () => this.notifyProjectUnloading(),
|
|
1192
|
+
});
|
|
1193
|
+
await this.completeProjectTransition();
|
|
1194
|
+
return result;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Unlock a project collection and initialize the project environment when
|
|
1198
|
+
* unlocking restores the active project.
|
|
1199
|
+
*
|
|
1200
|
+
* @param projectCollectionId - Project collection to unlock.
|
|
1201
|
+
* @param pin - User-entered PIN or phrase.
|
|
1202
|
+
* @returns The unlocked project collection and its ready access status.
|
|
1203
|
+
*/
|
|
1204
|
+
async unlockProjectCollection(projectCollectionId, pin) {
|
|
1205
|
+
const result = await this.projectManager.unlockProjectCollection(projectCollectionId, pin);
|
|
1206
|
+
if (this.projectManager.activeProjectCollection?.projectCollectionId === projectCollectionId &&
|
|
1207
|
+
this.projectManager.activeProject) {
|
|
1208
|
+
await this.completeProjectTransition();
|
|
1209
|
+
}
|
|
1210
|
+
return result;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Lock a project collection after flushing app-owned project state.
|
|
1214
|
+
*
|
|
1215
|
+
* @param projectCollectionId - Project collection to lock.
|
|
1216
|
+
*/
|
|
1217
|
+
async lockProjectCollection(projectCollectionId) {
|
|
1218
|
+
const locksActiveProject = this.projectManager.activeProjectCollection?.projectCollectionId === projectCollectionId &&
|
|
1219
|
+
this.projectManager.activeProject !== undefined;
|
|
1220
|
+
if (locksActiveProject) {
|
|
1221
|
+
await this.prepareProjectTransition();
|
|
1222
|
+
}
|
|
1223
|
+
await this.projectManager.lockProjectCollection(projectCollectionId, {
|
|
1224
|
+
beforeActiveProjectChange: () => this.notifyProjectUnloading(),
|
|
1225
|
+
});
|
|
1226
|
+
if (locksActiveProject) {
|
|
1227
|
+
this.completeProjectUnload();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async prepareProjectTransition() {
|
|
1231
|
+
// A project whose load hit an unreadable record has an empty brain cache,
|
|
1232
|
+
// and flushing it would write over brains the store still holds.
|
|
1233
|
+
if (this.projectManager.activeProject && !this._projectRecordFailure) {
|
|
1234
|
+
await this.saveAllBrains();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
notifyProjectUnloading() {
|
|
1238
|
+
for (const listener of this._projectUnloadingListeners) {
|
|
1239
|
+
listener();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
completeProjectUnload() {
|
|
1243
|
+
this._brainCache.clear();
|
|
1244
|
+
this._projectRecordFailure = undefined;
|
|
1245
|
+
this._pendingBrainRebuild = false;
|
|
1246
|
+
this.env.replaceActionBundle({ revision: "", tiles: [], actions: Dict.empty(), roots: [] });
|
|
1247
|
+
// Compiles invalidate types per project namespace, so the outgoing
|
|
1248
|
+
// project's registrations must be cleared here or they outlive it.
|
|
1249
|
+
this.env.brainServices.runtime.types.removeUserTypes();
|
|
1250
|
+
this._lastUserTileMetadata = undefined;
|
|
1251
|
+
this._installedSnapshots = {};
|
|
1252
|
+
this._installedContent = new Map();
|
|
1253
|
+
this._fetchFailures = new Map();
|
|
1254
|
+
this._lastCompileResult = undefined;
|
|
1255
|
+
this.setCompileDiagnostics(undefined);
|
|
1256
|
+
this.setLastResolution(undefined);
|
|
1257
|
+
this.bumpDocRevision();
|
|
1258
|
+
this.teardownBridge();
|
|
1259
|
+
}
|
|
1260
|
+
async completeProjectTransition() {
|
|
1261
|
+
this.completeProjectUnload();
|
|
1262
|
+
await this.initCompiler();
|
|
1263
|
+
this.absorbCatalogMovesOutcome(await this.applyCatalogMoves());
|
|
1264
|
+
await this.loadBrainsFromProject();
|
|
1265
|
+
for (const listener of this._projectLoadedListeners) {
|
|
1266
|
+
listener();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
// Brain rebuild flush
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
flushPendingBrainRebuilds() {
|
|
1273
|
+
if (!this._pendingBrainRebuild) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
this._pendingBrainRebuild = false;
|
|
1277
|
+
try {
|
|
1278
|
+
this.env.rebuildInvalidatedBrains();
|
|
1279
|
+
}
|
|
1280
|
+
catch (err) {
|
|
1281
|
+
logger.warn("Failed to rebuild invalidated brains:", err);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// ---------------------------------------------------------------------------
|
|
1285
|
+
// User tile metadata
|
|
1286
|
+
// ---------------------------------------------------------------------------
|
|
1287
|
+
get lastUserTileMetadata() {
|
|
1288
|
+
return this._lastUserTileMetadata;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Error compile diagnostics of the user tile registered under `key` together
|
|
1292
|
+
* with the defining source file's path, from the latest workspace compile.
|
|
1293
|
+
* Undefined when the tile compiled cleanly, produced only warnings or infos,
|
|
1294
|
+
* or is absent from the latest compile. Each stored diagnostic is the
|
|
1295
|
+
* compiler's verbatim object, exposing `message`, `severity`, and
|
|
1296
|
+
* `line`/`column`.
|
|
1297
|
+
*/
|
|
1298
|
+
getTileCompileDiagnostics(key) {
|
|
1299
|
+
return this._tileCompileDiagnostics.get(key);
|
|
1300
|
+
}
|
|
1301
|
+
// ---------------------------------------------------------------------------
|
|
1302
|
+
// Doc / VFS revision (useSyncExternalStore pattern)
|
|
1303
|
+
// ---------------------------------------------------------------------------
|
|
1304
|
+
get docRevision() {
|
|
1305
|
+
return this._docRevision;
|
|
1306
|
+
}
|
|
1307
|
+
bumpDocRevision() {
|
|
1308
|
+
this._docRevision++;
|
|
1309
|
+
for (const listener of this._docRevisionListeners) {
|
|
1310
|
+
listener();
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
bumpVfsRevision() {
|
|
1314
|
+
this._vfsRevision++;
|
|
1315
|
+
for (const listener of this._vfsRevisionListeners) {
|
|
1316
|
+
listener();
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
subscribeToDocRevision = (listener) => {
|
|
1320
|
+
this._docRevisionListeners.add(listener);
|
|
1321
|
+
return () => this._docRevisionListeners.delete(listener);
|
|
1322
|
+
};
|
|
1323
|
+
/**
|
|
1324
|
+
* Subscribe to brain-diagnostics revision changes for
|
|
1325
|
+
* `useSyncExternalStore`. The revision bumps whenever the diagnostics a brain
|
|
1326
|
+
* surfaces may have changed: after each extension transaction, after each brain
|
|
1327
|
+
* save, and after each workspace compile (which can change a used tile's
|
|
1328
|
+
* broken/clean status). Returns an unsubscribe function.
|
|
1329
|
+
*/
|
|
1330
|
+
subscribeToBrainDiagnostics = (listener) => {
|
|
1331
|
+
this._brainDiagnosticsListeners.add(listener);
|
|
1332
|
+
return () => this._brainDiagnosticsListeners.delete(listener);
|
|
1333
|
+
};
|
|
1334
|
+
/** Snapshot of the current brain-diagnostics revision for `useSyncExternalStore`. */
|
|
1335
|
+
getBrainDiagnosticsRevision = () => {
|
|
1336
|
+
return this._brainDiagnosticsRevision;
|
|
1337
|
+
};
|
|
1338
|
+
bumpBrainDiagnosticsRevision() {
|
|
1339
|
+
this._brainDiagnosticsRevision++;
|
|
1340
|
+
for (const listener of this._brainDiagnosticsListeners) {
|
|
1341
|
+
listener();
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Subscribe to workspace-compile diagnostic changes for
|
|
1346
|
+
* `useSyncExternalStore`. The listener fires after every workspace compile
|
|
1347
|
+
* and on project unload. Returns an unsubscribe function.
|
|
1348
|
+
*/
|
|
1349
|
+
subscribeToCompileDiagnostics = (listener) => {
|
|
1350
|
+
this._compileDiagnosticsListeners.add(listener);
|
|
1351
|
+
return () => this._compileDiagnosticsListeners.delete(listener);
|
|
1352
|
+
};
|
|
1353
|
+
/** Snapshot of the latest workspace compile's diagnostics for `useSyncExternalStore`; empty when clean. */
|
|
1354
|
+
getCompileDiagnosticsSnapshot = () => {
|
|
1355
|
+
return this._compileDiagnostics;
|
|
1356
|
+
};
|
|
1357
|
+
/** Record a compile's per-file diagnostics as one flat located list and notify subscribers. */
|
|
1358
|
+
setCompileDiagnostics(result) {
|
|
1359
|
+
// Rebuilt from the latest compile so a fixed tile drops out and a newly
|
|
1360
|
+
// broken tile appears; cleared to empty when a project unloads.
|
|
1361
|
+
this._tileCompileDiagnostics = result ? collectTileSourceCompileErrors(result) : new Map();
|
|
1362
|
+
const diagnostics = [];
|
|
1363
|
+
for (const [path, entries] of result?.files ?? []) {
|
|
1364
|
+
for (const entry of entries) {
|
|
1365
|
+
diagnostics.push({ ...entry, path });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (diagnostics.length === 0 && this._compileDiagnostics.length === 0) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
this._compileDiagnostics = diagnostics.length === 0 ? NO_COMPILE_DIAGNOSTICS : diagnostics;
|
|
1372
|
+
for (const listener of this._compileDiagnosticsListeners) {
|
|
1373
|
+
listener();
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
getDocRevisionSnapshot = () => {
|
|
1377
|
+
return this._docRevision;
|
|
1378
|
+
};
|
|
1379
|
+
subscribeToVfsRevision = (listener) => {
|
|
1380
|
+
this._vfsRevisionListeners.add(listener);
|
|
1381
|
+
return () => this._vfsRevisionListeners.delete(listener);
|
|
1382
|
+
};
|
|
1383
|
+
getVfsRevisionSnapshot = () => {
|
|
1384
|
+
return this._vfsRevision;
|
|
1385
|
+
};
|
|
1386
|
+
// ---------------------------------------------------------------------------
|
|
1387
|
+
// Bridge (optional -- only available if bridgeUrl was provided)
|
|
1388
|
+
// ---------------------------------------------------------------------------
|
|
1389
|
+
initBridge() {
|
|
1390
|
+
if (!this._bridgeUrl || !this._compiler) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
this.teardownBridge();
|
|
1394
|
+
this._bridge = createBridgeProject({
|
|
1395
|
+
projectCompiler: this._compiler,
|
|
1396
|
+
servedFileSystem: this.servedProjectFileSystem,
|
|
1397
|
+
bridgeUrl: this._bridgeUrl,
|
|
1398
|
+
bindingToken: this._loadBindingToken(),
|
|
1399
|
+
onBindingTokenChange: (token) => {
|
|
1400
|
+
this._saveBindingToken(token);
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
this.wireBridgeState(this._bridge.bridge);
|
|
1404
|
+
}
|
|
1405
|
+
connectBridge() {
|
|
1406
|
+
if (!this._bridge) {
|
|
1407
|
+
this.initBridge();
|
|
1408
|
+
}
|
|
1409
|
+
if (!this._bridge || this._bridge.bridge.snapshot().status !== "disconnected") {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
this._bridge.bridge.start();
|
|
1413
|
+
}
|
|
1414
|
+
disconnectBridge() {
|
|
1415
|
+
this._bridge?.bridge.stop();
|
|
1416
|
+
}
|
|
1417
|
+
teardownBridge() {
|
|
1418
|
+
this._bridgeStateUnsub?.();
|
|
1419
|
+
this._bridgeStateUnsub = undefined;
|
|
1420
|
+
this._remoteChangeUnsub?.();
|
|
1421
|
+
this._remoteChangeUnsub = undefined;
|
|
1422
|
+
this._bridge?.bridge.stop();
|
|
1423
|
+
this._bridge = undefined;
|
|
1424
|
+
if (this._bridgeStatus !== "disconnected") {
|
|
1425
|
+
this._bridgeStatus = "disconnected";
|
|
1426
|
+
for (const listener of this._bridgeStatusListeners) {
|
|
1427
|
+
listener();
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (this._bridgeJoinCode !== undefined) {
|
|
1431
|
+
this._bridgeJoinCode = undefined;
|
|
1432
|
+
for (const listener of this._bridgeJoinCodeListeners) {
|
|
1433
|
+
listener();
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
updateBridgeUrl(bridgeUrl) {
|
|
1438
|
+
this._bridgeUrl = bridgeUrl;
|
|
1439
|
+
if (!this._bridge) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const shouldStart = this._bridgeStatus !== "disconnected";
|
|
1443
|
+
this._bridgeStateUnsub?.();
|
|
1444
|
+
this._bridgeStateUnsub = undefined;
|
|
1445
|
+
this._bridge.recreateBridge(bridgeUrl);
|
|
1446
|
+
this.wireBridgeState(this._bridge.bridge);
|
|
1447
|
+
if (shouldStart) {
|
|
1448
|
+
this._bridge.bridge.start();
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
subscribeToBridgeStatus = (listener) => {
|
|
1452
|
+
this._bridgeStatusListeners.add(listener);
|
|
1453
|
+
return () => this._bridgeStatusListeners.delete(listener);
|
|
1454
|
+
};
|
|
1455
|
+
getBridgeStatusSnapshot = () => {
|
|
1456
|
+
return this._bridgeStatus;
|
|
1457
|
+
};
|
|
1458
|
+
subscribeToBridgeJoinCode = (listener) => {
|
|
1459
|
+
this._bridgeJoinCodeListeners.add(listener);
|
|
1460
|
+
return () => this._bridgeJoinCodeListeners.delete(listener);
|
|
1461
|
+
};
|
|
1462
|
+
getBridgeJoinCodeSnapshot = () => {
|
|
1463
|
+
return this._bridgeJoinCode;
|
|
1464
|
+
};
|
|
1465
|
+
/**
|
|
1466
|
+
* Apply a project file change observed outside the app (a folder-session
|
|
1467
|
+
* external edit): applies it to the project file system and the workspace
|
|
1468
|
+
* compiler, recompiles, and absorbs any `wendoo.json` manifest change.
|
|
1469
|
+
*/
|
|
1470
|
+
applyExternalProjectFileChange(change) {
|
|
1471
|
+
this.projectFileSystem.applyRemoteChange(change);
|
|
1472
|
+
if (this._compiler) {
|
|
1473
|
+
this._compiler.compiler.applyWorkspaceChange(change);
|
|
1474
|
+
this._compiler.compiler.compile();
|
|
1475
|
+
}
|
|
1476
|
+
this.handleRemoteProjectFileChange(change);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Absorb a project file change made by a remote peer: bumps the VFS
|
|
1480
|
+
* revision, and a `wendoo.json` write is diffed against the active
|
|
1481
|
+
* manifest -- an extensions change runs through the install transaction and
|
|
1482
|
+
* the remaining synced fields patch the manifest. The change itself must
|
|
1483
|
+
* already be applied to the project file system.
|
|
1484
|
+
*/
|
|
1485
|
+
handleRemoteProjectFileChange(change) {
|
|
1486
|
+
this.bumpVfsRevision();
|
|
1487
|
+
if (change.action === "write" && change.path === WENDOO_JSON_PATH && this.projectManager.activeProject) {
|
|
1488
|
+
const manifestText = fileContentText(change.content);
|
|
1489
|
+
const patch = manifestText === undefined
|
|
1490
|
+
? undefined
|
|
1491
|
+
: diffWendooJsonToManifest(manifestText, this.projectManager.activeProject.manifest);
|
|
1492
|
+
if (patch) {
|
|
1493
|
+
// An extensions change flows through the install transaction, the
|
|
1494
|
+
// same pipeline the extension browser uses; the remaining synced
|
|
1495
|
+
// fields patch the manifest directly.
|
|
1496
|
+
const { extensions, ...rest } = patch;
|
|
1497
|
+
if (extensions !== undefined) {
|
|
1498
|
+
void this.updateProjectExtensions(extensions);
|
|
1499
|
+
}
|
|
1500
|
+
if (Object.keys(rest).length > 0) {
|
|
1501
|
+
void this.projectManager.updateActive(rest);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
wireBridgeState(bridge) {
|
|
1507
|
+
this._bridgeStateUnsub?.();
|
|
1508
|
+
this._remoteChangeUnsub?.();
|
|
1509
|
+
this._bridgeStateUnsub = bridge.onStateChange(() => {
|
|
1510
|
+
this.applyBridgeSnapshot(bridge);
|
|
1511
|
+
});
|
|
1512
|
+
this._remoteChangeUnsub = bridge.onRemoteChange((change) => {
|
|
1513
|
+
this.handleRemoteProjectFileChange(change);
|
|
1514
|
+
});
|
|
1515
|
+
this.applyBridgeSnapshot(bridge);
|
|
1516
|
+
}
|
|
1517
|
+
applyBridgeSnapshot(bridge) {
|
|
1518
|
+
const snapshot = bridge.snapshot();
|
|
1519
|
+
if (snapshot.status !== this._bridgeStatus) {
|
|
1520
|
+
this._bridgeStatus = snapshot.status;
|
|
1521
|
+
for (const listener of this._bridgeStatusListeners) {
|
|
1522
|
+
listener();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (snapshot.joinCode !== this._bridgeJoinCode) {
|
|
1526
|
+
this._bridgeJoinCode = snapshot.joinCode;
|
|
1527
|
+
for (const listener of this._bridgeJoinCodeListeners) {
|
|
1528
|
+
listener();
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
// ---------------------------------------------------------------------------
|
|
1534
|
+
// Helpers
|
|
1535
|
+
// ---------------------------------------------------------------------------
|
|
1536
|
+
/** Stable empty warnings array, the snapshot value while no resolution is recorded. */
|
|
1537
|
+
const NO_RESOLUTION_WARNINGS = [];
|
|
1538
|
+
/** Identity key of a resolution warning: the fields that name the same finding across sources. */
|
|
1539
|
+
function resolutionWarningKey(warning) {
|
|
1540
|
+
const reference = "reference" in warning ? warning.reference : "";
|
|
1541
|
+
const code = warning.kind === "catalog-move-failed" ? warning.code : "";
|
|
1542
|
+
return `${warning.kind} ${warning.origin} ${reference} ${code}`;
|
|
1543
|
+
}
|
|
1544
|
+
function logWorkspaceCompile(result) {
|
|
1545
|
+
const resultsByPath = result.projectResult.results;
|
|
1546
|
+
for (const [path, diagnostics] of result.files) {
|
|
1547
|
+
if (diagnostics.length > 0) {
|
|
1548
|
+
logger.warn(`[user-tile-compiler] ${path}: ${diagnostics.length} diagnostic(s)`);
|
|
1549
|
+
for (const diagnostic of diagnostics) {
|
|
1550
|
+
const range = diagnostic.range;
|
|
1551
|
+
logger.warn(` ${path}:${range.startLine}:${range.startColumn} - ${diagnostic.message}`);
|
|
1552
|
+
}
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
const program = resultsByPath.get(path)?.program;
|
|
1556
|
+
if (program) {
|
|
1557
|
+
logger.debug(`[user-tile-compiler] ${path}: compiled ${program.kind} "${program.name}"`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|