@stone-js/use-react 0.1.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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.d.ts +1566 -0
- package/dist/index.js +1859 -0
- package/package.json +98 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1859 @@
|
|
|
1
|
+
import { isNotEmpty, isEmpty, InitializationError, isMetaClassModule, isMetaFactoryModule, isFunctionModule, isObjectLikeModule, isFunction, Logger, hasMetadata, getMetadata, isMatchedAdapter, mergeBlueprints, stoneBlueprint, classDecoratorLegacyWrapper, setMetadata, methodDecoratorLegacyWrapper, addMetadata, LIFECYCLE_HOOK_KEY, addBlueprint } from '@stone-js/core';
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { renderToString } from 'react-dom/server';
|
|
4
|
+
import { createContext, StrictMode, useContext, useState, useEffect } from 'react';
|
|
5
|
+
import { createRoot, hydrateRoot } from 'react-dom/client';
|
|
6
|
+
import { OutgoingHttpResponse, RedirectResponse, MetaStaticFileMiddleware, MetaCompressionMiddleware } from '@stone-js/http-core';
|
|
7
|
+
import { OutgoingBrowserResponse, RedirectBrowserResponse } from '@stone-js/browser-core';
|
|
8
|
+
import { Config } from '@stone-js/config';
|
|
9
|
+
import { GET, NAVIGATION_EVENT, NODE_CONSOLE_PLATFORM, Router, RouteEvent } from '@stone-js/router';
|
|
10
|
+
import { BROWSER_PLATFORM } from '@stone-js/browser-adapter';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stone DOM Attribute.
|
|
14
|
+
*/
|
|
15
|
+
const STONE_DOM_ATTR = 'data-stone-head';
|
|
16
|
+
/**
|
|
17
|
+
* Apply meta tags to the document document.head.
|
|
18
|
+
*
|
|
19
|
+
* @param document - The document object.
|
|
20
|
+
* @param meta - The meta tag descriptor.
|
|
21
|
+
*/
|
|
22
|
+
const applyMeta = (document, meta) => {
|
|
23
|
+
const metaProp = isNotEmpty(meta.property) ? `meta[property="${meta.property}"]` : null;
|
|
24
|
+
const selector = isNotEmpty(meta.name) ? `meta[name="${meta.name}"]` : metaProp;
|
|
25
|
+
if (isEmpty(selector))
|
|
26
|
+
return;
|
|
27
|
+
const existing = document.head.querySelector(`${selector}[${STONE_DOM_ATTR}]`);
|
|
28
|
+
if (isNotEmpty(existing)) {
|
|
29
|
+
if (existing.content !== meta.content) {
|
|
30
|
+
existing.content = meta.content;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const el = document.createElement('meta');
|
|
35
|
+
if (isNotEmpty(meta.name))
|
|
36
|
+
el.setAttribute('name', meta.name);
|
|
37
|
+
if (isNotEmpty(meta.property))
|
|
38
|
+
el.setAttribute('property', meta.property);
|
|
39
|
+
el.setAttribute('content', meta.content);
|
|
40
|
+
el.setAttribute(STONE_DOM_ATTR, '');
|
|
41
|
+
document.head.appendChild(el);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Apply link tags to the document document.head.
|
|
46
|
+
*
|
|
47
|
+
* @param document - The document object.
|
|
48
|
+
* @param link - The link tag descriptor.
|
|
49
|
+
*/
|
|
50
|
+
const applyLink = (document, link) => {
|
|
51
|
+
const selector = `link[rel="${link.rel}"][href="${link.href}"][${STONE_DOM_ATTR}]`;
|
|
52
|
+
const existing = document.head.querySelector(selector);
|
|
53
|
+
if (existing != null) {
|
|
54
|
+
let needsUpdate = false;
|
|
55
|
+
for (const [key, value] of Object.entries(link)) {
|
|
56
|
+
if (existing.getAttribute(key) !== value) {
|
|
57
|
+
needsUpdate = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (needsUpdate) {
|
|
62
|
+
for (const [key, value] of Object.entries(link)) {
|
|
63
|
+
existing.setAttribute(key, value);
|
|
64
|
+
}
|
|
65
|
+
existing.setAttribute(STONE_DOM_ATTR, '');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const el = document.createElement('link');
|
|
70
|
+
for (const [key, value] of Object.entries(link)) {
|
|
71
|
+
el.setAttribute(key, value);
|
|
72
|
+
}
|
|
73
|
+
el.setAttribute(STONE_DOM_ATTR, '');
|
|
74
|
+
document.head.appendChild(el);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Update attributes of an HTML element.
|
|
79
|
+
*
|
|
80
|
+
* @param el - The HTML element to update.
|
|
81
|
+
* @param attrs - The attributes to set on the element.
|
|
82
|
+
*/
|
|
83
|
+
const updateAttributes = (el, attrs) => {
|
|
84
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
85
|
+
if (typeof value === 'boolean') {
|
|
86
|
+
value ? el.setAttribute(key, '') : el.removeAttribute(key);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
el.setAttribute(key, String(value));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
el.setAttribute(STONE_DOM_ATTR, '');
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Check if an element needs attribute updates.
|
|
96
|
+
*
|
|
97
|
+
* @param el - The HTML element to check.
|
|
98
|
+
* @param attrs - The attributes to compare against the element's current attributes.
|
|
99
|
+
* @returns True if any attribute needs updating, false otherwise.
|
|
100
|
+
*/
|
|
101
|
+
const needsAttributeUpdate = (el, attrs) => {
|
|
102
|
+
return Object.entries(attrs).some(([key, value]) => {
|
|
103
|
+
const attr = el.getAttribute(key);
|
|
104
|
+
return typeof value === 'boolean' ? attr !== '' : attr !== String(value);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Apply script tags to the document.head.
|
|
109
|
+
*
|
|
110
|
+
* @param document - The document object.
|
|
111
|
+
* @param script - The script tag descriptor.
|
|
112
|
+
*/
|
|
113
|
+
const applyScript = (document, script) => {
|
|
114
|
+
const selector = `script[src="${script.src}"][${STONE_DOM_ATTR}]`;
|
|
115
|
+
const existing = document.head.querySelector(selector);
|
|
116
|
+
if (existing != null) {
|
|
117
|
+
if (needsAttributeUpdate(existing, script)) {
|
|
118
|
+
updateAttributes(existing, script);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const el = document.createElement('script');
|
|
123
|
+
updateAttributes(el, script);
|
|
124
|
+
document.head.appendChild(el);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Apply style tags to the document document.head.
|
|
129
|
+
*
|
|
130
|
+
* @param document - The document object.
|
|
131
|
+
* @param style - The style tag descriptor.
|
|
132
|
+
*/
|
|
133
|
+
const applyStyle = (document, style) => {
|
|
134
|
+
const existing = [...document.head.querySelectorAll(`style[${STONE_DOM_ATTR}]`)]
|
|
135
|
+
.find(s => s.textContent === style.content);
|
|
136
|
+
if (existing == null) {
|
|
137
|
+
const el = document.createElement('style');
|
|
138
|
+
if (isNotEmpty(style.type))
|
|
139
|
+
el.setAttribute('type', style.type);
|
|
140
|
+
if (isNotEmpty(style.media))
|
|
141
|
+
el.setAttribute('media', style.media);
|
|
142
|
+
el.textContent = style.content;
|
|
143
|
+
el.setAttribute(STONE_DOM_ATTR, '');
|
|
144
|
+
document.head.appendChild(el);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Apply the head context to the document document.head.
|
|
149
|
+
*
|
|
150
|
+
* @param document - The document object.
|
|
151
|
+
* @param context - The head context containing meta, link, script, and style descriptors.
|
|
152
|
+
*/
|
|
153
|
+
const applyHeadContextToDom = (document, context) => {
|
|
154
|
+
if (isNotEmpty(context.title) && document.title !== context.title) {
|
|
155
|
+
document.title = context.title;
|
|
156
|
+
}
|
|
157
|
+
if (isNotEmpty(context.description)) {
|
|
158
|
+
context.metas = context.metas ?? [];
|
|
159
|
+
context.metas.push({
|
|
160
|
+
name: 'description',
|
|
161
|
+
content: context.description
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
context.metas?.forEach(v => applyMeta(document, v));
|
|
165
|
+
context.links?.forEach(v => applyLink(document, v));
|
|
166
|
+
context.styles?.forEach(v => applyStyle(document, v));
|
|
167
|
+
context.scripts?.forEach(v => applyScript(document, v));
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Escape HTML special characters in a string.
|
|
171
|
+
*
|
|
172
|
+
* @param input - The input string to escape.
|
|
173
|
+
* @returns The escaped string.
|
|
174
|
+
*/
|
|
175
|
+
const escapeHtml = (input) => input
|
|
176
|
+
.replace(/&/g, '&')
|
|
177
|
+
.replace(/"/g, '"')
|
|
178
|
+
.replace(/'/g, ''')
|
|
179
|
+
.replace(/</g, '<')
|
|
180
|
+
.replace(/>/g, '>');
|
|
181
|
+
/**
|
|
182
|
+
* Escape HTML special characters in a string.
|
|
183
|
+
*
|
|
184
|
+
* @param context - The head context containing meta, link, script, and style descriptors.
|
|
185
|
+
* @param html - The HTML string to escape.
|
|
186
|
+
* @returns The escaped string.
|
|
187
|
+
*/
|
|
188
|
+
const applyHeadContextToHtmlString = (context, html) => {
|
|
189
|
+
if (isEmpty(context) || isEmpty(html))
|
|
190
|
+
return html;
|
|
191
|
+
// Replace the existing <title> tag with the new title (if provided)
|
|
192
|
+
if (isNotEmpty(context.title)) {
|
|
193
|
+
html = html.replace(/<title>.*?<\/title>/i, `<title>${escapeHtml(context.title)}</title>`);
|
|
194
|
+
}
|
|
195
|
+
// Build all additional head elements to insert into <!--app-head-->
|
|
196
|
+
const parts = [];
|
|
197
|
+
// Meta tags
|
|
198
|
+
context.metas?.forEach((meta) => {
|
|
199
|
+
const attrs = Object.entries(meta)
|
|
200
|
+
.map(([key, value]) => `${key}="${escapeHtml(value)}"`)
|
|
201
|
+
.join(' ');
|
|
202
|
+
parts.push(`<meta ${attrs}>`);
|
|
203
|
+
});
|
|
204
|
+
// Link tags
|
|
205
|
+
context.links?.forEach((link) => {
|
|
206
|
+
const attrs = Object.entries(link)
|
|
207
|
+
.map(([key, value]) => `${key}="${escapeHtml(String(value))}"`)
|
|
208
|
+
.join(' ');
|
|
209
|
+
parts.push(`<link ${attrs}>`);
|
|
210
|
+
});
|
|
211
|
+
// Script tags
|
|
212
|
+
context.scripts?.forEach((script) => {
|
|
213
|
+
const getKeyValue = (key, value) => isNotEmpty(value) ? `${key}` : '';
|
|
214
|
+
const attrs = Object.entries(script)
|
|
215
|
+
.map(([key, value]) => typeof value === 'boolean'
|
|
216
|
+
? getKeyValue(key, value)
|
|
217
|
+
: `${key}="${escapeHtml(String(value))}"`)
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(' ');
|
|
220
|
+
parts.push(`<script ${attrs}></script>`);
|
|
221
|
+
});
|
|
222
|
+
// Style tags
|
|
223
|
+
context.styles?.forEach((style) => {
|
|
224
|
+
const attrs = Object.entries(style)
|
|
225
|
+
.filter(([key]) => key !== 'content')
|
|
226
|
+
.map(([key, value]) => `${key}="${escapeHtml(String(value))}"`)
|
|
227
|
+
.join(' ');
|
|
228
|
+
const content = escapeHtml(style.content);
|
|
229
|
+
parts.push(`<style ${attrs}>${content}</style>`);
|
|
230
|
+
});
|
|
231
|
+
// Inject generated tags into the placeholder
|
|
232
|
+
const headString = parts.join('\n').concat('\n<!--app-head-->');
|
|
233
|
+
return html.replace('<!--app-head-->', headString);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Constants for the Stone SNAPSHOT
|
|
238
|
+
*/
|
|
239
|
+
const STONE_SNAPSHOT = '__STONE_SNAPSHOT__';
|
|
240
|
+
/**
|
|
241
|
+
* Constants for the Stone Page Event Outlet
|
|
242
|
+
*/
|
|
243
|
+
const STONE_PAGE_EVENT_OUTLET = 'stone:inject:react-page:outlet';
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Stone context.
|
|
247
|
+
* Usefull to pass data to the components.
|
|
248
|
+
*/
|
|
249
|
+
const StoneContext = createContext({});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Provides a scoped `StoneContext` for its children within a strict mode.
|
|
253
|
+
*
|
|
254
|
+
* - Wraps content in `React.StrictMode` for enhanced debugging.
|
|
255
|
+
* - Supplies a `StoneContext.Provider` to get access to the event, data and container.
|
|
256
|
+
*
|
|
257
|
+
* This component ensures that all child components have access to the provided
|
|
258
|
+
* context within a Stone.js application.
|
|
259
|
+
*
|
|
260
|
+
* @param options - The options to create the Stone Page.
|
|
261
|
+
* @returns The Stone Page component.
|
|
262
|
+
*/
|
|
263
|
+
const StonePage = ({ context, children }) => {
|
|
264
|
+
return (jsx(StrictMode, { children: jsx(StoneContext.Provider, { value: context, children: children }) }));
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Stone Error.
|
|
269
|
+
*/
|
|
270
|
+
const StoneError = () => {
|
|
271
|
+
return (jsxs(Fragment, { children: [jsx("h1", { children: "An error occured" }), jsx("p", { children: "Sorry, something went wrong." })] }));
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Custom error for react operations.
|
|
276
|
+
*/
|
|
277
|
+
class UseReactError extends InitializationError {
|
|
278
|
+
constructor(message, options) {
|
|
279
|
+
super(message, options);
|
|
280
|
+
this.name = 'UseReactError';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build the React application for the current route.
|
|
286
|
+
* Or for the main handler if the route is not defined.
|
|
287
|
+
*
|
|
288
|
+
* @param event - ReactIncomingEvent
|
|
289
|
+
* @param container - Service Container
|
|
290
|
+
* @param component - The component response.
|
|
291
|
+
* @param layout - The layout response.
|
|
292
|
+
* @param data - The data to pass to the component.
|
|
293
|
+
* @returns The resolved ReactNode.
|
|
294
|
+
*/
|
|
295
|
+
const buildAppComponent = async (event, container, component, layout, data, statusCode, error) => {
|
|
296
|
+
const componentElement = await buildPageComponent(event, container, component, data, statusCode, error);
|
|
297
|
+
const layoutElement = await buildLayoutComponent(container, componentElement, layout);
|
|
298
|
+
const context = { event, container, data };
|
|
299
|
+
const children = layoutElement ?? componentElement;
|
|
300
|
+
return jsx(StonePage, { context, children });
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Get response layout in the current route for mutli pages application.
|
|
304
|
+
* Or get it from the blueprint configuration for single page application.
|
|
305
|
+
* Or get the default layout defined by the user.
|
|
306
|
+
* If not defined, return undefined.
|
|
307
|
+
*
|
|
308
|
+
* @param container - Service Container.
|
|
309
|
+
* @param children - The children to render.
|
|
310
|
+
* @param layoutName - The layout name.
|
|
311
|
+
* @returns The resolved layout element.
|
|
312
|
+
*/
|
|
313
|
+
const buildLayoutComponent = async (container, children, layoutName) => {
|
|
314
|
+
const metavalue = container
|
|
315
|
+
.make('blueprint')
|
|
316
|
+
.get(`stone.useReact.layout.${String(layoutName)}`);
|
|
317
|
+
const handler = await resolveComponent(container, metavalue);
|
|
318
|
+
const componentType = handler?.render.bind(handler);
|
|
319
|
+
if (componentType !== undefined) {
|
|
320
|
+
return jsx(componentType, { container, children, 'data-layout': layoutName });
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Get response component in the current route.
|
|
325
|
+
* If not defined, return an empty object.
|
|
326
|
+
*
|
|
327
|
+
* @param event - ReactIncomingEvent
|
|
328
|
+
* @param container - Service Container
|
|
329
|
+
* @param component - The component response.
|
|
330
|
+
* @param data - The data to pass to the component.
|
|
331
|
+
* @param statusCode - The status code of the error.
|
|
332
|
+
* @param error - The error object.
|
|
333
|
+
* @returns The resolved component element.
|
|
334
|
+
*/
|
|
335
|
+
const buildPageComponent = (event, container, component, data, statusCode, error) => {
|
|
336
|
+
if (component !== undefined) {
|
|
337
|
+
return jsx(component, { event, container, data, statusCode, error });
|
|
338
|
+
}
|
|
339
|
+
return jsx('div', {});
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* Get adapter error component.
|
|
343
|
+
*
|
|
344
|
+
* This error handler is different from the kernel error handler.
|
|
345
|
+
* Because there is no container at adapter level.
|
|
346
|
+
*
|
|
347
|
+
* @param blueprint - The blueprint.
|
|
348
|
+
* @param context - The context of the adapter.
|
|
349
|
+
* @param statusCode - The status code of the error.
|
|
350
|
+
* @param error - The error object.
|
|
351
|
+
* @returns The resolved layout element.
|
|
352
|
+
*/
|
|
353
|
+
const buildAdapterErrorComponent = async (blueprint, context, statusCode, error) => {
|
|
354
|
+
const handlerMeta = blueprint.get(`stone.useReact.adapterErrorPages.${String(error?.name ?? 'default')}`);
|
|
355
|
+
const handlerMetavalue = await resolveLazyComponent(handlerMeta);
|
|
356
|
+
const layoutMetavalue = await resolveLazyComponent(blueprint.get(`stone.useReact.layout.${String(handlerMeta?.layout)}`));
|
|
357
|
+
let layoutHandler;
|
|
358
|
+
let handler;
|
|
359
|
+
if (isMetaClassModule(handlerMetavalue)) {
|
|
360
|
+
handler = new handlerMetavalue.module.prototype.constructor({ blueprint });
|
|
361
|
+
}
|
|
362
|
+
else if (isMetaFactoryModule(handlerMetavalue)) {
|
|
363
|
+
handler = handlerMetavalue.module({ blueprint });
|
|
364
|
+
}
|
|
365
|
+
if (isMetaClassModule(layoutMetavalue)) {
|
|
366
|
+
layoutHandler = new layoutMetavalue.module.prototype.constructor({ blueprint });
|
|
367
|
+
}
|
|
368
|
+
else if (isMetaFactoryModule(layoutMetavalue)) {
|
|
369
|
+
layoutHandler = layoutMetavalue.module({ blueprint });
|
|
370
|
+
}
|
|
371
|
+
await handler?.handle?.(error, context);
|
|
372
|
+
const componentType = handler?.render.bind(handler);
|
|
373
|
+
const layoutType = layoutHandler?.render.bind(layoutHandler);
|
|
374
|
+
if (componentType !== undefined && layoutType !== undefined) {
|
|
375
|
+
const children = jsx(componentType, { blueprint, error, statusCode });
|
|
376
|
+
return jsx(layoutType, { blueprint, children });
|
|
377
|
+
}
|
|
378
|
+
else if (componentType !== undefined) {
|
|
379
|
+
return jsx(componentType, { blueprint, error, statusCode });
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
return jsx(StoneError, { blueprint, error, statusCode });
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Resolve the event handler for the component.
|
|
387
|
+
*
|
|
388
|
+
* Can also resolve dynamically loaded components.
|
|
389
|
+
*
|
|
390
|
+
* @param container - The service container.
|
|
391
|
+
* @param metaComponent - The meta component event handler.
|
|
392
|
+
* @returns The resolved element type.
|
|
393
|
+
*/
|
|
394
|
+
const resolveComponent = async (container, metaComponent) => {
|
|
395
|
+
metaComponent = await resolveLazyComponent(metaComponent);
|
|
396
|
+
if (isMetaClassModule(metaComponent)) {
|
|
397
|
+
return container.resolve(metaComponent.module);
|
|
398
|
+
}
|
|
399
|
+
else if (isMetaFactoryModule(metaComponent)) {
|
|
400
|
+
return metaComponent.module(container);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Resolve lazy loaded components.
|
|
405
|
+
*
|
|
406
|
+
* @param metaComponent - The meta component event handler.
|
|
407
|
+
* @returns The resolved element type.
|
|
408
|
+
*/
|
|
409
|
+
const resolveLazyComponent = async (metaComponent) => {
|
|
410
|
+
if (metaComponent?.lazy === true &&
|
|
411
|
+
isFunctionModule(metaComponent?.module)) {
|
|
412
|
+
metaComponent.lazy = false;
|
|
413
|
+
metaComponent.module = await metaComponent.module();
|
|
414
|
+
}
|
|
415
|
+
return metaComponent;
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* Get the root element to render the React components.
|
|
419
|
+
*
|
|
420
|
+
* @param blueprint - The blueprint.
|
|
421
|
+
* @returns The root element to render the React components.
|
|
422
|
+
* @throws {UseReactError} If the root container is not found.
|
|
423
|
+
*/
|
|
424
|
+
const getAppRootElement = (blueprint) => {
|
|
425
|
+
const rootElementId = blueprint.get('stone.useReact.rootElementId', 'root');
|
|
426
|
+
const appContainer = document.getElementById(rootElementId) ?? undefined;
|
|
427
|
+
if (appContainer === undefined) {
|
|
428
|
+
throw new UseReactError('Root container is required to render React components.');
|
|
429
|
+
}
|
|
430
|
+
return appContainer;
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Renders the React app.
|
|
434
|
+
*
|
|
435
|
+
* @param app - The React app to render.
|
|
436
|
+
* @param blueprint - The blueprint.
|
|
437
|
+
* @returns The React root instance.
|
|
438
|
+
*/
|
|
439
|
+
const renderReactApp = (app, blueprint) => {
|
|
440
|
+
const reactRoot = blueprint.get('stone.useReact.reactRoot') ??
|
|
441
|
+
createRoot(getAppRootElement(blueprint));
|
|
442
|
+
reactRoot.render(app);
|
|
443
|
+
blueprint.setIf('stone.useReact.reactRoot', reactRoot);
|
|
444
|
+
return reactRoot;
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Hydrates the React app when SSR is enabled.
|
|
448
|
+
*
|
|
449
|
+
* @param app - The React app to hydrate.
|
|
450
|
+
* @param blueprint - The blueprint.
|
|
451
|
+
* @returns The React root instance.
|
|
452
|
+
*/
|
|
453
|
+
const hydrateReactApp = (app, blueprint) => {
|
|
454
|
+
const reactRoot = hydrateRoot(getAppRootElement(blueprint), app);
|
|
455
|
+
blueprint.setIf('stone.useReact.reactRoot', reactRoot);
|
|
456
|
+
return reactRoot;
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* Check if the current environment is the server.
|
|
460
|
+
*
|
|
461
|
+
* @returns True if the current environment is the server.
|
|
462
|
+
*/
|
|
463
|
+
const isServer = () => typeof window === 'undefined';
|
|
464
|
+
/**
|
|
465
|
+
* Check if the current environment is the client.
|
|
466
|
+
*
|
|
467
|
+
* @returns True if the current environment is the client.
|
|
468
|
+
*/
|
|
469
|
+
const isClient = () => !isServer();
|
|
470
|
+
/**
|
|
471
|
+
* Get the HTML template for the React application.
|
|
472
|
+
*
|
|
473
|
+
* @param blueprint - The blueprint.
|
|
474
|
+
* @returns The HTML template.
|
|
475
|
+
*/
|
|
476
|
+
const htmlTemplate = (blueprint) => {
|
|
477
|
+
const content = blueprint.get('stone.useReact.htmlTemplateContent');
|
|
478
|
+
if (isNotEmpty(content)) {
|
|
479
|
+
return content;
|
|
480
|
+
}
|
|
481
|
+
throw new UseReactError('HTML template content is required for server-side rendering. Please provide the `htmlTemplateContent` in the blueprint configuration.');
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
484
|
+
* Determine if the application is running on the server side.
|
|
485
|
+
*
|
|
486
|
+
* @returns True if the application is running on the server side, false otherwise.
|
|
487
|
+
*/
|
|
488
|
+
function isSSR() {
|
|
489
|
+
return import.meta.env.SSR || typeof window === 'undefined';
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Execute the handler.
|
|
493
|
+
*
|
|
494
|
+
* This method will try to get data from the snapshot
|
|
495
|
+
* If the snapshot is not present, it will execute the handler.
|
|
496
|
+
* If the handler is not present, it will return undefined.
|
|
497
|
+
*
|
|
498
|
+
* @param response - The response object.
|
|
499
|
+
* @returns The data from the response.
|
|
500
|
+
*/
|
|
501
|
+
async function executeHandler(event, response, snapshot, handler, error) {
|
|
502
|
+
let result = snapshot;
|
|
503
|
+
if (!snapshot.ssr) {
|
|
504
|
+
if (isNotEmpty(error) && isObjectLikeModule(handler)) {
|
|
505
|
+
result = await handler.handle?.(error, event);
|
|
506
|
+
}
|
|
507
|
+
else if (isObjectLikeModule(handler)) {
|
|
508
|
+
result = await handler.handle?.(event);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
result = undefined;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (isNotEmpty(result?.statusCode)) {
|
|
515
|
+
response.setStatus(result.statusCode);
|
|
516
|
+
}
|
|
517
|
+
if (isNotEmpty(result?.headers) &&
|
|
518
|
+
isNotEmpty(response) &&
|
|
519
|
+
isFunction(response.setHeaders)) {
|
|
520
|
+
response.setHeaders(result.headers);
|
|
521
|
+
}
|
|
522
|
+
return result?.content ?? result?.data ?? result;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Keep track of the current layout.
|
|
526
|
+
* This is used to determine if the layout has changed.
|
|
527
|
+
* We make a full render each time the layout changes.
|
|
528
|
+
*
|
|
529
|
+
* @returns The current layout.
|
|
530
|
+
*/
|
|
531
|
+
let currentLayout;
|
|
532
|
+
/**
|
|
533
|
+
* Get the browser content.
|
|
534
|
+
*
|
|
535
|
+
* @param app - The app component to render.
|
|
536
|
+
* @param component - The component to render.
|
|
537
|
+
* @param layout - The layout to use.
|
|
538
|
+
* @param snapshot - The response snapshot.
|
|
539
|
+
* @param head - The head context.
|
|
540
|
+
* @returns The browser response content.
|
|
541
|
+
*/
|
|
542
|
+
function getBrowserContent(app, component, layout, snapshot, head) {
|
|
543
|
+
const content = { head, app, component, fullRender: currentLayout !== layout, ssr: snapshot.ssr };
|
|
544
|
+
currentLayout = layout;
|
|
545
|
+
return content;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Get the server content.
|
|
549
|
+
*
|
|
550
|
+
* @param component - The React component to hydrate.
|
|
551
|
+
* @param data - The data to pass to the components.
|
|
552
|
+
* @param container - The service container.
|
|
553
|
+
* @param event - The incoming browser event.
|
|
554
|
+
* @param head - The head context.
|
|
555
|
+
* @returns A promise that resolves when the content is hydrated.
|
|
556
|
+
*/
|
|
557
|
+
async function getServerContent(component, data, container, event, head) {
|
|
558
|
+
const html = renderToString(component).concat('\n<!--app-html-->');
|
|
559
|
+
const template = await htmlTemplate(container.make('blueprint'));
|
|
560
|
+
const snapshot = snapshotResponse(event, container, data).concat('\n<!--app-head-->');
|
|
561
|
+
return applyHeadContextToHtmlString(head ?? {}, template)
|
|
562
|
+
.replace('<!--app-html-->', html)
|
|
563
|
+
.replace('<!--app-head-->', snapshot);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get the response snapshot.
|
|
567
|
+
*
|
|
568
|
+
* @param event - The incoming browser event.
|
|
569
|
+
* @returns The response snapshot.
|
|
570
|
+
*/
|
|
571
|
+
function getResponseSnapshot(event, container) {
|
|
572
|
+
return container.make('snapshot').get(event.fingerprint(), { ssr: false });
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Snapshot the response data.
|
|
576
|
+
*
|
|
577
|
+
* @param event - The incoming HTTP event.
|
|
578
|
+
* @param data - The data to snapshot.
|
|
579
|
+
*/
|
|
580
|
+
function snapshotResponse(event, container, data) {
|
|
581
|
+
const snapshot = container.make('snapshot');
|
|
582
|
+
return renderStoneSnapshot(snapshot.add(event.fingerprint(), { ...data, ssr: true }).toJson());
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Render Stone snapshot.
|
|
586
|
+
*
|
|
587
|
+
* @param snapshot - The snapshot to render.
|
|
588
|
+
* @returns The script tag.
|
|
589
|
+
*/
|
|
590
|
+
function renderStoneSnapshot(snapshot) {
|
|
591
|
+
return `<script id="${STONE_SNAPSHOT}" type="application/json">${snapshot}</script>`;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Execute hooks.
|
|
595
|
+
*
|
|
596
|
+
* @param name - The name of the hook.
|
|
597
|
+
* @param context - The context of the adapter.
|
|
598
|
+
*/
|
|
599
|
+
async function executeHooks(name, context) {
|
|
600
|
+
const hooks = context.container.make('blueprint').get('stone.lifecycleHooks', {});
|
|
601
|
+
if (Array.isArray(hooks[name])) {
|
|
602
|
+
for (const listener of hooks[name]) {
|
|
603
|
+
await listener(context);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Class representing a ReactRuntime.
|
|
610
|
+
*
|
|
611
|
+
* This class is responsible for handling the React runtime environment,
|
|
612
|
+
* including create snapshots and managing errors.
|
|
613
|
+
*/
|
|
614
|
+
class ReactRuntime {
|
|
615
|
+
_snapshot;
|
|
616
|
+
container;
|
|
617
|
+
blueprint;
|
|
618
|
+
/**
|
|
619
|
+
* The ReactRuntime instance.
|
|
620
|
+
*
|
|
621
|
+
* @type {ReactRuntime}
|
|
622
|
+
*/
|
|
623
|
+
static instance;
|
|
624
|
+
/**
|
|
625
|
+
* Create a ReactRuntime.
|
|
626
|
+
*
|
|
627
|
+
* @param options - ReactRuntime options.
|
|
628
|
+
*/
|
|
629
|
+
constructor({ container, blueprint, snapshot }) {
|
|
630
|
+
this._snapshot = snapshot;
|
|
631
|
+
this.container = container;
|
|
632
|
+
this.blueprint = blueprint;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Create a snapshot.
|
|
636
|
+
*
|
|
637
|
+
* This method will create a snapshot of the current event.
|
|
638
|
+
* If the environment is server, it will create a snapshot.
|
|
639
|
+
* If the environment is client, it will return the snapshot.
|
|
640
|
+
*
|
|
641
|
+
* @param key - The key to store the snapshot.
|
|
642
|
+
* @param handler - The handler to create the snapshot.
|
|
643
|
+
* @returns The snapshot value.
|
|
644
|
+
*/
|
|
645
|
+
async snapshot(key, handler) {
|
|
646
|
+
const event = this.container.make('event');
|
|
647
|
+
const snapshotKey = `${event.fingerprint()}.${key}`;
|
|
648
|
+
if (isServer()) {
|
|
649
|
+
const value = await handler(this.container);
|
|
650
|
+
this._snapshot.set(snapshotKey, value);
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
return this._snapshot.get(snapshotKey) ?? await handler(this.container);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Set html head tags.
|
|
659
|
+
*
|
|
660
|
+
* This method will set the head elements in the document.
|
|
661
|
+
*
|
|
662
|
+
* @param value - The head context to set.
|
|
663
|
+
*/
|
|
664
|
+
head(value) {
|
|
665
|
+
applyHeadContextToDom(document, value);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Throw an error.
|
|
669
|
+
*
|
|
670
|
+
* This method will handle the error and render the error component.
|
|
671
|
+
* If no error handler is found, the error will be thrown.
|
|
672
|
+
*
|
|
673
|
+
* @param error - The error to throw.
|
|
674
|
+
* @param statusCode - The status code to return.
|
|
675
|
+
* @returns void
|
|
676
|
+
* @throws error
|
|
677
|
+
*/
|
|
678
|
+
async throwError(error, statusCode = 500) {
|
|
679
|
+
const metavalue = this.blueprint.get(`stone.useReact.errorPages.${String(error.name)}`, this.blueprint.get('stone.useReact.errorPages.default', {}));
|
|
680
|
+
if (isEmpty(metavalue)) {
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
await this.renderErrorComponent(error, metavalue, statusCode);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Render an error component.
|
|
687
|
+
*
|
|
688
|
+
* This method will render the error component.
|
|
689
|
+
*
|
|
690
|
+
* @param error - The error to render.
|
|
691
|
+
* @param metavalue - The meta value to render.
|
|
692
|
+
* @param statusCode - The status code to return.
|
|
693
|
+
* @returns void
|
|
694
|
+
*/
|
|
695
|
+
async renderErrorComponent(error, metavalue, statusCode = 500) {
|
|
696
|
+
let data;
|
|
697
|
+
const event = this.container.make('event');
|
|
698
|
+
const handler = await resolveComponent(this.container, { ...metavalue, error });
|
|
699
|
+
if (isObjectLikeModule(handler)) {
|
|
700
|
+
const response = await handler.handle?.(error, event);
|
|
701
|
+
data = response?.content ?? response;
|
|
702
|
+
statusCode = response?.statusCode ?? statusCode;
|
|
703
|
+
}
|
|
704
|
+
const componentType = handler?.render.bind(handler);
|
|
705
|
+
const appComponent = await buildAppComponent(event, this.container, componentType, metavalue.layout, data, statusCode, error);
|
|
706
|
+
// Render the component
|
|
707
|
+
renderReactApp(appComponent, this.blueprint);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* MetaReactRuntime
|
|
712
|
+
*/
|
|
713
|
+
const MetaReactRuntime = { module: ReactRuntime, isClass: true, alias: 'reactRuntime', singleton: true };
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Class representing an UseReactBrowserErrorHandler.
|
|
717
|
+
*
|
|
718
|
+
* Adapter level error handler for React applications.
|
|
719
|
+
*/
|
|
720
|
+
class UseReactBrowserErrorHandler {
|
|
721
|
+
logger;
|
|
722
|
+
blueprint;
|
|
723
|
+
/**
|
|
724
|
+
* Create an UseReactBrowserErrorHandler.
|
|
725
|
+
*
|
|
726
|
+
* @param options - UseReactBrowserErrorHandler options.
|
|
727
|
+
*/
|
|
728
|
+
constructor({ blueprint }) {
|
|
729
|
+
this.blueprint = blueprint;
|
|
730
|
+
this.logger = Logger.getInstance();
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Handle an error.
|
|
734
|
+
*
|
|
735
|
+
* @param error - The error to handle.
|
|
736
|
+
* @param context - The context of the adapter.
|
|
737
|
+
* @returns The raw response.
|
|
738
|
+
*/
|
|
739
|
+
async handle(error, context) {
|
|
740
|
+
this.logger.error(error.message, { error });
|
|
741
|
+
return context
|
|
742
|
+
.rawResponseBuilder
|
|
743
|
+
.add('render', async () => await this.renderError(error, context));
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get the error body.
|
|
747
|
+
*
|
|
748
|
+
* @param error - The error to handle.
|
|
749
|
+
* @returns The error body.
|
|
750
|
+
*/
|
|
751
|
+
async renderError(error, context) {
|
|
752
|
+
const app = await buildAdapterErrorComponent(this.blueprint, context, error.statusCode ?? 500, error);
|
|
753
|
+
// Render the component
|
|
754
|
+
renderReactApp(app, this.blueprint);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* A useReact event handler for processing incoming events
|
|
760
|
+
* For single event handler.
|
|
761
|
+
*
|
|
762
|
+
* Multiple event handlers will be processed by the router.
|
|
763
|
+
*
|
|
764
|
+
* @template IncomingEventType - The type representing the incoming event.
|
|
765
|
+
* @template OutgoingResponseType - The type representing the outgoing response.
|
|
766
|
+
*/
|
|
767
|
+
class UseReactEventHandler {
|
|
768
|
+
blueprint;
|
|
769
|
+
/**
|
|
770
|
+
* Constructs a `UseReactEventHandler` instance.
|
|
771
|
+
*
|
|
772
|
+
* @param options - The UseReactEventHandler options including blueprint.
|
|
773
|
+
*/
|
|
774
|
+
constructor({ blueprint }) {
|
|
775
|
+
this.blueprint = blueprint;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Handle an incoming event.
|
|
779
|
+
*
|
|
780
|
+
* @returns The outgoing response.
|
|
781
|
+
*/
|
|
782
|
+
handle() {
|
|
783
|
+
return this.getComponentEventHandler();
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Get the component event handler.
|
|
787
|
+
*
|
|
788
|
+
* @returns The component event handler.
|
|
789
|
+
* @throws {UseReactError} If the component event handler is missing.
|
|
790
|
+
*/
|
|
791
|
+
getComponentEventHandler() {
|
|
792
|
+
const handler = this.blueprint.get('stone.useReact.componentEventHandler');
|
|
793
|
+
if (isEmpty(handler)) {
|
|
794
|
+
throw new UseReactError('The component event handler is missing.');
|
|
795
|
+
}
|
|
796
|
+
return handler;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Prepare the page to render.
|
|
802
|
+
*
|
|
803
|
+
* Here we prepare the page to render by resolving
|
|
804
|
+
* the handler, handler the event, and rendering the component.
|
|
805
|
+
*
|
|
806
|
+
* @param event - The incoming HTTP event.
|
|
807
|
+
* @param response - The outgoing HTTP response.
|
|
808
|
+
* @param container - The service container.
|
|
809
|
+
* @param snapshot - The response snapshot.
|
|
810
|
+
*/
|
|
811
|
+
async function preparePage(event, response, container, snapshot) {
|
|
812
|
+
const { layout = 'default' } = response.content;
|
|
813
|
+
const page = await resolveComponent(container, response.content);
|
|
814
|
+
const data = await executeHandler(event, response, snapshot, page);
|
|
815
|
+
const componentType = page?.render.bind(page);
|
|
816
|
+
const head = await page?.head?.({ event, data, statusCode: response.statusCode });
|
|
817
|
+
await executeHooks('onPreparingPage', { event, response, container, snapshot, data, componentType, head });
|
|
818
|
+
const snapshotData = { data, layout, statusCode: response.statusCode };
|
|
819
|
+
const component = await buildPageComponent(event, container, componentType, data, response.statusCode);
|
|
820
|
+
const appComponent = await buildAppComponent(event, container, componentType, layout, data, response.statusCode);
|
|
821
|
+
response.setContent(isSSR()
|
|
822
|
+
? await getServerContent(appComponent, snapshotData, container, event, head)
|
|
823
|
+
: getBrowserContent(appComponent, component, layout, snapshot, head));
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Prepare the error page to render.
|
|
827
|
+
*
|
|
828
|
+
* Error pages are prepared sepatately because their handler
|
|
829
|
+
* is different from the normal page handler.
|
|
830
|
+
* Their handler takes an error as the first argument and the event as the second.
|
|
831
|
+
*
|
|
832
|
+
* @param event - The incoming HTTP event.
|
|
833
|
+
* @param response - The outgoing HTTP response.
|
|
834
|
+
* @param container - The service container.
|
|
835
|
+
* @param snapshot - The response snapshot.
|
|
836
|
+
*/
|
|
837
|
+
async function prepareErrorPage(event, response, container, snapshot) {
|
|
838
|
+
const { error = {}, layout } = response.content;
|
|
839
|
+
const errorPage = await resolveComponent(container, response.content);
|
|
840
|
+
const data = await executeHandler(event, response, snapshot, errorPage, error);
|
|
841
|
+
const componentType = errorPage?.render.bind(errorPage) ?? StoneError;
|
|
842
|
+
const head = await errorPage?.head?.({ event, data, statusCode: response.statusCode, error });
|
|
843
|
+
await executeHooks('onPreparingPage', { event, response, container, snapshot, data, componentType, head, error });
|
|
844
|
+
const snapshotData = { data, layout, statusCode: response.statusCode, error: { name: error.name } };
|
|
845
|
+
const component = await buildPageComponent(event, container, componentType, data, response.statusCode, error);
|
|
846
|
+
const appComponent = await buildAppComponent(event, container, componentType, layout, data, response.statusCode, error);
|
|
847
|
+
response.setContent(isSSR()
|
|
848
|
+
? await getServerContent(appComponent, snapshotData, container, event, head)
|
|
849
|
+
: getBrowserContent(appComponent, component, layout, snapshot, head));
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Prepare the fallback error page to render.
|
|
853
|
+
*
|
|
854
|
+
* We prepare a fallback error page if no event nor error handler is provided.
|
|
855
|
+
*
|
|
856
|
+
* @param event - The incoming event.
|
|
857
|
+
* @param response - The outgoing response.
|
|
858
|
+
* @param container - The service container.
|
|
859
|
+
* @param snapshot - The response snapshot.
|
|
860
|
+
*/
|
|
861
|
+
async function prepareFallbackErrorPage(event, response, container, snapshot) {
|
|
862
|
+
const { layout, error, statusCode = 500 } = snapshot;
|
|
863
|
+
const blueprint = container.make('blueprint');
|
|
864
|
+
const metavalue = blueprint.get(`stone.useReact.errorPages.${String(error?.name)}`, blueprint.get('stone.useReact.errorPages.default', {}));
|
|
865
|
+
const content = { ...metavalue, layout };
|
|
866
|
+
content.error = error ?? (response.content instanceof Error ? response.content : new Error('An error occurred.'));
|
|
867
|
+
response
|
|
868
|
+
.setContent(content)
|
|
869
|
+
.setStatus(statusCode);
|
|
870
|
+
await prepareErrorPage(event, response, container, snapshot);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Hook that runs just before preparing the response.
|
|
875
|
+
*
|
|
876
|
+
* @param context - The context of the hook.
|
|
877
|
+
*/
|
|
878
|
+
async function onPreparingResponse({ event, response, container }) {
|
|
879
|
+
const snapshot = getResponseSnapshot(event, container);
|
|
880
|
+
if (isNotEmpty(snapshot.error)) {
|
|
881
|
+
await prepareFallbackErrorPage(event, response, container, snapshot);
|
|
882
|
+
}
|
|
883
|
+
else if (response.isError()) {
|
|
884
|
+
await prepareErrorPage(event, response, container, snapshot);
|
|
885
|
+
}
|
|
886
|
+
else if (isFunction(response.content?.module)) {
|
|
887
|
+
await preparePage(event, response, container, snapshot);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Class representing an UseReactUseReactKernelErrorHandler.
|
|
893
|
+
*
|
|
894
|
+
* Kernel level error handler for React applications.
|
|
895
|
+
*/
|
|
896
|
+
class UseReactKernelErrorHandler {
|
|
897
|
+
blueprint;
|
|
898
|
+
/**
|
|
899
|
+
* Create an UseReactUseReactKernelErrorHandler.
|
|
900
|
+
*
|
|
901
|
+
* @param options - UseReactUseReactKernelErrorHandler options.
|
|
902
|
+
*/
|
|
903
|
+
constructor({ blueprint }) {
|
|
904
|
+
this.blueprint = blueprint;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Handle an error.
|
|
908
|
+
*
|
|
909
|
+
* @param error - The error to handle.
|
|
910
|
+
* @returns The outgoing http response.
|
|
911
|
+
*/
|
|
912
|
+
handle(error) {
|
|
913
|
+
const metavalue = this.blueprint.get(`stone.useReact.errorPages.${String(error?.name)}`) ??
|
|
914
|
+
this.blueprint.get('stone.useReact.errorPages.default', {});
|
|
915
|
+
return { content: { ...metavalue, error }, statusCode: error?.statusCode ?? 500 };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Create an UseReact response.
|
|
921
|
+
*
|
|
922
|
+
* @param options - The options for creating the response.
|
|
923
|
+
* @returns The React response.
|
|
924
|
+
*/
|
|
925
|
+
const reactResponse = async (options) => {
|
|
926
|
+
if (isNotEmpty(options) &&
|
|
927
|
+
(isNotEmpty(options.url) ||
|
|
928
|
+
(isNotEmpty(options.content) && isNotEmpty(options.content.redirect)))) {
|
|
929
|
+
return await reactRedirectResponse(options);
|
|
930
|
+
}
|
|
931
|
+
return import.meta.env.SSR
|
|
932
|
+
? OutgoingHttpResponse.create(options)
|
|
933
|
+
: OutgoingBrowserResponse.create(options);
|
|
934
|
+
};
|
|
935
|
+
/**
|
|
936
|
+
* Create an UseReact redirect response.
|
|
937
|
+
*
|
|
938
|
+
* @param options - The options for creating the response.
|
|
939
|
+
* @returns The React redirect response.
|
|
940
|
+
*/
|
|
941
|
+
const reactRedirectResponse = async (options) => {
|
|
942
|
+
return import.meta.env.SSR
|
|
943
|
+
? RedirectResponse.create({ statusCode: 302, ...options })
|
|
944
|
+
: RedirectBrowserResponse.create({ statusCode: 302, ...options });
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Class representing an UseReactServerErrorHandler.
|
|
949
|
+
*
|
|
950
|
+
* Adapter level error handler for React applications.
|
|
951
|
+
*/
|
|
952
|
+
class UseReactServerErrorHandler {
|
|
953
|
+
logger;
|
|
954
|
+
blueprint;
|
|
955
|
+
/**
|
|
956
|
+
* Create an UseReactServerErrorHandler.
|
|
957
|
+
*
|
|
958
|
+
* @param options - UseReactServerErrorHandler options.
|
|
959
|
+
*/
|
|
960
|
+
constructor({ blueprint }) {
|
|
961
|
+
this.blueprint = blueprint;
|
|
962
|
+
this.logger = Logger.getInstance();
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Handle an error.
|
|
966
|
+
*
|
|
967
|
+
* @param error - The error to handle.
|
|
968
|
+
* @param context - The context of the adapter.
|
|
969
|
+
* @returns The raw response.
|
|
970
|
+
*/
|
|
971
|
+
async handle(error, context) {
|
|
972
|
+
this.logger.error(error.message, { error });
|
|
973
|
+
return context
|
|
974
|
+
.rawResponseBuilder
|
|
975
|
+
.add('statusCode', error.statusCode ?? 500)
|
|
976
|
+
.add('headers', new Headers({ 'Content-Type': 'text/html' }))
|
|
977
|
+
.add('body', await this.getErrorBody(error, context));
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Get the error body.
|
|
981
|
+
*
|
|
982
|
+
* @param error - The error to handle.
|
|
983
|
+
* @returns The error body.
|
|
984
|
+
*/
|
|
985
|
+
async getErrorBody(error, context) {
|
|
986
|
+
const statusCode = error.statusCode ?? 500;
|
|
987
|
+
const template = await htmlTemplate(this.blueprint);
|
|
988
|
+
const ClientApp = await buildAdapterErrorComponent(this.blueprint, context, statusCode, error);
|
|
989
|
+
return template.replace('<!--app-html-->', renderToString(ClientApp));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Use React Service Provider.
|
|
995
|
+
*/
|
|
996
|
+
class UseReactServiceProvider {
|
|
997
|
+
container;
|
|
998
|
+
/**
|
|
999
|
+
* Constructs a new `UseReactServiceProvider` instance.
|
|
1000
|
+
*
|
|
1001
|
+
* @param container - The container to register services in.
|
|
1002
|
+
*/
|
|
1003
|
+
constructor(container) {
|
|
1004
|
+
this.container = container;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Register method for the service provider.
|
|
1008
|
+
*/
|
|
1009
|
+
register() {
|
|
1010
|
+
this.registerSnapshot();
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Boot method for the service provider.
|
|
1014
|
+
*/
|
|
1015
|
+
boot() {
|
|
1016
|
+
ReactRuntime.instance = this.container.make(ReactRuntime);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Register the snapshot.
|
|
1020
|
+
*
|
|
1021
|
+
* We save the snapshot on server side rendering and
|
|
1022
|
+
* we use it to hydrate the application on the client side.
|
|
1023
|
+
*/
|
|
1024
|
+
registerSnapshot() {
|
|
1025
|
+
const textContent = isSSR() ? '{}' : (window.document.getElementById(STONE_SNAPSHOT)?.textContent ?? '{}');
|
|
1026
|
+
this.container.singletonIf('snapshot', () => Config.fromJson(textContent));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* MetaUseReactServiceProvider
|
|
1031
|
+
*/
|
|
1032
|
+
const MetaUseReactServiceProvider = { module: UseReactServiceProvider, isClass: true };
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Utility function to define an adapter error page.
|
|
1036
|
+
*
|
|
1037
|
+
* @param module - The adapter error page module.
|
|
1038
|
+
* @param options - Optional adapter error page options.
|
|
1039
|
+
* @returns The UseReactBlueprint.
|
|
1040
|
+
*/
|
|
1041
|
+
function defineAdapterErrorPage(module, options) {
|
|
1042
|
+
const error = options?.error ?? 'default';
|
|
1043
|
+
const adapterErrorPages = Object.fromEntries([error].flat().map((err) => [
|
|
1044
|
+
err,
|
|
1045
|
+
{
|
|
1046
|
+
...options,
|
|
1047
|
+
module,
|
|
1048
|
+
error: err,
|
|
1049
|
+
isFactory: options?.isClass !== true
|
|
1050
|
+
}
|
|
1051
|
+
]));
|
|
1052
|
+
return {
|
|
1053
|
+
stone: {
|
|
1054
|
+
useReact: {
|
|
1055
|
+
adapterErrorPages
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Utility function to define a page.
|
|
1063
|
+
*
|
|
1064
|
+
* @param module - The EventHandler module.
|
|
1065
|
+
* @param options - Page definition options.
|
|
1066
|
+
* @returns The UseReactBlueprint.
|
|
1067
|
+
*/
|
|
1068
|
+
function definePage(module, options) {
|
|
1069
|
+
return {
|
|
1070
|
+
stone: {
|
|
1071
|
+
router: {
|
|
1072
|
+
definitions: [
|
|
1073
|
+
{
|
|
1074
|
+
...options,
|
|
1075
|
+
method: GET,
|
|
1076
|
+
methods: [],
|
|
1077
|
+
children: undefined,
|
|
1078
|
+
handler: {
|
|
1079
|
+
module,
|
|
1080
|
+
isComponent: true,
|
|
1081
|
+
layout: options?.layout,
|
|
1082
|
+
isClass: options?.isClass,
|
|
1083
|
+
isFactory: options?.isClass !== true
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Utility function to define a page layout.
|
|
1093
|
+
*
|
|
1094
|
+
* @param module - The layout module.
|
|
1095
|
+
* @param options - Optional page layout definition options.
|
|
1096
|
+
* @returns The UseReactBlueprint.
|
|
1097
|
+
*/
|
|
1098
|
+
function definePageLayout(module, options) {
|
|
1099
|
+
const name = options?.name ?? 'default';
|
|
1100
|
+
return {
|
|
1101
|
+
stone: {
|
|
1102
|
+
useReact: {
|
|
1103
|
+
layout: {
|
|
1104
|
+
[name]: {
|
|
1105
|
+
module,
|
|
1106
|
+
isClass: options?.isClass,
|
|
1107
|
+
isFactory: options?.isClass !== true
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Utility function to define an error page.
|
|
1116
|
+
*
|
|
1117
|
+
* @param module - The layout module.
|
|
1118
|
+
* @param options - Optional page layout definition options.
|
|
1119
|
+
* @returns The UseReactBlueprint.
|
|
1120
|
+
*/
|
|
1121
|
+
function defineErrorPage(module, options) {
|
|
1122
|
+
const error = options?.error ?? 'default';
|
|
1123
|
+
const errorPages = Object.fromEntries([error].flat().map((err) => [
|
|
1124
|
+
err,
|
|
1125
|
+
{
|
|
1126
|
+
...options,
|
|
1127
|
+
module,
|
|
1128
|
+
error: err,
|
|
1129
|
+
isFactory: options?.isClass !== true
|
|
1130
|
+
}
|
|
1131
|
+
]));
|
|
1132
|
+
return {
|
|
1133
|
+
stone: {
|
|
1134
|
+
useReact: {
|
|
1135
|
+
errorPages
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Constants are defined here to prevent Circular dependency between modules
|
|
1143
|
+
* This pattern must be applied to all Stone libraries or third party libraries.
|
|
1144
|
+
*/
|
|
1145
|
+
/**
|
|
1146
|
+
* A unique symbol key to mark classes as React Page component.
|
|
1147
|
+
*/
|
|
1148
|
+
const REACT_PAGE_KEY = Symbol.for('ReactPage');
|
|
1149
|
+
/**
|
|
1150
|
+
* A unique symbol key to mark classes as React Page layout component.
|
|
1151
|
+
*/
|
|
1152
|
+
const REACT_PAGE_LAYOUT_KEY = Symbol.for('ReactPageLayout');
|
|
1153
|
+
/**
|
|
1154
|
+
* A unique symbol key to mark classes as React Error handler component.
|
|
1155
|
+
*/
|
|
1156
|
+
const REACT_ERROR_PAGE_KEY = Symbol.for('ReactErrorPage');
|
|
1157
|
+
/**
|
|
1158
|
+
* A unique symbol key to mark classes as React Adapter Error handler component.
|
|
1159
|
+
*/
|
|
1160
|
+
const REACT_ADAPTER_ERROR_PAGE_KEY = Symbol.for('ReactAdapterErrorPage');
|
|
1161
|
+
/**
|
|
1162
|
+
* A unique symbol key to mark classes as React Stone application entry point.
|
|
1163
|
+
*/
|
|
1164
|
+
const STONE_REACT_APP_KEY = Symbol.for('StoneReactApp');
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Adapter Middleware for handling outgoing responses and rendering them in the browser.
|
|
1168
|
+
*/
|
|
1169
|
+
class BrowserResponseMiddleware {
|
|
1170
|
+
isRendered;
|
|
1171
|
+
blueprint;
|
|
1172
|
+
/**
|
|
1173
|
+
* Create a BrowserResponseMiddleware.
|
|
1174
|
+
*
|
|
1175
|
+
* @param {blueprint} options - Options for creating the BrowserResponseMiddleware.
|
|
1176
|
+
*/
|
|
1177
|
+
constructor({ blueprint }) {
|
|
1178
|
+
this.blueprint = blueprint;
|
|
1179
|
+
this.isRendered = blueprint.has('stone.useReact.reactRoot');
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Handles the outgoing response, processes it, and invokes the next middleware in the pipeline.
|
|
1183
|
+
*
|
|
1184
|
+
* @param context - The adapter context containing the raw event, execution context, and other data.
|
|
1185
|
+
* @param next - The next middleware to be invoked in the pipeline.
|
|
1186
|
+
* @returns A promise resolving to the processed context.
|
|
1187
|
+
* @throws {NodeHttpAdapterError} If required components are missing in the context.
|
|
1188
|
+
*/
|
|
1189
|
+
async handle(context, next) {
|
|
1190
|
+
const rawResponseBuilder = await next(context);
|
|
1191
|
+
this.ensureValidContext(context, rawResponseBuilder);
|
|
1192
|
+
return rawResponseBuilder.add('render', async () => await this.renderComponent(context.outgoingResponse));
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Ensures the context and response builder have the required components.
|
|
1196
|
+
*/
|
|
1197
|
+
ensureValidContext(context, rawResponseBuilder) {
|
|
1198
|
+
if (context.rawEvent === undefined ||
|
|
1199
|
+
context.incomingEvent === undefined ||
|
|
1200
|
+
context.outgoingResponse === undefined ||
|
|
1201
|
+
rawResponseBuilder?.add === undefined) {
|
|
1202
|
+
throw new UseReactError('The context is missing required components.');
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Renders the provided React content.
|
|
1207
|
+
*
|
|
1208
|
+
* @param response - The response object.
|
|
1209
|
+
*/
|
|
1210
|
+
async renderComponent(response) {
|
|
1211
|
+
if (isEmpty(response)) {
|
|
1212
|
+
throw new UseReactError('No response provided for rendering.');
|
|
1213
|
+
}
|
|
1214
|
+
const content = response.content;
|
|
1215
|
+
const targetUrl = response.targetUrl;
|
|
1216
|
+
if (isNotEmpty(targetUrl)) {
|
|
1217
|
+
return this.handleRedirect(targetUrl);
|
|
1218
|
+
}
|
|
1219
|
+
if (isNotEmpty(content?.head)) {
|
|
1220
|
+
applyHeadContextToDom(document, content.head);
|
|
1221
|
+
}
|
|
1222
|
+
if (content?.ssr === true && !this.isRendered && isNotEmpty(content?.app)) {
|
|
1223
|
+
return this.hydrateReactApp(content.app);
|
|
1224
|
+
}
|
|
1225
|
+
if (isNotEmpty(content?.app) && (!this.isRendered || content?.fullRender === true)) {
|
|
1226
|
+
return this.renderReactApp(content.app);
|
|
1227
|
+
}
|
|
1228
|
+
if (isNotEmpty(content?.component)) {
|
|
1229
|
+
return await this.dispatchComponentToOutlet(content.component);
|
|
1230
|
+
}
|
|
1231
|
+
throw new UseReactError('Invalid content provided for rendering.');
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Handles navigation redirection.
|
|
1235
|
+
*
|
|
1236
|
+
* @param path - The path to redirect to.
|
|
1237
|
+
*/
|
|
1238
|
+
handleRedirect(path) {
|
|
1239
|
+
window.history.pushState({ path }, '', path);
|
|
1240
|
+
window.dispatchEvent(new Event(NAVIGATION_EVENT));
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Hydrates the React app when SSR is enabled.
|
|
1244
|
+
*
|
|
1245
|
+
* @param app - The React app to hydrate.
|
|
1246
|
+
*/
|
|
1247
|
+
hydrateReactApp(app) {
|
|
1248
|
+
hydrateReactApp(app, this.blueprint);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Renders the React app.
|
|
1252
|
+
*
|
|
1253
|
+
* @param app - The React app to render.
|
|
1254
|
+
*/
|
|
1255
|
+
renderReactApp(app) {
|
|
1256
|
+
renderReactApp(app, this.blueprint);
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Dispatches a component to the layout outlet when no layout is defined.
|
|
1260
|
+
*
|
|
1261
|
+
* @param component - The component to dispatch.
|
|
1262
|
+
*/
|
|
1263
|
+
async dispatchComponentToOutlet(component) {
|
|
1264
|
+
window.dispatchEvent(new CustomEvent(STONE_PAGE_EVENT_OUTLET, { detail: component }));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Meta Middleware for processing browser responses.
|
|
1269
|
+
*/
|
|
1270
|
+
const MetaBrowserResponseMiddleware = { module: BrowserResponseMiddleware, isClass: true };
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Blueprint middleware to dynamically set lifecycle hooks for react.
|
|
1274
|
+
*
|
|
1275
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1276
|
+
* @param next - The next pipeline function to continue processing.
|
|
1277
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1278
|
+
*
|
|
1279
|
+
* @example
|
|
1280
|
+
* ```typescript
|
|
1281
|
+
* SetUseReactHooksMiddleware(context, next)
|
|
1282
|
+
* ```
|
|
1283
|
+
*/
|
|
1284
|
+
const SetUseReactHooksMiddleware = (context, next) => {
|
|
1285
|
+
if (context.blueprint.get('stone.adapter.platform') !== NODE_CONSOLE_PLATFORM) {
|
|
1286
|
+
context
|
|
1287
|
+
.blueprint
|
|
1288
|
+
.add('stone.lifecycleHooks.onPreparingResponse', [onPreparingResponse]);
|
|
1289
|
+
}
|
|
1290
|
+
return next(context);
|
|
1291
|
+
};
|
|
1292
|
+
/**
|
|
1293
|
+
* Blueprint middleware to set BrowserResponseMiddleware for the Browser adapter.
|
|
1294
|
+
*
|
|
1295
|
+
* The MetaBrowserResponseMiddleware is an adapter middleware and is useful
|
|
1296
|
+
* for handling outgoing responses and rendering them in the browser.
|
|
1297
|
+
*
|
|
1298
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1299
|
+
* @param next - The next pipeline function to continue processing.
|
|
1300
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1301
|
+
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* ```typescript
|
|
1304
|
+
* SetBrowserResponseMiddlewareMiddleware(context, next)
|
|
1305
|
+
* ```
|
|
1306
|
+
*/
|
|
1307
|
+
const SetBrowserResponseMiddlewareMiddleware = async (context, next) => {
|
|
1308
|
+
if (context.blueprint.get('stone.adapter.platform') === BROWSER_PLATFORM) {
|
|
1309
|
+
context.blueprint.add('stone.adapter.middleware', [MetaBrowserResponseMiddleware]);
|
|
1310
|
+
}
|
|
1311
|
+
return await next(context);
|
|
1312
|
+
};
|
|
1313
|
+
/**
|
|
1314
|
+
* Blueprint middleware to process and register kernel error page definitions from modules.
|
|
1315
|
+
*
|
|
1316
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1317
|
+
* @param next - The next pipeline function to continue processing.
|
|
1318
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1319
|
+
*
|
|
1320
|
+
* @example
|
|
1321
|
+
* ```typescript
|
|
1322
|
+
* SetReactKernelErrorPageMiddleware(context, next)
|
|
1323
|
+
* ```
|
|
1324
|
+
*/
|
|
1325
|
+
const SetReactKernelErrorPageMiddleware = (context, next) => {
|
|
1326
|
+
context
|
|
1327
|
+
.blueprint
|
|
1328
|
+
.set('stone.kernel.errorHandlers.default', { module: UseReactKernelErrorHandler, isClass: true });
|
|
1329
|
+
context
|
|
1330
|
+
.modules
|
|
1331
|
+
.filter(module => hasMetadata(module, REACT_ERROR_PAGE_KEY))
|
|
1332
|
+
.forEach(module => {
|
|
1333
|
+
const { error, layout } = getMetadata(module, REACT_ERROR_PAGE_KEY, { error: 'default' });
|
|
1334
|
+
Array(error).flat().forEach(name => {
|
|
1335
|
+
context
|
|
1336
|
+
.blueprint
|
|
1337
|
+
.set(`stone.useReact.errorPages.${name}`, { layout, module, isClass: true });
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
// Process both eager and lazy loaded error pages
|
|
1341
|
+
Object
|
|
1342
|
+
.keys(context.blueprint.get('stone.useReact.errorPages', {}))
|
|
1343
|
+
.forEach((name) => {
|
|
1344
|
+
context
|
|
1345
|
+
.blueprint
|
|
1346
|
+
.set(`stone.kernel.errorHandlers.${name}`, { module: UseReactKernelErrorHandler, isClass: true });
|
|
1347
|
+
});
|
|
1348
|
+
return next(context);
|
|
1349
|
+
};
|
|
1350
|
+
/**
|
|
1351
|
+
* Blueprint middleware to process and register adapter error page definitions from modules.
|
|
1352
|
+
*
|
|
1353
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1354
|
+
* @param next - The next pipeline function to continue processing.
|
|
1355
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* ```typescript
|
|
1359
|
+
* SetReactAdapterErrorPageMiddleware(context, next)
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
const SetReactAdapterErrorPageMiddleware = (context, next) => {
|
|
1363
|
+
const UseReactAdapterErrorHandler = import.meta.env.SSR
|
|
1364
|
+
? UseReactServerErrorHandler
|
|
1365
|
+
: UseReactBrowserErrorHandler;
|
|
1366
|
+
context
|
|
1367
|
+
.blueprint
|
|
1368
|
+
.set('stone.adapter.errorHandlers.default', { module: UseReactAdapterErrorHandler, isClass: true });
|
|
1369
|
+
context
|
|
1370
|
+
.modules
|
|
1371
|
+
.filter(module => hasMetadata(module, REACT_ADAPTER_ERROR_PAGE_KEY))
|
|
1372
|
+
.forEach(module => {
|
|
1373
|
+
const { error, layout, adapterAlias, platform } = getMetadata(module, REACT_ADAPTER_ERROR_PAGE_KEY, { error: 'default' });
|
|
1374
|
+
if (isMatchedAdapter(context.blueprint, platform, adapterAlias)) {
|
|
1375
|
+
Array(error).flat().forEach(name => {
|
|
1376
|
+
context
|
|
1377
|
+
.blueprint
|
|
1378
|
+
.set(`stone.useReact.adapterErrorPages.${name}`, { isClass: true, layout, module });
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
// Process both eager and lazy loaded error pages
|
|
1383
|
+
Object
|
|
1384
|
+
.keys(context.blueprint.get('stone.useReact.adapterErrorPages', {}))
|
|
1385
|
+
.forEach((name) => {
|
|
1386
|
+
context
|
|
1387
|
+
.blueprint
|
|
1388
|
+
.set(`stone.adapter.errorHandlers.${name}`, { module: UseReactAdapterErrorHandler, isClass: true });
|
|
1389
|
+
});
|
|
1390
|
+
return next(context);
|
|
1391
|
+
};
|
|
1392
|
+
/**
|
|
1393
|
+
* Blueprint middleware to set StaticFileMiddleware for SSR adapter.
|
|
1394
|
+
*
|
|
1395
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1396
|
+
* @param next - The next pipeline function to continue processing.
|
|
1397
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1398
|
+
*
|
|
1399
|
+
* @example
|
|
1400
|
+
* ```typescript
|
|
1401
|
+
* SetSSRStaticFileMiddleware(context, next)
|
|
1402
|
+
* ```
|
|
1403
|
+
*/
|
|
1404
|
+
const SetSSRStaticFileMiddleware = async (context, next) => {
|
|
1405
|
+
import.meta.env.SSR && context.blueprint.add('stone.kernel.middleware', [MetaStaticFileMiddleware]);
|
|
1406
|
+
return await next(context);
|
|
1407
|
+
};
|
|
1408
|
+
/**
|
|
1409
|
+
* Blueprint middleware to set CompressionMiddleware for SSR adapter.
|
|
1410
|
+
*
|
|
1411
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1412
|
+
* @param next - The next pipeline function to continue processing.
|
|
1413
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1414
|
+
*
|
|
1415
|
+
* @example
|
|
1416
|
+
* ```typescript
|
|
1417
|
+
* SetSSRCompressionMiddleware(context, next)
|
|
1418
|
+
* ```
|
|
1419
|
+
*/
|
|
1420
|
+
const SetSSRCompressionMiddleware = async (context, next) => {
|
|
1421
|
+
import.meta.env.SSR && context.blueprint.add('stone.kernel.middleware', [MetaCompressionMiddleware]);
|
|
1422
|
+
return await next(context);
|
|
1423
|
+
};
|
|
1424
|
+
/**
|
|
1425
|
+
* Blueprint middleware to process and register route definitions from modules.
|
|
1426
|
+
*
|
|
1427
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1428
|
+
* @param next - The next pipeline function to continue processing.
|
|
1429
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1430
|
+
*
|
|
1431
|
+
* @example
|
|
1432
|
+
* ```typescript
|
|
1433
|
+
* SetReactRouteDefinitionsMiddleware(context, next)
|
|
1434
|
+
* ```
|
|
1435
|
+
*/
|
|
1436
|
+
const SetReactRouteDefinitionsMiddleware = (context, next) => {
|
|
1437
|
+
context
|
|
1438
|
+
.modules
|
|
1439
|
+
.filter(module => hasMetadata(module, REACT_PAGE_KEY))
|
|
1440
|
+
.forEach(module => {
|
|
1441
|
+
const options = getMetadata(module, REACT_PAGE_KEY, { path: '/' });
|
|
1442
|
+
const definition = {
|
|
1443
|
+
...options,
|
|
1444
|
+
handler: { ...options.handler, module }
|
|
1445
|
+
};
|
|
1446
|
+
context.blueprint.add('stone.router.definitions', [definition]);
|
|
1447
|
+
});
|
|
1448
|
+
return next(context);
|
|
1449
|
+
};
|
|
1450
|
+
/**
|
|
1451
|
+
* Blueprint middleware to process and register layout definitions from modules.
|
|
1452
|
+
*
|
|
1453
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1454
|
+
* @param next - The next pipeline function to continue processing.
|
|
1455
|
+
* @returns The updated blueprint or a promise resolving to it.
|
|
1456
|
+
*
|
|
1457
|
+
* @example
|
|
1458
|
+
* ```typescript
|
|
1459
|
+
* SetReactPageLayoutMiddleware(context, next)
|
|
1460
|
+
* ```
|
|
1461
|
+
*/
|
|
1462
|
+
const SetReactPageLayoutMiddleware = (context, next) => {
|
|
1463
|
+
context
|
|
1464
|
+
.modules
|
|
1465
|
+
.filter(module => hasMetadata(module, REACT_PAGE_LAYOUT_KEY))
|
|
1466
|
+
.forEach(module => {
|
|
1467
|
+
const { name = 'default' } = getMetadata(module, REACT_PAGE_LAYOUT_KEY, { name: 'default' });
|
|
1468
|
+
context.blueprint.set(`stone.useReact.layout.${name}`, { isClass: true, module });
|
|
1469
|
+
});
|
|
1470
|
+
return next(context);
|
|
1471
|
+
};
|
|
1472
|
+
/**
|
|
1473
|
+
* Blueprint middleware to set the UseReact as the main event handler for the application.
|
|
1474
|
+
*
|
|
1475
|
+
* Set as fallback event handler if none of the other event handlers are registered.
|
|
1476
|
+
*
|
|
1477
|
+
* @param context - The configuration context containing modules and blueprint.
|
|
1478
|
+
* @param next - The next function in the pipeline.
|
|
1479
|
+
* @returns The updated blueprint.
|
|
1480
|
+
*
|
|
1481
|
+
* @example
|
|
1482
|
+
* ```typescript
|
|
1483
|
+
* SetUseReactEventHandlerMiddleware({ modules, blueprint }, next);
|
|
1484
|
+
* ```
|
|
1485
|
+
*/
|
|
1486
|
+
async function SetUseReactEventHandlerMiddleware(context, next) {
|
|
1487
|
+
const blueprint = await next(context);
|
|
1488
|
+
const module = context.modules.find(module => hasMetadata(module, STONE_REACT_APP_KEY));
|
|
1489
|
+
blueprint.setIf('stone.kernel.eventHandler', { module: UseReactEventHandler, isClass: true });
|
|
1490
|
+
if (isNotEmpty(module)) {
|
|
1491
|
+
blueprint.set('stone.useReact.componentEventHandler', { module, isComponent: true, isClass: true });
|
|
1492
|
+
}
|
|
1493
|
+
return blueprint;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Configuration for react processing middleware.
|
|
1497
|
+
*
|
|
1498
|
+
* This array defines a list of middleware pipes, each with a `pipe` function and a `priority`.
|
|
1499
|
+
* These pipes are executed in the order of their priority values, with lower values running first.
|
|
1500
|
+
*/
|
|
1501
|
+
const metaUseReactBlueprintMiddleware = [
|
|
1502
|
+
{ module: SetSSRStaticFileMiddleware, priority: 10 },
|
|
1503
|
+
{ module: SetUseReactHooksMiddleware, priority: 10 },
|
|
1504
|
+
{ module: SetSSRCompressionMiddleware, priority: 10 },
|
|
1505
|
+
{ module: SetReactPageLayoutMiddleware, priority: 10 },
|
|
1506
|
+
{ module: SetUseReactEventHandlerMiddleware, priority: 2 },
|
|
1507
|
+
{ module: SetReactKernelErrorPageMiddleware, priority: 10 },
|
|
1508
|
+
{ module: SetReactAdapterErrorPageMiddleware, priority: 10 },
|
|
1509
|
+
{ module: SetReactRouteDefinitionsMiddleware, priority: 10 },
|
|
1510
|
+
{ module: SetBrowserResponseMiddlewareMiddleware, priority: 10 }
|
|
1511
|
+
];
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Default blueprint for a React-based Stone.js application.
|
|
1515
|
+
*
|
|
1516
|
+
* - Defines middleware, lifecycle hooks, and the default HTML template path.
|
|
1517
|
+
*/
|
|
1518
|
+
const useReactBlueprint = {
|
|
1519
|
+
stone: {
|
|
1520
|
+
useReact: {},
|
|
1521
|
+
blueprint: {
|
|
1522
|
+
middleware: metaUseReactBlueprintMiddleware
|
|
1523
|
+
},
|
|
1524
|
+
services: [MetaReactRuntime],
|
|
1525
|
+
providers: [MetaUseReactServiceProvider]
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Defines a Stone React app using a factory-based or class-based main handler.
|
|
1531
|
+
*
|
|
1532
|
+
* @param moduleOrOptions - A factory function or class constructor for the main page.
|
|
1533
|
+
* @param optionsOrBlueprints - Optional application-level configuration.
|
|
1534
|
+
* @param maybeBlueprints - Additional blueprints to merge.
|
|
1535
|
+
* @returns A fully merged Stone blueprint.
|
|
1536
|
+
*/
|
|
1537
|
+
function defineStoneReactApp(moduleOrOptions = {}, optionsOrBlueprints, maybeBlueprints) {
|
|
1538
|
+
let module;
|
|
1539
|
+
let options = {};
|
|
1540
|
+
let blueprints = [];
|
|
1541
|
+
// Pattern: defineStoneReactApp(handler, options?, blueprints?)
|
|
1542
|
+
if (isFunctionModule(moduleOrOptions)) {
|
|
1543
|
+
module = moduleOrOptions;
|
|
1544
|
+
if (isObjectLikeModule(optionsOrBlueprints)) {
|
|
1545
|
+
options = optionsOrBlueprints;
|
|
1546
|
+
blueprints = Array.isArray(maybeBlueprints) ? maybeBlueprints : [];
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
else if (isObjectLikeModule(moduleOrOptions)) { // Pattern: defineStoneReactApp(options, blueprints?)
|
|
1550
|
+
options = moduleOrOptions;
|
|
1551
|
+
blueprints = Array.isArray(optionsOrBlueprints) ? optionsOrBlueprints : [];
|
|
1552
|
+
}
|
|
1553
|
+
const stonePart = {
|
|
1554
|
+
...options,
|
|
1555
|
+
useReact: {
|
|
1556
|
+
...options.useReact
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
if (isNotEmpty(module)) {
|
|
1560
|
+
stonePart.useReact.componentEventHandler = {
|
|
1561
|
+
module,
|
|
1562
|
+
isComponent: true,
|
|
1563
|
+
isClass: options.isClass,
|
|
1564
|
+
isFactory: options.isClass !== true
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
return mergeBlueprints(stoneBlueprint, useReactBlueprint, ...blueprints, { stone: stonePart });
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* A class decorator for defining a class as a React Handler layout.
|
|
1572
|
+
*
|
|
1573
|
+
* @param options - Configuration options for the layout definition.
|
|
1574
|
+
* @returns A method decorator to be applied to a class method.
|
|
1575
|
+
*
|
|
1576
|
+
* @example
|
|
1577
|
+
* ```typescript
|
|
1578
|
+
* import { AdapterErrorPage } from '@stone-js/use-react';
|
|
1579
|
+
*
|
|
1580
|
+
* @AdapterErrorPage({ error: 'UserNotFoundError' })
|
|
1581
|
+
* class UserAdapterErrorPage {
|
|
1582
|
+
* render({ error }) {
|
|
1583
|
+
* return <h1>User name: {error.message}</h1>;
|
|
1584
|
+
* }
|
|
1585
|
+
* }
|
|
1586
|
+
* ```
|
|
1587
|
+
*/
|
|
1588
|
+
const AdapterErrorPage = (options) => {
|
|
1589
|
+
return classDecoratorLegacyWrapper((_target, context) => {
|
|
1590
|
+
setMetadata(context, REACT_ADAPTER_ERROR_PAGE_KEY, { ...options, isClass: true });
|
|
1591
|
+
});
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* A class decorator for defining a class as a React Handler layout.
|
|
1596
|
+
*
|
|
1597
|
+
* @param options - Configuration options for the layout definition.
|
|
1598
|
+
* @returns A method decorator to be applied to a class method.
|
|
1599
|
+
*
|
|
1600
|
+
* @example
|
|
1601
|
+
* ```typescript
|
|
1602
|
+
* import { ErrorPage } from '@stone-js/use-react';
|
|
1603
|
+
*
|
|
1604
|
+
* @ErrorPage({ error: 'UserNotFoundError' })
|
|
1605
|
+
* class UserErrorPage {
|
|
1606
|
+
* render({ error }) {
|
|
1607
|
+
* return <h1>User name: {error.message}</h1>;
|
|
1608
|
+
* }
|
|
1609
|
+
* }
|
|
1610
|
+
* ```
|
|
1611
|
+
*/
|
|
1612
|
+
const ErrorPage = (options) => {
|
|
1613
|
+
return classDecoratorLegacyWrapper((_target, context) => {
|
|
1614
|
+
setMetadata(context, REACT_ERROR_PAGE_KEY, { ...options, isClass: true });
|
|
1615
|
+
});
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Hook decorator to mark a method as a lifecycle hook
|
|
1620
|
+
* And automatically add it to the global lifecycle hook registry.
|
|
1621
|
+
*
|
|
1622
|
+
* @example
|
|
1623
|
+
* ```typescript
|
|
1624
|
+
* class MyClass {
|
|
1625
|
+
* // ...
|
|
1626
|
+
* @Hook('onPreparingPage')
|
|
1627
|
+
* onPreparingPage () {}
|
|
1628
|
+
* }
|
|
1629
|
+
* ```
|
|
1630
|
+
*
|
|
1631
|
+
* @param name - The name of the lifecycle hook.
|
|
1632
|
+
* @returns A class decorator function that sets the metadata using the provided options.
|
|
1633
|
+
*/
|
|
1634
|
+
const Hook = (name) => {
|
|
1635
|
+
return methodDecoratorLegacyWrapper((_target, context) => {
|
|
1636
|
+
addMetadata(context, LIFECYCLE_HOOK_KEY, { name, method: context.name });
|
|
1637
|
+
});
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* A class decorator for defining a class as a React Page route action.
|
|
1642
|
+
* Uses the `Match` decorator internally to register the route with the HTTP `GET` method.
|
|
1643
|
+
*
|
|
1644
|
+
* @param options - Configuration options for the route definition, excluding the `methods` property.
|
|
1645
|
+
* @returns A method decorator to be applied to a class method.
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* ```typescript
|
|
1649
|
+
* import { Page } from '@stone-js/use-react';
|
|
1650
|
+
*
|
|
1651
|
+
* @Page('/user-profile')
|
|
1652
|
+
* class UserPage {
|
|
1653
|
+
* handle({ event }): Record<string, string> {
|
|
1654
|
+
* return { name: 'Jane Doe' };
|
|
1655
|
+
* }
|
|
1656
|
+
*
|
|
1657
|
+
* render({ data }) {
|
|
1658
|
+
* return <h1>User name: {data.name}</h1>;
|
|
1659
|
+
* }
|
|
1660
|
+
* }
|
|
1661
|
+
* ```
|
|
1662
|
+
*/
|
|
1663
|
+
const Page = (path, options = {}) => {
|
|
1664
|
+
return classDecoratorLegacyWrapper((target, context) => {
|
|
1665
|
+
setMetadata(context, REACT_PAGE_KEY, {
|
|
1666
|
+
...options,
|
|
1667
|
+
path,
|
|
1668
|
+
method: GET,
|
|
1669
|
+
methods: [],
|
|
1670
|
+
handler: { isClass: true, isComponent: true, layout: options.layout, module: target }
|
|
1671
|
+
});
|
|
1672
|
+
});
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* A class decorator for defining a class as a React Page layout.
|
|
1677
|
+
*
|
|
1678
|
+
* @param options - Configuration options for the layout definition.
|
|
1679
|
+
* @returns A method decorator to be applied to a class method.
|
|
1680
|
+
*
|
|
1681
|
+
* @example
|
|
1682
|
+
* ```typescript
|
|
1683
|
+
* import { PageLayout } from '@stone-js/use-react';
|
|
1684
|
+
*
|
|
1685
|
+
* @PageLayout({ name: 'UserPageLayout' })
|
|
1686
|
+
* class UserPageLayout {
|
|
1687
|
+
* render({ data }) {
|
|
1688
|
+
* return <h1>User name: {data.name}</h1>;
|
|
1689
|
+
* }
|
|
1690
|
+
* }
|
|
1691
|
+
* ```
|
|
1692
|
+
*/
|
|
1693
|
+
const PageLayout = (options) => {
|
|
1694
|
+
return classDecoratorLegacyWrapper((_target, context) => {
|
|
1695
|
+
setMetadata(context, REACT_PAGE_LAYOUT_KEY, { ...options, isClass: true });
|
|
1696
|
+
});
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Decorator to set the status code of the response.
|
|
1701
|
+
*
|
|
1702
|
+
* @param statusCode - The status code of the response.
|
|
1703
|
+
* @param headers - The headers for the response.
|
|
1704
|
+
* @returns A method decorator.
|
|
1705
|
+
*
|
|
1706
|
+
* @example
|
|
1707
|
+
* ```typescript
|
|
1708
|
+
* import { Page, PageStatus } from '@stone-js/use-react';
|
|
1709
|
+
*
|
|
1710
|
+
* @Page('/user-profile')
|
|
1711
|
+
* class UserPage {
|
|
1712
|
+
* @PageStatus()
|
|
1713
|
+
* handle() {
|
|
1714
|
+
* return { name: 'John Doe' };
|
|
1715
|
+
* }
|
|
1716
|
+
* }
|
|
1717
|
+
* ```
|
|
1718
|
+
*/
|
|
1719
|
+
const PageStatus = (statusCode = 200, headers = {}) => {
|
|
1720
|
+
return methodDecoratorLegacyWrapper((target, _context) => {
|
|
1721
|
+
return async function (...args) {
|
|
1722
|
+
const content = await target.apply(this, args);
|
|
1723
|
+
return { content, statusCode, headers };
|
|
1724
|
+
};
|
|
1725
|
+
});
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Decorator to create a snapshot of the current data.
|
|
1730
|
+
*
|
|
1731
|
+
* @param name - The name of the snapshot.
|
|
1732
|
+
* @returns A method decorator.
|
|
1733
|
+
*
|
|
1734
|
+
* @example
|
|
1735
|
+
* ```typescript
|
|
1736
|
+
* import { Service } from '@stone-js/core';
|
|
1737
|
+
* import { Snapshot } from '@stone-js/use-react';
|
|
1738
|
+
*
|
|
1739
|
+
* @Service({ alias: 'userService' })
|
|
1740
|
+
* class UserService {
|
|
1741
|
+
* @Snapshot()
|
|
1742
|
+
* showProfile() {
|
|
1743
|
+
* return { name: 'John Doe' };
|
|
1744
|
+
* }
|
|
1745
|
+
* }
|
|
1746
|
+
* ```
|
|
1747
|
+
*/
|
|
1748
|
+
const Snapshot = (name) => {
|
|
1749
|
+
return methodDecoratorLegacyWrapper((target, context) => {
|
|
1750
|
+
return async function (...args) {
|
|
1751
|
+
name = name ?? `${String(Object.getPrototypeOf(this).constructor.name)}.${String(context.name)}`;
|
|
1752
|
+
return await ReactRuntime.instance?.snapshot(name, () => target.apply(this, args));
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* UseReact decorator.
|
|
1759
|
+
*
|
|
1760
|
+
* UseReact is a class decorator that allows you to use React components in your Stone application.
|
|
1761
|
+
* The decorator is used to define the React configuration for the class.
|
|
1762
|
+
*
|
|
1763
|
+
* @param options - UseReactOptions
|
|
1764
|
+
* @returns ClassDecorator
|
|
1765
|
+
*/
|
|
1766
|
+
const UseReact = (options = {}) => {
|
|
1767
|
+
return classDecoratorLegacyWrapper((target, context) => {
|
|
1768
|
+
setMetadata(context, STONE_REACT_APP_KEY, { isComponent: true, isClass: true });
|
|
1769
|
+
addBlueprint(target, context, useReactBlueprint, { stone: { useReact: options } });
|
|
1770
|
+
});
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Stone Client.
|
|
1775
|
+
* This component is used to wrap content
|
|
1776
|
+
* that should only be rendered on the client.
|
|
1777
|
+
*
|
|
1778
|
+
* @param options - The options to create the Stone Client.
|
|
1779
|
+
*/
|
|
1780
|
+
const StoneClient = ({ children }) => {
|
|
1781
|
+
return isClient() ? jsx(Fragment, { children: children }) : jsx(Fragment, {});
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Internal link component using Stone.js router.
|
|
1786
|
+
*/
|
|
1787
|
+
const InternalLink = ({ to, href, noRel, children, className, defaultNav, selectedClass = 'selected', ariaCurrentValue = 'page', rel = 'noopener noreferrer' }) => {
|
|
1788
|
+
const router = useContext(StoneContext).container.resolve(Router);
|
|
1789
|
+
const path = isObjectLikeModule(to) ? router.generate(to) : to ?? href;
|
|
1790
|
+
const [currentRoute, setCurrentRoute] = useState(router.getCurrentRoute());
|
|
1791
|
+
const selectedClassName = currentRoute?.path === path ? selectedClass : undefined;
|
|
1792
|
+
const elemClassName = [className, selectedClassName].filter(Boolean).join(' ').trim();
|
|
1793
|
+
const handleClick = (event) => {
|
|
1794
|
+
event.preventDefault();
|
|
1795
|
+
router.navigate(to ?? '');
|
|
1796
|
+
};
|
|
1797
|
+
useEffect(() => {
|
|
1798
|
+
const routerEventHandler = (event) => {
|
|
1799
|
+
setCurrentRoute(event.get('route'));
|
|
1800
|
+
};
|
|
1801
|
+
router.on(RouteEvent.ROUTED, routerEventHandler);
|
|
1802
|
+
return () => {
|
|
1803
|
+
router.off(RouteEvent.ROUTED, routerEventHandler);
|
|
1804
|
+
};
|
|
1805
|
+
}, [router]);
|
|
1806
|
+
return defaultNav === true
|
|
1807
|
+
? (jsx("a", { href: path, className: elemClassName, "aria-current": ariaCurrentValue, rel: noRel !== undefined ? undefined : rel, children: children }))
|
|
1808
|
+
: (jsx("button", { onClick: handleClick, className: elemClassName, "aria-current": ariaCurrentValue, rel: noRel !== undefined ? undefined : rel, children: children }));
|
|
1809
|
+
};
|
|
1810
|
+
/**
|
|
1811
|
+
* External link component rendering a regular <a> tag.
|
|
1812
|
+
*/
|
|
1813
|
+
const ExternalLink = ({ to, href, noRel, target, children, className, ariaCurrentValue = 'page', rel = 'noopener noreferrer' }) => (jsx("a", { target: target, className: className, "aria-current": ariaCurrentValue, rel: noRel !== undefined ? undefined : rel, href: typeof to === 'string' ? to : href, children: children }));
|
|
1814
|
+
/**
|
|
1815
|
+
* Main StoneLink component delegating to internal or external versions.
|
|
1816
|
+
*/
|
|
1817
|
+
const StoneLink = (props) => {
|
|
1818
|
+
return props.external === true ? jsx(ExternalLink, { ...props }) : jsx(InternalLink, { ...props });
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* A dynamic rendering component that updates its content based on a global event.
|
|
1823
|
+
*
|
|
1824
|
+
* - Listens for `stone:inject:react-page:outlet` and updates its view when triggered.
|
|
1825
|
+
* - Uses `useState` to manage the currently displayed content.
|
|
1826
|
+
* - Automatically cleans up event listeners on unmount.
|
|
1827
|
+
*
|
|
1828
|
+
* This component enables dynamic content updates within a Stone.js application.
|
|
1829
|
+
*
|
|
1830
|
+
* @param options - The options to create the Stone Outlet.
|
|
1831
|
+
* @returns The Stone Outlet component.
|
|
1832
|
+
*/
|
|
1833
|
+
const StoneOutlet = ({ children }) => {
|
|
1834
|
+
const [currentView, setCurrentView] = useState(children);
|
|
1835
|
+
useEffect(() => {
|
|
1836
|
+
const eventName = STONE_PAGE_EVENT_OUTLET;
|
|
1837
|
+
const handleEvent = (e) => {
|
|
1838
|
+
if (isNotEmpty(e) && isNotEmpty(e.detail)) {
|
|
1839
|
+
setCurrentView(e.detail);
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
window.addEventListener(eventName, handleEvent);
|
|
1843
|
+
return () => window.removeEventListener(eventName, handleEvent);
|
|
1844
|
+
}, []);
|
|
1845
|
+
return jsx("div", { "data-stone-outlet": 'true', children: currentView });
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
/**
|
|
1849
|
+
* Stone Server.
|
|
1850
|
+
* This component is used to wrap content
|
|
1851
|
+
* that should only be rendered on the server.
|
|
1852
|
+
*
|
|
1853
|
+
* @param options - The options to create the Stone Server.
|
|
1854
|
+
*/
|
|
1855
|
+
const StoneServer = ({ children }) => {
|
|
1856
|
+
return isServer() ? jsx(Fragment, { children: children }) : jsx(Fragment, {});
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
export { AdapterErrorPage, BrowserResponseMiddleware, ErrorPage, Hook, MetaBrowserResponseMiddleware, MetaReactRuntime, MetaUseReactServiceProvider, Page, PageLayout, PageStatus, REACT_ADAPTER_ERROR_PAGE_KEY, REACT_ERROR_PAGE_KEY, REACT_PAGE_KEY, REACT_PAGE_LAYOUT_KEY, ReactRuntime, STONE_DOM_ATTR, STONE_PAGE_EVENT_OUTLET, STONE_REACT_APP_KEY, STONE_SNAPSHOT, SetBrowserResponseMiddlewareMiddleware, SetReactAdapterErrorPageMiddleware, SetReactKernelErrorPageMiddleware, SetReactPageLayoutMiddleware, SetReactRouteDefinitionsMiddleware, SetSSRCompressionMiddleware, SetSSRStaticFileMiddleware, SetUseReactEventHandlerMiddleware, SetUseReactHooksMiddleware, Snapshot, StoneClient, StoneContext, StoneError, StoneLink, StoneOutlet, StonePage, StoneServer, UseReact, UseReactBrowserErrorHandler, UseReactError, UseReactEventHandler, UseReactKernelErrorHandler, UseReactServerErrorHandler, UseReactServiceProvider, applyHeadContextToDom, applyHeadContextToHtmlString, applyMeta, buildAdapterErrorComponent, buildAppComponent, buildLayoutComponent, buildPageComponent, defineAdapterErrorPage, defineErrorPage, definePage, definePageLayout, defineStoneReactApp, executeHandler, executeHooks, getAppRootElement, getBrowserContent, getResponseSnapshot, getServerContent, htmlTemplate, hydrateReactApp, isClient, isSSR, isServer, metaUseReactBlueprintMiddleware, onPreparingResponse, prepareErrorPage, prepareFallbackErrorPage, preparePage, reactRedirectResponse, reactResponse, renderReactApp, renderStoneSnapshot, resolveComponent, resolveLazyComponent, snapshotResponse, useReactBlueprint };
|