@wabot-dev/framework 0.9.80 → 2.0.0-beta.0

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/bin/skills.mjs +151 -0
  2. package/bin/wabot-skills.mjs +120 -0
  3. package/dist/build/build.js +1031 -8
  4. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
  5. package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
  6. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
  7. package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
  8. package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
  9. package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
  10. package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
  11. package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
  12. package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
  13. package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
  14. package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
  15. package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
  16. package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
  17. package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
  18. package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
  19. package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
  20. package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
  21. package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
  22. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
  23. package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
  24. package/dist/src/addon/ui/preact/outlet.js +22 -0
  25. package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
  26. package/dist/src/core/repository/CrudRepository.js +7 -7
  27. package/dist/src/feature/async/computeDedupKey.js +1 -1
  28. package/dist/src/feature/pg/@pgExtension.js +2 -4
  29. package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
  30. package/dist/src/feature/project-runner/scanner.js +1 -1
  31. package/dist/src/feature/repository/@memExtension.js +1 -2
  32. package/dist/src/feature/ui-controller/actions.js +35 -0
  33. package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
  34. package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
  35. package/dist/src/feature/ui-controller/bundler/index.js +4 -0
  36. package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
  37. package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
  38. package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
  39. package/dist/src/feature/ui-controller/document/escape.js +17 -0
  40. package/dist/src/feature/ui-controller/document/helpers.js +13 -0
  41. package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
  42. package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
  43. package/dist/src/feature/ui-controller/island/island.js +40 -0
  44. package/dist/src/feature/ui-controller/island/serialize.js +35 -0
  45. package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
  46. package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
  47. package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
  48. package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
  49. package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
  50. package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
  51. package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
  52. package/dist/src/index.d.ts +632 -3
  53. package/dist/src/index.js +30 -1
  54. package/dist/src/testing/index.d.ts +43 -1
  55. package/dist/src/testing/index.js +1 -0
  56. package/dist/src/testing/uiHarness.js +102 -0
  57. package/dist/src/ui/client.js +6 -0
  58. package/dist/src/ui/index.d.ts +427 -0
  59. package/dist/src/ui/index.js +29 -0
  60. package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
  61. package/dist/src/ui/jsx-dev-runtime.js +1 -0
  62. package/dist/src/ui/jsx-runtime.d.ts +1 -0
  63. package/dist/src/ui/jsx-runtime.js +1 -0
  64. package/package.json +33 -13
  65. package/skills/wabot-async/SKILL.md +143 -0
  66. package/skills/wabot-auth/SKILL.md +153 -0
  67. package/skills/wabot-chat/SKILL.md +140 -0
  68. package/skills/wabot-di-config/SKILL.md +117 -0
  69. package/skills/wabot-framework/SKILL.md +81 -0
  70. package/skills/wabot-framework/references/quickstart.md +85 -0
  71. package/skills/wabot-mindset/SKILL.md +159 -0
  72. package/skills/wabot-ops/SKILL.md +151 -0
  73. package/skills/wabot-persistence/SKILL.md +159 -0
  74. package/skills/wabot-rest-socket/SKILL.md +167 -0
  75. package/skills/wabot-testing/SKILL.md +214 -0
  76. package/skills/wabot-ui/SKILL.md +201 -0
  77. package/skills/wabot-validation/SKILL.md +108 -0
@@ -29,6 +29,11 @@ import { runCommandHandlers } from '../async/runCommandHandlers.js';
29
29
  import { runCronHandlers } from '../async/runCronHandlers.js';
30
30
  import { SocketControllerMetadataStore } from '../socket-controller/metadata/SocketControllerMetadataStore.js';
31
31
  import { runSocketControllers } from '../socket-controller/runSocketControllers.js';
