@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
package/dist/src/index.js CHANGED
@@ -130,6 +130,20 @@ export { socketController } from './feature/socket-controller/metadata/@socketCo
130
130
  export { onSocketEvent } from './feature/socket-controller/metadata/@onSocketEvent.js';
131
131
  export { SocketControllerMetadataStore } from './feature/socket-controller/metadata/SocketControllerMetadataStore.js';
132
132
  export { runSocketControllers } from './feature/socket-controller/runSocketControllers.js';
133
+ export { uiController } from './feature/ui-controller/metadata/@uiController.js';
134
+ export { view } from './feature/ui-controller/metadata/@view.js';
135
+ export { action } from './feature/ui-controller/metadata/@action.js';
136
+ export { uiMiddleware } from './feature/ui-controller/metadata/@uiMiddleware.js';
137
+ export { UiControllerMetadataStore } from './feature/ui-controller/metadata/UiControllerMetadataStore.js';
138
+ export { UiRendererRegistry } from './feature/ui-controller/renderer/UiRendererRegistry.js';
139
+ export { ISLAND_MARKER, getIslandMeta, isIsland, island, setIslandId } from './feature/ui-controller/island/island.js';
140
+ export { deserializeProps, serializeProps } from './feature/ui-controller/island/serialize.js';
141
+ export { ISLAND_FILE_PATTERN, IslandRegistry, isIslandFile, toIslandId } from './feature/ui-controller/island/IslandRegistry.js';
142
+ export { renderDocument } from './feature/ui-controller/document/renderDocument.js';
143
+ export { REDIRECT_MARKER, isRedirect, redirect } from './feature/ui-controller/document/helpers.js';
144
+ export { escapeAttr, escapeHtml } from './feature/ui-controller/document/escape.js';
145
+ export { actionUrl, callAction } from './feature/ui-controller/actions.js';
146
+ export { registerUiControllers, runUiControllers } from './feature/ui-controller/runUiControllers.js';
133
147
  export { PgCronJobRepository } from './addon/async/pg/PgCronJobRepository.js';
134
148
  export { PgJobRepository } from './addon/async/pg/PgJobRepository.js';
135
149
  export { PgTransactionAdapter } from './addon/async/pg/PgTransactionAdapter.js';
@@ -165,6 +179,7 @@ export { PgChatMemory } from './addon/chat-bot/pg/PgChatMemory.js';
165
179
  export { InMemoryChatMemory } from './addon/chat-bot/in-memory/InMemoryChatMemory.js';
166
180
  export { InMemoryChatRepository } from './addon/chat-bot/in-memory/InMemoryChatRepository.js';
167
181
  export { WabotChatAdapter } from './addon/chat-bot/wabot/WabotChatAdapter.js';
182
+ export { XAIChatAdapter } from './addon/chat-bot/xai/XAIChatAdapter.js';
168
183
  export { cmd } from './addon/chat-controller/cmd/@cmd.js';
169
184
  export { CmdChannel, readJsonFromFile, writeJsonToFile } from './addon/chat-controller/cmd/CmdChannel.js';
170
185
  export { CmdChannelConfig } from './addon/chat-controller/cmd/CmdChannelConfig.js';
@@ -172,6 +187,21 @@ export { CmdChannelServer } from './addon/chat-controller/cmd/CmdChannelServer.j
172
187
  export { cmdChannelName } from './addon/chat-controller/cmd/cmdChannelName.js';
173
188
  export { cmdChannelSocketPath } from './addon/chat-controller/cmd/cmdChannelSocketPath.js';
174
189
  export { runCmdClient } from './addon/chat-controller/cmd/runCmdClient.js';
190
+ export { hubspotChannelName } from './addon/chat-controller/hubspot/hubspotChannelName.js';
191
+ export { HubSpotChannelConfig } from './addon/chat-controller/hubspot/HubSpotChannelConfig.js';
192
+ export { verifyHubSpotSignatureV3 } from './addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js';
193
+ export { downloadHubSpotAttachments } from './addon/chat-controller/hubspot/downloadHubSpotAttachments.js';
194
+ export { HubSpotSender } from './addon/chat-controller/hubspot/HubSpotSender.js';
195
+ export { HubSpotWebhookController } from './addon/chat-controller/hubspot/HubSpotWebhookController.js';
196
+ export { HubSpotReceiver } from './addon/chat-controller/hubspot/HubSpotReceiver.js';
197
+ export { HubSpotChannel } from './addon/chat-controller/hubspot/HubSpotChannel.js';
198
+ export { markdownToChatHtml as markdownToHubSpotHtml, markdownToChatHtml as markdownToTelegramHtml } from './addon/chat-controller/markdown/markdownToChatHtml.js';
199
+ export { hubspot } from './addon/chat-controller/hubspot/@hubspot.js';
200
+ export { slack } from './addon/chat-controller/slack/@slack.js';
201
+ export { SlackChannelConfig } from './addon/chat-controller/slack/SlackChannelConfig.js';
202
+ export { SlackChannel } from './addon/chat-controller/slack/SlackChannel.js';
203
+ export { slackChannelName } from './addon/chat-controller/slack/slackChannelName.js';
204
+ export { markdownToSlackMrkdwn } from './addon/chat-controller/slack/markdownToSlackMrkdwn.js';
175
205
  export { socket } from './addon/chat-controller/socket/@socket.js';
