@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,404 @@
|
|
|
1
|
+
import { fileContentEquals } from "@wendoo/app-host";
|
|
2
|
+
import { createWorkspaceCompiler, } from "@wendoo/ts-compiler";
|
|
3
|
+
import { createAppBridge } from "./app-bridge.js";
|
|
4
|
+
function buildFeatureStatus(file, diagnostics) {
|
|
5
|
+
let errorCount = 0;
|
|
6
|
+
let warningCount = 0;
|
|
7
|
+
for (const diagnostic of diagnostics) {
|
|
8
|
+
if (diagnostic.severity === "error") {
|
|
9
|
+
errorCount++;
|
|
10
|
+
}
|
|
11
|
+
else if (diagnostic.severity === "warning") {
|
|
12
|
+
warningCount++;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
file,
|
|
17
|
+
success: errorCount === 0,
|
|
18
|
+
diagnosticCount: {
|
|
19
|
+
error: errorCount,
|
|
20
|
+
warning: warningCount,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build an {@link AppBridgeFeature} that compiles project files on remote
|
|
26
|
+
* changes and publishes diagnostics (and, by default, per-file status) to the
|
|
27
|
+
* peer.
|
|
28
|
+
*/
|
|
29
|
+
export function createCompilationFeature(options) {
|
|
30
|
+
return {
|
|
31
|
+
attach(context) {
|
|
32
|
+
let lastSnapshot;
|
|
33
|
+
const previousDiagnosticFiles = new Set();
|
|
34
|
+
const publishStatus = options.publishStatus ?? true;
|
|
35
|
+
const publishSnapshot = (snapshot) => {
|
|
36
|
+
if (context.snapshot().status !== "connected") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const currentFiles = new Set();
|
|
40
|
+
for (const [file, diagnostics] of snapshot.files) {
|
|
41
|
+
currentFiles.add(file);
|
|
42
|
+
if (diagnostics.length > 0 || previousDiagnosticFiles.has(file)) {
|
|
43
|
+
context.publishDiagnostics(file, diagnostics);
|
|
44
|
+
if (publishStatus) {
|
|
45
|
+
context.publishStatus(buildFeatureStatus(file, diagnostics));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const file of previousDiagnosticFiles) {
|
|
50
|
+
if (!currentFiles.has(file)) {
|
|
51
|
+
context.publishDiagnostics(file, []);
|
|
52
|
+
if (publishStatus) {
|
|
53
|
+
context.publishStatus(buildFeatureStatus(file, []));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
previousDiagnosticFiles.clear();
|
|
58
|
+
for (const [file, diagnostics] of snapshot.files) {
|
|
59
|
+
if (diagnostics.length > 0) {
|
|
60
|
+
previousDiagnosticFiles.add(file);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const compileAndPublish = () => {
|
|
65
|
+
const snapshot = options.compiler.compile();
|
|
66
|
+
if (lastSnapshot !== snapshot) {
|
|
67
|
+
lastSnapshot = snapshot;
|
|
68
|
+
publishSnapshot(snapshot);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const compileUnsub = options.compiler.onDidCompile((snapshot) => {
|
|
72
|
+
lastSnapshot = snapshot;
|
|
73
|
+
publishSnapshot(snapshot);
|
|
74
|
+
});
|
|
75
|
+
options.compiler.replaceProjectFiles(context.projectFileSnapshot());
|
|
76
|
+
compileAndPublish();
|
|
77
|
+
const remoteChangeUnsub = context.onRemoteChange((change) => {
|
|
78
|
+
options.compiler.applyProjectFileChange(change);
|
|
79
|
+
compileAndPublish();
|
|
80
|
+
});
|
|
81
|
+
const syncUnsub = context.onDidSync(() => {
|
|
82
|
+
if (lastSnapshot) {
|
|
83
|
+
publishSnapshot(lastSnapshot);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
syncUnsub();
|
|
88
|
+
remoteChangeUnsub();
|
|
89
|
+
compileUnsub();
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Drives a {@link CompilationProvider} from incoming filesystem notifications
|
|
96
|
+
* and emits diagnostics to the peer. Sends version-tagged `compile:diagnostics`
|
|
97
|
+
* and `compile:status` messages when connected.
|
|
98
|
+
*/
|
|
99
|
+
export class CompilationManager {
|
|
100
|
+
_provider;
|
|
101
|
+
_send;
|
|
102
|
+
_isConnected;
|
|
103
|
+
_versions = new Map();
|
|
104
|
+
_previousFiles = new Set();
|
|
105
|
+
_compilationListeners = new Set();
|
|
106
|
+
_lastResult;
|
|
107
|
+
_removalListeners = new Set();
|
|
108
|
+
constructor(provider, send, isConnected) {
|
|
109
|
+
this._provider = provider;
|
|
110
|
+
this._send = send;
|
|
111
|
+
this._isConnected = isConnected;
|
|
112
|
+
}
|
|
113
|
+
handleFileChange(ev) {
|
|
114
|
+
switch (ev.action) {
|
|
115
|
+
case "write":
|
|
116
|
+
this._provider.fileWritten(ev.path, ev.content);
|
|
117
|
+
break;
|
|
118
|
+
case "delete":
|
|
119
|
+
this._provider.fileDeleted(ev.path);
|
|
120
|
+
break;
|
|
121
|
+
case "rename":
|
|
122
|
+
this._provider.fileRenamed(ev.oldPath, ev.newPath);
|
|
123
|
+
break;
|
|
124
|
+
case "import":
|
|
125
|
+
this._provider.fullSync(ev.entries);
|
|
126
|
+
break;
|
|
127
|
+
case "mkdir":
|
|
128
|
+
case "rmdir":
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.compileAndEmit();
|
|
132
|
+
}
|
|
133
|
+
onCompilation(fn) {
|
|
134
|
+
this._compilationListeners.add(fn);
|
|
135
|
+
return () => {
|
|
136
|
+
this._compilationListeners.delete(fn);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
onRemoval(fn) {
|
|
140
|
+
this._removalListeners.add(fn);
|
|
141
|
+
return () => {
|
|
142
|
+
this._removalListeners.delete(fn);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
sendDiagnostics() {
|
|
146
|
+
if (!this._lastResult || !this._isConnected())
|
|
147
|
+
return;
|
|
148
|
+
for (const [file, diagnostics] of this._lastResult.files) {
|
|
149
|
+
if (diagnostics.length > 0) {
|
|
150
|
+
this.emitDiagnostics(file, diagnostics);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
compileAndEmit() {
|
|
155
|
+
const result = this._provider.compileAll();
|
|
156
|
+
this._lastResult = result;
|
|
157
|
+
for (const fn of this._compilationListeners) {
|
|
158
|
+
fn(result);
|
|
159
|
+
}
|
|
160
|
+
if (!this._isConnected())
|
|
161
|
+
return;
|
|
162
|
+
const currentFiles = new Set();
|
|
163
|
+
for (const [file, diagnostics] of result.files) {
|
|
164
|
+
currentFiles.add(file);
|
|
165
|
+
if (diagnostics.length > 0 || this._previousFiles.has(file)) {
|
|
166
|
+
this.emitDiagnostics(file, diagnostics);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const file of this._previousFiles) {
|
|
170
|
+
if (!currentFiles.has(file)) {
|
|
171
|
+
this.emitDiagnostics(file, []);
|
|
172
|
+
for (const fn of this._removalListeners) {
|
|
173
|
+
fn(file);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this._previousFiles.clear();
|
|
178
|
+
for (const [file, diagnostics] of result.files) {
|
|
179
|
+
if (diagnostics.length > 0) {
|
|
180
|
+
this._previousFiles.add(file);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
emitDiagnostics(file, diagnostics) {
|
|
185
|
+
const version = (this._versions.get(file) ?? 0) + 1;
|
|
186
|
+
this._versions.set(file, version);
|
|
187
|
+
this._send({
|
|
188
|
+
type: "compile:diagnostics",
|
|
189
|
+
payload: { file, version, diagnostics },
|
|
190
|
+
});
|
|
191
|
+
let errorCount = 0;
|
|
192
|
+
let warningCount = 0;
|
|
193
|
+
for (const d of diagnostics) {
|
|
194
|
+
if (d.severity === "error")
|
|
195
|
+
errorCount++;
|
|
196
|
+
else if (d.severity === "warning")
|
|
197
|
+
warningCount++;
|
|
198
|
+
}
|
|
199
|
+
this._send({
|
|
200
|
+
type: "compile:status",
|
|
201
|
+
payload: {
|
|
202
|
+
file,
|
|
203
|
+
success: errorCount === 0,
|
|
204
|
+
diagnosticCount: { error: errorCount, warning: warningCount },
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Wrap a {@link ProjectFileSystem} as a TS workspace compiler over the live project files. */
|
|
210
|
+
export function createProjectCompiler(options) {
|
|
211
|
+
const { mounts, environment, filesystem, projectNamespace, dependencies, dependencyMounts } = options;
|
|
212
|
+
const compiler = createWorkspaceCompiler({
|
|
213
|
+
projectNamespace,
|
|
214
|
+
mounts,
|
|
215
|
+
environment,
|
|
216
|
+
dependencies,
|
|
217
|
+
dependencyMounts,
|
|
218
|
+
});
|
|
219
|
+
if (options.onDidCompile) {
|
|
220
|
+
compiler.onDidCompile(options.onDidCompile);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
compiler,
|
|
224
|
+
initialize() {
|
|
225
|
+
compiler.replaceWorkspace(filesystem.exportSnapshot());
|
|
226
|
+
compiler.compile();
|
|
227
|
+
},
|
|
228
|
+
replaceProjectFiles() {
|
|
229
|
+
compiler.replaceWorkspace(filesystem.exportSnapshot());
|
|
230
|
+
compiler.compile();
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Wire up an {@link AppBridge} that uses `projectCompiler` for diagnostics and
|
|
236
|
+
* surfaces compiler-controlled files to the remote peer.
|
|
237
|
+
*/
|
|
238
|
+
export function createBridgeProject(options) {
|
|
239
|
+
const { projectCompiler, servedFileSystem } = options;
|
|
240
|
+
const compiler = projectCompiler.compiler;
|
|
241
|
+
let latestBindingToken = options.bindingToken;
|
|
242
|
+
const onBindingTokenChange = (token) => {
|
|
243
|
+
latestBindingToken = token;
|
|
244
|
+
options.onBindingTokenChange?.(token);
|
|
245
|
+
};
|
|
246
|
+
let currentBridge = buildBridge({
|
|
247
|
+
bridgeUrl: options.bridgeUrl,
|
|
248
|
+
filesystem: servedFileSystem,
|
|
249
|
+
bindingToken: latestBindingToken,
|
|
250
|
+
onBindingTokenChange,
|
|
251
|
+
}, compiler);
|
|
252
|
+
return {
|
|
253
|
+
get bridge() {
|
|
254
|
+
return currentBridge;
|
|
255
|
+
},
|
|
256
|
+
recreateBridge(bridgeUrl) {
|
|
257
|
+
currentBridge.stop();
|
|
258
|
+
currentBridge = buildBridge({ bridgeUrl, filesystem: servedFileSystem, bindingToken: latestBindingToken, onBindingTokenChange }, compiler);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** The directory portion of `path` (empty for a root-level path). */
|
|
263
|
+
function parentDirectory(path) {
|
|
264
|
+
const idx = path.lastIndexOf("/");
|
|
265
|
+
return idx <= 0 ? "" : path.slice(0, idx);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Ensure `dirPath` and each of its ancestors are present in `snapshot` as
|
|
269
|
+
* directory entries, adding any that are missing shallowest-first. An empty
|
|
270
|
+
* `dirPath` (a root-level path's parent) adds nothing.
|
|
271
|
+
*/
|
|
272
|
+
function ensureSnapshotDirectory(snapshot, dirPath) {
|
|
273
|
+
const segments = dirPath.split("/").filter((segment) => segment.length > 0);
|
|
274
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
275
|
+
const ancestor = segments.slice(0, i).join("/");
|
|
276
|
+
if (!snapshot.has(ancestor)) {
|
|
277
|
+
snapshot.set(ancestor, { kind: "directory" });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** True when the two compiler-controlled file maps carry a different set of paths or content. */
|
|
282
|
+
function compilerControlledFilesChanged(previous, current) {
|
|
283
|
+
if (previous.size !== current.size) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
for (const [path, content] of current) {
|
|
287
|
+
const before = previous.get(path);
|
|
288
|
+
if (before === undefined || !fileContentEquals(before, content)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Wrap a {@link ProjectFileSystem} so its exported snapshot also carries the
|
|
296
|
+
* compiler-controlled files (ambient declarations, `tsconfig.json`, and the
|
|
297
|
+
* read-only installed-extensions tree), all marked read-only. Local and remote
|
|
298
|
+
* changes targeting those augmented paths are filtered out, leaving them
|
|
299
|
+
* read-only from the peer's side.
|
|
300
|
+
*
|
|
301
|
+
* When a compile changes the compiler-controlled file set (installing or
|
|
302
|
+
* uninstalling an extension adds or removes its `.libraries/` subtree), the
|
|
303
|
+
* wrapper emits one full-snapshot `import` local change and invokes the
|
|
304
|
+
* options' change callback with the new set. The peer reconciles the whole
|
|
305
|
+
* tree from the import: newly installed paths appear and uninstalled paths
|
|
306
|
+
* are pruned. The read-only compiler-controlled paths cannot be updated by an
|
|
307
|
+
* incremental write/delete notification, so the full-snapshot import is their
|
|
308
|
+
* propagation channel.
|
|
309
|
+
*/
|
|
310
|
+
export function augmentProjectFileSystem(filesystem, compiler, options) {
|
|
311
|
+
const isAugmentedPath = (path) => compiler.getCompilerControlledFiles().has(path);
|
|
312
|
+
const filterChange = (change) => {
|
|
313
|
+
switch (change.action) {
|
|
314
|
+
case "write":
|
|
315
|
+
case "delete":
|
|
316
|
+
case "mkdir":
|
|
317
|
+
case "rmdir":
|
|
318
|
+
return isAugmentedPath(change.path) ? undefined : change;
|
|
319
|
+
case "rename":
|
|
320
|
+
return isAugmentedPath(change.oldPath) || isAugmentedPath(change.newPath) ? undefined : change;
|
|
321
|
+
case "import":
|
|
322
|
+
return { action: "import", entries: change.entries.filter(([path]) => !isAugmentedPath(path)) };
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const buildSnapshot = () => {
|
|
326
|
+
const snapshot = filesystem.exportSnapshot();
|
|
327
|
+
const controlledFiles = compiler.getCompilerControlledFiles();
|
|
328
|
+
for (const [path, content] of controlledFiles) {
|
|
329
|
+
ensureSnapshotDirectory(snapshot, parentDirectory(path));
|
|
330
|
+
snapshot.set(path, { kind: "file", content, etag: "compiler-controlled", isReadonly: true });
|
|
331
|
+
}
|
|
332
|
+
return snapshot;
|
|
333
|
+
};
|
|
334
|
+
const localChangeListeners = new Set();
|
|
335
|
+
let previousControlledFiles = new Map(compiler.getCompilerControlledFiles());
|
|
336
|
+
compiler.onDidCompile(() => {
|
|
337
|
+
const currentControlledFiles = compiler.getCompilerControlledFiles();
|
|
338
|
+
if (!compilerControlledFilesChanged(previousControlledFiles, currentControlledFiles)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
previousControlledFiles = new Map(currentControlledFiles);
|
|
342
|
+
options?.onCompilerControlledFilesChanged?.(currentControlledFiles);
|
|
343
|
+
const change = { action: "import", entries: [...buildSnapshot()] };
|
|
344
|
+
for (const listener of localChangeListeners) {
|
|
345
|
+
listener(change);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
return {
|
|
349
|
+
exportSnapshot() {
|
|
350
|
+
return buildSnapshot();
|
|
351
|
+
},
|
|
352
|
+
applyRemoteChange(change) {
|
|
353
|
+
const filtered = filterChange(change);
|
|
354
|
+
if (filtered) {
|
|
355
|
+
filesystem.applyRemoteChange(filtered);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
applyLocalChange(change) {
|
|
359
|
+
const filtered = filterChange(change);
|
|
360
|
+
if (filtered) {
|
|
361
|
+
filesystem.applyLocalChange(filtered);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
onLocalChange(listener) {
|
|
365
|
+
localChangeListeners.add(listener);
|
|
366
|
+
const unsubscribeUnderlying = filesystem.onLocalChange(listener);
|
|
367
|
+
return () => {
|
|
368
|
+
localChangeListeners.delete(listener);
|
|
369
|
+
unsubscribeUnderlying();
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
onAnyChange(listener) {
|
|
373
|
+
return filesystem.onAnyChange(listener);
|
|
374
|
+
},
|
|
375
|
+
flush() {
|
|
376
|
+
filesystem.flush();
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function buildBridge(options, compiler) {
|
|
381
|
+
return createAppBridge({
|
|
382
|
+
bridgeUrl: options.bridgeUrl,
|
|
383
|
+
filesystem: options.filesystem,
|
|
384
|
+
features: [createCompilationFeature({ compiler: createProjectFileCompilerAdapter(compiler) })],
|
|
385
|
+
bindingToken: options.bindingToken,
|
|
386
|
+
onBindingTokenChange: options.onBindingTokenChange,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function createProjectFileCompilerAdapter(compiler) {
|
|
390
|
+
return {
|
|
391
|
+
replaceProjectFiles(snapshot) {
|
|
392
|
+
compiler.replaceWorkspace(snapshot);
|
|
393
|
+
},
|
|
394
|
+
applyProjectFileChange(change) {
|
|
395
|
+
compiler.applyWorkspaceChange(change);
|
|
396
|
+
},
|
|
397
|
+
compile() {
|
|
398
|
+
return compiler.compile();
|
|
399
|
+
},
|
|
400
|
+
onDidCompile(listener) {
|
|
401
|
+
return compiler.onDidCompile((result) => listener(result));
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core layer's `<owner>/<repo>` coordinate: its identity, its compiler
|
|
3
|
+
* namespace, and the name it is imported and stored under
|
|
4
|
+
* (`@lib/wendoo-lang/lib-core`). The single shared language base at the bottom of
|
|
5
|
+
* every Wendoo platform's stack.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CORE_LIB_COORDINATE = "wendoo-lang/lib-core";
|
|
8
|
+
/** Manifest reference form delivering the core layer from a host application's embed record. */
|
|
9
|
+
export declare const CORE_LIB_REFERENCE = "embedded:wendoo-lang/lib-core";
|
|
10
|
+
//# sourceMappingURL=core-extension.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-extension.d.ts","sourceRoot":"","sources":["../src/core-extension.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,kCAAkC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core layer's `<owner>/<repo>` coordinate: its identity, its compiler
|
|
3
|
+
* namespace, and the name it is imported and stored under
|
|
4
|
+
* (`@lib/wendoo-lang/lib-core`). The single shared language base at the bottom of
|
|
5
|
+
* every Wendoo platform's stack.
|
|
6
|
+
*/
|
|
7
|
+
export const CORE_LIB_COORDINATE = "wendoo-lang/lib-core";
|
|
8
|
+
/** Manifest reference form delivering the core layer from a host application's embed record. */
|
|
9
|
+
export const CORE_LIB_REFERENCE = "embedded:wendoo-lang/lib-core";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BrainServices } from "@wendoo/core/brain";
|
|
2
|
+
import type { EmbeddedExtension } from "./embedded-extensions.js";
|
|
3
|
+
/**
|
|
4
|
+
* A declaration in a repo-embedded extension that ships without an explicit
|
|
5
|
+
* stable `id`. Read-only extension source is regenerated on load, so the
|
|
6
|
+
* compiler cannot mint and persist an id for it; the id must be present in the
|
|
7
|
+
* bundled source.
|
|
8
|
+
*/
|
|
9
|
+
export interface EmbeddedExtensionIdViolation {
|
|
10
|
+
/** The `<owner>/<repo>` coordinate of the extension whose source is missing an id. */
|
|
11
|
+
coordinate: string;
|
|
12
|
+
/** The extension-relative path of the file declaring the id-less tile. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** The compiler's diagnostic message, naming the declaration kind and name. */
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compile every extension in `embedRecord` as read-only roots and collect each
|
|
19
|
+
* declared Sensor, Actuator, or Conversion that ships without an explicit
|
|
20
|
+
* stable `id`. Systems carry no explicit id (their identity is structural) and
|
|
21
|
+
* are never reported. Returns an empty array when every declaration carries an
|
|
22
|
+
* id.
|
|
23
|
+
*
|
|
24
|
+
* The compile mirrors how a consuming project mounts these extensions: each
|
|
25
|
+
* origin's transitive closure is resolved from the record, and every origin is
|
|
26
|
+
* compiled under its own coordinate namespace into `services`. Supply a
|
|
27
|
+
* `services` built with the app's modules so the ambient the extensions compile
|
|
28
|
+
* against matches the app's platform.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findEmbeddedExtensionsMissingStableIds(embedRecord: readonly EmbeddedExtension[], services: BrainServices): EmbeddedExtensionIdViolation[];
|
|
31
|
+
/**
|
|
32
|
+
* Render {@link EmbeddedExtensionIdViolation}s as an author-facing failure
|
|
33
|
+
* message that names each offending extension coordinate and file and states
|
|
34
|
+
* how to obtain a stable id. Use it as the assertion message of a build gate
|
|
35
|
+
* over an app's embedded extensions.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatEmbeddedExtensionIdViolations(violations: readonly EmbeddedExtensionIdViolation[]): string;
|
|
38
|
+
//# sourceMappingURL=embedded-extension-id-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedded-extension-id-gate.d.ts","sourceRoot":"","sources":["../src/embedded-extension-id-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE;;;;;GAKG;AACH,MAAM,WAAW,4BAA4B;IAC3C,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sCAAsC,CACpD,WAAW,EAAE,SAAS,iBAAiB,EAAE,EACzC,QAAQ,EAAE,aAAa,GACtB,4BAA4B,EAAE,CA4BhC;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,UAAU,EAAE,SAAS,4BAA4B,EAAE,GAAG,MAAM,CAQ/G"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CompileDiagCode, MultiRootSession } from "@wendoo/ts-compiler";
|
|
2
|
+
import { resolveProjectExtensions } from "./embedded-extensions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compile every extension in `embedRecord` as read-only roots and collect each
|
|
5
|
+
* declared Sensor, Actuator, or Conversion that ships without an explicit
|
|
6
|
+
* stable `id`. Systems carry no explicit id (their identity is structural) and
|
|
7
|
+
* are never reported. Returns an empty array when every declaration carries an
|
|
8
|
+
* id.
|
|
9
|
+
*
|
|
10
|
+
* The compile mirrors how a consuming project mounts these extensions: each
|
|
11
|
+
* origin's transitive closure is resolved from the record, and every origin is
|
|
12
|
+
* compiled under its own coordinate namespace into `services`. Supply a
|
|
13
|
+
* `services` built with the app's modules so the ambient the extensions compile
|
|
14
|
+
* against matches the app's platform.
|
|
15
|
+
*/
|
|
16
|
+
export function findEmbeddedExtensionsMissingStableIds(embedRecord, services) {
|
|
17
|
+
const references = {};
|
|
18
|
+
for (const extension of embedRecord) {
|
|
19
|
+
references[extension.canonicalOrigin] = `embedded:${extension.canonicalOrigin}`;
|
|
20
|
+
}
|
|
21
|
+
const { dependencyMounts } = resolveProjectExtensions(references, { embedded: embedRecord });
|
|
22
|
+
const roots = dependencyMounts.map((mount) => ({
|
|
23
|
+
namespace: mount.namespace,
|
|
24
|
+
files: mount.files,
|
|
25
|
+
dependencies: mount.dependencies,
|
|
26
|
+
readOnlySource: true,
|
|
27
|
+
}));
|
|
28
|
+
const session = new MultiRootSession({ services });
|
|
29
|
+
session.setRoots(roots);
|
|
30
|
+
const { roots: results } = session.compile();
|
|
31
|
+
const violations = [];
|
|
32
|
+
for (const [coordinate, result] of results) {
|
|
33
|
+
for (const [path, compileResult] of result.results) {
|
|
34
|
+
for (const diagnostic of compileResult.diagnostics) {
|
|
35
|
+
if (diagnostic.code === CompileDiagCode.ExtensionDeclarationMissingId) {
|
|
36
|
+
violations.push({ coordinate, path, message: diagnostic.message });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return violations;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render {@link EmbeddedExtensionIdViolation}s as an author-facing failure
|
|
45
|
+
* message that names each offending extension coordinate and file and states
|
|
46
|
+
* how to obtain a stable id. Use it as the assertion message of a build gate
|
|
47
|
+
* over an app's embedded extensions.
|
|
48
|
+
*/
|
|
49
|
+
export function formatEmbeddedExtensionIdViolations(violations) {
|
|
50
|
+
const lines = violations.map((v) => ` ${v.coordinate} (${v.path}): ${v.message}`);
|
|
51
|
+
return ("Repo-embedded extension(s) ship a declaration without an explicit stable id:\n" +
|
|
52
|
+
`${lines.join("\n")}\n` +
|
|
53
|
+
"To obtain an id, compile the extension as a writable project to mint one, " +
|
|
54
|
+
"then set that id explicitly in the declaration's source.");
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { EmbeddedExtension } from "./embedded-extensions.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return the declared `files` entries an extension names but that do not exist on
|
|
4
|
+
* disk, resolved relative to the extension's manifest directory. An empty result
|
|
5
|
+
* means every listed file is present. Files present on disk but absent from the
|
|
6
|
+
* list are valid content exclusions and are never reported: this checks only the
|
|
7
|
+
* error direction, a listed file the build cannot assemble.
|
|
8
|
+
*
|
|
9
|
+
* @param dir - Directory holding the extension's `wendoo.json`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findMissingExtensionFiles(dir: string): readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Absolute paths of every on-disk file that backs an embedded extension: its
|
|
14
|
+
* `wendoo.json` plus each file its `files` list names. A build-time provider
|
|
15
|
+
* watches these so editing extension source refreshes the assembled bundle.
|
|
16
|
+
*
|
|
17
|
+
* @param dir - Directory holding the extension's `wendoo.json`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extensionSourceFiles(dir: string): readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Assemble an embedded extension by reading its `wendoo.json` from `dir`,
|
|
22
|
+
* loading exactly the files its `files` list names, and returning the bundle
|
|
23
|
+
* keyed by `canonicalOrigin`. The manifest is included at the extension root as
|
|
24
|
+
* `wendoo.json` and is never listed by `files`. Each listed entry is resolved
|
|
25
|
+
* relative to `dir` and bundled at its extension-relative path.
|
|
26
|
+
*
|
|
27
|
+
* @param dir - Directory holding the extension's `wendoo.json`.
|
|
28
|
+
* @param canonicalOrigin - The `<owner>/<repo>` coordinate the bundle is keyed under.
|
|
29
|
+
* @throws {Error} when the manifest is missing, invalid, declares neither
|
|
30
|
+
* `files` nor a `hostApp` bundle, or names a file absent from disk.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildEmbeddedExtensionFromDir(dir: string, canonicalOrigin: string): EmbeddedExtension;
|
|
33
|
+
//# sourceMappingURL=embedded-extension-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedded-extension-loader.d.ts","sourceRoot":"","sources":["../src/embedded-extension-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAyB,MAAM,0BAA0B,CAAC;AA8CzF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAGxE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAGnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,iBAAiB,CAiBrG"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { parseProjectContentManifest, WENDOO_JSON_PATH } from "@wendoo/app-host";
|
|
4
|
+
import { findMissingListedFiles } from "./manifest-files.js";
|
|
5
|
+
/**
|
|
6
|
+
* Map a manifest `files` entry to the extension-relative path it occupies in the
|
|
7
|
+
* assembled bundle. An entry that stays within the extension directory keeps its
|
|
8
|
+
* directory-relative path; an entry the manifest pulls in from outside that
|
|
9
|
+
* directory (a generated artifact reached through `..`) is placed at the
|
|
10
|
+
* extension root under its base name.
|
|
11
|
+
*/
|
|
12
|
+
function bundlePathFor(dir, entry) {
|
|
13
|
+
const rel = relative(dir, resolve(dir, entry));
|
|
14
|
+
return rel.startsWith("..") ? basename(entry) : rel.split(sep).join("/");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read and parse the `files` list an extension declares in its own
|
|
18
|
+
* `wendoo.json`. An extension must declare content `files` or a `hostApp`
|
|
19
|
+
* bundle: a library names its content files, and a target (a `hostApp`) carries
|
|
20
|
+
* no library content, so it resolves to an empty file list. A manifest that
|
|
21
|
+
* declares neither is rejected.
|
|
22
|
+
*/
|
|
23
|
+
function readManifestFiles(dir) {
|
|
24
|
+
const manifestPath = resolve(dir, WENDOO_JSON_PATH);
|
|
25
|
+
if (!existsSync(manifestPath)) {
|
|
26
|
+
throw new Error(`Embedded extension at ${dir} has no ${WENDOO_JSON_PATH}.`);
|
|
27
|
+
}
|
|
28
|
+
const manifestText = readFileSync(manifestPath, "utf8");
|
|
29
|
+
const parsed = parseProjectContentManifest(manifestText);
|
|
30
|
+
if (!parsed.ok) {
|
|
31
|
+
throw new Error(`Embedded extension manifest at ${manifestPath} is invalid: ` +
|
|
32
|
+
parsed.errors.map((e) => `${e.path} ${e.message}`).join("; "));
|
|
33
|
+
}
|
|
34
|
+
if (parsed.manifest.files === undefined) {
|
|
35
|
+
if (parsed.manifest.hostApp !== undefined) {
|
|
36
|
+
return { manifestText, files: [] };
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Embedded extension manifest at ${manifestPath} must declare a "files" list naming its content, or a "hostApp" bundle.`);
|
|
39
|
+
}
|
|
40
|
+
return { manifestText, files: parsed.manifest.files };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Return the declared `files` entries an extension names but that do not exist on
|
|
44
|
+
* disk, resolved relative to the extension's manifest directory. An empty result
|
|
45
|
+
* means every listed file is present. Files present on disk but absent from the
|
|
46
|
+
* list are valid content exclusions and are never reported: this checks only the
|
|
47
|
+
* error direction, a listed file the build cannot assemble.
|
|
48
|
+
*
|
|
49
|
+
* @param dir - Directory holding the extension's `wendoo.json`.
|
|
50
|
+
*/
|
|
51
|
+
export function findMissingExtensionFiles(dir) {
|
|
52
|
+
const { files } = readManifestFiles(dir);
|
|
53
|
+
return findMissingListedFiles(files, (entry) => existsSync(resolve(dir, entry)));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Absolute paths of every on-disk file that backs an embedded extension: its
|
|
57
|
+
* `wendoo.json` plus each file its `files` list names. A build-time provider
|
|
58
|
+
* watches these so editing extension source refreshes the assembled bundle.
|
|
59
|
+
*
|
|
60
|
+
* @param dir - Directory holding the extension's `wendoo.json`.
|
|
61
|
+
*/
|
|
62
|
+
export function extensionSourceFiles(dir) {
|
|
63
|
+
const { files } = readManifestFiles(dir);
|
|
64
|
+
return [resolve(dir, WENDOO_JSON_PATH), ...files.map((entry) => resolve(dir, entry))];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Assemble an embedded extension by reading its `wendoo.json` from `dir`,
|
|
68
|
+
* loading exactly the files its `files` list names, and returning the bundle
|
|
69
|
+
* keyed by `canonicalOrigin`. The manifest is included at the extension root as
|
|
70
|
+
* `wendoo.json` and is never listed by `files`. Each listed entry is resolved
|
|
71
|
+
* relative to `dir` and bundled at its extension-relative path.
|
|
72
|
+
*
|
|
73
|
+
* @param dir - Directory holding the extension's `wendoo.json`.
|
|
74
|
+
* @param canonicalOrigin - The `<owner>/<repo>` coordinate the bundle is keyed under.
|
|
75
|
+
* @throws {Error} when the manifest is missing, invalid, declares neither
|
|
76
|
+
* `files` nor a `hostApp` bundle, or names a file absent from disk.
|
|
77
|
+
*/
|
|
78
|
+
export function buildEmbeddedExtensionFromDir(dir, canonicalOrigin) {
|
|
79
|
+
const { manifestText, files: declared } = readManifestFiles(dir);
|
|
80
|
+
const missing = findMissingExtensionFiles(dir);
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
throw new Error(`Embedded extension "${canonicalOrigin}" at ${dir} declares files absent from disk: ${missing.join(", ")}.`);
|
|
83
|
+
}
|
|
84
|
+
const files = declared.map((entry) => ({
|
|
85
|
+
path: bundlePathFor(dir, entry),
|
|
86
|
+
content: readFileSync(resolve(dir, entry), "utf8"),
|
|
87
|
+
}));
|
|
88
|
+
files.push({ path: WENDOO_JSON_PATH, content: manifestText });
|
|
89
|
+
return { canonicalOrigin, files };
|
|
90
|
+
}
|