react-on-rails-pro 16.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/CallbackRegistry.d.ts +18 -0
- package/lib/CallbackRegistry.js +112 -0
- package/lib/ClientSideRenderer.d.ts +8 -0
- package/lib/ClientSideRenderer.js +238 -0
- package/lib/ComponentRegistry.d.ts +22 -0
- package/lib/ComponentRegistry.js +57 -0
- package/lib/PostSSRHookTracker.d.ts +32 -0
- package/lib/PostSSRHookTracker.js +70 -0
- package/lib/RSCProvider.d.ts +52 -0
- package/lib/RSCProvider.js +90 -0
- package/lib/RSCRequestTracker.d.ts +75 -0
- package/lib/RSCRequestTracker.js +130 -0
- package/lib/RSCRoute.d.ts +30 -0
- package/lib/RSCRoute.js +57 -0
- package/lib/ReactOnRails.client.d.ts +4 -0
- package/lib/ReactOnRails.client.js +20 -0
- package/lib/ReactOnRails.full.d.ts +4 -0
- package/lib/ReactOnRails.full.js +26 -0
- package/lib/ReactOnRails.node.d.ts +3 -0
- package/lib/ReactOnRails.node.js +21 -0
- package/lib/ReactOnRailsRSC.d.ts +4 -0
- package/lib/ReactOnRailsRSC.js +81 -0
- package/lib/ServerComponentFetchError.d.ts +15 -0
- package/lib/ServerComponentFetchError.js +33 -0
- package/lib/StoreRegistry.d.ts +59 -0
- package/lib/StoreRegistry.js +108 -0
- package/lib/createReactOnRailsPro.d.ts +7 -0
- package/lib/createReactOnRailsPro.js +111 -0
- package/lib/getReactServerComponent.client.d.ts +47 -0
- package/lib/getReactServerComponent.client.js +156 -0
- package/lib/getReactServerComponent.server.d.ts +39 -0
- package/lib/getReactServerComponent.server.js +70 -0
- package/lib/handleError.d.ts +5 -0
- package/lib/handleError.js +8 -0
- package/lib/handleErrorRSC.d.ts +4 -0
- package/lib/handleErrorRSC.js +11 -0
- package/lib/injectRSCPayload.d.ts +31 -0
- package/lib/injectRSCPayload.js +276 -0
- package/lib/loadJsonFile.d.ts +4 -0
- package/lib/loadJsonFile.js +36 -0
- package/lib/registerServerComponent/client.d.ts +33 -0
- package/lib/registerServerComponent/client.js +43 -0
- package/lib/registerServerComponent/server.d.ts +22 -0
- package/lib/registerServerComponent/server.js +31 -0
- package/lib/registerServerComponent/server.rsc.d.ts +24 -0
- package/lib/registerServerComponent/server.rsc.js +37 -0
- package/lib/streamServerRenderedReactComponent.d.ts +5 -0
- package/lib/streamServerRenderedReactComponent.js +76 -0
- package/lib/streamingUtils.d.ts +34 -0
- package/lib/streamingUtils.js +201 -0
- package/lib/transformRSCNodeStream.d.ts +16 -0
- package/lib/transformRSCNodeStream.js +56 -0
- package/lib/transformRSCStreamAndReplayConsoleLogs.d.ts +16 -0
- package/lib/transformRSCStreamAndReplayConsoleLogs.js +83 -0
- package/lib/utils.d.ts +26 -0
- package/lib/utils.js +43 -0
- package/lib/wrapServerComponentRenderer/client.d.ts +23 -0
- package/lib/wrapServerComponentRenderer/client.js +80 -0
- package/lib/wrapServerComponentRenderer/server.d.ts +23 -0
- package/lib/wrapServerComponentRenderer/server.js +59 -0
- package/lib/wrapServerComponentRenderer/server.rsc.d.ts +3 -0
- package/lib/wrapServerComponentRenderer/server.rsc.js +19 -0
- package/package.json +83 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default class CallbackRegistry<T> {
|
|
2
|
+
private readonly registryType;
|
|
3
|
+
private registeredItems;
|
|
4
|
+
private waitingPromises;
|
|
5
|
+
private notUsedItems;
|
|
6
|
+
private timeoutEventsInitialized;
|
|
7
|
+
private timedout;
|
|
8
|
+
constructor(registryType: string);
|
|
9
|
+
private initializeTimeoutEvents;
|
|
10
|
+
set(name: string, item: T): void;
|
|
11
|
+
get(name: string): T;
|
|
12
|
+
has(name: string): boolean;
|
|
13
|
+
clear(): void;
|
|
14
|
+
getAll(): Map<string, T>;
|
|
15
|
+
getOrWaitForItem(name: string): Promise<T>;
|
|
16
|
+
private createNotFoundError;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=CallbackRegistry.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Shakacode LLC
|
|
3
|
+
*
|
|
4
|
+
* This file is NOT licensed under the MIT (open source) license.
|
|
5
|
+
* It is part of the React on Rails Pro offering and is licensed separately.
|
|
6
|
+
*
|
|
7
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
8
|
+
* via any medium, is strictly prohibited without a valid license agreement
|
|
9
|
+
* from Shakacode LLC.
|
|
10
|
+
*
|
|
11
|
+
* For licensing terms, please see:
|
|
12
|
+
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
|
|
13
|
+
*/
|
|
14
|
+
import { onPageLoaded, onPageUnloaded } from 'react-on-rails/pageLifecycle';
|
|
15
|
+
import { getRailsContext } from 'react-on-rails/context';
|
|
16
|
+
export default class CallbackRegistry {
|
|
17
|
+
constructor(registryType) {
|
|
18
|
+
this.registeredItems = new Map();
|
|
19
|
+
this.waitingPromises = new Map();
|
|
20
|
+
this.notUsedItems = new Set();
|
|
21
|
+
this.timeoutEventsInitialized = false;
|
|
22
|
+
this.timedout = false;
|
|
23
|
+
this.registryType = registryType;
|
|
24
|
+
}
|
|
25
|
+
initializeTimeoutEvents() {
|
|
26
|
+
if (this.timeoutEventsInitialized)
|
|
27
|
+
return;
|
|
28
|
+
this.timeoutEventsInitialized = true;
|
|
29
|
+
let timeoutId;
|
|
30
|
+
const triggerTimeout = () => {
|
|
31
|
+
this.timedout = true;
|
|
32
|
+
this.waitingPromises.forEach((waitingPromiseInfo, itemName) => {
|
|
33
|
+
waitingPromiseInfo.reject(this.createNotFoundError(itemName));
|
|
34
|
+
});
|
|
35
|
+
this.notUsedItems.forEach((itemName) => {
|
|
36
|
+
console.warn(`Warning: ${this.registryType} '${itemName}' was registered but never used. This may indicate unused code that can be removed.`);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
onPageLoaded(() => {
|
|
40
|
+
const registryTimeout = getRailsContext()?.componentRegistryTimeout;
|
|
41
|
+
if (!registryTimeout)
|
|
42
|
+
return;
|
|
43
|
+
timeoutId = setTimeout(triggerTimeout, registryTimeout);
|
|
44
|
+
});
|
|
45
|
+
onPageUnloaded(() => {
|
|
46
|
+
this.waitingPromises.clear();
|
|
47
|
+
this.timedout = false;
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
set(name, item) {
|
|
52
|
+
this.registeredItems.set(name, item);
|
|
53
|
+
if (this.timedout)
|
|
54
|
+
return;
|
|
55
|
+
const waitingPromiseInfo = this.waitingPromises.get(name);
|
|
56
|
+
if (waitingPromiseInfo) {
|
|
57
|
+
waitingPromiseInfo.resolve(item);
|
|
58
|
+
this.waitingPromises.delete(name);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.notUsedItems.add(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
get(name) {
|
|
65
|
+
const item = this.registeredItems.get(name);
|
|
66
|
+
if (!item) {
|
|
67
|
+
throw this.createNotFoundError(name);
|
|
68
|
+
}
|
|
69
|
+
this.notUsedItems.delete(name);
|
|
70
|
+
return item;
|
|
71
|
+
}
|
|
72
|
+
has(name) {
|
|
73
|
+
return this.registeredItems.has(name);
|
|
74
|
+
}
|
|
75
|
+
clear() {
|
|
76
|
+
this.registeredItems.clear();
|
|
77
|
+
this.notUsedItems.clear();
|
|
78
|
+
}
|
|
79
|
+
getAll() {
|
|
80
|
+
return new Map(this.registeredItems);
|
|
81
|
+
}
|
|
82
|
+
async getOrWaitForItem(name) {
|
|
83
|
+
this.initializeTimeoutEvents();
|
|
84
|
+
try {
|
|
85
|
+
return this.get(name);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (this.timedout) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const existingWaitingPromiseInfo = this.waitingPromises.get(name);
|
|
92
|
+
if (existingWaitingPromiseInfo) {
|
|
93
|
+
return existingWaitingPromiseInfo.promise;
|
|
94
|
+
}
|
|
95
|
+
let promiseResolve = () => { };
|
|
96
|
+
let promiseReject = () => { };
|
|
97
|
+
const promise = new Promise((resolve, reject) => {
|
|
98
|
+
promiseResolve = resolve;
|
|
99
|
+
promiseReject = reject;
|
|
100
|
+
});
|
|
101
|
+
this.waitingPromises.set(name, { resolve: promiseResolve, reject: promiseReject, promise });
|
|
102
|
+
return promise;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
createNotFoundError(itemName) {
|
|
106
|
+
const keys = Array.from(this.registeredItems.keys()).join(', ');
|
|
107
|
+
return new Error(`Could not find ${this.registryType} registered with name ${itemName}. ` +
|
|
108
|
+
`Registered ${this.registryType} names include [ ${keys} ]. ` +
|
|
109
|
+
`Maybe you forgot to register the ${this.registryType}?`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=CallbackRegistry.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function renderOrHydrateComponent(domIdOrElement: string | Element): Promise<void>;
|
|
2
|
+
export declare const renderOrHydrateImmediateHydratedComponents: () => Promise<void>;
|
|
3
|
+
export declare const renderOrHydrateAllComponents: () => Promise<void>;
|
|
4
|
+
export declare function hydrateStore(storeNameOrElement: string | Element): Promise<void>;
|
|
5
|
+
export declare const hydrateImmediateHydratedStores: () => Promise<void>;
|
|
6
|
+
export declare const hydrateAllStores: () => Promise<void>;
|
|
7
|
+
export declare function unmountAll(): void;
|
|
8
|
+
//# sourceMappingURL=ClientSideRenderer.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Shakacode LLC
|
|
3
|
+
*
|
|
4
|
+
* This file is NOT licensed under the MIT (open source) license.
|
|
5
|
+
* It is part of the React on Rails Pro offering and is licensed separately.
|
|
6
|
+
*
|
|
7
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
8
|
+
* via any medium, is strictly prohibited without a valid license agreement
|
|
9
|
+
* from Shakacode LLC.
|
|
10
|
+
*
|
|
11
|
+
* For licensing terms, please see:
|
|
12
|
+
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
|
|
13
|
+
*/
|
|
14
|
+
import { getRailsContext, resetRailsContext } from 'react-on-rails/context';
|
|
15
|
+
import createReactOutput from 'react-on-rails/createReactOutput';
|
|
16
|
+
import { isServerRenderHash } from 'react-on-rails/isServerRenderResult';
|
|
17
|
+
import { supportsHydrate, supportsRootApi, unmountComponentAtNode } from 'react-on-rails/reactApis';
|
|
18
|
+
import reactHydrateOrRender from 'react-on-rails/reactHydrateOrRender';
|
|
19
|
+
import { debugTurbolinks } from 'react-on-rails/turbolinksUtils';
|
|
20
|
+
import { onPageLoaded } from 'react-on-rails/pageLifecycle';
|
|
21
|
+
import * as StoreRegistry from "./StoreRegistry.js";
|
|
22
|
+
import * as ComponentRegistry from "./ComponentRegistry.js";
|
|
23
|
+
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
|
|
24
|
+
const IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a React on Rails Pro license. " +
|
|
25
|
+
'Please visit https://shakacode.com/react-on-rails-pro to get a license.';
|
|
26
|
+
async function delegateToRenderer(componentObj, props, railsContext, domNodeId, trace) {
|
|
27
|
+
const { name, component, isRenderer } = componentObj;
|
|
28
|
+
if (isRenderer) {
|
|
29
|
+
if (trace) {
|
|
30
|
+
console.log(`DELEGATING TO RENDERER ${name} for dom node with id: ${domNodeId} with props, railsContext:`, props, railsContext);
|
|
31
|
+
}
|
|
32
|
+
await component(props, railsContext, domNodeId);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const getDomId = (domIdOrElement) => typeof domIdOrElement === 'string' ? domIdOrElement : domIdOrElement.getAttribute('data-dom-id') || '';
|
|
38
|
+
class ComponentRenderer {
|
|
39
|
+
constructor(domIdOrElement) {
|
|
40
|
+
const domId = getDomId(domIdOrElement);
|
|
41
|
+
this.domNodeId = domId;
|
|
42
|
+
this.state = 'rendering';
|
|
43
|
+
const el = typeof domIdOrElement === 'string'
|
|
44
|
+
? document.querySelector(`[data-dom-id="${CSS.escape(domId)}"]`)
|
|
45
|
+
: domIdOrElement;
|
|
46
|
+
if (!el)
|
|
47
|
+
return;
|
|
48
|
+
const storeDependencies = el.getAttribute('data-store-dependencies');
|
|
49
|
+
const storeDependenciesArray = storeDependencies ? JSON.parse(storeDependencies) : [];
|
|
50
|
+
const railsContext = getRailsContext();
|
|
51
|
+
if (!railsContext)
|
|
52
|
+
return;
|
|
53
|
+
// Wait for all store dependencies to be loaded
|
|
54
|
+
this.renderPromise = Promise.all(storeDependenciesArray.map((storeName) => StoreRegistry.getOrWaitForStore(storeName))).then(() => {
|
|
55
|
+
if (this.state === 'unmounted')
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
return this.render(el, railsContext);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or
|
|
62
|
+
* delegates to a renderer registered by the user.
|
|
63
|
+
*/
|
|
64
|
+
async render(el, railsContext) {
|
|
65
|
+
const isImmediateHydrationRequested = el.getAttribute('data-immediate-hydration') === 'true';
|
|
66
|
+
const hasProLicense = railsContext.rorPro;
|
|
67
|
+
// Handle immediate_hydration feature usage without Pro license
|
|
68
|
+
if (isImmediateHydrationRequested && !hasProLicense) {
|
|
69
|
+
console.warn(IMMEDIATE_HYDRATION_PRO_WARNING);
|
|
70
|
+
// Fallback to standard behavior: wait for page load before hydrating
|
|
71
|
+
if (document.readyState === 'loading') {
|
|
72
|
+
await new Promise((resolve) => {
|
|
73
|
+
onPageLoaded(resolve);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// This must match lib/react_on_rails/helper.rb
|
|
78
|
+
const name = el.getAttribute('data-component-name') || '';
|
|
79
|
+
const { domNodeId } = this;
|
|
80
|
+
const props = el.textContent !== null ? JSON.parse(el.textContent) : {};
|
|
81
|
+
const trace = el.getAttribute('data-trace') === 'true';
|
|
82
|
+
try {
|
|
83
|
+
const domNode = document.getElementById(domNodeId);
|
|
84
|
+
if (domNode) {
|
|
85
|
+
const componentObj = await ComponentRegistry.getOrWaitForComponent(name);
|
|
86
|
+
if (this.state === 'unmounted') {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if ((await delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) ||
|
|
90
|
+
// @ts-expect-error The state can change while awaiting delegateToRenderer
|
|
91
|
+
this.state === 'unmounted') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Hydrate if available and was server rendered
|
|
95
|
+
const shouldHydrate = supportsHydrate && !!domNode.innerHTML;
|
|
96
|
+
const reactElementOrRouterResult = createReactOutput({
|
|
97
|
+
componentObj,
|
|
98
|
+
props,
|
|
99
|
+
domNodeId,
|
|
100
|
+
trace,
|
|
101
|
+
railsContext,
|
|
102
|
+
shouldHydrate,
|
|
103
|
+
});
|
|
104
|
+
if (isServerRenderHash(reactElementOrRouterResult)) {
|
|
105
|
+
throw new Error(`\
|
|
106
|
+
You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)}
|
|
107
|
+
You should return a React.Component always for the client side entry point.`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const rootOrElement = reactHydrateOrRender(domNode, reactElementOrRouterResult, shouldHydrate);
|
|
111
|
+
this.state = 'rendered';
|
|
112
|
+
if (supportsRootApi) {
|
|
113
|
+
this.root = rootOrElement;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
const error = e instanceof Error ? e : new Error(e?.toString() ?? 'Unknown error');
|
|
120
|
+
console.error(error.message);
|
|
121
|
+
error.message = `ReactOnRails encountered an error while rendering component: ${name}. See above error message.`;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
unmount() {
|
|
126
|
+
if (this.state === 'rendering') {
|
|
127
|
+
this.state = 'unmounted';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.state = 'unmounted';
|
|
131
|
+
if (supportsRootApi) {
|
|
132
|
+
this.root?.unmount();
|
|
133
|
+
this.root = undefined;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const domNode = document.getElementById(this.domNodeId);
|
|
137
|
+
if (!domNode) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
unmountComponentAtNode(domNode);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
const error = e instanceof Error ? e : new Error('Unknown error');
|
|
145
|
+
console.info(`Caught error calling unmountComponentAtNode: ${error.message} for domNode`, domNode, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
waitUntilRendered() {
|
|
150
|
+
if (this.state === 'rendering' && this.renderPromise) {
|
|
151
|
+
return this.renderPromise;
|
|
152
|
+
}
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
class StoreRenderer {
|
|
157
|
+
constructor(storeDataElement) {
|
|
158
|
+
this.state = 'hydrating';
|
|
159
|
+
const railsContext = getRailsContext();
|
|
160
|
+
if (!railsContext) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const name = storeDataElement.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
|
|
164
|
+
const props = storeDataElement.textContent !== null
|
|
165
|
+
? JSON.parse(storeDataElement.textContent)
|
|
166
|
+
: {};
|
|
167
|
+
this.hydratePromise = this.hydrate(railsContext, name, props);
|
|
168
|
+
}
|
|
169
|
+
async hydrate(railsContext, name, props) {
|
|
170
|
+
const storeGenerator = await StoreRegistry.getOrWaitForStoreGenerator(name);
|
|
171
|
+
if (this.state === 'unmounted') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const store = storeGenerator(props, railsContext);
|
|
175
|
+
StoreRegistry.setStore(name, store);
|
|
176
|
+
this.state = 'hydrated';
|
|
177
|
+
}
|
|
178
|
+
waitUntilHydrated() {
|
|
179
|
+
if (this.state === 'hydrating' && this.hydratePromise) {
|
|
180
|
+
return this.hydratePromise;
|
|
181
|
+
}
|
|
182
|
+
return Promise.resolve();
|
|
183
|
+
}
|
|
184
|
+
unmount() {
|
|
185
|
+
this.state = 'unmounted';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const renderedRoots = new Map();
|
|
189
|
+
export function renderOrHydrateComponent(domIdOrElement) {
|
|
190
|
+
const domId = getDomId(domIdOrElement);
|
|
191
|
+
debugTurbolinks('renderOrHydrateComponent', domId);
|
|
192
|
+
let root = renderedRoots.get(domId);
|
|
193
|
+
if (!root) {
|
|
194
|
+
root = new ComponentRenderer(domIdOrElement);
|
|
195
|
+
renderedRoots.set(domId, root);
|
|
196
|
+
}
|
|
197
|
+
return root.waitUntilRendered();
|
|
198
|
+
}
|
|
199
|
+
async function forAllElementsAsync(selector, callback) {
|
|
200
|
+
const els = document.querySelectorAll(selector);
|
|
201
|
+
await Promise.all(Array.from(els).map(callback));
|
|
202
|
+
}
|
|
203
|
+
export const renderOrHydrateImmediateHydratedComponents = () => forAllElementsAsync('.js-react-on-rails-component[data-immediate-hydration="true"]', renderOrHydrateComponent);
|
|
204
|
+
export const renderOrHydrateAllComponents = () => forAllElementsAsync('.js-react-on-rails-component', renderOrHydrateComponent);
|
|
205
|
+
function unmountAllComponents() {
|
|
206
|
+
renderedRoots.forEach((root) => root.unmount());
|
|
207
|
+
renderedRoots.clear();
|
|
208
|
+
resetRailsContext();
|
|
209
|
+
}
|
|
210
|
+
const storeRenderers = new Map();
|
|
211
|
+
export async function hydrateStore(storeNameOrElement) {
|
|
212
|
+
const storeName = typeof storeNameOrElement === 'string'
|
|
213
|
+
? storeNameOrElement
|
|
214
|
+
: storeNameOrElement.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
|
|
215
|
+
let storeRenderer = storeRenderers.get(storeName);
|
|
216
|
+
if (!storeRenderer) {
|
|
217
|
+
const storeDataElement = typeof storeNameOrElement === 'string'
|
|
218
|
+
? document.querySelector(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}="${CSS.escape(storeNameOrElement)}"]`)
|
|
219
|
+
: storeNameOrElement;
|
|
220
|
+
if (!storeDataElement) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
storeRenderer = new StoreRenderer(storeDataElement);
|
|
224
|
+
storeRenderers.set(storeName, storeRenderer);
|
|
225
|
+
}
|
|
226
|
+
await storeRenderer.waitUntilHydrated();
|
|
227
|
+
}
|
|
228
|
+
export const hydrateImmediateHydratedStores = () => forAllElementsAsync(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}][data-immediate-hydration="true"]`, hydrateStore);
|
|
229
|
+
export const hydrateAllStores = () => forAllElementsAsync(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}]`, hydrateStore);
|
|
230
|
+
function unmountAllStores() {
|
|
231
|
+
storeRenderers.forEach((storeRenderer) => storeRenderer.unmount());
|
|
232
|
+
storeRenderers.clear();
|
|
233
|
+
}
|
|
234
|
+
export function unmountAll() {
|
|
235
|
+
unmountAllComponents();
|
|
236
|
+
unmountAllStores();
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=ClientSideRenderer.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type RegisteredComponent, type ReactComponentOrRenderFunction } from 'react-on-rails/types';
|
|
2
|
+
/**
|
|
3
|
+
* @param components { component1: component1, component2: component2, etc. }
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export declare function register(components: Record<string, ReactComponentOrRenderFunction>): void;
|
|
7
|
+
/**
|
|
8
|
+
* @param name
|
|
9
|
+
* @returns { name, component, isRenderFunction, isRenderer }
|
|
10
|
+
*/
|
|
11
|
+
export declare const get: (name: string) => RegisteredComponent;
|
|
12
|
+
export declare const getOrWaitForComponent: (name: string) => Promise<RegisteredComponent>;
|
|
13
|
+
/**
|
|
14
|
+
* Get a Map containing all registered components. Useful for debugging.
|
|
15
|
+
* @returns Map where key is the component name and values are the
|
|
16
|
+
* { name, component, renderFunction, isRenderer}
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare const components: () => Map<string, RegisteredComponent>;
|
|
20
|
+
/** @internal Exported only for tests */
|
|
21
|
+
export declare function clear(): void;
|
|
22
|
+
//# sourceMappingURL=ComponentRegistry.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Shakacode LLC
|
|
3
|
+
*
|
|
4
|
+
* This file is NOT licensed under the MIT (open source) license.
|
|
5
|
+
* It is part of the React on Rails Pro offering and is licensed separately.
|
|
6
|
+
*
|
|
7
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
8
|
+
* via any medium, is strictly prohibited without a valid license agreement
|
|
9
|
+
* from Shakacode LLC.
|
|
10
|
+
*
|
|
11
|
+
* For licensing terms, please see:
|
|
12
|
+
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
|
|
13
|
+
*/
|
|
14
|
+
import isRenderFunction from 'react-on-rails/isRenderFunction';
|
|
15
|
+
import CallbackRegistry from "./CallbackRegistry.js";
|
|
16
|
+
const componentRegistry = new CallbackRegistry('component');
|
|
17
|
+
/**
|
|
18
|
+
* @param components { component1: component1, component2: component2, etc. }
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export function register(components) {
|
|
22
|
+
Object.keys(components).forEach((name) => {
|
|
23
|
+
if (componentRegistry.has(name)) {
|
|
24
|
+
console.warn('Called register for component that is already registered', name);
|
|
25
|
+
}
|
|
26
|
+
const component = components[name];
|
|
27
|
+
if (!component) {
|
|
28
|
+
throw new Error(`Called register with null component named ${name}`);
|
|
29
|
+
}
|
|
30
|
+
const renderFunction = isRenderFunction(component);
|
|
31
|
+
const isRenderer = renderFunction && component.length === 3;
|
|
32
|
+
componentRegistry.set(name, {
|
|
33
|
+
name,
|
|
34
|
+
component,
|
|
35
|
+
renderFunction,
|
|
36
|
+
isRenderer,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @param name
|
|
42
|
+
* @returns { name, component, isRenderFunction, isRenderer }
|
|
43
|
+
*/
|
|
44
|
+
export const get = (name) => componentRegistry.get(name);
|
|
45
|
+
export const getOrWaitForComponent = (name) => componentRegistry.getOrWaitForItem(name);
|
|
46
|
+
/**
|
|
47
|
+
* Get a Map containing all registered components. Useful for debugging.
|
|
48
|
+
* @returns Map where key is the component name and values are the
|
|
49
|
+
* { name, component, renderFunction, isRenderer}
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
52
|
+
export const components = () => componentRegistry.getAll();
|
|
53
|
+
/** @internal Exported only for tests */
|
|
54
|
+
export function clear() {
|
|
55
|
+
componentRegistry.clear();
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=ComponentRegistry.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type PostSSRHook = () => void;
|
|
2
|
+
/**
|
|
3
|
+
* Post-SSR Hook Tracker - manages post-SSR hooks for a single request.
|
|
4
|
+
*
|
|
5
|
+
* This class provides a local alternative to the global hook management,
|
|
6
|
+
* allowing each request to have its own isolated hook tracker without sharing state.
|
|
7
|
+
*
|
|
8
|
+
* The tracker ensures that:
|
|
9
|
+
* - Hooks are executed exactly once when SSR ends
|
|
10
|
+
* - No hooks can be added after SSR has completed
|
|
11
|
+
* - Proper cleanup occurs to prevent memory leaks
|
|
12
|
+
*/
|
|
13
|
+
declare class PostSSRHookTracker {
|
|
14
|
+
private hooks;
|
|
15
|
+
private hasSSREnded;
|
|
16
|
+
/**
|
|
17
|
+
* Adds a hook to be executed when SSR ends for this request.
|
|
18
|
+
*
|
|
19
|
+
* @param hook - Function to call when SSR ends
|
|
20
|
+
* @throws Error if called after SSR has already ended
|
|
21
|
+
*/
|
|
22
|
+
addPostSSRHook(hook: PostSSRHook): void;
|
|
23
|
+
/**
|
|
24
|
+
* Notifies all registered hooks that SSR has ended and clears the hook list.
|
|
25
|
+
* This should be called exactly once when server-side rendering is complete.
|
|
26
|
+
*
|
|
27
|
+
* @throws Error if called multiple times
|
|
28
|
+
*/
|
|
29
|
+
notifySSREnd(): void;
|
|
30
|
+
}
|
|
31
|
+
export default PostSSRHookTracker;
|
|
32
|
+
//# sourceMappingURL=PostSSRHookTracker.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Shakacode LLC
|
|
3
|
+
*
|
|
4
|
+
* This file is NOT licensed under the MIT (open source) license.
|
|
5
|
+
* It is part of the React on Rails Pro offering and is licensed separately.
|
|
6
|
+
*
|
|
7
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
8
|
+
* via any medium, is strictly prohibited without a valid license agreement
|
|
9
|
+
* from Shakacode LLC.
|
|
10
|
+
*
|
|
11
|
+
* For licensing terms, please see:
|
|
12
|
+
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Post-SSR Hook Tracker - manages post-SSR hooks for a single request.
|
|
16
|
+
*
|
|
17
|
+
* This class provides a local alternative to the global hook management,
|
|
18
|
+
* allowing each request to have its own isolated hook tracker without sharing state.
|
|
19
|
+
*
|
|
20
|
+
* The tracker ensures that:
|
|
21
|
+
* - Hooks are executed exactly once when SSR ends
|
|
22
|
+
* - No hooks can be added after SSR has completed
|
|
23
|
+
* - Proper cleanup occurs to prevent memory leaks
|
|
24
|
+
*/
|
|
25
|
+
class PostSSRHookTracker {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.hooks = [];
|
|
28
|
+
this.hasSSREnded = false;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Adds a hook to be executed when SSR ends for this request.
|
|
32
|
+
*
|
|
33
|
+
* @param hook - Function to call when SSR ends
|
|
34
|
+
* @throws Error if called after SSR has already ended
|
|
35
|
+
*/
|
|
36
|
+
addPostSSRHook(hook) {
|
|
37
|
+
if (this.hasSSREnded) {
|
|
38
|
+
console.error('Cannot add post-SSR hook: SSR has already ended for this request. ' +
|
|
39
|
+
'Hooks must be registered before or during the SSR process.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.hooks.push(hook);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Notifies all registered hooks that SSR has ended and clears the hook list.
|
|
46
|
+
* This should be called exactly once when server-side rendering is complete.
|
|
47
|
+
*
|
|
48
|
+
* @throws Error if called multiple times
|
|
49
|
+
*/
|
|
50
|
+
notifySSREnd() {
|
|
51
|
+
if (this.hasSSREnded) {
|
|
52
|
+
console.warn('notifySSREnd() called multiple times. This may indicate a bug in the SSR lifecycle.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.hasSSREnded = true;
|
|
56
|
+
// Execute all hooks and handle any errors gracefully
|
|
57
|
+
this.hooks.forEach((hook, index) => {
|
|
58
|
+
try {
|
|
59
|
+
hook();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`Error executing post-SSR hook ${index}:`, error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Clear hooks to free memory
|
|
66
|
+
this.hooks = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export default PostSSRHookTracker;
|
|
70
|
+
//# sourceMappingURL=PostSSRHookTracker.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { ClientGetReactServerComponentProps } from './getReactServerComponent.client.ts';
|
|
3
|
+
type RSCContextType = {
|
|
4
|
+
getComponent: (componentName: string, componentProps: unknown) => Promise<ReactNode>;
|
|
5
|
+
refetchComponent: (componentName: string, componentProps: unknown) => Promise<ReactNode>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Creates a provider context for React Server Components.
|
|
9
|
+
*
|
|
10
|
+
* RSCProvider is a foundational component that:
|
|
11
|
+
* 1. Provides caching for server components to prevent redundant requests
|
|
12
|
+
* 2. Manages the fetching of server components through getComponent
|
|
13
|
+
* 3. Offers environment-agnostic access to server components
|
|
14
|
+
*
|
|
15
|
+
* This factory function accepts an environment-specific getServerComponent implementation,
|
|
16
|
+
* allowing it to work correctly in both client and server environments.
|
|
17
|
+
*
|
|
18
|
+
* @param railsContext - Context for the current request
|
|
19
|
+
* @param getServerComponent - Environment-specific function for fetching server components
|
|
20
|
+
* @returns A provider component that wraps children with RSC context
|
|
21
|
+
*
|
|
22
|
+
* @important This is an internal function. End users should not use this directly.
|
|
23
|
+
* Instead, use wrapServerComponentRenderer from 'react-on-rails/wrapServerComponentRenderer/client'
|
|
24
|
+
* for client-side rendering or 'react-on-rails/wrapServerComponentRenderer/server' for server-side rendering.
|
|
25
|
+
*/
|
|
26
|
+
export declare const createRSCProvider: ({ getServerComponent, }: {
|
|
27
|
+
getServerComponent: (props: ClientGetReactServerComponentProps) => Promise<ReactNode>;
|
|
28
|
+
}) => ({ children }: {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
/**
|
|
32
|
+
* Hook to access the RSC context within client components.
|
|
33
|
+
*
|
|
34
|
+
* This hook provides access to:
|
|
35
|
+
* - getComponent: For fetching and rendering server components
|
|
36
|
+
* - refetchComponent: For refetching server components
|
|
37
|
+
*
|
|
38
|
+
* It must be used within a component wrapped by RSCProvider (typically done
|
|
39
|
+
* automatically by wrapServerComponentRenderer).
|
|
40
|
+
*
|
|
41
|
+
* @returns The RSC context containing methods for working with server components
|
|
42
|
+
* @throws Error if used outside of an RSCProvider
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const { getComponent } = useRSC();
|
|
47
|
+
* const serverComponent = use(getComponent('MyServerComponent', props));
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare const useRSC: () => RSCContextType;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=RSCProvider.d.ts.map
|