32
+ import { ExpressProvider } from '../express/ExpressProvider.js';
33
+ import { UiControllerMetadataStore } from '../ui-controller/metadata/UiControllerMetadataStore.js';
34
+ import { UiRendererRegistry } from '../ui-controller/renderer/UiRendererRegistry.js';
35
+ import { IslandRegistry } from '../ui-controller/island/IslandRegistry.js';
36
+ import { runUiControllers } from '../ui-controller/runUiControllers.js';
32
37
  import { MemoryRepositoryAdapter } from '../repository/MemoryRepositoryAdapter.js';
33
38
  import { RepositoryMetadataStore } from '../repository/RepositoryMetadataStore.js';
34
39
  import { RepositoryAdapterRegistry } from '../repository/RepositoryAdapterRegistry.js';
@@ -42,13 +47,11 @@ const DEFAULT_ADAPTER_LOADERS = {
42
47
  },
43
48
  openrouter: {
44
49
  apiKeyEnv: 'OPENROUTER_API_KEY',
45
- load: async () => (await import('../../addon/chat-bot/openrouter/OpenRouterChatAdapter.js'))
46
- .OpenRouterChatAdapter,
50
+ load: async () => (await import('../../addon/chat-bot/openrouter/OpenRouterChatAdapter.js')).OpenRouterChatAdapter,
47
51
  },
48
52
  anthropic: {
49
53
  apiKeyEnv: 'ANTHROPIC_API_KEY',
50
- load: async () => (await import('../../addon/chat-bot/anthropic/AnthropicChatAdapter.js'))
51
- .AnthropicChatAdapter,
54
+ load: async () => (await import('../../addon/chat-bot/anthropic/AnthropicChatAdapter.js')).AnthropicChatAdapter,
52
55
  },
