@wabot-dev/framework 0.9.27 → 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 (94) hide show
  1. package/README.md +27 -0
  2. package/bin/skills.mjs +151 -0
  3. package/bin/wabot-skills.mjs +120 -0
  4. package/dist/build/build.js +1031 -8
  5. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
  6. package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
  7. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
  8. package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
  9. package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
  10. package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
  11. package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
  12. package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
  13. package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
  14. package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
  15. package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
  16. package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
  17. package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
  18. package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
  19. package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
  20. package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
  21. package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
  22. package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
  23. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
  24. package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
  25. package/dist/src/addon/ui/preact/outlet.js +22 -0
  26. package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
  27. package/dist/src/core/repository/CrudRepository.js +7 -7
  28. package/dist/src/feature/async/computeDedupKey.js +1 -1
  29. package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
  30. package/dist/src/feature/pg/@pgExtension.js +2 -4
  31. package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
  32. package/dist/src/feature/project-runner/scanner.js +1 -1
  33. package/dist/src/feature/repository/@memExtension.js +1 -2
  34. package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
  35. package/dist/src/feature/ui-controller/actions.js +35 -0
  36. package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
  37. package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
  38. package/dist/src/feature/ui-controller/bundler/index.js +4 -0
  39. package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
  40. package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
  41. package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
  42. package/dist/src/feature/ui-controller/document/escape.js +17 -0
  43. package/dist/src/feature/ui-controller/document/helpers.js +13 -0
  44. package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
  45. package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
  46. package/dist/src/feature/ui-controller/island/island.js +40 -0
  47. package/dist/src/feature/ui-controller/island/serialize.js +35 -0
  48. package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
  49. package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
  50. package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
  51. package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
  52. package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
  53. package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
  54. package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
  55. package/dist/src/index.d.ts +640 -3
  56. package/dist/src/index.js +32 -3
  57. package/dist/src/testing/LlmJudge.js +93 -0
  58. package/dist/src/testing/MockChatAdapter.js +68 -0
  59. package/dist/src/testing/TestChatMemory.js +73 -0
  60. package/dist/src/testing/asyncHarness.js +66 -0
  61. package/dist/src/testing/auth.js +114 -0
  62. package/dist/src/testing/chatBotHarness.js +88 -0
  63. package/dist/src/testing/chatControllerHarness.js +94 -0
  64. package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
  65. package/dist/src/testing/fixtures.js +53 -0
  66. package/dist/src/testing/helpers.js +42 -0
  67. package/dist/src/testing/index.d.ts +818 -0
  68. package/dist/src/testing/index.js +14 -0
  69. package/dist/src/testing/repositories.js +34 -0
  70. package/dist/src/testing/restHarness.js +127 -0
  71. package/dist/src/testing/testImageBase64.js +5 -0
  72. package/dist/src/testing/uiHarness.js +102 -0
  73. package/dist/src/testing/validation.js +66 -0
  74. package/dist/src/ui/client.js +6 -0
  75. package/dist/src/ui/index.d.ts +427 -0
  76. package/dist/src/ui/index.js +29 -0
  77. package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
  78. package/dist/src/ui/jsx-dev-runtime.js +1 -0
  79. package/dist/src/ui/jsx-runtime.d.ts +1 -0
  80. package/dist/src/ui/jsx-runtime.js +1 -0
  81. package/package.json +48 -11
  82. package/skills/wabot-async/SKILL.md +143 -0
  83. package/skills/wabot-auth/SKILL.md +153 -0
  84. package/skills/wabot-chat/SKILL.md +140 -0
  85. package/skills/wabot-di-config/SKILL.md +117 -0
  86. package/skills/wabot-framework/SKILL.md +81 -0
  87. package/skills/wabot-framework/references/quickstart.md +85 -0
  88. package/skills/wabot-mindset/SKILL.md +159 -0
  89. package/skills/wabot-ops/SKILL.md +151 -0
  90. package/skills/wabot-persistence/SKILL.md +159 -0
  91. package/skills/wabot-rest-socket/SKILL.md +167 -0
  92. package/skills/wabot-testing/SKILL.md +214 -0
  93. package/skills/wabot-ui/SKILL.md +201 -0
  94. package/skills/wabot-validation/SKILL.md +108 -0
@@ -1,7 +1,16 @@
1
1
  import { realpathSync, existsSync } from 'node:fs';
