@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.
- package/README.md +27 -0
- package/bin/skills.mjs +151 -0
- package/bin/wabot-skills.mjs +120 -0
- package/dist/build/build.js +1031 -8
- package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
- package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
- package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
- package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
- package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
- package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
- package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
- package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
- package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
- package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
- package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
- package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
- package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
- package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
- package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
- package/dist/src/addon/ui/preact/outlet.js +22 -0
- package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
- package/dist/src/core/repository/CrudRepository.js +7 -7
- package/dist/src/feature/async/computeDedupKey.js +1 -1
- package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
- package/dist/src/feature/pg/@pgExtension.js +2 -4
- package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
- package/dist/src/feature/project-runner/scanner.js +1 -1
- package/dist/src/feature/repository/@memExtension.js +1 -2
- package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
- package/dist/src/feature/ui-controller/actions.js +35 -0
- package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
- package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
- package/dist/src/feature/ui-controller/bundler/index.js +4 -0
- package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
- package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
- package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
- package/dist/src/feature/ui-controller/document/escape.js +17 -0
- package/dist/src/feature/ui-controller/document/helpers.js +13 -0
- package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
- package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
- package/dist/src/feature/ui-controller/island/island.js +40 -0
- package/dist/src/feature/ui-controller/island/serialize.js +35 -0
- package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
- package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
- package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
- package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
- package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
- package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
- package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
- package/dist/src/index.d.ts +640 -3
- package/dist/src/index.js +32 -3
- package/dist/src/testing/LlmJudge.js +93 -0
- package/dist/src/testing/MockChatAdapter.js +68 -0
- package/dist/src/testing/TestChatMemory.js +73 -0
- package/dist/src/testing/asyncHarness.js +66 -0
- package/dist/src/testing/auth.js +114 -0
- package/dist/src/testing/chatBotHarness.js +88 -0
- package/dist/src/testing/chatControllerHarness.js +94 -0
- package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
- package/dist/src/testing/fixtures.js +53 -0
- package/dist/src/testing/helpers.js +42 -0
- package/dist/src/testing/index.d.ts +818 -0
- package/dist/src/testing/index.js +14 -0
- package/dist/src/testing/repositories.js +34 -0
- package/dist/src/testing/restHarness.js +127 -0
- package/dist/src/testing/testImageBase64.js +5 -0
- package/dist/src/testing/uiHarness.js +102 -0
- package/dist/src/testing/validation.js +66 -0
- package/dist/src/ui/client.js +6 -0
- package/dist/src/ui/index.d.ts +427 -0
- package/dist/src/ui/index.js +29 -0
- package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
- package/dist/src/ui/jsx-dev-runtime.js +1 -0
- package/dist/src/ui/jsx-runtime.d.ts +1 -0
- package/dist/src/ui/jsx-runtime.js +1 -0
- package/package.json +48 -11
- package/skills/wabot-async/SKILL.md +143 -0
- package/skills/wabot-auth/SKILL.md +153 -0
- package/skills/wabot-chat/SKILL.md +140 -0
- package/skills/wabot-di-config/SKILL.md +117 -0
- package/skills/wabot-framework/SKILL.md +81 -0
- package/skills/wabot-framework/references/quickstart.md +85 -0
- package/skills/wabot-mindset/SKILL.md +159 -0
- package/skills/wabot-ops/SKILL.md +151 -0
- package/skills/wabot-persistence/SKILL.md +159 -0
- package/skills/wabot-rest-socket/SKILL.md +167 -0
- package/skills/wabot-testing/SKILL.md +214 -0
- package/skills/wabot-ui/SKILL.md +201 -0
- package/skills/wabot-validation/SKILL.md +108 -0
package/dist/build/build.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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) {
|