53
56
  google: {
54
57
  apiKeyEnv: 'GOOGLE_API_KEY',
@@ -62,6 +65,7 @@ class ProjectRunner {
62
65
  connectionString;
63
66
  isPg;
64
67
  preloaded;
68
+ ui;
65
69
  pool = null;
66
70
  constructor(config = {}) {
67
71
  this.directories = config.directories ?? ['src'];
@@ -70,8 +74,10 @@ class ProjectRunner {
70
74
  this.connectionString = this.resolveConnectionString(config.connectionString);
71
75
  this.isPg = this.connectionString != null && isPostgresUrl(this.connectionString);
72
76
  this.preloaded = config.preloaded === true;
77
+ this.ui = config.ui ?? {};
73
78
  }
74
79
  async run() {
80
+ let scannedFiles = [];
75
81
  if (this.preloaded) {
76
82
  if (this.isPg)
77
83
  await this.initPool();
@@ -81,11 +87,12 @@ class ProjectRunner {
81
87
  this.isPg ? this.initPool() : Promise.resolve(),
82
88
  scanProjectFiles({ directories: this.directories, exclude: this.exclude }),
83
89
  ]);
84
- await this.importFiles(files);
90
+ scannedFiles = files;
91
+ await this.importFiles(scannedFiles);
85
92
  }
86
93
  const components = this.discoverComponents();
87
94
  await this.registerAdapters(components);
88
- await this.startComponents(components);
95
+ await this.startComponents(components, scannedFiles);
89
96
  }
90
97
  resolveConnectionString(configValue) {
91
98
  const cs = configValue ?? process.env.DATABASE_URL ?? null;
@@ -151,6 +158,7 @@ class ProjectRunner {
151
158
  socketControllers: container
152
159
  .resolve(SocketControllerMetadataStore)
153
160
  .getAllSocketControllerConstructors(),
161
+ uiControllers: container.resolve(UiControllerMetadataStore).getAllUiControllerConstructors(),
154
162
  };
155
163
  }
156
164
  registerAdapters(components) {
@@ -199,9 +207,7 @@ class ProjectRunner {
199
207
  hasCommandHandlers || hasCronHandlers
200
208
  ? import('../../addon/async/pg/PgJobRepository.js')
201
209
  : Promise.resolve(null),
202
- hasCronHandlers
203
- ? import('../../addon/async/pg/PgCronJobRepository.js')
204
- : Promise.resolve(null),
210
+ hasCronHandlers ? import('../../addon/async/pg/PgCronJobRepository.js') : Promise.resolve(null),
205
211
  ]);
206
212
  container.register(ChatRepository, { useToken: chatBotMod.PgChatRepository });
207
213
  container.register(Locker, { useToken: lockerMod.PgLocker });
@@ -218,7 +224,7 @@ class ProjectRunner {
218
224
  }
219
225
  logger.info('Configured with PostgreSQL adapters');
220
226
  }
221
- async startComponents(components) {
227
+ async startComponents(components, files = []) {
222
228
  const chatAdapters = this.chatAdapters ?? (await this.resolveDefaultChatAdapters());
223
229
  if (chatAdapters.length > 0) {
224
230
  runChatAdapters(chatAdapters);
@@ -231,6 +237,9 @@ class ProjectRunner {
231
237
  logger.info(`Starting ${components.restControllers.length} REST controller(s)`);
232
238
  runRestControllers(components.restControllers);
233
239
  }
240
+ if (components.uiControllers.length > 0) {
241
+ await this.startUiControllers(components.uiControllers, files);
242
+ }
234
243
  if (components.commandHandlers.length > 0) {
235
244
  logger.info(`Starting ${components.commandHandlers.length} command handler(s)`);
236
245
  runCommandHandlers(components.commandHandlers);
@@ -244,6 +253,49 @@ class ProjectRunner {
244
253
  runSocketControllers(components.socketControllers);
245
254
  }
246
255
  }
256
+ async startUiControllers(uiControllers, files) {
257
+ const rendererRegistry = container.resolve(UiRendererRegistry);
258
+ if (!rendererRegistry.hasDefault()) {
259
+ const { PreactRenderer } = await import('../../addon/ui/preact/PreactRenderer.js');
260
+ rendererRegistry.setDefault(new PreactRenderer());
261
+ }
262
+ const client = rendererRegistry.get().client;
263
+ let pageAssets;
264
+ if (client) {
265
+ // The bundler pulls in esbuild, so only load it when UI islands are in play.
266
+ const bundlerMod = await import('../ui-controller/bundler/index.js');
267
+ pageAssets = this.preloaded
268
+ ? await this.setupProdUiAssets(bundlerMod)
269
+ : await this.setupDevUiAssets(bundlerMod, client, files);
270
+ }
271
+ logger.info(`Starting ${uiControllers.length} UI controller(s)`);
272
+ runUiControllers(uiControllers, { pageAssets });
273
+ }
274
+ async setupDevUiAssets(bundlerMod, client, files) {
275
+ const islands = await container.resolve(IslandRegistry).discover(files);
276
+ if (islands.length === 0)
277
+ return undefined;
278
+ const bundler = new bundlerMod.UiBundler({ islands, client, alias: this.ui.bundlerAlias });
279
+ await bundler.startDev();
280
+ bundlerMod.mountUiDevAssets(container.resolve(ExpressProvider).getExpress(), bundler);
281
+ return (used) => bundlerMod.pageAssetsFromManifest(bundler.getManifest(), used, {
282
+ liveReloadPath: '/_wabot/livereload',
283
+ });
284
+ }
285
+ async setupProdUiAssets(bundlerMod) {
286
+ const fs = await import('node:fs');
287
+ const nodePath = await import('node:path');
288
+ const distUi = nodePath.resolve(process.cwd(), 'dist/ui');
289
+ const manifestPath = nodePath.join(distUi, 'manifest.json');
290
+ if (!fs.existsSync(manifestPath)) {
291
+ logger.warn(`UI manifest not found at ${manifestPath}; islands will not hydrate`);
292
+ return undefined;
293
+ }
294
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
295
+ const { default: express } = await import('express');
296
+ container.resolve(ExpressProvider).getExpress().use('/_wabot', express.static(distUi));
297
+ return (used) => bundlerMod.pageAssetsFromManifest(manifest, used);
298
+ }
247
299
  async resolveDefaultChatAdapters() {
248
300
  const keys = Object.keys(DEFAULT_ADAPTER_LOADERS);
249
301
  const emptyKeys = [];
@@ -55,7 +55,7 @@ async function scanDir(dir, excludedNames, excludedPaths) {
55
55
  }
56
56
  if (!entry.isFile())
57
57
  return [];
58
- if (!(name.endsWith('.ts') || name.endsWith('.js')))
58
+ if (!/\.(ts|tsx|js|jsx)$/.test(name))
59
59
  return [];
60
60
  if (name.endsWith('.d.ts'))
61
61
  return [];
@@ -13,8 +13,7 @@ function inheritsFrom(ctor, base) {
13
13
  }
14
14
  function memExtension(repositoryClass) {
15
15
  if (typeof repositoryClass !== 'function') {
16
- throw new Error(`@memoryExtension: repository argument must be a class, ` +
17
- `got ${typeof repositoryClass}`);
16
+ throw new Error(`@memoryExtension: repository argument must be a class, ` + `got ${typeof repositoryClass}`);
18
17
  }
19
18
  return function (target) {
20
19
  if (!inheritsFrom(target, MemoryRepositoryExtension)) {
@@ -0,0 +1,35 @@
1
+ // Client-safe helpers for calling @action endpoints. No Node imports so these
2
+ // can be bundled into islands.
3
+ function joinUrl(...parts) {
4
+ const joined = parts
5
+ .map((p) => p.replace(/^\/+|\/+$/g, ''))
6
+ .filter(Boolean)
7
+ .join('/');
8
+ return '/' + joined;
9
+ }
10
+ /** Build the URL of an @action: `<controllerPath>/_action/<actionName>`. */
11
+ function actionUrl(controllerPath, actionName) {
12
+ return joinUrl(controllerPath, '_action', actionName);
13
+ }
14
+ /**
15
+ * POST JSON to an @action and return its JSON result. Throws on non-2xx with
16
+ * the server-provided error message. Intended for use from islands; plain
17
+ * <form action=…> posts also work for progressive enhancement.
18
+ */
19
+ async function callAction(url, data, options = {}) {
20
+ const response = await fetch(url, {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json', ...options.headers },
23
+ body: data === undefined ? undefined : JSON.stringify(data),
24
+ signal: options.signal,
25
+ });
26
+ const text = await response.text();
27
+ const parsed = text ? JSON.parse(text) : null;
28
+ if (!response.ok) {
29
+ const message = parsed?.error?.message ?? `Action failed with status ${response.status}`;
30
+ throw new Error(message);
31
+ }
32
+ return parsed;
33
+ }
34
+
35
+ export { actionUrl, callAction };
@@ -0,0 +1,191 @@
1
+ import path__default from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as esbuild from 'esbuild';
4
+ import { Logger } from '../../../core/logger/Logger.js';
5
+ import { emptyManifest, manifestFromMetafile, NAV_ENTRY_ID, ISLAND_ENTRY_NAMESPACE } from './manifest.js';
6
+
7
+ /** Absolute path to the generic boosted-navigation client core, bundled as an
8
+ * extra entry so it shares the island runtime chunk. */
9
+ const NAV_RUNTIME_MODULE = fileURLToPath(new URL('./navRuntime', import.meta.url));
10
+ /**
11
+ * Source of the nav entry. Imports the island hydrate/unmount hooks from the
12
+ * renderer's runtime module (the same module island entries import, so they
13
+ * share one registry instance) and wires them into the generic nav core.
14
+ */
15
+ function navEntrySource(runtimeModule) {
16
+ return (`import { hydrateAll, unmountRemoved } from ${JSON.stringify(runtimeModule)}\n` +
17
+ `import { startNavigation } from ${JSON.stringify(NAV_RUNTIME_MODULE)}\n` +
18
+ `startNavigation({ hydrateAll, unmountRemoved })\n`);
19
+ }
20
+ function mimeFor(servePath) {
21
+ if (servePath.endsWith('.js'))
22
+ return 'text/javascript; charset=utf-8';
23
+ if (servePath.endsWith('.css'))
24
+ return 'text/css; charset=utf-8';
25
+ if (servePath.endsWith('.map'))
26
+ return 'application/json; charset=utf-8';
27
+ return 'application/octet-stream';
28
+ }
29
+ function entrySources(islands, client) {
30
+ const sources = new Map();
31
+ for (const island of islands) {
32
+ // keyed by bare id; esbuild records the entry as `${namespace}:${path}`.
33
+ sources.set(island.id, client.islandEntrySource({ id: island.id, importPath: island.importPath }));
34
+ }
35
+ sources.set(NAV_ENTRY_ID, navEntrySource(client.runtimeModule));
36
+ return sources;
37
+ }
38
+ function islandEntryPlugin(sources, cwd) {
39
+ const prefix = `${ISLAND_ENTRY_NAMESPACE}:`;
40
+ const filter = new RegExp(`^${ISLAND_ENTRY_NAMESPACE}:`);
41
+ return {
42
+ name: 'wabot-island-entries',
43
+ setup(build) {
44
+ build.onResolve({ filter }, (args) => ({
45
+ path: args.path.slice(prefix.length),
46
+ namespace: ISLAND_ENTRY_NAMESPACE,
47
+ }));
48
+ build.onLoad({ filter: /.*/, namespace: ISLAND_ENTRY_NAMESPACE }, (args) => ({
49
+ contents: sources.get(args.path) ?? '',
50
+ loader: 'js',
51
+ resolveDir: cwd,
52
+ }));
53
+ },
54
+ };
55
+ }
56
+ function baseBuildOptions(islands, client, opts) {
57
+ const jsxMode = client.esbuildJsx?.jsx ?? 'automatic';
58
+ return {
59
+ entryPoints: [
60
+ ...islands.map((i) => ({ in: `${ISLAND_ENTRY_NAMESPACE}:${i.id}`, out: i.id })),
61
+ // Boosted-navigation runtime, shared across the app's views.
62
+ { in: `${ISLAND_ENTRY_NAMESPACE}:${NAV_ENTRY_ID}`, out: '_nav' },
63
+ ],
64
+ bundle: true,
65
+ splitting: true,
66
+ format: 'esm',
67
+ platform: 'browser',
68
+ outdir: opts.outdir,
69
+ absWorkingDir: opts.cwd,
70
+ metafile: true,
71
+ alias: opts.alias,
72
+ sourcemap: opts.dev ? 'linked' : false,
73
+ minify: !opts.dev,
74
+ entryNames: '[name]',
75
+ chunkNames: 'chunks/[name]-[hash]',
76
+ assetNames: 'assets/[name]-[hash]',
77
+ jsx: jsxMode,
78
+ ...(jsxMode === 'automatic'
79
+ ? { jsxImportSource: client.esbuildJsx?.jsxImportSource }
80
+ : {
81
+ jsxFactory: client.esbuildJsx?.jsxFactory,
82
+ jsxFragment: client.esbuildJsx?.jsxFragmentFactory,
83
+ }),
84
+ loader: { '.module.css': 'local-css' },
85
+ logLevel: 'silent',
86
+ };
87
+ }
88
+ /**
89
+ * Bundles island client code with esbuild. In dev it watches and serves from
90
+ * memory; in prod it writes hashed assets to disk and returns a manifest.
91
+ */
92
+ class UiBundler {
93
+ islands;
94
+ client;
95
+ base;
96
+ cwd;
97
+ alias;
98
+ logger = new Logger('wabot:ui:bundler');
99
+ ctx = null;
100
+ served = new Map();
101
+ manifest;
102
+ rebuildListeners = new Set();
103
+ constructor(options) {
104
+ this.islands = options.islands;
105
+ this.client = options.client;
106
+ this.base = options.base ?? '/_wabot/';
107
+ this.cwd = options.cwd ?? process.cwd();
108
+ this.alias = options.alias;
109
+ this.manifest = emptyManifest(this.base);
110
+ }
111
+ /** Start a watching dev build; assets are kept in memory and served via getFile(). */
112
+ async startDev() {
113
+ const outdir = path__default.resolve(this.cwd, '.wabot-ui-dev');
114
+ const sources = entrySources(this.islands, this.client);
115
+ const onEnd = {
116
+ name: 'wabot-dev-refresh',
117
+ setup: (build) => {
118
+ build.onEnd((result) => {
119
+ this.ingest(result, outdir);
120
+ for (const listener of this.rebuildListeners)
121
+ listener();
122
+ });
123
+ },
124
+ };
125
+ this.ctx = await esbuild.context({
126
+ ...baseBuildOptions(this.islands, this.client, {
127
+ outdir,
128
+ dev: true,
129
+ cwd: this.cwd,
130
+ alias: this.alias,
131
+ }),
132
+ write: false,
133
+ plugins: [islandEntryPlugin(sources, this.cwd), onEnd],
134
+ });
135
+ await this.ctx.rebuild();
136
+ await this.ctx.watch();
137
+ this.logger.info(`watching ${this.islands.length} island(s)`);
138
+ }
139
+ /** Build once and write hashed assets to outDir; returns the manifest. */
140
+ async buildProd(outDir) {
141
+ const outdir = path__default.resolve(this.cwd, outDir);
142
+ const sources = entrySources(this.islands, this.client);
143
+ const result = await esbuild.build({
144
+ ...baseBuildOptions(this.islands, this.client, {
145
+ outdir,
146
+ dev: false,
147
+ cwd: this.cwd,
148
+ alias: this.alias,
149
+ }),
150
+ write: true,
151
+ plugins: [islandEntryPlugin(sources, this.cwd)],
152
+ });
153
+ this.manifest = manifestFromMetafile(result.metafile, {
154
+ base: this.base,
155
+ outdir,
156
+ cwd: this.cwd,
157
+ });
158
+ this.logger.info(`built ${this.islands.length} island(s) -> ${outDir}`);
159
+ return this.manifest;
160
+ }
161
+ ingest(result, outdir) {
162
+ this.served.clear();
163
+ for (const file of result.outputFiles ?? []) {
164
+ const rel = path__default.relative(outdir, file.path).split(path__default.sep).join('/');
165
+ const servePath = this.base + rel;
166
+ this.served.set(servePath, { contents: file.contents, type: mimeFor(servePath) });
167
+ }
168
+ if (result.metafile) {
169
+ this.manifest = manifestFromMetafile(result.metafile, {
170
+ base: this.base,
171
+ outdir,
172
+ cwd: this.cwd,
173
+ });
174
+ }
175
+ }
176
+ getFile(servePath) {
177
+ return this.served.get(servePath);
178
+ }
179
+ getManifest() {
180
+ return this.manifest;
181
+ }
182
+ onRebuild(listener) {
183
+ this.rebuildListeners.add(listener);
184
+ }
185
+ async dispose() {
186
+ await this.ctx?.dispose();
187
+ this.ctx = null;
188
+ }
189
+ }
190
+
191
+ export { UiBundler };
@@ -0,0 +1,41 @@
1
+ import { Logger } from '../../../core/logger/Logger.js';
2
+
3
+ /**
4
+ * Mounts the dev island assets and a live-reload SSE endpoint on the shared
5
+ * Express app. Each successful rebuild pushes a "reload" event to connected
6
+ * pages (the snippet from {@link liveReloadSnippet} listens for it).
7
+ */
8
+ function mountUiDevAssets(app, bundler, options = {}) {
9
+ const logger = new Logger('wabot:ui:dev');
10
+ const base = options.base ?? '/_wabot/';
11
+ const mountPath = base.replace(/\/$/, '');
12
+ const liveReloadPath = options.liveReloadPath ?? '/_wabot/livereload';
13
+ const clients = new Set();
14
+ bundler.onRebuild(() => {
15
+ for (const res of clients)
16
+ res.write('data: reload\n\n');
17
+ });
18
+ app.get(liveReloadPath, (req, res) => {
19
+ res.set({
20
+ 'Content-Type': 'text/event-stream',
21
+ 'Cache-Control': 'no-cache, no-transform',
22
+ Connection: 'keep-alive',
23
+ });
24
+ res.flushHeaders?.();
25
+ res.write('data: connected\n\n');
26
+ clients.add(res);
27
+ req.on('close', () => clients.delete(res));
28
+ });
29
+ app.use(mountPath, (req, res, next) => {
30
+ const servePath = mountPath + req.path;
31
+ const file = bundler.getFile(servePath);
32
+ if (!file)
33
+ return next();
34
+ res.set('Content-Type', file.type);
35
+ res.set('Cache-Control', 'no-cache');
36
+ res.send(Buffer.from(file.contents));
37
+ });
38
+ logger.info(`serving island assets at ${base} (live reload via ${liveReloadPath})`);
39
+ }
40
+
41
+ export { mountUiDevAssets };
@@ -0,0 +1,4 @@
1
+ export { ISLAND_ENTRY_NAMESPACE, NAV_ENTRY_ID, emptyManifest, manifestFromMetafile } from './manifest.js';
2
+ export { UiBundler } from './UiBundler.js';
3
+ export { liveReloadSnippet, pageAssetsFromManifest } from './pageAssets.js';
4
+ export { mountUiDevAssets } from './devMiddleware.js';
@@ -0,0 +1,34 @@
1
+ import path__default from 'node:path';
2
+
3
+ const ISLAND_ENTRY_NAMESPACE = 'wabot-island';
4
+ /** Reserved island-entry id for the boosted-navigation runtime bundle. */
5
+ const NAV_ENTRY_ID = '__wabot_nav';
6
+ function toUrl(base, outdir, cwd, outPath) {
7
+ const abs = path__default.resolve(cwd, outPath);
8
+ const rel = path__default.relative(outdir, abs).split(path__default.sep).join('/');
9
+ return base + rel;
10
+ }
11
+ /** Build the island asset manifest from an esbuild metafile. */
12
+ function manifestFromMetafile(metafile, opts) {
13
+ const { base, outdir, cwd } = opts;
14
+ const islands = {};
15
+ let nav;
16
+ for (const [outPath, output] of Object.entries(metafile.outputs)) {
17
+ const entryPoint = output.entryPoint;
18
+ if (!entryPoint || !entryPoint.startsWith(`${ISLAND_ENTRY_NAMESPACE}:`))
19
+ continue;
20
+ const id = entryPoint.slice(ISLAND_ENTRY_NAMESPACE.length + 1);
21
+ if (id === NAV_ENTRY_ID) {
22
+ nav = toUrl(base, outdir, cwd, outPath);
23
+ continue;
24
+ }
25
+ const css = output.cssBundle ? [toUrl(base, outdir, cwd, output.cssBundle)] : [];
26
+ islands[id] = { js: toUrl(base, outdir, cwd, outPath), css };
27
+ }
28
+ return { base, islands, nav };
29
+ }
30
+ function emptyManifest(base = '/_wabot/') {
31
+ return { base, islands: {} };
32
+ }
33
+
34
+ export { ISLAND_ENTRY_NAMESPACE, NAV_ENTRY_ID, emptyManifest, manifestFromMetafile };