2
2
  import { readdir, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
3
- import { resolve, join, relative, sep, isAbsolute } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
3
+ import path, { resolve, join, relative, sep, isAbsolute } from 'node:path';
4
+ import { pathToFileURL, fileURLToPath } from 'node:url';
5
+ import { __decorate, __metadata } from 'tslib';
6
+ import { createHash } from 'node:crypto';
7
+ import debug from 'debug';
8
+ import * as esbuild from 'esbuild';
9
+ import { createContext, options, h } from 'preact';
10
+ import { renderToString } from 'preact-render-to-string';
11
+ import { Server } from 'node:http';
12
+ import express from 'express';
13
+ import 'preact/hooks';
5
14
 
6
15
  const TEST_FILE_PATTERNS = /\.(test|spec|unit|integration|e2e|multiprocess)\.(ts|js)$/;
7
16
  const DEFAULT_DIRECTORIES = ['src'];
@@ -57,7 +66,7 @@ async function scanDir(dir, excludedNames, excludedPaths) {
57
66
  }
58
67
  if (!entry.isFile())
59
68
  return [];
60
- if (!(name.endsWith('.ts') || name.endsWith('.js')))
69
+ if (!/\.(ts|tsx|js|jsx)$/.test(name))
61
70
  return [];
62
71
  if (name.endsWith('.d.ts'))
63
72
  return [];
@@ -68,6 +77,983 @@ async function scanDir(dir, excludedNames, excludedPaths) {
68
77
  return subResults.flat();
69
78
  }
70
79
 
80
+ await import('reflect-metadata');
81
+ const { injectable, container, singleton, inject, scoped, Lifecycle } = await import('tsyringe');
82
+
83
+ function errorToPlainObject(error) {
84
+ const { name, message, stack } = error;
85
+ const extra = {};
86
+ for (const key of Object.keys(error)) {
87
+ if (key === 'message' || key === 'stack')
88
+ continue;
89
+ extra[key] = error[key];
90
+ }
91
+ return { name, message, stack, ...extra };
92
+ }
93
+
94
+ const levelColors = {
95
+ trace: 0,
96
+ debug: 0,
97
+ info: 0,
98
+ warn: 5,
99
+ error: 1,
100
+ fatal: 1,
101
+ };
102
+ const levelToSeverity = {
103
+ warn: 'warning',
104
+ error: 'error',
105
+ fatal: 'fatal',
106
+ };
107
+ /**
108
+ * Logger with 6 severity levels. Uses the `debug` library for output.
109
+ *
110
+ * ## Level verbosity contract
111
+ *
112
+ * - **fatal** — The process cannot continue. Something is critically broken
113
+ * (uncaught exceptions, unhandled rejections). Investigate immediately.
114
+ *
115
+ * - **error** — An operation failed unexpectedly. Always include: what failed,
116
+ * why (the Error), and enough context to locate the problem (IDs, names).
117
+ *
118
+ * - **warn** — Something unusual happened but the system handled it gracefully.
119
+ * Known limitations, safety guards triggered, recoverable issues.
120
+ *
121
+ * - **info** — Key lifecycle events the user cares about: systems starting or
122
+ * stopping, configuration applied, significant state changes. Should read
123
+ * like a high-level audit log.
124
+ *
125
+ * - **debug** — Internal operational details for developers troubleshooting.
126
+ * Step-by-step flow, lock acquisition, query execution, reconciliation steps.
127
+ *
128
+ * - **trace** — Very fine-grained: every HTTP request, every socket event,
129
+ * every message sent or received.
130
+ */
131
+ class Logger {
132
+ static monitor = null;
133
+ debuggers;
134
+ name;
135
+ constructor(name) {
136
+ this.name = name;
137
+ this.debuggers = {};
138
+ for (const level of Object.keys(levelColors)) {
139
+ const dbg = debug(`${name}:${level}`);
140
+ dbg.color = '' + levelColors[level];
141
+ this.debuggers[level] = dbg;
142
+ }
143
+ }
144
+ static setMonitor(monitor) {
145
+ Logger.monitor = monitor;
146
+ }
147
+ static getMonitor() {
148
+ return Logger.monitor;
149
+ }
150
+ /** Very fine-grained: every HTTP request, socket event, message sent/received. */
151
+ trace(...args) {
152
+ this.log('trace', args);
153
+ }
154
+ /** Internal operational details for developers: step-by-step flow, lock acquisition, queries. */
155
+ debug(...args) {
156
+ this.log('debug', args);
157
+ }
158
+ /** Key lifecycle events: systems start/stop, configuration applied, significant state changes. */
159
+ info(...args) {
160
+ this.log('info', args);
161
+ }
162
+ /** Something unusual happened but the system recovered. Known limitations, safety guards. */
163
+ warn(...args) {
164
+ this.log('warn', args);
165
+ }
166
+ /** Operation failed unexpectedly. Always include: what failed + why (Error) + identifiers. */
167
+ error(...args) {
168
+ this.log('error', args);
169
+ }
170
+ /** Process cannot continue. Uncaught exceptions, unhandled rejections. Investigate immediately. */
171
+ fatal(...args) {
172
+ this.log('fatal', args);
173
+ }
174
+ log(level, args) {
175
+ const debugg = this.debuggers[level];
176
+ const formattedArgs = this.formatArgs(args);
177
+ debugg(...formattedArgs);
178
+ this.sendToMonitor(level, args);
179
+ }
180
+ sendToMonitor(level, args) {
181
+ const severity = levelToSeverity[level];
182
+ if (!severity || !Logger.monitor)
183
+ return;
184
+ const context = {
185
+ logger: this.name,
186
+ level: severity,
187
+ timestamp: new Date(),
188
+ extra: this.extractExtra(args),
189
+ };
190
+ const error = args.find((arg) => arg instanceof Error);
191
+ if (error) {
192
+ Logger.monitor.captureError(error, context);
193
+ }
194
+ else {
195
+ const message = args
196
+ .filter((arg) => !(arg instanceof Error))
197
+ .map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
198
+ .join(' ');
199
+ Logger.monitor.captureMessage(message, context);
200
+ }
201
+ }
202
+ extractExtra(args) {
203
+ const extra = {};
204
+ let index = 0;
205
+ for (const arg of args) {
206
+ if (arg instanceof Error)
207
+ continue;
208
+ if (typeof arg === 'object' && arg !== null) {
209
+ Object.assign(extra, arg);
210
+ }
211
+ else if (typeof arg !== 'string') {
212
+ extra[`arg${index}`] = arg;
213
+ }
214
+ index++;
215
+ }
216
+ return Object.keys(extra).length > 0 ? extra : {};
217
+ }
218
+ formatArgs(args) {
219
+ return args.map((arg) => {
220
+ if (arg instanceof Error) {
221
+ return JSON.stringify(errorToPlainObject(arg));
222
+ }
223
+ if (arg === null) {
224
+ return 'null';
225
+ }
226
+ if (typeof arg === 'object') {
227
+ try {
228
+ return JSON.stringify(arg);
229
+ }
230
+ catch (e) {
231
+ return '[Circular]';
232
+ }
233
+ }
234
+ return arg;
235
+ });
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Marker attached to components wrapped with {@link island}. The UI renderer
241
+ * uses it to decide which components must hydrate on the client; the bundler
242
+ * uses {@link IslandMeta.id} (assigned from the `*.island.tsx` file location) to
243
+ * emit a per-island client bundle.
244
+ */
245
+ const ISLAND_MARKER = Symbol.for('wabot.ui.island');
246
+ function getIslandMeta(component) {
247
+ return typeof component === 'function'
248
+ ? component[ISLAND_MARKER]
249
+ : undefined;
250
+ }
251
+ function isIsland(component) {
252
+ return getIslandMeta(component) != null;
253
+ }
254
+ /** Assign the stable bundle id to an island, done during island discovery. */
255
+ function setIslandId(component, id) {
256
+ const meta = getIslandMeta(component);
257
+ if (meta)
258
+ meta.id = id;
259
+ }
260
+
261
+ /** Files matching this are treated as islands (default export wrapped with island()). */
262
+ const ISLAND_FILE_PATTERN = /\.island\.(tsx|jsx)$/;
263
+ /** Deterministic, readable, collision-resistant id from a project-relative path. */
264
+ function toIslandId(relPath) {
265
+ const base = path
266
+ .basename(relPath)
267
+ .replace(ISLAND_FILE_PATTERN, '')
268
+ .replace(/[^a-zA-Z0-9_-]/g, '_') || 'island';
269
+ const hash = createHash('sha1').update(relPath).digest('hex').slice(0, 8);
270
+ return `${base}-${hash}`;
271
+ }
272
+ function isIslandFile(file) {
273
+ return ISLAND_FILE_PATTERN.test(file);
274
+ }
275
+ /**
276
+ * Discovers `*.island.tsx` modules, assigns each a stable id, and stamps that
277
+ * id onto the imported island component (ESM module singletons mean the view
278
+ * renders the same instance, so the renderer can read the id during SSR).
279
+ */
280
+ let IslandRegistry = class IslandRegistry {
281
+ islands = new Map();
282
+ logger = new Logger('wabot:ui:islands');
283
+ async discover(files, cwd = process.cwd()) {
284
+ for (const absPath of files.filter(isIslandFile)) {
285
+ const relPath = path.relative(cwd, absPath).split(path.sep).join('/');
286
+ const id = toIslandId(relPath);
287
+ try {
288
+ const mod = await import(pathToFileURL(absPath).href);
289
+ const component = mod.default;
290
+ if (!isIsland(component)) {
291
+ this.logger.warn(`${relPath}: default export is not an island(); skipping`);
292
+ continue;
293
+ }
294
+ setIslandId(component, id);
295
+ this.islands.set(id, { id, importPath: absPath, relPath });
296
+ }
297
+ catch (err) {
298
+ this.logger.error(`Failed to load island ${relPath}`, err);
299
+ }
300
+ }
301
+ return this.list();
302
+ }
303
+ register(island) {
304
+ this.islands.set(island.id, island);
305
+ }
306
+ list() {
307
+ return [...this.islands.values()];
308
+ }
309
+ get(id) {
310
+ return this.islands.get(id);
311
+ }
312
+ get size() {
313
+ return this.islands.size;
314
+ }
315
+ };
316
+ IslandRegistry = __decorate([
317
+ singleton()
318
+ ], IslandRegistry);
319
+
320
+ const ISLAND_ENTRY_NAMESPACE = 'wabot-island';
321
+ /** Reserved island-entry id for the boosted-navigation runtime bundle. */
322
+ const NAV_ENTRY_ID = '__wabot_nav';
323
+ function toUrl(base, outdir, cwd, outPath) {
324
+ const abs = path.resolve(cwd, outPath);
325
+ const rel = path.relative(outdir, abs).split(path.sep).join('/');
326
+ return base + rel;
327
+ }
328
+ /** Build the island asset manifest from an esbuild metafile. */
329
+ function manifestFromMetafile(metafile, opts) {
330
+ const { base, outdir, cwd } = opts;
331
+ const islands = {};
332
+ let nav;
333
+ for (const [outPath, output] of Object.entries(metafile.outputs)) {
334
+ const entryPoint = output.entryPoint;
335
+ if (!entryPoint || !entryPoint.startsWith(`${ISLAND_ENTRY_NAMESPACE}:`))
336
+ continue;
337
+ const id = entryPoint.slice(ISLAND_ENTRY_NAMESPACE.length + 1);
338
+ if (id === NAV_ENTRY_ID) {
339
+ nav = toUrl(base, outdir, cwd, outPath);
340
+ continue;
341
+ }
342
+ const css = output.cssBundle ? [toUrl(base, outdir, cwd, output.cssBundle)] : [];
343
+ islands[id] = { js: toUrl(base, outdir, cwd, outPath), css };
344
+ }
345
+ return { base, islands, nav };
346
+ }
347
+ function emptyManifest(base = '/_wabot/') {
348
+ return { base, islands: {} };
349
+ }
350
+
351
+ /** Absolute path to the generic boosted-navigation client core, bundled as an
352
+ * extra entry so it shares the island runtime chunk. */
353
+ const NAV_RUNTIME_MODULE = fileURLToPath(new URL('./navRuntime', import.meta.url));
354
+ /**
355
+ * Source of the nav entry. Imports the island hydrate/unmount hooks from the
356
+ * renderer's runtime module (the same module island entries import, so they
357
+ * share one registry instance) and wires them into the generic nav core.
358
+ */
359
+ function navEntrySource(runtimeModule) {
360
+ return (`import { hydrateAll, unmountRemoved } from ${JSON.stringify(runtimeModule)}\n` +
361
+ `import { startNavigation } from ${JSON.stringify(NAV_RUNTIME_MODULE)}\n` +
362
+ `startNavigation({ hydrateAll, unmountRemoved })\n`);
363
+ }
364
+ function mimeFor(servePath) {
365
+ if (servePath.endsWith('.js'))
366
+ return 'text/javascript; charset=utf-8';
367
+ if (servePath.endsWith('.css'))
368
+ return 'text/css; charset=utf-8';
369
+ if (servePath.endsWith('.map'))
370
+ return 'application/json; charset=utf-8';
371
+ return 'application/octet-stream';
372
+ }
373
+ function entrySources(islands, client) {
374
+ const sources = new Map();
375
+ for (const island of islands) {
376
+ // keyed by bare id; esbuild records the entry as `${namespace}:${path}`.
377
+ sources.set(island.id, client.islandEntrySource({ id: island.id, importPath: island.importPath }));
378
+ }
379
+ sources.set(NAV_ENTRY_ID, navEntrySource(client.runtimeModule));
380
+ return sources;
381
+ }
382
+ function islandEntryPlugin(sources, cwd) {
383
+ const prefix = `${ISLAND_ENTRY_NAMESPACE}:`;
384
+ const filter = new RegExp(`^${ISLAND_ENTRY_NAMESPACE}:`);
385
+ return {
386
+ name: 'wabot-island-entries',
387
+ setup(build) {
388
+ build.onResolve({ filter }, (args) => ({
389
+ path: args.path.slice(prefix.length),
390
+ namespace: ISLAND_ENTRY_NAMESPACE,
391
+ }));
392
+ build.onLoad({ filter: /.*/, namespace: ISLAND_ENTRY_NAMESPACE }, (args) => ({
393
+ contents: sources.get(args.path) ?? '',
394
+ loader: 'js',
395
+ resolveDir: cwd,
396
+ }));
397
+ },
398
+ };
399
+ }
400
+ function baseBuildOptions(islands, client, opts) {
401
+ const jsxMode = client.esbuildJsx?.jsx ?? 'automatic';
402
+ return {
403
+ entryPoints: [
404
+ ...islands.map((i) => ({ in: `${ISLAND_ENTRY_NAMESPACE}:${i.id}`, out: i.id })),
405
+ // Boosted-navigation runtime, shared across the app's views.
406
+ { in: `${ISLAND_ENTRY_NAMESPACE}:${NAV_ENTRY_ID}`, out: '_nav' },
407
+ ],
408
+ bundle: true,
409
+ splitting: true,
410
+ format: 'esm',
411
+ platform: 'browser',
412
+ outdir: opts.outdir,
413
+ absWorkingDir: opts.cwd,
414
+ metafile: true,
415
+ alias: opts.alias,
416
+ sourcemap: opts.dev ? 'linked' : false,
417
+ minify: !opts.dev,
418
+ entryNames: '[name]',
419
+ chunkNames: 'chunks/[name]-[hash]',
420
+ assetNames: 'assets/[name]-[hash]',
421
+ jsx: jsxMode,
422
+ ...(jsxMode === 'automatic'
423
+ ? { jsxImportSource: client.esbuildJsx?.jsxImportSource }
424
+ : {
425
+ jsxFactory: client.esbuildJsx?.jsxFactory,
426
+ jsxFragment: client.esbuildJsx?.jsxFragmentFactory,
427
+ }),
428
+ loader: { '.module.css': 'local-css' },
429
+ logLevel: 'silent',
430
+ };
431
+ }
432
+ /**
433
+ * Bundles island client code with esbuild. In dev it watches and serves from
434
+ * memory; in prod it writes hashed assets to disk and returns a manifest.
435
+ */
436
+ class UiBundler {
437
+ islands;
438
+ client;
439
+ base;
440
+ cwd;
441
+ alias;
442
+ logger = new Logger('wabot:ui:bundler');
443
+ ctx = null;
444
+ served = new Map();
445
+ manifest;
446
+ rebuildListeners = new Set();
447
+ constructor(options) {
448
+ this.islands = options.islands;
449
+ this.client = options.client;
450
+ this.base = options.base ?? '/_wabot/';
451
+ this.cwd = options.cwd ?? process.cwd();
452
+ this.alias = options.alias;
453
+ this.manifest = emptyManifest(this.base);
454
+ }
455
+ /** Start a watching dev build; assets are kept in memory and served via getFile(). */
456
+ async startDev() {
457
+ const outdir = path.resolve(this.cwd, '.wabot-ui-dev');
458
+ const sources = entrySources(this.islands, this.client);
459
+ const onEnd = {
460
+ name: 'wabot-dev-refresh',
461
+ setup: (build) => {
462
+ build.onEnd((result) => {
463
+ this.ingest(result, outdir);
464
+ for (const listener of this.rebuildListeners)
465
+ listener();
466
+ });
467
+ },
468
+ };
469
+ this.ctx = await esbuild.context({
470
+ ...baseBuildOptions(this.islands, this.client, {
471
+ outdir,
472
+ dev: true,
473
+ cwd: this.cwd,
474
+ alias: this.alias,
475
+ }),
476
+ write: false,
477
+ plugins: [islandEntryPlugin(sources, this.cwd), onEnd],
478
+ });
479
+ await this.ctx.rebuild();
480
+ await this.ctx.watch();
481
+ this.logger.info(`watching ${this.islands.length} island(s)`);
482
+ }
483
+ /** Build once and write hashed assets to outDir; returns the manifest. */
484
+ async buildProd(outDir) {
485
+ const outdir = path.resolve(this.cwd, outDir);
486
+ const sources = entrySources(this.islands, this.client);
487
+ const result = await esbuild.build({
488
+ ...baseBuildOptions(this.islands, this.client, {
489
+ outdir,
490
+ dev: false,
491
+ cwd: this.cwd,
492
+ alias: this.alias,
493
+ }),
494
+ write: true,
495
+ plugins: [islandEntryPlugin(sources, this.cwd)],
496
+ });
497
+ this.manifest = manifestFromMetafile(result.metafile, {
498
+ base: this.base,
499
+ outdir,
500
+ cwd: this.cwd,
501
+ });
502
+ this.logger.info(`built ${this.islands.length} island(s) -> ${outDir}`);
503
+ return this.manifest;
504
+ }
505
+ ingest(result, outdir) {
506
+ this.served.clear();
507
+ for (const file of result.outputFiles ?? []) {
508
+ const rel = path.relative(outdir, file.path).split(path.sep).join('/');
509
+ const servePath = this.base + rel;
510
+ this.served.set(servePath, { contents: file.contents, type: mimeFor(servePath) });
511
+ }
512
+ if (result.metafile) {
513
+ this.manifest = manifestFromMetafile(result.metafile, {
514
+ base: this.base,
515
+ outdir,
516
+ cwd: this.cwd,
517
+ });
518
+ }
519
+ }
520
+ getFile(servePath) {
521
+ return this.served.get(servePath);
522
+ }
523
+ getManifest() {
524
+ return this.manifest;
525
+ }
526
+ onRebuild(listener) {
527
+ this.rebuildListeners.add(listener);
528
+ }
529
+ async dispose() {
530
+ await this.ctx?.dispose();
531
+ this.ctx = null;
532
+ }
533
+ }
534
+
535
+ function getClassHierarchy$2(cls) {
536
+ const classes = [];
537
+ let proto = Object.getPrototypeOf(cls.prototype);
538
+ while (proto && proto.constructor !== Object) {
539
+ classes.push(proto.constructor);
540
+ proto = Object.getPrototypeOf(proto);
541
+ }
542
+ return classes;
543
+ }
544
+ let UiControllerMetadataStore = class UiControllerMetadataStore {
545
+ controllers = new Map();
546
+ views = new Map();
547
+ actions = new Map();
548
+ middlewares = new Map();
549
+ saveControllerMetadata(metadata) {
550
+ this.controllers.set(metadata.controllerConstructor, metadata);
551
+ }
552
+ saveViewMetadata(metadata) {
553
+ let controllerViews = this.views.get(metadata.controllerConstructor);
554
+ if (!controllerViews) {
555
+ this.views.set(metadata.controllerConstructor, (controllerViews = new Map()));
556
+ }
557
+ controllerViews.set(metadata.functionName, metadata);
558
+ }
559
+ saveActionMetadata(metadata) {
560
+ let controllerActions = this.actions.get(metadata.controllerConstructor);
561
+ if (!controllerActions) {
562
+ this.actions.set(metadata.controllerConstructor, (controllerActions = new Map()));
563
+ }
564
+ controllerActions.set(metadata.functionName, metadata);
565
+ }
566
+ saveMiddlewareMetadata(metadata) {
567
+ let controllerMiddlewares = this.middlewares.get(metadata.controllerConstructor);
568
+ if (!controllerMiddlewares) {
569
+ this.middlewares.set(metadata.controllerConstructor, (controllerMiddlewares = new Map()));
570
+ }
571
+ let methodMiddlewares = controllerMiddlewares.get(metadata.functionName);
572
+ if (!methodMiddlewares) {
573
+ controllerMiddlewares.set(metadata.functionName, (methodMiddlewares = []));
574
+ }
575
+ methodMiddlewares.unshift(metadata);
576
+ }
577
+ getAllUiControllerConstructors() {
578
+ return Array.from(this.controllers.keys());
579
+ }
580
+ getController(controllerConstructor) {
581
+ const controller = this.controllers.get(controllerConstructor);
582
+ if (!controller) {
583
+ throw new Error(`${controllerConstructor.name} should be decorated with @uiController`);
584
+ }
585
+ return controller;
586
+ }
587
+ collectMethodMiddlewares(hierarchy, functionName) {
588
+ const middlewares = [];
589
+ for (const cls of [...hierarchy].reverse()) {
590
+ const classMiddlewares = this.middlewares.get(cls)?.get(functionName);
591
+ if (classMiddlewares) {
592
+ middlewares.push(...classMiddlewares);
593
+ }
594
+ }
595
+ return middlewares;
596
+ }
597
+ getControllerViewsInfo(controllerConstructor) {
598
+ const controller = this.getController(controllerConstructor);
599
+ const hierarchy = [controllerConstructor, ...getClassHierarchy$2(controllerConstructor)];
600
+ const viewsMap = new Map();
601
+ for (const cls of [...hierarchy].reverse()) {
602
+ const classViews = this.views.get(cls);
603
+ if (classViews) {
604
+ for (const [name, view] of classViews)
605
+ viewsMap.set(name, view);
606
+ }
607
+ }
608
+ return [...viewsMap.values()].map((view) => ({
609
+ ...view,
610
+ controllerConstructor,
611
+ controller,
612
+ middlewares: this.collectMethodMiddlewares(hierarchy, view.functionName),
613
+ }));
614
+ }
615
+ getControllerActionsInfo(controllerConstructor) {
616
+ const controller = this.getController(controllerConstructor);
617
+ const hierarchy = [controllerConstructor, ...getClassHierarchy$2(controllerConstructor)];
618
+ const actionsMap = new Map();
619
+ for (const cls of [...hierarchy].reverse()) {
620
+ const classActions = this.actions.get(cls);
621
+ if (classActions) {
622
+ for (const [name, action] of classActions)
623
+ actionsMap.set(name, action);
624
+ }
625
+ }
626
+ return [...actionsMap.values()].map((action) => ({
627
+ ...action,
628
+ controllerConstructor,
629
+ controller,
630
+ middlewares: this.collectMethodMiddlewares(hierarchy, action.functionName),
631
+ }));
632
+ }
633
+ };
634
+ UiControllerMetadataStore = __decorate([
635
+ singleton()
636
+ ], UiControllerMetadataStore);
637
+
638
+ let UiRendererRegistry = class UiRendererRegistry {
639
+ renderers = new Map();
640
+ defaultRenderer = null;
641
+ /** Register a renderer. The first one registered becomes the default. */
642
+ register(renderer) {
643
+ this.renderers.set(renderer.id, renderer);
644
+ if (!this.defaultRenderer)
645
+ this.defaultRenderer = renderer;
646
+ }
647
+ /** Register a renderer and make it the default. */
648
+ setDefault(renderer) {
649
+ this.renderers.set(renderer.id, renderer);
650
+ this.defaultRenderer = renderer;
651
+ }
652
+ has(id) {
653
+ return this.renderers.has(id);
654
+ }
655
+ hasDefault() {
656
+ return this.defaultRenderer != null;
657
+ }
658
+ get(id) {
659
+ if (id) {
660
+ const renderer = this.renderers.get(id);
661
+ if (!renderer) {
662
+ throw new Error(`UI renderer "${id}" is not registered`);
663
+ }
664
+ return renderer;
665
+ }
666
+ if (!this.defaultRenderer) {
667
+ throw new Error('No default UI renderer registered. Import "@wabot-dev/framework/ui" to register the Preact renderer, or register your own with UiRendererRegistry.setDefault().');
668
+ }
669
+ return this.defaultRenderer;
670
+ }
671
+ };
672
+ UiRendererRegistry = __decorate([
673
+ singleton()
674
+ ], UiRendererRegistry);
675
+
676
+ const SKIP_PROPS = new Set(['children', 'ref', 'key']);
677
+ /**
678
+ * Serialize the props an island was rendered with so the client can hydrate it
679
+ * with the same data. Drops `children`/`ref`/`key` and any function-valued
680
+ * props (event handlers belong inside the island, not in its serialized props).
681
+ */
682
+ function serializeProps(props) {
683
+ const out = {};
684
+ for (const key in props) {
685
+ if (SKIP_PROPS.has(key))
686
+ continue;
687
+ const value = props[key];
688
+ if (typeof value === 'function')
689
+ continue;
690
+ out[key] = value;
691
+ }
692
+ try {
693
+ return JSON.stringify(out);
694
+ }
695
+ catch {
696
+ return '{}';
697
+ }
698
+ }
699
+
700
+ ({
701
+ logger: new Logger('wabot:error'),
702
+ });
703
+
704
+ const _IS_OPTIONAL_DUMMY_VALIDATOR_ = (value) => {
705
+ return { value, errors: [] };
706
+ };
707
+
708
+ function validateArray(value, options) {
709
+ if (!Array.isArray(value)) {
710
+ return {
711
+ error: { description: 'Should be an array', items: [] },
712
+ };
713
+ }
714
+ if (options?.minLength != null && value.length < options.minLength) {
715
+ return {
716
+ error: { description: 'exceeds the established min length limit', items: [] },
717
+ };
718
+ }
719
+ if (options?.maxLength != null && value.length > options.maxLength) {
720
+ return {
721
+ error: { description: 'exceeds the established max length limit', items: [] },
722
+ };
723
+ }
724
+ const { itemsValidator } = options ?? {};
725
+ const valueOut = [];
726
+ const errorItems = [];
727
+ for (const item of value) {
728
+ let itemOut = item;
729
+ const itemErrors = [];
730
+ for (const itemValidator of itemsValidator ?? []) {
731
+ const { error, value } = itemValidator.validator(itemOut, itemValidator.options);
732
+ if (error) {
733
+ itemErrors.push(error);
734
+ }
735
+ else {
736
+ itemOut = value;
737
+ }
738
+ }
739
+ if (itemErrors.length == 0) {
740
+ valueOut.push(itemOut);
741
+ errorItems.push(null);
742
+ }
743
+ else {
744
+ valueOut.push(null);
745
+ errorItems.push(itemErrors);
746
+ }
747
+ }
748
+ if (errorItems.some((x) => x != null)) {
749
+ return {
750
+ error: { description: 'Error on some items', items: errorItems },
751
+ };
752
+ }
753
+ return {
754
+ value: valueOut,
755
+ };
756
+ }
757
+
758
+ function getClassHierarchy$1(cls) {
759
+ const classes = [];
760
+ let proto = Object.getPrototypeOf(cls.prototype);
761
+ while (proto && proto.constructor !== Object) {
762
+ classes.push(proto.constructor);
763
+ proto = Object.getPrototypeOf(proto);
764
+ }
765
+ return classes;
766
+ }
767
+ let ValidationMetadataStore = class ValidationMetadataStore {
768
+ validators = new Map();
769
+ saveValidatorMetadata(validatorMetadata) {
770
+ let modelValidators = this.validators.get(validatorMetadata.modelConstructor);
771
+ if (!modelValidators) {
772
+ this.validators.set(validatorMetadata.modelConstructor, (modelValidators = new Map()));
773
+ }
774
+ let propertyValidators = modelValidators.get(validatorMetadata.propertyName);
775
+ if (!propertyValidators) {
776
+ propertyValidators = [];
777
+ modelValidators.set(validatorMetadata.propertyName, propertyValidators);
778
+ }
779
+ propertyValidators.unshift(validatorMetadata);
780
+ const arrayValidatorMetadata = propertyValidators.find((x) => x.validator === validateArray);
781
+ if (!arrayValidatorMetadata) {
782
+ return;
783
+ }
784
+ if (!arrayValidatorMetadata.validatorOptions) {
785
+ arrayValidatorMetadata.validatorOptions = {};
786
+ }
787
+ const arrayValidatorOptions = arrayValidatorMetadata.validatorOptions;
788
+ if (!arrayValidatorOptions.itemsValidator) {
789
+ arrayValidatorOptions.itemsValidator = [];
790
+ }
791
+ const removeValidatorsMetadata = [];
792
+ for (const validatorMetadata of propertyValidators) {
793
+ if (validatorMetadata.validator === validateArray ||
794
+ validatorMetadata.validator === _IS_OPTIONAL_DUMMY_VALIDATOR_) {
795
+ continue;
796
+ }
797
+ arrayValidatorOptions.itemsValidator.push({
798
+ options: validatorMetadata.validatorOptions,
799
+ validator: validatorMetadata.validator,
800
+ });
801
+ removeValidatorsMetadata.push(validatorMetadata);
802
+ }
803
+ for (const toRemove of removeValidatorsMetadata) {
804
+ const indexToRemove = propertyValidators.indexOf(toRemove);
805
+ propertyValidators.splice(indexToRemove, 1);
806
+ }
807
+ }
808
+ getModelValidatorsInfo(modelConstructor) {
809
+ const constructors = getClassHierarchy$1(modelConstructor);
810
+ constructors.unshift(modelConstructor);
811
+ const modelValidators = {
812
+ modelConstructor: modelConstructor,
813
+ modelHierarchy: constructors,
814
+ properties: Object.assign({}, ...constructors.map((x) => this.getConstructorPropertiesValidatorsInfo(x))),
815
+ };
816
+ return modelValidators;
817
+ }
818
+ getConstructorPropertiesValidatorsInfo(modelConstructor) {
819
+ const properties = {};
820
+ [...(this.validators.get(modelConstructor)?.values() ?? [])].forEach((propertyValidatorsMetadata) => {
821
+ const propertyName = propertyValidatorsMetadata.at(0)?.propertyName;
822
+ if (!propertyName) {
823
+ return;
824
+ }
825
+ let propertyInfo = properties[propertyName];
826
+ if (!propertyInfo) {
827
+ propertyInfo = {};
828
+ properties[propertyName] = propertyInfo;
829
+ }
830
+ let validators = propertyInfo.validators;
831
+ if (!validators) {
832
+ validators = [];
833
+ propertyInfo.validators = validators;
834
+ }
835
+ propertyValidatorsMetadata.forEach((propertyValidatorMetadata) => {
836
+ if (propertyValidatorMetadata.validator === _IS_OPTIONAL_DUMMY_VALIDATOR_) {
837
+ propertyInfo.isOptional = true;
838
+ }
839
+ else {
840
+ validators.push(propertyValidatorMetadata);
841
+ }
842
+ });
843
+ });
844
+ return properties;
845
+ }
846
+ };
847
+ ValidationMetadataStore = __decorate([
848
+ singleton()
849
+ ], ValidationMetadataStore);
850
+
851
+ let HttpServerProvider = class HttpServerProvider {
852
+ server = null;
853
+ listening = false;
854
+ logger = new Logger('wabot:http');
855
+ getHttpServer() {
856
+ if (!this.server) {
857
+ this.server = new Server();
858
+ }
859
+ return this.server;
860
+ }
861
+ listen() {
862
+ if (!this.server || this.listening) {
863
+ return;
864
+ }
865
+ this.listening = true;
866
+ const PORT = process.env.PORT || 3000;
867
+ this.server.listen(PORT, () => {
868
+ this.logger.info(`Server listening on port ${PORT}`);
869
+ });
870
+ }
871
+ };
872
+ HttpServerProvider = __decorate([
873
+ singleton()
874
+ ], HttpServerProvider);
875
+
876
+ let ExpressProvider = class ExpressProvider {
877
+ httpServerProvider;
878
+ expressApp = null;
879
+ logger = new Logger('wabot:express');
880
+ constructor(httpServerProvider) {
881
+ this.httpServerProvider = httpServerProvider;
882
+ }
883
+ getExpress() {
884
+ if (!this.expressApp) {
885
+ this.expressApp = this.createExpress();
886
+ }
887
+ return this.expressApp;
888
+ }
889
+ listen() {
890
+ this.httpServerProvider.listen();
891
+ }
892
+ createExpress() {
893
+ const expressApp = express();
894
+ expressApp.use((req, res, next) => {
895
+ const start = process.hrtime();
896
+ res.on('finish', () => {
897
+ const [seconds, nanoseconds] = process.hrtime(start);
898
+ const ms = (seconds * 1000 + nanoseconds / 1e6).toFixed(2);
899
+ this.logger.trace(`${req.method} ${req.originalUrl} ${res.statusCode} - ${ms}ms`);
900
+ });
901
+ next();
902
+ });
903
+ const httpServer = this.httpServerProvider.getHttpServer();
904
+ httpServer.on('request', expressApp);
905
+ return expressApp;
906
+ }
907
+ };
908
+ ExpressProvider = __decorate([
909
+ singleton(),
910
+ __metadata("design:paramtypes", [HttpServerProvider])
911
+ ], ExpressProvider);
912
+
913
+ function getClassHierarchy(cls) {
914
+ const classes = [];
915
+ let proto = Object.getPrototypeOf(cls.prototype);
916
+ while (proto && proto.constructor !== Object) {
917
+ classes.push(proto.constructor);
918
+ proto = Object.getPrototypeOf(proto);
919
+ }
920
+ return classes;
921
+ }
922
+ let RestControllerMetadataStore = class RestControllerMetadataStore {
923
+ endPoints = new Map();
924
+ middlewares = new Map();
925
+ restControllers = new Map();
926
+ saveControllerMetadata(controllerMetadata) {
927
+ this.restControllers.set(controllerMetadata.controllerConstructor, controllerMetadata);
928
+ }
929
+ saveEndPointMetadata(endPointMetadata) {
930
+ let controllerEndPoints = this.endPoints.get(endPointMetadata.controllerConstructor);
931
+ if (!controllerEndPoints) {
932
+ this.endPoints.set(endPointMetadata.controllerConstructor, (controllerEndPoints = new Map()));
933
+ }
934
+ controllerEndPoints.set(endPointMetadata.functionName, endPointMetadata);
935
+ }
936
+ saveMiddlewareMetadata(middlewareMetadata) {
937
+ let controllerMiddlewares = this.middlewares.get(middlewareMetadata.controllerConstructor);
938
+ if (!controllerMiddlewares) {
939
+ this.middlewares.set(middlewareMetadata.controllerConstructor, (controllerMiddlewares = new Map()));
940
+ }
941
+ let methodMiddlewares = controllerMiddlewares.get(middlewareMetadata.functionName);
942
+ if (!methodMiddlewares) {
943
+ controllerMiddlewares.set(middlewareMetadata.functionName, (methodMiddlewares = []));
944
+ }
945
+ methodMiddlewares.unshift(middlewareMetadata);
946
+ }
947
+ getAllRestControllerConstructors() {
948
+ return Array.from(this.restControllers.keys());
949
+ }
950
+ getControllerEndPointsInfo(controllerConstructor) {
951
+ const controller = this.restControllers.get(controllerConstructor);
952
+ if (!controller) {
953
+ throw new Error(`${controllerConstructor.name} should be decorated with @restController`);
954
+ }
955
+ const hierarchy = [controllerConstructor, ...getClassHierarchy(controllerConstructor)];
956
+ const endPointsMap = new Map();
957
+ for (const cls of [...hierarchy].reverse()) {
958
+ const classEndPoints = this.endPoints.get(cls);
959
+ if (classEndPoints) {
960
+ for (const [name, endPoint] of classEndPoints) {
961
+ endPointsMap.set(name, endPoint);
962
+ }
963
+ }
964
+ }
965
+ if (!endPointsMap.size) {
966
+ return [];
967
+ }
968
+ return [...endPointsMap.values()].map((endPoint) => {
969
+ const middlewares = [];
970
+ for (const cls of [...hierarchy].reverse()) {
971
+ const classMiddlewares = this.middlewares.get(cls)?.get(endPoint.functionName);
972
+ if (classMiddlewares) {
973
+ middlewares.push(...classMiddlewares);
974
+ }
975
+ }
976
+ return {
977
+ ...endPoint,
978
+ controllerConstructor,
979
+ middlewares,
980
+ controller,
981
+ };
982
+ });
983
+ }
984
+ };
985
+ RestControllerMetadataStore = __decorate([
986
+ singleton()
987
+ ], RestControllerMetadataStore);
988
+
989
+ /** SSR context carrying the current view node so `<Outlet/>` can render it. */
990
+ const OutletContext = createContext(null);
991
+
992
+ /** Absolute path (no extension) to the browser hydration runtime, resolved by esbuild. */
993
+ const PREACT_CLIENT_RUNTIME = fileURLToPath(new URL('./preactClientRuntime', import.meta.url));
994
+ let currentCollector = null;
995
+ const WRAPPED = '__wabotIslandWrapped';
996
+ // Global Preact diff hook (`__b`), invoked per vnode *during* rendering by
997
+ // preact-render-to-string. While an SSR collector is active, replace each
998
+ // island vnode with a <wabot-island> host element that wraps the island's
999
+ // server HTML and carries its serialized props, and record the island so the
1000
+ // page can ship its client bundle. Installed once on import (server only).
1001
+ const preactOptions = options;
1002
+ const previousDiffHook = preactOptions.__b;
1003
+ preactOptions.__b = (vnode) => {
1004
+ if (currentCollector && vnode && typeof vnode.type === 'function' && !vnode[WRAPPED]) {
1005
+ const meta = getIslandMeta(vnode.type);
1006
+ if (meta) {
1007
+ const props = vnode.props ?? {};
1008
+ const id = meta.id ?? meta.name;
1009
+ if (!currentCollector.seen.has(id)) {
1010
+ currentCollector.seen.add(id);
1011
+ currentCollector.islands.push({ id, props });
1012
+ }
1013
+ vnode.type = 'wabot-island';
1014
+ vnode.props = {
1015
+ 'data-island': id,
1016
+ 'data-props': serializeProps(props),
1017
+ children: h(meta.component, props),
1018
+ };
1019
+ vnode[WRAPPED] = true;
1020
+ }
1021
+ }
1022
+ previousDiffHook?.(vnode);
1023
+ };
1024
+ /**
1025
+ * Default UI renderer backed by Preact + @preact/signals. Renders views to HTML
1026
+ * and emits hydration hosts for any component wrapped with `island()`.
1027
+ */
1028
+ class PreactRenderer {
1029
+ id = 'preact';
1030
+ client = {
1031
+ runtimeModule: PREACT_CLIENT_RUNTIME,
1032
+ esbuildJsx: { jsx: 'automatic', jsxImportSource: 'preact' },
1033
+ islandEntrySource: ({ id, importPath }) => `import { registerIsland } from ${JSON.stringify(PREACT_CLIENT_RUNTIME)}\n` +
1034
+ `import Island from ${JSON.stringify(importPath)}\n` +
1035
+ `registerIsland(${JSON.stringify(id)}, Island)\n`,
1036
+ };
1037
+ renderToString(node, context) {
1038
+ // With a layout, render the view inside the shell where <Outlet/> sits.
1039
+ // Without one (or for boosted-nav fragments), render the view directly.
1040
+ const Layout = context?.layout;
1041
+ const tree = Layout
1042
+ ? h(OutletContext.Provider, { value: node }, h(Layout, {}))
1043
+ : node;
1044
+ const collector = { islands: [], seen: new Set() };
1045
+ const previous = currentCollector;
1046
+ currentCollector = collector;
1047
+ try {
1048
+ const html = renderToString(tree);
1049
+ return { html, islands: collector.islands, styles: [] };
1050
+ }
1051
+ finally {
1052
+ currentCollector = previous;
1053
+ }
1054
+ }
1055
+ }
1056
+
71
1057
  const MANIFEST_BANNER = '// auto-generated by @wabot-dev/framework build, do not edit';
72
1058
  /**
73
1059
  * Convert an absolute source path into an ESM import specifier relative to
@@ -83,13 +1069,27 @@ function toManifestImport(absFile, manifestDir) {
83
1069
  return rel.replace(/\.tsx?$/, '.js').replace(/\.jsx$/, '.js');
84
1070
  }
85
1071
  function generateManifest(absFiles, manifestDir) {
86
- const imports = [...absFiles]
87
- .sort()
88
- .map((f) => `import '${toManifestImport(f, manifestDir)}'`);
1072
+ const imports = [...absFiles].sort().map((f) => `import '${toManifestImport(f, manifestDir)}'`);
89
1073
  return `${MANIFEST_BANNER}\n${imports.join('\n')}\n`;
90
1074
  }
91
- function generateEntry(manifestDir, consumerEntry, frameworkPackageName) {
1075
+ /**
1076
+ * Generates a module that imports each island and stamps its stable id, so the
1077
+ * preloaded (prod) server can emit the matching `data-island` markers at SSR.
1078
+ */
1079
+ function generateIslandsRegistration(islands, manifestDir, frameworkPackageName) {
1080
+ const lines = [MANIFEST_BANNER, `import { setIslandId } from '${frameworkPackageName}'`];
1081
+ [...islands]
1082
+ .sort((a, b) => a.absFile.localeCompare(b.absFile))
1083
+ .forEach((island, i) => {
1084
+ lines.push(`import __island${i} from '${toManifestImport(island.absFile, manifestDir)}'`);
1085
+ lines.push(`setIslandId(__island${i}, ${JSON.stringify(island.id)})`);
1086
+ });
1087
+ return lines.join('\n') + '\n';
1088
+ }
1089
+ function generateEntry(manifestDir, consumerEntry, frameworkPackageName, hasIslands = false) {
92
1090
  const lines = [MANIFEST_BANNER, "import './manifest.js'"];
1091
+ if (hasIslands)
1092
+ lines.push("import './islands.js'");
93
1093
  if (consumerEntry) {
94
1094
  const spec = toManifestImport(consumerEntry, manifestDir);
95
1095
  lines.push(`import * as __wabot_user_entry from '${spec}'`);
@@ -144,10 +1144,18 @@ async function runBuild(options = {}) {
144
1144
  }
145
1145
  const entryResolved = existsSync(entry) ? entry : null;
146
1146
  const filesForManifest = discovered.filter((f) => f !== entry);
1147
+ const islands = filesForManifest.filter(isIslandFile).map((absFile) => ({
1148
+ absFile,
1149
+ id: toIslandId(relative(cwd, absFile).split(sep).join('/')),
1150
+ }));
1151
+ const hasIslands = islands.length > 0;
147
1152
  const manifestSrc = generateManifest(filesForManifest, manifestDir);
148
- const entrySrc = generateEntry(manifestDir, entryResolved, FRAMEWORK_PACKAGE);
1153
+ const entrySrc = generateEntry(manifestDir, entryResolved, FRAMEWORK_PACKAGE, hasIslands);
149
1154
  await writeFile(resolve(manifestDir, 'manifest.ts'), manifestSrc, 'utf-8');
150
1155
  await writeFile(resolve(manifestDir, 'entry.ts'), entrySrc, 'utf-8');
1156
+ if (hasIslands) {
1157
+ await writeFile(resolve(manifestDir, 'islands.ts'), generateIslandsRegistration(islands, manifestDir, FRAMEWORK_PACKAGE), 'utf-8');
1158
+ }
151
1159
  let tsup;
152
1160
  try {
153
1161
  tsup = await import('tsup');
@@ -171,6 +1179,21 @@ async function runBuild(options = {}) {
171
1179
  : undefined,
172
1180
  external: config.external ?? ['pg'],
173
1181
  });
1182
+ // Emit island client bundles after tsup (which cleans outDir first).
1183
+ if (hasIslands) {
1184
+ const uiOut = resolve(outDir, 'ui');
1185
+ const bundler = new UiBundler({
1186
+ islands: islands.map(({ absFile, id }) => ({
1187
+ id,
1188
+ importPath: absFile,
1189
+ relPath: relative(cwd, absFile).split(sep).join('/'),
1190
+ })),
1191
+ client: new PreactRenderer().client,
1192
+ cwd,
1193
+ });
1194
+ const uiManifest = await bundler.buildProd(uiOut);
1195
+ await writeFile(resolve(uiOut, 'manifest.json'), JSON.stringify(uiManifest, null, 2), 'utf-8');
1196
+ }
174
1197
  }
175
1198
  finally {
176
1199
  if (!options.keep) {