@wendoo/bridge-app 0.2.2

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