176
206
  export { SocketChannel, SocketChannelMessageFile, SocketChannelReceivedMessage } from './addon/chat-controller/socket/SocketChannel.js';
177
207
  export { SocketChannelConfig } from './addon/chat-controller/socket/SocketChannelConfig.js';
@@ -180,7 +210,6 @@ export { telegram } from './addon/chat-controller/telegram/@telegram.js';
180
210
  export { TelegramChannelConfig } from './addon/chat-controller/telegram/TelegramChannelConfig.js';
181
211
  export { TelegramChannel } from './addon/chat-controller/telegram/TelegramChannel.js';
182
212
  export { telegramChannelName } from './addon/chat-controller/telegram/telegramChannelName.js';
183
- export { markdownToTelegramHtml } from './addon/chat-controller/telegram/markdownToTelegramHtml.js';
184
213
  export { WhatsAppReceiverByCloudApi } from './addon/chat-controller/whatsapp/cloud-api/WhatsAppReceiverByCloudApi.js';
185
214
  export { WhatsAppApiSender } from './addon/chat-controller/whatsapp/cloud-api/WhatsAppApiSender.js';
186
215
  export { kapso } from './addon/chat-controller/whatsapp/kapso/@kapso.js';
@@ -701,6 +701,48 @@ declare class RestHarness {
701
701
  }
702
702
  declare function createRestHarness(options: IRestHarnessOptions): Promise<RestHarness>;
703
703
 
