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