@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { __decorate } from 'tslib';
|
|
2
|
+
import { singleton } from '../../../core/injection/index.js';
|
|
3
|
+
|
|
4
|
+
let UiRendererRegistry = class UiRendererRegistry {
|
|
5
|
+
renderers = new Map();
|
|
6
|
+
defaultRenderer = null;
|
|
7
|
+
/** Register a renderer. The first one registered becomes the default. */
|
|
8
|
+
register(renderer) {
|
|
9
|
+
this.renderers.set(renderer.id, renderer);
|
|
10
|
+
if (!this.defaultRenderer)
|
|
11
|
+
this.defaultRenderer = renderer;
|
|
12
|
+
}
|
|
13
|
+
/** Register a renderer and make it the default. */
|
|
14
|
+
setDefault(renderer) {
|
|
15
|
+
this.renderers.set(renderer.id, renderer);
|
|
16
|
+
this.defaultRenderer = renderer;
|
|
17
|
+
}
|
|
18
|
+
has(id) {
|
|
19
|
+
return this.renderers.has(id);
|
|
20
|
+
}
|
|
21
|
+
hasDefault() {
|
|
22
|
+
return this.defaultRenderer != null;
|
|
23
|
+
}
|
|
24
|
+
get(id) {
|
|
25
|
+
if (id) {
|
|
26
|
+
const renderer = this.renderers.get(id);
|
|
27
|
+
if (!renderer) {
|
|
28
|
+
throw new Error(`UI renderer "${id}" is not registered`);
|
|
29
|
+
}
|
|
30
|
+
return renderer;
|
|
31
|
+
}
|
|
32
|
+
if (!this.defaultRenderer) {
|
|
33
|
+
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().');
|
|
34
|
+
}
|
|
35
|
+
return this.defaultRenderer;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
UiRendererRegistry = __decorate([
|
|
39
|
+
singleton()
|
|
40
|
+
], UiRendererRegistry);
|
|
41
|
+
|
|
42
|
+
export { UiRendererRegistry };
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { CustomError, errorToPlainObject } from '../../core/error/CustomError.js';
|
|
2
|
+
import '../../core/error/setupErrorHandlers.js';
|
|
3
|
+
import { container } from '../../core/injection/index.js';
|
|
4
|
+
import { Logger } from '../../core/logger/Logger.js';
|
|
5
|
+
import { validateModel } from '../../core/validation/core/validateModel.js';
|
|
6
|
+
import { ValidationMetadataStore } from '../../core/validation/metadata/ValidationMetadataStore.js';
|
|
7
|
+
import { ExpressProvider } from '../express/ExpressProvider.js';
|
|
8
|
+
import '../rest-controller/metadata/RestControllerMetadataStore.js';
|
|
9
|
+
import { json, urlencoded } from 'express';
|
|
10
|
+
import path__default from 'node:path';
|
|
11
|
+
import { EXPRESS_REQ, EXPRESS_RES } from '../rest-controller/injection-tokens.js';
|
|
12
|
+
import 'node:http';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { UiControllerMetadataStore } from './metadata/UiControllerMetadataStore.js';
|
|
15
|
+
import { UiRendererRegistry } from './renderer/UiRendererRegistry.js';
|
|
16
|
+
import { renderDocument } from './document/renderDocument.js';
|
|
17
|
+
import { isRedirect } from './document/helpers.js';
|
|
18
|
+
import { escapeHtml } from './document/escape.js';
|
|
19
|
+
import { Container } from '../../core/injection/Container.js';
|
|
20
|
+
|
|
21
|
+
function buildRequest(req) {
|
|
22
|
+
return Object.assign({}, req.body, req.query, req.params);
|
|
23
|
+
}
|
|
24
|
+
function joinRoute(...parts) {
|
|
25
|
+
return path__default.join(...parts).replaceAll('\\', '/');
|
|
26
|
+
}
|
|
27
|
+
/** Header the client nav runtime sends to request a fragment instead of a full document. */
|
|
28
|
+
const NAV_HEADER = 'X-Wabot-Nav';
|
|
29
|
+
/** Canonical form of a route path: leading slash, no trailing slash (except root). */
|
|
30
|
+
function normalizeRoutePath(routePath) {
|
|
31
|
+
return '/' + routePath.replace(/^\/+|\/+$/g, '');
|
|
32
|
+
}
|
|
33
|
+
/** Turn a controller's head hints into <link> attribute maps for renderDocument. */
|
|
34
|
+
function headLinks(head) {
|
|
35
|
+
if (!head)
|
|
36
|
+
return undefined;
|
|
37
|
+
const links = [];
|
|
38
|
+
for (const entry of head.preconnect ?? []) {
|
|
39
|
+
const { href, crossorigin } = typeof entry === 'string' ? { href: entry, crossorigin: undefined } : entry;
|
|
40
|
+
links.push({ rel: 'preconnect', href, ...(crossorigin ? { crossorigin: true } : {}) });
|
|
41
|
+
}
|
|
42
|
+
for (const p of head.preload ?? []) {
|
|
43
|
+
// Fonts are always fetched cross-origin; default crossorigin so the preload
|
|
44
|
+
// matches the real request instead of fetching the font twice.
|
|
45
|
+
const crossorigin = p.crossorigin ?? (p.as === 'font' ? true : undefined);
|
|
46
|
+
links.push({
|
|
47
|
+
rel: 'preload',
|
|
48
|
+
href: p.href,
|
|
49
|
+
as: p.as,
|
|
50
|
+
...(p.type ? { type: p.type } : {}),
|
|
51
|
+
...(crossorigin ? { crossorigin } : {}),
|
|
52
|
+
...(p.media ? { media: p.media } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return links.length ? links : undefined;
|
|
56
|
+
}
|
|
57
|
+
function registerUiControllers(controllers, options = {}) {
|
|
58
|
+
const logger = new Logger('wabot:ui');
|
|
59
|
+
const baseContainer = options.baseContainer ?? container;
|
|
60
|
+
const store = baseContainer.resolve(UiControllerMetadataStore);
|
|
61
|
+
const expressProvider = options.expressProvider ?? baseContainer.resolve(ExpressProvider);
|
|
62
|
+
const validationStore = baseContainer.resolve(ValidationMetadataStore);
|
|
63
|
+
const rendererRegistry = baseContainer.resolve(UiRendererRegistry);
|
|
64
|
+
const expressApp = expressProvider.getExpress();
|
|
65
|
+
const dev = process.env.NODE_ENV !== 'production';
|
|
66
|
+
controllers.forEach((controller) => {
|
|
67
|
+
const viewInfos = store.getControllerViewsInfo(controller);
|
|
68
|
+
// The exact view routes of this controller: the nav runtime soft-navigates
|
|
69
|
+
// only these, so a controller mounted at "/" doesn't hijack the whole origin.
|
|
70
|
+
const appRoutes = viewInfos.map((v) => normalizeRoutePath(joinRoute(v.controller.path, v.config?.path ?? '')));
|
|
71
|
+
viewInfos.forEach((view) => {
|
|
72
|
+
const route = joinRoute(view.controller.path, view.config?.path ?? '');
|
|
73
|
+
const middlewareCtors = [
|
|
74
|
+
...view.controller.middlewares,
|
|
75
|
+
...view.middlewares.map((m) => m.middlewareConstructor),
|
|
76
|
+
];
|
|
77
|
+
logger.info(`view GET ${route}`);
|
|
78
|
+
// A parameterized view under boosted nav needs an swr.version(params) so
|
|
79
|
+
// revalidation keys off the parameter; otherwise it can only revalidate by
|
|
80
|
+
// hashing a full re-render on every visit.
|
|
81
|
+
const isParameterized = route.split('/').some((seg) => seg.startsWith(':'));
|
|
82
|
+
if (view.controller.app && isParameterized && !view.config?.swr?.version) {
|
|
83
|
+
logger.warn(`view GET ${route}: parameterized app views should declare swr.version(params) ` +
|
|
84
|
+
`so boosted navigation can revalidate per parameter without re-rendering`);
|
|
85
|
+
}
|
|
86
|
+
expressApp.get(route, json(), urlencoded({ extended: true }), async (req, res) => {
|
|
87
|
+
const requestContainer = newRequestContainer(baseContainer, req, res);
|
|
88
|
+
try {
|
|
89
|
+
if (await runMiddlewares(middlewareCtors, requestContainer, req, res))
|
|
90
|
+
return;
|
|
91
|
+
const isApp = view.controller.app === true;
|
|
92
|
+
const softNav = isApp && !!req.get(NAV_HEADER);
|
|
93
|
+
// Cheap revalidation: if the view declares a `version`, answer 304
|
|
94
|
+
// without running the handler or SSR when it matches If-None-Match.
|
|
95
|
+
let versionEtag;
|
|
96
|
+
if (softNav && view.config?.swr?.version) {
|
|
97
|
+
const version = await view.config.swr.version(buildRequest(req));
|
|
98
|
+
versionEtag = `"v:${version}"`;
|
|
99
|
+
res.set('ETag', versionEtag);
|
|
100
|
+
res.set('Cache-Control', 'no-cache');
|
|
101
|
+
if (req.get('If-None-Match') === versionEtag) {
|
|
102
|
+
res.status(304).end();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const instance = requestContainer.resolve(view.controllerConstructor);
|
|
107
|
+
const args = resolveHandlerArgs(view.paramsTypes, req, validationStore);
|
|
108
|
+
const result = await instance[view.functionName].apply(instance, args);
|
|
109
|
+
if (isRedirect(result)) {
|
|
110
|
+
res.redirect(result.status, result.location);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const renderer = rendererRegistry.get();
|
|
114
|
+
// Full loads render inside the controller's layout (the persistent
|
|
115
|
+
// shell); boosted-nav fragments render just the view (the outlet body).
|
|
116
|
+
const rendered = await renderer.renderToString(result, {
|
|
117
|
+
dev,
|
|
118
|
+
layout: softNav ? undefined : view.controller.layout,
|
|
119
|
+
});
|
|
120
|
+
const assets = options.pageAssets?.(rendered.islands) ?? {};
|
|
121
|
+
const styles = [...(rendered.styles ?? []), ...(assets.styles ?? [])];
|
|
122
|
+
// Boosted navigation: send a JSON fragment (+ ETag) instead of the
|
|
123
|
+
// full document. A matching If-None-Match means nothing changed, so
|
|
124
|
+
// answer 304 and let the client keep its cached view (no re-render).
|
|
125
|
+
if (softNav) {
|
|
126
|
+
const payload = JSON.stringify({
|
|
127
|
+
html: rendered.html,
|
|
128
|
+
title: view.config?.title ?? null,
|
|
129
|
+
meta: view.config?.meta ?? null,
|
|
130
|
+
scripts: assets.scripts ?? [],
|
|
131
|
+
styles,
|
|
132
|
+
maxAge: view.config?.swr?.maxAge ?? 0,
|
|
133
|
+
});
|
|
134
|
+
const etag = versionEtag ?? `"${createHash('sha1').update(payload).digest('base64')}"`;
|
|
135
|
+
res.set('ETag', etag);
|
|
136
|
+
res.set('Cache-Control', 'no-cache');
|
|
137
|
+
if (req.get('If-None-Match') === etag) {
|
|
138
|
+
res.status(304).end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
res.status(200).type('application/json').send(payload);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const scripts = [...(assets.scripts ?? [])];
|
|
145
|
+
let headHtml = assets.headHtml;
|
|
146
|
+
if (isApp && assets.navScript) {
|
|
147
|
+
// `<` escaped so a route string can never break out of the inline script.
|
|
148
|
+
const bootstrap = `<script>window.__wabotApp=${JSON.stringify({
|
|
149
|
+
routes: appRoutes,
|
|
150
|
+
}).replace(/</g, '\\u003c')}</script>`;
|
|
151
|
+
headHtml = (headHtml ?? '') + bootstrap;
|
|
152
|
+
scripts.push(assets.navScript);
|
|
153
|
+
}
|
|
154
|
+
const html = renderDocument({
|
|
155
|
+
bodyHtml: rendered.html,
|
|
156
|
+
title: view.config?.title,
|
|
157
|
+
meta: view.config?.meta,
|
|
158
|
+
styles,
|
|
159
|
+
links: headLinks(view.controller.head),
|
|
160
|
+
scripts,
|
|
161
|
+
headHtml,
|
|
162
|
+
bodyEndHtml: assets.bodyEndHtml,
|
|
163
|
+
});
|
|
164
|
+
res.status(200).type('html').send(html);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
sendHtmlError(res, err, logger, `GET ${route}`, dev);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
requestContainer.dispose();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
store.getControllerActionsInfo(controller).forEach((action) => {
|
|
175
|
+
const route = joinRoute(action.controller.path, '_action', action.config?.path ?? action.functionName);
|
|
176
|
+
const middlewareCtors = [
|
|
177
|
+
...action.controller.middlewares,
|
|
178
|
+
...action.middlewares.map((m) => m.middlewareConstructor),
|
|
179
|
+
];
|
|
180
|
+
logger.info(`action POST ${route}`);
|
|
181
|
+
expressApp.post(route, json(), urlencoded({ extended: true }), async (req, res) => {
|
|
182
|
+
const requestContainer = newRequestContainer(baseContainer, req, res);
|
|
183
|
+
try {
|
|
184
|
+
if (await runMiddlewares(middlewareCtors, requestContainer, req, res))
|
|
185
|
+
return;
|
|
186
|
+
const instance = requestContainer.resolve(action.controllerConstructor);
|
|
187
|
+
const args = resolveHandlerArgs(action.paramsTypes, req, validationStore);
|
|
188
|
+
const result = await instance[action.functionName].apply(instance, args);
|
|
189
|
+
if (isRedirect(result)) {
|
|
190
|
+
res.redirect(result.status, result.location);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
res.status(200).json(removeCircular(result ?? null));
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
sendJsonError(res, err, logger, `POST ${route}`);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
requestContainer.dispose();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
return expressProvider;
|
|
205
|
+
}
|
|
206
|
+
function runUiControllers(controllers, options = {}) {
|
|
207
|
+
const expressProvider = registerUiControllers(controllers, options);
|
|
208
|
+
expressProvider.listen();
|
|
209
|
+
}
|
|
210
|
+
function newRequestContainer(baseContainer, req, res) {
|
|
211
|
+
const requestContainer = baseContainer.createChildContainer();
|
|
212
|
+
requestContainer.register(Container, { useValue: requestContainer });
|
|
213
|
+
requestContainer.register(EXPRESS_REQ, { useValue: req });
|
|
214
|
+
requestContainer.register(EXPRESS_RES, { useValue: res });
|
|
215
|
+
return requestContainer;
|
|
216
|
+
}
|
|
217
|
+
/** Returns true if a middleware already handled the response (caller should stop). */
|
|
218
|
+
async function runMiddlewares(middlewareCtors, requestContainer, req, res) {
|
|
219
|
+
for (const ctor of middlewareCtors) {
|
|
220
|
+
const middleware = requestContainer.resolve(ctor);
|
|
221
|
+
await middleware.handle(req, res, requestContainer);
|
|
222
|
+
if (res.headersSent)
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
function resolveHandlerArgs(paramsTypes, req, validationStore) {
|
|
228
|
+
if (!paramsTypes || paramsTypes.length === 0)
|
|
229
|
+
return [];
|
|
230
|
+
if (paramsTypes.length > 1) {
|
|
231
|
+
throw new Error('ui view/action handlers accept at most one parameter');
|
|
232
|
+
}
|
|
233
|
+
const paramType = paramsTypes[0];
|
|
234
|
+
if (typeof paramType !== 'function') {
|
|
235
|
+
throw new Error('invalid ui handler parameter type');
|
|
236
|
+
}
|
|
237
|
+
const paramInfo = validationStore.getModelValidatorsInfo(paramType);
|
|
238
|
+
const { value, error } = validateModel(buildRequest(req), paramInfo);
|
|
239
|
+
if (error) {
|
|
240
|
+
throw new CustomError({ httpCode: 400, message: error.description, info: error });
|
|
241
|
+
}
|
|
242
|
+
return [value];
|
|
243
|
+
}
|
|
244
|
+
function sendHtmlError(res, err, logger, label, dev) {
|
|
245
|
+
logger.error(`${label} failed`, err);
|
|
246
|
+
if (res.headersSent)
|
|
247
|
+
return;
|
|
248
|
+
const status = err instanceof Error ? (errorToPlainObject(err).httpCode ?? 500) : 500;
|
|
249
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
250
|
+
const detail = dev && err instanceof Error && err.stack ? `<pre>${escapeHtml(err.stack)}</pre>` : '';
|
|
251
|
+
const html = renderDocument({
|
|
252
|
+
title: `Error ${status}`,
|
|
253
|
+
bodyHtml: `<main><h1>Error ${status}</h1><p>${escapeHtml(message)}</p>${detail}</main>`,
|
|
254
|
+
});
|
|
255
|
+
res.status(status).type('html').send(html);
|
|
256
|
+
}
|
|
257
|
+
function sendJsonError(res, err, logger, label) {
|
|
258
|
+
logger.error(`${label} failed`, err);
|
|
259
|
+
if (res.headersSent)
|
|
260
|
+
return;
|
|
261
|
+
if (err instanceof Error) {
|
|
262
|
+
const { name: _name, stack, httpCode, ...info } = errorToPlainObject(err);
|
|
263
|
+
res
|
|
264
|
+
.status(httpCode ?? 500)
|
|
265
|
+
.json(removeCircular({ error: { message: err.message, stack, ...info } }));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
res.status(500).json({ error: { message: 'Unknown error' } });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function removeCircular(obj, seen = new WeakSet()) {
|
|
272
|
+
if (obj && typeof obj === 'object') {
|
|
273
|
+
if (seen.has(obj))
|
|
274
|
+
return undefined;
|
|
275
|
+
seen.add(obj);
|
|
276
|
+
const clone = Array.isArray(obj) ? [] : {};
|
|
277
|
+
for (const key in obj) {
|
|
278
|
+
clone[key] = removeCircular(obj[key], seen);
|
|
279
|
+
}
|
|
280
|
+
return clone;
|
|
281
|
+
}
|
|
282
|
+
return obj;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { registerUiControllers, runUiControllers };
|