704
+ interface IUiHarnessOptions {
705
+ controllers: IConstructor<any>[];
706
+ /** Extra DI registrations visible to middlewares and controllers: [token, instance]. */
707
+ register?: [any, any][];
708
+ }
709
+ interface IUiRequestOptions {
710
+ body?: unknown;
711
+ headers?: Record<string, string>;
712
+ query?: Record<string, string>;
713
+ /** Follow redirects ("follow", default) or capture them ("manual"). */
714
+ redirect?: 'follow' | 'manual';
715
+ }
716
+ interface IUiResponse {
717
+ status: number;
718
+ /** Raw response body. */
719
+ text: string;
720
+ headers: Headers;
721
+ /** Parse the body as JSON (for @action responses). */
722
+ json(): any;
723
+ }
724
+ /**
725
+ * Mounts @uiController classes on a private HTTP server (ephemeral port) and
726
+ * exercises the real pipeline: middlewares/guards, validation, SSR and actions.
727
+ * Islands render as static SSR HTML (no client bundling needed for tests).
728
+ */
729
+ declare class UiHarness {
730
+ readonly container: DependencyContainer;
731
+ private server;
732
+ private baseUrl;
733
+ private constructor();
734
+ static create(options: IUiHarnessOptions): Promise<UiHarness>;
735
+ private listen;
736
+ get url(): string;
737
+ /** GET a view; returns the rendered HTML document. */
738
+ get(path: string, options?: IUiRequestOptions): Promise<IUiResponse>;
739
+ /** POST to an @action route (e.g. "/my/ui/_action/addTodo"). */
740
+ action(path: string, body?: unknown, options?: IUiRequestOptions): Promise<IUiResponse>;
741
+ request(method: string, path: string, options?: IUiRequestOptions): Promise<IUiResponse>;
742
+ close(): Promise<void>;
743
+ }
744
+ declare function createUiHarness(options: IUiHarnessOptions): Promise<UiHarness>;
745
+
704
746
  interface ICronHandler {
705
747
  handle(): void | Promise<void>;
706
748
  handleError?(e: any): void | Promise<void>;
@@ -773,4 +815,4 @@ interface ICronValidationOptions {
773
815
  /** Check that a sequence of dates matches consecutive firings of a cron expression. */
774
816
  declare function isValidCronSequence(cronExpression: string, dates: Date[], options?: ICronValidationOptions): boolean;
775
817
 
776
- export { AsyncHarness, ChatBotHarness, ChatControllerHarness, type IAsyncHarnessOptions, type IChatAdapterConformanceCase, type IChatAdapterConformanceReq, type IChatBotHarnessOptions, type IChatBotTurn, type IChatControllerHarnessOptions, type ICronValidationOptions, type IEntityFixtureOptions, type IFileFixtureOptions, type ILlmJudgeEvaluateReq, type ILlmJudgeOptions, type ILlmJudgeVerdict, type IMockChatAdapterOptions, type IMockChatAdapterResponse, type IRestHarnessOptions, type IRestRequestOptions, type IRestResponse, type ITestJwtOptions, type IValidationFixtureResult, type IValidationIssue, LlmJudge, MockChatAdapter, RestHarness, TestApiKeyRepository, TestChatMemory, TestChatRepository, TestJwt, assertInvalid, assertValid, botItem, chatAdapterConformanceCases, createAsyncHarness, createChatBotHarness, createChatControllerHarness, createRestHarness, documentMessage, entityFixture, humanItem, humanMessage, imageMessage, isValidCronSequence, renderTranscript, setupTestJwt, testImageBase64Url, testPdfBase64Url, useMemoryRepositories, validateFixture, wait, waitUntil };
818
+ export { AsyncHarness, ChatBotHarness, ChatControllerHarness, type IAsyncHarnessOptions, type IChatAdapterConformanceCase, type IChatAdapterConformanceReq, type IChatBotHarnessOptions, type IChatBotTurn, type IChatControllerHarnessOptions, type ICronValidationOptions, type IEntityFixtureOptions, type IFileFixtureOptions, type ILlmJudgeEvaluateReq, type ILlmJudgeOptions, type ILlmJudgeVerdict, type IMockChatAdapterOptions, type IMockChatAdapterResponse, type IRestHarnessOptions, type IRestRequestOptions, type IRestResponse, type ITestJwtOptions, type IUiHarnessOptions, type IUiRequestOptions, type IUiResponse, type IValidationFixtureResult, type IValidationIssue, LlmJudge, MockChatAdapter, RestHarness, TestApiKeyRepository, TestChatMemory, TestChatRepository, TestJwt, UiHarness, assertInvalid, assertValid, botItem, chatAdapterConformanceCases, createAsyncHarness, createChatBotHarness, createChatControllerHarness, createRestHarness, createUiHarness, documentMessage, entityFixture, humanItem, humanMessage, imageMessage, isValidCronSequence, renderTranscript, setupTestJwt, testImageBase64Url, testPdfBase64Url, useMemoryRepositories, validateFixture, wait, waitUntil };
@@ -6,6 +6,7 @@ export { LlmJudge, renderTranscript } from './LlmJudge.js';
6
6
  export { botItem, documentMessage, humanItem, humanMessage, imageMessage, testImageBase64Url, testPdfBase64Url } from './fixtures.js';
7
7
  export { chatAdapterConformanceCases } from './conformance/chatAdapterConformanceCases.js';
8
8
  export { RestHarness, createRestHarness } from './restHarness.js';
9
+ export { UiHarness, createUiHarness } from './uiHarness.js';
9
10
  export { AsyncHarness, createAsyncHarness } from './asyncHarness.js';
10
11
  export { entityFixture, useMemoryRepositories } from './repositories.js';
11
12
  export { TestApiKeyRepository, TestJwt, setupTestJwt } from './auth.js';
@@ -0,0 +1,102 @@
1
+ import { container } from '../core/injection/index.js';
2
+ import { ExpressProvider } from '../feature/express/ExpressProvider.js';
3
+ import { HttpServerProvider } from '../feature/http/HttpServerProvider.js';
4
+ import '../feature/ui-controller/metadata/UiControllerMetadataStore.js';
5
+ import { UiRendererRegistry } from '../feature/ui-controller/renderer/UiRendererRegistry.js';
6
+ import '../feature/ui-controller/island/IslandRegistry.js';
7
+ import { registerUiControllers } from '../feature/ui-controller/runUiControllers.js';
8
+ import { PreactRenderer } from '../addon/ui/preact/PreactRenderer.js';
9
+ import '../addon/ui/preact/outlet.js';
10
+ import { Container } from '../core/injection/Container.js';
11
+
12
+ /**
13
+ * Mounts @uiController classes on a private HTTP server (ephemeral port) and
14
+ * exercises the real pipeline: middlewares/guards, validation, SSR and actions.
15
+ * Islands render as static SSR HTML (no client bundling needed for tests).
16
+ */
17
+ class UiHarness {
18
+ container;
19
+ server;
20
+ baseUrl = '';
21
+ constructor(options) {
22
+ const child = container.createChildContainer();
23
+ child.register(Container, { useValue: child });
24
+ for (const [token, instance] of options.register ?? []) {
25
+ child.registerInstance(token, instance);
26
+ }
27
+ const rendererRegistry = child.resolve(UiRendererRegistry);
28
+ if (!rendererRegistry.hasDefault()) {
29
+ rendererRegistry.setDefault(new PreactRenderer());
30
+ }
31
+ this.container = child;
32
+ const httpServerProvider = new HttpServerProvider();
33
+ const expressProvider = new ExpressProvider(httpServerProvider);
34
+ registerUiControllers(options.controllers, { baseContainer: child, expressProvider });
35
+ this.server = httpServerProvider.getHttpServer();
36
+ }
37
+ static async create(options) {
38
+ const harness = new UiHarness(options);
39
+ await harness.listen();
40
+ return harness;
41
+ }
42
+ listen() {
43
+ return new Promise((resolve, reject) => {
44
+ this.server.once('error', reject);
45
+ this.server.listen(0, '127.0.0.1', () => {
46
+ const address = this.server.address();
47
+ if (!address || typeof address === 'string') {
48
+ reject(new Error('UiHarness: could not determine server port'));
49
+ return;
50
+ }
51
+ this.baseUrl = `http://127.0.0.1:${address.port}`;
52
+ resolve();
53
+ });
54
+ });
55
+ }
56
+ get url() {
57
+ return this.baseUrl;
58
+ }
59
+ /** GET a view; returns the rendered HTML document. */
60
+ get(path, options = {}) {
61
+ return this.request('GET', path, options);
62
+ }
63
+ /** POST to an @action route (e.g. "/my/ui/_action/addTodo"). */
64
+ action(path, body, options = {}) {
65
+ return this.request('POST', path, { ...options, body });
66
+ }
67
+ async request(method, path, options = {}) {
68
+ const url = new URL(path, this.baseUrl);
69
+ for (const [key, value] of Object.entries(options.query ?? {})) {
70
+ url.searchParams.set(key, value);
71
+ }
72
+ const headers = { ...options.headers };
73
+ let body;
74
+ if (options.body !== undefined) {
75
+ headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
76
+ body = JSON.stringify(options.body);
77
+ }
78
+ const response = await fetch(url, {
79
+ method: method.toUpperCase(),
80
+ headers,
81
+ body,
82
+ redirect: options.redirect ?? 'follow',
83
+ });
84
+ const text = await response.text();
85
+ return {
86
+ status: response.status,
87
+ text,
88
+ headers: response.headers,
89
+ json: () => (text ? JSON.parse(text) : null),
90
+ };
91
+ }
92
+ async close() {
93
+ await new Promise((resolve, reject) => {
94
+ this.server.close((err) => (err ? reject(err) : resolve()));
95
+ });
96
+ }
97
+ }
98
+ function createUiHarness(options) {
99
+ return UiHarness.create(options);
100
+ }
101
+
102
+ export { UiHarness, createUiHarness };
@@ -0,0 +1,6 @@
1
+ export { getIslandMeta, isIsland, island, setIslandId } from '../feature/ui-controller/island/island.js';
2
+ export { actionUrl, callAction } from '../feature/ui-controller/actions.js';
3
+ export { Outlet } from '../addon/ui/preact/outlet.js';
4
+ export { Fragment, cloneElement, createContext, h as createElement, createRef, h, isValidElement, toChildArray } from 'preact';
5
+ export * from 'preact/hooks';
6
+ export { Signal, batch, computed, createModel, effect, signal, action as signalAction, untracked, useComputed, useModel, useSignal, useSignalEffect } from '@preact/signals';
@@ -0,0 +1,427 @@
1
+ import { DependencyContainer } from 'tsyringe';
2
+ import { Request, Response, Express } from 'express';
3
+ import { Server } from 'node:http';
4
+ import * as preact from 'preact';
5
+ export { ComponentChild, ComponentChildren, Fragment, RefObject, VNode, cloneElement, createContext, h as createElement, createRef, h, isValidElement, toChildArray } from 'preact';
6
+ export * from 'preact/hooks';
7
+ export { Model, ModelConstructor, ReadonlySignal, Signal, batch, computed, createModel, effect, signal, action as signalAction, untracked, useComputed, useModel, useSignal, useSignalEffect } from '@preact/signals';
8
+
9
+ type IConstructor<T> = new (...args: any[]) => T;
10
+
11
+ interface IMiddleware {
12
+ handle(req: Request, res: Response, container: DependencyContainer): Promise<void>;
13
+ }
14
+
15
+ declare class HttpServerProvider {
16
+ server: Server | null;
17
+ private listening;
18
+ private logger;
19
+ getHttpServer(): Server;
20
+ listen(): void;
21
+ }
22
+
23
+ declare class ExpressProvider {
24
+ private httpServerProvider;
25
+ private expressApp;
26
+ private logger;
27
+ constructor(httpServerProvider: HttpServerProvider);
28
+ getExpress(): Express;
29
+ listen(): void;
30
+ private createExpress;
31
+ }
32
+
33
+ /** A resource to `<link rel="preload">` (font, critical image, …). */
34
+ interface IPreloadLink {
35
+ href: string;
36
+ /** Destination: 'font' | 'image' | 'style' | 'script' | 'fetch' | … */
37
+ as: string;
38
+ /** MIME type, e.g. 'font/woff2'. Recommended so the preload matches the request. */
39
+ type?: string;
40
+ /**
41
+ * CORS mode. Fonts are always fetched cross-origin, so `as: 'font'` defaults to
42
+ * `true` (a bare `crossorigin`) — omitting it would fetch the font twice.
43
+ */
44
+ crossorigin?: boolean | 'anonymous' | 'use-credentials';
45
+ media?: string;
46
+ }
47
+ /** An origin to `<link rel="preconnect">` (e.g. a font CDN). */
48
+ type IPreconnect = string | {
49
+ href: string;
50
+ crossorigin?: boolean;
51
+ };
52
+ /** Extra `<head>` resource hints rendered on full document loads. */
53
+ interface IControllerHead {
54
+ /** Origins to open a connection to early (TLS handshake), e.g. a font host. */
55
+ preconnect?: IPreconnect[];
56
+ /** Resources to fetch early in parallel (fonts, hero image, …). */
57
+ preload?: IPreloadLink[];
58
+ }
59
+ interface IUiControllerConfig {
60
+ /** Base path every view/action of the controller is mounted under. */
61
+ path: string;
62
+ /** Middlewares (e.g. auth guards) applied to every view and action. */
63
+ middlewares?: IConstructor<IMiddleware>[];
64
+ /**
65
+ * Opt into client-side ("boosted") navigation between this controller's
66
+ * views: links within the controller's `path` are intercepted and swapped in
67
+ * without a full browser reload, backed by a stale-while-revalidate cache.
68
+ * Views still SSR normally and keep working without JS. Defaults to false.
69
+ */
70
+ app?: boolean;
71
+ /**
72
+ * Persistent app shell rendered once around every view of the controller. The
73
+ * layout renders an `<Outlet/>` where the current view goes; during boosted
74
+ * navigation only the outlet swaps, so the shell (and its islands) keep their
75
+ * state. A renderer component (e.g. a Preact function component).
76
+ */
77
+ layout?: unknown;
78
+ /**
79
+ * `<head>` resource hints (preconnect/preload) rendered on full document
80
+ * loads — the natural home for app-wide fonts, since the head persists across
81
+ * boosted navigation and is cached for the whole session.
82
+ */
83
+ head?: IControllerHead;
84
+ }
85
+
86
+ declare function uiController(config: string | IUiControllerConfig): (target: IConstructor<any>) => void;
87
+
88
+ interface IViewConfig {
89
+ /** Sub-path appended to the controller path. Empty/omitted = controller index. */
90
+ path?: string;
91
+ /** Document <title> for this page. */
92
+ title?: string;
93
+ /** Extra <meta> tags rendered into the document head: name -> content. */
94
+ meta?: Record<string, string>;
95
+ /**
96
+ * Stale-while-revalidate hints for boosted navigation (`app: true`
97
+ * controllers). Only meaningful for the soft-nav client cache.
98
+ */
99
+ swr?: {
100
+ /**
101
+ * Seconds a cached fragment is considered fresh: within this window a
102
+ * revisit renders from cache without a background revalidation. Default 0
103
+ * (always revalidate in the background).
104
+ */
105
+ maxAge?: number;
106
+ /**
107
+ * Cheap revalidation tag (the SWR key). When provided, boosted-nav
108
+ * revalidation compares this against `If-None-Match` and can answer 304
109
+ * *without* running the view handler or SSR. Receives the request with
110
+ * body+query+params merged, so parameterized routes can (and should) key off
111
+ * their route params — e.g. `version: ({ id }) => docRevision(id)`.
112
+ * Return a short, deterministic string (a data timestamp or revision).
113
+ */
114
+ version?: (request: any) => string | Promise<string>;
115
+ };
116
+ }
117
+
118
+ declare function view(config?: string | IViewConfig): (target: object, propertyKey: string | symbol) => void;
119
+
120
+ interface IActionConfig {
121
+ /** Sub-path of the action. Defaults to the method name. Mounted under `<controller>/_action/<path>`. */
122
+ path?: string;
123
+ }
124
+
125
+ declare function action(config?: string | IActionConfig): (target: object, propertyKey: string | symbol) => void;
126
+
127
+ /**
128
+ * Attach a middleware (e.g. an auth guard) to a single @view or @action.
129
+ * Controller-wide middlewares can instead be declared on @uiController({ middlewares }).
130
+ */
131
+ declare function uiMiddleware(middlewareConstructor: IConstructor<IMiddleware>): (target: object, propertyKey: string | symbol) => void;
132
+
133
+ interface IUiControllerMetadata {
134
+ controllerConstructor: IConstructor<any>;
135
+ path: string;
136
+ middlewares: IConstructor<IMiddleware>[];
137
+ /** Whether this controller opts into client-side ("boosted") navigation. */
138
+ app?: boolean;
139
+ /** Persistent app-shell component rendered around every view (with an `<Outlet/>`). */
140
+ layout?: unknown;
141
+ /** `<head>` resource hints (preconnect/preload) for full document loads. */
142
+ head?: IControllerHead;
143
+ }
144
+
145
+ interface IViewMetadata {
146
+ controllerConstructor: IConstructor<any>;
147
+ functionName: string;
148
+ config?: IViewConfig;
149
+ paramsTypes: any[];
150
+ }
151
+
152
+ interface IActionMetadata {
153
+ controllerConstructor: IConstructor<any>;
154
+ functionName: string;
155
+ config?: IActionConfig;
156
+ paramsTypes: any[];
157
+ }
158
+
159
+ interface IUiMiddlewareMetadata {
160
+ controllerConstructor: IConstructor<any>;
161
+ functionName: string;
162
+ middlewareConstructor: IConstructor<IMiddleware>;
163
+ }
164
+
165
+ declare class UiControllerMetadataStore {
166
+ private controllers;
167
+ private views;
168
+ private actions;
169
+ private middlewares;
170
+ saveControllerMetadata(metadata: IUiControllerMetadata): void;
171
+ saveViewMetadata(metadata: IViewMetadata): void;
172
+ saveActionMetadata(metadata: IActionMetadata): void;
173
+ saveMiddlewareMetadata(metadata: IUiMiddlewareMetadata): void;
174
+ getAllUiControllerConstructors(): IConstructor<any>[];
175
+ private getController;
176
+ private collectMethodMiddlewares;
177
+ getControllerViewsInfo(controllerConstructor: IConstructor<any>): {
178
+ controllerConstructor: IConstructor<any>;
179
+ controller: IUiControllerMetadata;
180
+ middlewares: IUiMiddlewareMetadata[];
181
+ functionName: string;
182
+ config?: IViewConfig;
183
+ paramsTypes: any[];
184
+ }[];
185
+ getControllerActionsInfo(controllerConstructor: IConstructor<any>): {
186
+ controllerConstructor: IConstructor<any>;
187
+ controller: IUiControllerMetadata;
188
+ middlewares: IUiMiddlewareMetadata[];
189
+ functionName: string;
190
+ config?: IActionConfig;
191
+ paramsTypes: any[];
192
+ }[];
193
+ }
194
+
195
+ interface IRenderContext {
196
+ /** True while running under the dev server (enables hydration in dev, etc.). */
197
+ dev?: boolean;
198
+ /**
199
+ * Optional layout component to wrap the view in (the persistent app shell).
200
+ * The renderer places the view where the layout renders its `<Outlet/>`.
201
+ * Passed only for full-document loads; omitted for boosted-nav fragments so
202
+ * only the view (the outlet's content) is rendered.
203
+ */
204
+ layout?: unknown;
205
+ }
206
+ interface IRenderedIsland {
207
+ /** Stable island id (module-derived), used to load its client bundle. */
208
+ id: string;
209
+ /** Serializable props the island was rendered with, for client hydration. */
210
+ props: unknown;
211
+ }
212
+ interface IRenderResult {
213
+ /** Server-rendered HTML for the view body. */
214
+ html: string;
215
+ /** Islands encountered while rendering, in document order. */
216
+ islands: IRenderedIsland[];
217
+ /** Style hrefs (or inline CSS ids) the page depends on, if any. */
218
+ styles: string[];
219
+ }
220
+ interface IIslandEntryArgs {
221
+ /** Stable island id (also the client bundle's output name). */
222
+ id: string;
223
+ /** Absolute path to the island's source module (default export = island). */
224
+ importPath: string;
225
+ }
226
+ /** Renderer-specific knobs the island bundler needs to build client bundles. */
227
+ interface IUiClientConfig {
228
+ /** Module the generated island entry imports its hydration `registerIsland` from. */
229
+ runtimeModule: string;
230
+ /** esbuild JSX settings for compiling islands on the client. */
231
+ esbuildJsx?: {
232
+ jsx?: 'automatic' | 'transform' | 'preserve';
233
+ jsxImportSource?: string;
234
+ jsxFactory?: string;
235
+ jsxFragmentFactory?: string;
236
+ };
237
+ /** Source of the client entry that registers one island for hydration. */
238
+ islandEntrySource(args: IIslandEntryArgs): string;
239
+ }
240
+ /**
241
+ * A pluggable UI rendering engine. The default implementation is Preact
242
+ * (`@wabot-dev/framework/ui`), but any framework can be adapted by implementing
243
+ * this interface and registering it in {@link UiRendererRegistry}.
244
+ */
245
+ interface UiRenderer {
246
+ /** Unique id, e.g. "preact" or "react". */
247
+ readonly id: string;
248
+ /** Render a view's returned node tree to HTML, collecting island usage. */
249
+ renderToString(node: unknown, context?: IRenderContext): IRenderResult | Promise<IRenderResult>;
250
+ /** Bundling/hydration config. Required to ship interactive islands. */
251
+ readonly client?: IUiClientConfig;
252
+ }
253
+
254
+ declare class UiRendererRegistry {
255
+ private renderers;
256
+ private defaultRenderer;
257
+ /** Register a renderer. The first one registered becomes the default. */
258
+ register(renderer: UiRenderer): void;
259
+ /** Register a renderer and make it the default. */
260
+ setDefault(renderer: UiRenderer): void;
261
+ has(id: string): boolean;
262
+ hasDefault(): boolean;
263
+ get(id?: string): UiRenderer;
264
+ }
265
+
266
+ /**
267
+ * Marker attached to components wrapped with {@link island}. The UI renderer
268
+ * uses it to decide which components must hydrate on the client; the bundler
269
+ * uses {@link IslandMeta.id} (assigned from the `*.island.tsx` file location) to
270
+ * emit a per-island client bundle.
271
+ */
272
+ declare const ISLAND_MARKER: unique symbol;
273
+ interface IslandMeta {
274
+ /** The original component, rendered for SSR and hydrated on the client. */
275
+ component: (props: any) => any;
276
+ /** Human-readable name (component name unless overridden). */
277
+ name: string;
278
+ /** Stable bundle id, assigned during island discovery. Undefined until then. */
279
+ id?: string;
280
+ }
281
+ interface IslandComponent<P = any> {
282
+ (props: P): any;
283
+ [ISLAND_MARKER]: IslandMeta;
284
+ }
285
+ /**
286
+ * Mark a component as an interactive client "island". The component still
287
+ * renders on the server, but only islands ship JavaScript and hydrate in the
288
+ * browser. Islands must be the export of a `*.island.tsx` file so the bundler
289
+ * can give them a stable id.
290
+ *
291
+ * // Counter.island.tsx
292
+ * function Counter() { ... }
293
+ * export default island(Counter)
294
+ */
295
+ declare function island<P>(component: (props: P) => any, name?: string): IslandComponent<P>;
296
+ declare function getIslandMeta(component: unknown): IslandMeta | undefined;
297
+ declare function isIsland(component: unknown): boolean;
298
+ /** Assign the stable bundle id to an island, done during island discovery. */
299
+ declare function setIslandId(component: unknown, id: string): void;
300
+
301
+ /**
302
+ * Serialize the props an island was rendered with so the client can hydrate it
303
+ * with the same data. Drops `children`/`ref`/`key` and any function-valued
304
+ * props (event handlers belong inside the island, not in its serialized props).
305
+ */
306
+ declare function serializeProps(props: Record<string, unknown>): string;
307
+ declare function deserializeProps(raw: string | undefined | null): Record<string, unknown>;
308
+
309
+ interface IDiscoveredIsland {
310
+ /** Stable id derived from the file's project-relative path. */
311
+ id: string;
312
+ /** Absolute path to the island source module. */
313
+ importPath: string;
314
+ /** Project-relative posix path (the basis of the id). */
315
+ relPath: string;
316
+ }
317
+ /** Files matching this are treated as islands (default export wrapped with island()). */
318
+ declare const ISLAND_FILE_PATTERN: RegExp;
319
+ /** Deterministic, readable, collision-resistant id from a project-relative path. */
320
+ declare function toIslandId(relPath: string): string;
321
+ declare function isIslandFile(file: string): boolean;
322
+ /**
323
+ * Discovers `*.island.tsx` modules, assigns each a stable id, and stamps that
324
+ * id onto the imported island component (ESM module singletons mean the view
325
+ * renders the same instance, so the renderer can read the id during SSR).
326
+ */
327
+ declare class IslandRegistry {
328
+ private islands;
329
+ private logger;
330
+ discover(files: string[], cwd?: string): Promise<IDiscoveredIsland[]>;
331
+ register(island: IDiscoveredIsland): void;
332
+ list(): IDiscoveredIsland[];
333
+ get(id: string): IDiscoveredIsland | undefined;
334
+ get size(): number;
335
+ }
336
+
337
+ interface IDocumentOptions {
338
+ /** Server-rendered HTML placed inside <body>. */
339
+ bodyHtml: string;
340
+ /** Document title. */
341
+ title?: string;
342
+ /** <meta name=.. content=..> tags. */
343
+ meta?: Record<string, string>;
344
+ /** Stylesheet hrefs rendered as <link rel="stylesheet">. */
345
+ styles?: string[];
346
+ /** Extra <link> tags (preload/preconnect/…) as attribute maps. `true` => bare attribute. */
347
+ links?: Array<Record<string, string | boolean>>;
348
+ /** Module script srcs rendered as <script type="module">. */
349
+ scripts?: string[];
350
+ /** Raw HTML appended to <head> (e.g. inline bootstrap data). */
351
+ headHtml?: string;
352
+ /** Raw HTML appended to the end of <body> (e.g. dev live-reload snippet). */
353
+ bodyEndHtml?: string;
354
+ /** <html lang>. Defaults to "en". */
355
+ lang?: string;
356
+ }
357
+ /**
358
+ * Render the default HTML document shell around a view's body. Kept
359
+ * renderer-agnostic (plain string templating) so it works with any UiRenderer.
360
+ */
361
+ declare function renderDocument(options: IDocumentOptions): string;
362
+
363
+ declare const REDIRECT_MARKER: unique symbol;
364
+ interface UiRedirect {
365
+ readonly [REDIRECT_MARKER]: true;
366
+ location: string;
367
+ status: number;
368
+ }
369
+ /**
370
+ * Return this from a @view or @action to send an HTTP redirect instead of
371
+ * rendering. Used for the post/redirect/get pattern after form actions.
372
+ */
373
+ declare function redirect(location: string, status?: number): UiRedirect;
374
+ declare function isRedirect(value: unknown): value is UiRedirect;
375
+
376
+ /** Escape text for safe interpolation into HTML element content. */
377
+ declare function escapeHtml(value: string): string;
378
+ /** Escape a value for safe interpolation into a double-quoted HTML attribute. */
379
+ declare function escapeAttr(value: string): string;
380
+
381
+ /** Build the URL of an @action: `<controllerPath>/_action/<actionName>`. */
382
+ declare function actionUrl(controllerPath: string, actionName: string): string;
383
+ interface ICallActionOptions {
384
+ headers?: Record<string, string>;
385
+ signal?: AbortSignal;
386
+ }
387
+ /**
388
+ * POST JSON to an @action and return its JSON result. Throws on non-2xx with
389
+ * the server-provided error message. Intended for use from islands; plain
390
+ * <form action=…> posts also work for progressive enhancement.
391
+ */
392
+ declare function callAction<T = unknown>(url: string, data?: unknown, options?: ICallActionOptions): Promise<T>;
393
+
394
+ /** Page-level assets (scripts/styles) to inject, computed from the islands a view rendered. */
395
+ interface IPageAssets {
396
+ scripts?: string[];
397
+ styles?: string[];
398
+ headHtml?: string;
399
+ bodyEndHtml?: string;
400
+ /** URL of the boosted-navigation runtime; injected on `app: true` pages. */
401
+ navScript?: string;
402
+ }
403
+ interface IRegisterUiControllersOptions {
404
+ baseContainer?: DependencyContainer;
405
+ expressProvider?: ExpressProvider;
406
+ /** Hook used by the bundler/dev server to inject island client bundles + CSS. */
407
+ pageAssets?: (islands: IRenderedIsland[]) => IPageAssets;
408
+ }
409
+ declare function registerUiControllers(controllers: IConstructor<any>[], options?: IRegisterUiControllersOptions): ExpressProvider;
410
+ declare function runUiControllers(controllers: IConstructor<any>[], options?: IRegisterUiControllersOptions): void;
411
+
412
+ /**
413
+ * Placeholder a controller `layout` renders where the current view goes. Emits a
414
+ * `<wabot-outlet>` host wrapping the view's server HTML; during boosted
415
+ * navigation only this element's content is swapped, so the surrounding shell
416
+ * (and its islands) keep their state.
417
+ *
418
+ * function Layout() {
419
+ * return <div class="app"><Nav /><main><Outlet /></main></div>
420
+ * }
421
+ * @uiController({ path: '/panel', app: true, layout: Layout })
422
+ */
423
+ declare function Outlet(): preact.VNode<preact.Attributes & {
424
+ 'data-wabot-outlet': string;
425
+ }>;
426
+
427
+ export { type IActionConfig, type IActionMetadata, type ICallActionOptions, type IControllerHead, type IDiscoveredIsland, type IDocumentOptions, type IIslandEntryArgs, type IPageAssets, type IPreconnect, type IPreloadLink, type IRegisterUiControllersOptions, type IRenderContext, type IRenderResult, type IRenderedIsland, ISLAND_FILE_PATTERN, ISLAND_MARKER, type IUiClientConfig, type IUiControllerConfig, type IUiControllerMetadata, type IUiMiddlewareMetadata, type IViewConfig, type IViewMetadata, type IslandComponent, type IslandMeta, IslandRegistry, Outlet, REDIRECT_MARKER, UiControllerMetadataStore, type UiRedirect, type UiRenderer, UiRendererRegistry, action, actionUrl, callAction, deserializeProps, escapeAttr, escapeHtml, getIslandMeta, isIsland, isIslandFile, isRedirect, island, redirect, registerUiControllers, renderDocument, runUiControllers, serializeProps, setIslandId, toIslandId, uiController, uiMiddleware, view };