react-on-rails 17.0.0-rc.5 → 17.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/lib/ClientRenderer.js +170 -37
- package/lib/StoreRegistry.d.ts +4 -0
- package/lib/StoreRegistry.js +11 -6
- package/lib/base/client.d.ts +1 -0
- package/lib/base/client.js +36 -175
- package/lib/base/full.js +7 -25
- package/lib/base/full.rsc.js +7 -13
- package/lib/capabilities/core.d.ts +2 -0
- package/lib/capabilities/core.js +3 -1
- package/lib/captureReactOwnerStack.d.ts +9 -0
- package/lib/captureReactOwnerStack.js +64 -0
- package/lib/createReactOutput.js +5 -2
- package/lib/errorUtils.d.ts +4 -0
- package/lib/errorUtils.js +50 -0
- package/lib/railsAction.d.ts +68 -0
- package/lib/railsAction.js +373 -0
- package/lib/rootErrorHandlers.js +72 -6
- package/lib/serverRenderUtils.d.ts +1 -2
- package/lib/serverRenderUtils.js +2 -49
- package/lib/types/index.d.ts +31 -13
- package/lib/types/index.js +13 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -81,11 +81,12 @@ ReactOnRails.register({
|
|
|
81
81
|
|
|
82
82
|
## Exports
|
|
83
83
|
|
|
84
|
-
| Export Path
|
|
85
|
-
|
|
|
86
|
-
| `react-on-rails`
|
|
87
|
-
| `react-on-rails/client`
|
|
88
|
-
| `react-on-rails/
|
|
84
|
+
| Export Path | Description |
|
|
85
|
+
| ---------------------------- | ------------------------------------ |
|
|
86
|
+
| `react-on-rails` | Full build with SSR utilities |
|
|
87
|
+
| `react-on-rails/client` | Client-only build (smaller, no SSR) |
|
|
88
|
+
| `react-on-rails/railsAction` | CSRF-aware typed Rails action caller |
|
|
89
|
+
| `react-on-rails/types` | TypeScript type exports |
|
|
89
90
|
|
|
90
91
|
## Rails-Side Setup
|
|
91
92
|
|
package/lib/ClientRenderer.js
CHANGED
|
@@ -8,6 +8,7 @@ import { onPageUnloaded } from "./pageLifecycle.js";
|
|
|
8
8
|
import { supportsRootApi, unmountComponentAtNode } from "./reactApis.cjs";
|
|
9
9
|
import { isRendererTeardownResult } from "./rendererTeardown.js";
|
|
10
10
|
import { buildRootErrorCallbackOptions } from "./rootErrorHandlers.js";
|
|
11
|
+
import { convertToError } from "./errorUtils.js";
|
|
11
12
|
import { isThenable } from "./isThenable.js";
|
|
12
13
|
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
|
|
13
14
|
// Track all rendered roots for cleanup
|
|
@@ -16,8 +17,8 @@ const renderedRoots = new Map();
|
|
|
16
17
|
* Invokes a renderer teardown, swallowing async rejections so a failing teardown cannot produce an
|
|
17
18
|
* unhandled promise rejection. Synchronous throws propagate to the caller's try/catch. `domNodeId`
|
|
18
19
|
* is included in the log so a failure can be traced to its mount.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* MIRROR OF: packages/react-on-rails-pro/src/ClientSideRenderer.ts (sibling helper). If you change
|
|
21
|
+
* the error-handling logic or log format here, update that copy too.
|
|
21
22
|
*/
|
|
22
23
|
function invokeRendererTeardown(teardown, domNodeId) {
|
|
23
24
|
if (!teardown)
|
|
@@ -39,6 +40,10 @@ function invokeRendererTeardown(teardown, domNodeId) {
|
|
|
39
40
|
* abort cleanup of the remaining entries.
|
|
40
41
|
*/
|
|
41
42
|
function teardownEntry(entry, domNodeId) {
|
|
43
|
+
if (entry.kind === 'scheduled') {
|
|
44
|
+
entry.cancel();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
42
47
|
if (entry.kind === 'renderer') {
|
|
43
48
|
invokeRendererTeardown(entry.teardown, domNodeId);
|
|
44
49
|
return;
|
|
@@ -52,6 +57,15 @@ function teardownEntry(entry, domNodeId) {
|
|
|
52
57
|
unmountComponentAtNode(entry.domNode);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
60
|
+
function teardownErrorLabel(entry, domNodeId) {
|
|
61
|
+
if (entry.kind === 'renderer') {
|
|
62
|
+
return `Error in renderer teardown for dom node "${domNodeId}":`;
|
|
63
|
+
}
|
|
64
|
+
if (entry.kind === 'scheduled') {
|
|
65
|
+
return `Error canceling scheduled render for dom node "${domNodeId}":`;
|
|
66
|
+
}
|
|
67
|
+
return `Error unmounting component for dom node "${domNodeId}":`;
|
|
68
|
+
}
|
|
55
69
|
function initializeStore(el, railsContext) {
|
|
56
70
|
const name = el.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
|
|
57
71
|
const props = el.textContent !== null ? JSON.parse(el.textContent) : {};
|
|
@@ -68,6 +82,103 @@ function forEachStore(railsContext) {
|
|
|
68
82
|
function domNodeIdForEl(el) {
|
|
69
83
|
return el.getAttribute('data-dom-id') || '';
|
|
70
84
|
}
|
|
85
|
+
function hydrateOnForEl(el) {
|
|
86
|
+
const hydrateOn = el.getAttribute('data-hydrate-on');
|
|
87
|
+
if (!hydrateOn || hydrateOn === 'immediate' || hydrateOn === 'visible' || hydrateOn === 'idle') {
|
|
88
|
+
return (hydrateOn || 'immediate');
|
|
89
|
+
}
|
|
90
|
+
console.warn(`[react-on-rails] Unsupported hydrate_on: ${hydrateOn}.`);
|
|
91
|
+
return 'immediate';
|
|
92
|
+
}
|
|
93
|
+
function hasObservableArea(element) {
|
|
94
|
+
const rects = element.getClientRects();
|
|
95
|
+
for (let index = 0; index < rects.length; index += 1) {
|
|
96
|
+
const rect = rects[index];
|
|
97
|
+
if (rect.width > 0 && rect.height > 0)
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const boundingRect = element.getBoundingClientRect();
|
|
101
|
+
return boundingRect.width > 0 && boundingRect.height > 0;
|
|
102
|
+
}
|
|
103
|
+
function scheduleTimeout(callback, delay) {
|
|
104
|
+
const timeoutId = window.setTimeout(callback, delay);
|
|
105
|
+
return () => window.clearTimeout(timeoutId);
|
|
106
|
+
}
|
|
107
|
+
function scheduleWhenVisible(domNode, callback) {
|
|
108
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
109
|
+
console.warn('[react-on-rails] No IntersectionObserver.');
|
|
110
|
+
return scheduleTimeout(callback, 0);
|
|
111
|
+
}
|
|
112
|
+
if (!domNode.hasChildNodes() && !hasObservableArea(domNode)) {
|
|
113
|
+
return scheduleTimeout(callback, 0);
|
|
114
|
+
}
|
|
115
|
+
const observer = new IntersectionObserver((entries) => {
|
|
116
|
+
// Disconnect early if the target was removed outside Turbo navigation to avoid a
|
|
117
|
+
// leaked observer holding a live reference to a detached node.
|
|
118
|
+
const target = entries[0]?.target;
|
|
119
|
+
if (target && !target.isConnected) {
|
|
120
|
+
observer.disconnect();
|
|
121
|
+
callback();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const isVisible = entries.some((entry) => entry.isIntersecting || entry.intersectionRatio > 0);
|
|
125
|
+
if (!isVisible)
|
|
126
|
+
return;
|
|
127
|
+
observer.disconnect();
|
|
128
|
+
callback();
|
|
129
|
+
}, { rootMargin: '200px 0px' });
|
|
130
|
+
observer.observe(domNode);
|
|
131
|
+
return () => {
|
|
132
|
+
observer.disconnect();
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function scheduleWhenIdle(callback) {
|
|
136
|
+
const idleWindow = window;
|
|
137
|
+
if (typeof idleWindow.requestIdleCallback === 'function') {
|
|
138
|
+
const idleCallbackId = idleWindow.requestIdleCallback(callback, { timeout: 2000 });
|
|
139
|
+
return () => {
|
|
140
|
+
if (typeof idleWindow.cancelIdleCallback === 'function') {
|
|
141
|
+
idleWindow.cancelIdleCallback(idleCallbackId);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Safari lacks requestIdleCallback; 50 ms defers past the current frame without a layout-blocking
|
|
146
|
+
// setTimeout(0) but still far enough before most long-idle periods (matching common polyfill defaults).
|
|
147
|
+
return scheduleTimeout(callback, 50);
|
|
148
|
+
}
|
|
149
|
+
function scheduleHydration(hydrateOn, domNode, callback) {
|
|
150
|
+
if (hydrateOn === 'visible') {
|
|
151
|
+
return scheduleWhenVisible(domNode, callback);
|
|
152
|
+
}
|
|
153
|
+
if (hydrateOn === 'idle') {
|
|
154
|
+
return scheduleWhenIdle(callback);
|
|
155
|
+
}
|
|
156
|
+
// Defensive: `renderElement` short-circuits for `immediate` before calling `scheduleHydration`, so
|
|
157
|
+
// this branch is normally unreachable; it acts as a safe fallback guard for future callers.
|
|
158
|
+
callback();
|
|
159
|
+
return () => { };
|
|
160
|
+
}
|
|
161
|
+
// Logs a normalized copy of the original thrown value, then returns a fresh ReactOnRails-prefixed
|
|
162
|
+
// error for callers to throw/report. This must not mutate the original value: user code can throw
|
|
163
|
+
// strings, null, cross-realm errors, or frozen Error instances, and delayed hydration catches run
|
|
164
|
+
// inside scheduler callbacks where an error reporter that throws would escape the callback.
|
|
165
|
+
function prepareRenderError(componentName, error) {
|
|
166
|
+
const originalError = convertToError(error);
|
|
167
|
+
console.error(originalError);
|
|
168
|
+
const renderError = new Error(`ReactOnRails encountered an error while rendering component: ${componentName}. See above error message.`);
|
|
169
|
+
renderError.cause = originalError;
|
|
170
|
+
if (typeof originalError.stack === 'string') {
|
|
171
|
+
renderError.stack = originalError.stack;
|
|
172
|
+
}
|
|
173
|
+
return renderError;
|
|
174
|
+
}
|
|
175
|
+
function raiseRenderError(componentName, error) {
|
|
176
|
+
throw prepareRenderError(componentName, error);
|
|
177
|
+
}
|
|
178
|
+
function reportRenderError(componentName, error) {
|
|
179
|
+
// prepareRenderError already logged the original error; do not log again (avoids the double-log).
|
|
180
|
+
prepareRenderError(componentName, error);
|
|
181
|
+
}
|
|
71
182
|
function delegateToRenderer(componentObj, props, railsContext, domNodeId, trace) {
|
|
72
183
|
const { name, component, isRenderer } = componentObj;
|
|
73
184
|
if (isRenderer) {
|
|
@@ -160,7 +271,9 @@ function trackRendererMount(domNodeId, domNode, result) {
|
|
|
160
271
|
* delegates to a renderer registered by the user.
|
|
161
272
|
*/
|
|
162
273
|
function renderElement(el, railsContext) {
|
|
163
|
-
// This must match lib/react_on_rails/
|
|
274
|
+
// This must match the data-* attributes emitted by lib/react_on_rails/pro_helper.rb.
|
|
275
|
+
// Covered end-to-end by every spec/dummy client-rendering test (renaming either side fails
|
|
276
|
+
// loudly), so no MIRROR VALUES guard is added here.
|
|
164
277
|
const name = el.getAttribute('data-component-name') || '';
|
|
165
278
|
const domNodeId = domNodeIdForEl(el);
|
|
166
279
|
const props = el.textContent !== null ? JSON.parse(el.textContent) : {};
|
|
@@ -173,10 +286,14 @@ function renderElement(el, railsContext) {
|
|
|
173
286
|
// (e.g., for asynchronously loaded content)
|
|
174
287
|
const existing = renderedRoots.get(domNodeId);
|
|
175
288
|
if (existing) {
|
|
176
|
-
// Only skip if it's the exact same DOM node
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
289
|
+
// Only skip if it's the exact same DOM node, it's still connected to the document, AND it has
|
|
290
|
+
// actually mounted. A `scheduled` entry has not mounted yet: if its node was detached and later
|
|
291
|
+
// reattached (e.g. Turbo cache restore or a DOM move), its IntersectionObserver was disconnected
|
|
292
|
+
// and will never re-observe, so we must fall through to cancel the stale schedule and re-schedule
|
|
293
|
+
// fresh rather than skip (which would leave the island permanently non-interactive).
|
|
294
|
+
// If the node was replaced (e.g., via innerHTML or Turbo), we likewise need to unmount/cancel the
|
|
295
|
+
// old entry and re-render to the new node to prevent memory leaks and ensure rendering works.
|
|
296
|
+
const sameNode = existing.kind !== 'scheduled' && existing.domNode === domNode && existing.domNode.isConnected;
|
|
180
297
|
if (sameNode) {
|
|
181
298
|
if (trace) {
|
|
182
299
|
console.log(`Skipping already rendered component: ${name} (dom id: ${domNodeId})`);
|
|
@@ -192,10 +309,7 @@ function renderElement(el, railsContext) {
|
|
|
192
309
|
// Surface the failure unconditionally (matching unmountAllComponents) so a teardown/unmount
|
|
193
310
|
// error on node replacement is as visible as one on page unload, using the same greppable
|
|
194
311
|
// labels. We still continue: the old mount may leak, but the new node must be rendered.
|
|
195
|
-
|
|
196
|
-
? `Error in renderer teardown for dom node "${domNodeId}":`
|
|
197
|
-
: `Error unmounting component for dom node "${domNodeId}":`;
|
|
198
|
-
console.error(label, unmountError);
|
|
312
|
+
console.error(teardownErrorLabel(existing, domNodeId), unmountError);
|
|
199
313
|
}
|
|
200
314
|
renderedRoots.delete(domNodeId);
|
|
201
315
|
}
|
|
@@ -207,38 +321,62 @@ function renderElement(el, railsContext) {
|
|
|
207
321
|
trackRendererMount(domNodeId, domNode, delegation.result);
|
|
208
322
|
return;
|
|
209
323
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
324
|
+
const mountReactRoot = () => {
|
|
325
|
+
// Hydrate if the DOM node has content (server-rendered HTML)
|
|
326
|
+
// Since we skip already-rendered components above, this check now correctly
|
|
327
|
+
// identifies only server-rendered content, not previously client-rendered content
|
|
328
|
+
const shouldHydrate = !!domNode.innerHTML;
|
|
329
|
+
const reactElementOrRouterResult = createReactOutput({
|
|
330
|
+
componentObj,
|
|
331
|
+
props,
|
|
332
|
+
domNodeId,
|
|
333
|
+
trace,
|
|
334
|
+
railsContext,
|
|
335
|
+
shouldHydrate,
|
|
336
|
+
});
|
|
337
|
+
if (isServerRenderHash(reactElementOrRouterResult)) {
|
|
338
|
+
throw new Error(`\
|
|
224
339
|
You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)}
|
|
225
340
|
You should return a React.Component always for the client side entry point.`);
|
|
226
|
-
|
|
227
|
-
else {
|
|
341
|
+
}
|
|
228
342
|
const root = reactHydrateOrRender(domNode, reactElementOrRouterResult, shouldHydrate,
|
|
229
343
|
// Attach user-registered root error callbacks (and the dev-mode hydration-mismatch
|
|
230
344
|
// logger) to every root, enriched with this mount's component name and dom id.
|
|
231
345
|
buildRootErrorCallbackOptions({ componentName: name || undefined, domNodeId: domNodeId || undefined }, shouldHydrate));
|
|
232
346
|
// Track the root for cleanup
|
|
233
347
|
renderedRoots.set(domNodeId, { kind: 'react', root, domNode });
|
|
348
|
+
};
|
|
349
|
+
const hydrateOn = hydrateOnForEl(el);
|
|
350
|
+
if (hydrateOn === 'immediate') {
|
|
351
|
+
mountReactRoot();
|
|
352
|
+
return;
|
|
234
353
|
}
|
|
354
|
+
let scheduledEntry;
|
|
355
|
+
const runScheduledRender = () => {
|
|
356
|
+
if (renderedRoots.get(domNodeId) !== scheduledEntry)
|
|
357
|
+
return;
|
|
358
|
+
if (!domNode.isConnected) {
|
|
359
|
+
renderedRoots.delete(domNodeId);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
mountReactRoot();
|
|
364
|
+
}
|
|
365
|
+
catch (scheduledError) {
|
|
366
|
+
renderedRoots.delete(domNodeId);
|
|
367
|
+
reportRenderError(name, scheduledError);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
scheduledEntry = {
|
|
371
|
+
kind: 'scheduled',
|
|
372
|
+
domNode,
|
|
373
|
+
cancel: scheduleHydration(hydrateOn, domNode, runScheduledRender),
|
|
374
|
+
};
|
|
375
|
+
renderedRoots.set(domNodeId, scheduledEntry);
|
|
235
376
|
}
|
|
236
377
|
}
|
|
237
378
|
catch (e) {
|
|
238
|
-
|
|
239
|
-
console.error(error.message);
|
|
240
|
-
error.message = `ReactOnRails encountered an error while rendering component: ${name}. See above error message.`;
|
|
241
|
-
throw error;
|
|
379
|
+
raiseRenderError(name, e);
|
|
242
380
|
}
|
|
243
381
|
}
|
|
244
382
|
/**
|
|
@@ -295,12 +433,7 @@ function unmountAllComponents() {
|
|
|
295
433
|
teardownEntry(entry, domNodeId);
|
|
296
434
|
}
|
|
297
435
|
catch (error) {
|
|
298
|
-
|
|
299
|
-
// whether the teardown threw synchronously (here) or rejected (invokeRendererTeardown).
|
|
300
|
-
const label = entry.kind === 'renderer'
|
|
301
|
-
? `Error in renderer teardown for dom node "${domNodeId}":`
|
|
302
|
-
: `Error unmounting component for dom node "${domNodeId}":`;
|
|
303
|
-
console.error(label, error);
|
|
436
|
+
console.error(teardownErrorLabel(entry, domNodeId), error);
|
|
304
437
|
}
|
|
305
438
|
});
|
|
306
439
|
renderedRoots.clear();
|
package/lib/StoreRegistry.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ declare const _default: {
|
|
|
29
29
|
* Internally used function to completely clear hydratedStores Map.
|
|
30
30
|
*/
|
|
31
31
|
clearHydratedStores(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Internally used function to completely clear registeredStoreGenerators Map.
|
|
34
|
+
*/
|
|
35
|
+
clearStoreGenerators(): void;
|
|
32
36
|
/**
|
|
33
37
|
* Get a Map containing all registered store generators. Useful for debugging.
|
|
34
38
|
* @returns Map where key is the component name and values are the store generators.
|
package/lib/StoreRegistry.js
CHANGED
|
@@ -33,6 +33,9 @@ export default {
|
|
|
33
33
|
if (hydratedStores.has(name)) {
|
|
34
34
|
return hydratedStores.get(name);
|
|
35
35
|
}
|
|
36
|
+
if (!throwIfMissing) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
36
39
|
const storeKeys = Array.from(hydratedStores.keys()).join(', ');
|
|
37
40
|
if (storeKeys.length === 0) {
|
|
38
41
|
const msg = `There are no stores hydrated and you are requesting the store ${name}.
|
|
@@ -42,12 +45,8 @@ This can happen if you are server rendering and either:
|
|
|
42
45
|
2. You do not render redux_store_hydration_data anywhere on your page.`;
|
|
43
46
|
throw new Error(msg);
|
|
44
47
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
throw new Error(`Could not find hydrated store with name '${name}'. ` +
|
|
48
|
-
`Hydrated store names include [${storeKeys}].`);
|
|
49
|
-
}
|
|
50
|
-
return undefined;
|
|
48
|
+
console.log('storeKeys', storeKeys);
|
|
49
|
+
throw new Error(`Could not find hydrated store with name '${name}'. Hydrated store names include [${storeKeys}].`);
|
|
51
50
|
},
|
|
52
51
|
/**
|
|
53
52
|
* Internally used function to get the store creator that was passed to `register`.
|
|
@@ -77,6 +76,12 @@ This can happen if you are server rendering and either:
|
|
|
77
76
|
clearHydratedStores() {
|
|
78
77
|
hydratedStores.clear();
|
|
79
78
|
},
|
|
79
|
+
/**
|
|
80
|
+
* Internally used function to completely clear registeredStoreGenerators Map.
|
|
81
|
+
*/
|
|
82
|
+
clearStoreGenerators() {
|
|
83
|
+
registeredStoreGenerators.clear();
|
|
84
|
+
},
|
|
80
85
|
/**
|
|
81
86
|
* Get a Map containing all registered store generators. Useful for debugging.
|
|
82
87
|
* @returns Map where key is the component name and values are the store generators.
|
package/lib/base/client.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ interface Registries {
|
|
|
16
16
|
getStoreGenerator: (name: string) => StoreGenerator;
|
|
17
17
|
setStore: (name: string, store: Store) => void;
|
|
18
18
|
clearHydratedStores: () => void;
|
|
19
|
+
clearStoreGenerators: () => void;
|
|
19
20
|
storeGenerators: () => Map<string, StoreGenerator>;
|
|
20
21
|
stores: () => Map<string, Store>;
|
|
21
22
|
};
|
package/lib/base/client.js
CHANGED
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
* @deprecated Use `capabilities/core.ts` instead. This file is kept for backward compatibility
|
|
3
3
|
* with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/client`.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import { createCoreCapability } from "../capabilities/core.js";
|
|
6
|
+
// Pro-only methods that `createCoreCapability` includes as stubs but the historical base
|
|
7
|
+
// surface omits. This list drives the runtime key-stripping in `createBaseClientObject`.
|
|
8
|
+
// `satisfies readonly (keyof ReactOnRailsInternal)[]` makes a typo or a stale name a compile
|
|
9
|
+
// error, and the `assertProOnlyMethodsMatchOmit` guard inside `createBaseClientObject` pins this
|
|
10
|
+
// list to the `Omit<...>` in `BaseClientObjectType` so the runtime list and the exported type can
|
|
11
|
+
// never drift.
|
|
12
|
+
const PRO_ONLY_METHODS = [
|
|
13
|
+
'getOrWaitForComponent',
|
|
14
|
+
'getOrWaitForStore',
|
|
15
|
+
'getOrWaitForStoreGenerator',
|
|
16
|
+
'reactOnRailsStoreLoaded',
|
|
17
|
+
'streamServerRenderedReactComponent',
|
|
18
|
+
'serverRenderRSCReactComponent',
|
|
19
|
+
'addAsyncPropsCapabilityToComponentProps',
|
|
20
|
+
'getOrCreateAsyncPropsManager',
|
|
21
|
+
];
|
|
17
22
|
// Cache to track created objects and their registries
|
|
18
23
|
let cachedObject = null;
|
|
19
24
|
let cachedRegistries = null;
|
|
20
25
|
export function createBaseClientObject(registries, currentObject = null) {
|
|
21
|
-
const { ComponentRegistry, StoreRegistry } = registries;
|
|
22
26
|
// Error detection: currentObject is null but we have a cached object
|
|
23
27
|
// This indicates webpack misconfiguration (multiple runtime chunks)
|
|
24
28
|
if (currentObject === null && cachedObject !== null) {
|
|
@@ -60,168 +64,25 @@ Fix: Use only react-on-rails OR react-on-rails-pro, not both.`);
|
|
|
60
64
|
if (cachedObject !== null) {
|
|
61
65
|
return cachedObject;
|
|
62
66
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const { traceTurbolinks, turbo, debugMode, logComponentRegistration, rootErrorHandlers, ...rest } = newOptions;
|
|
83
|
-
if (typeof traceTurbolinks !== 'undefined') {
|
|
84
|
-
this.options.traceTurbolinks = traceTurbolinks;
|
|
85
|
-
}
|
|
86
|
-
if (typeof turbo !== 'undefined') {
|
|
87
|
-
this.options.turbo = turbo;
|
|
88
|
-
}
|
|
89
|
-
if (typeof debugMode !== 'undefined') {
|
|
90
|
-
this.options.debugMode = debugMode;
|
|
91
|
-
if (debugMode) {
|
|
92
|
-
console.log('[ReactOnRails] Debug mode enabled');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (typeof logComponentRegistration !== 'undefined') {
|
|
96
|
-
this.options.logComponentRegistration = logComponentRegistration;
|
|
97
|
-
if (logComponentRegistration) {
|
|
98
|
-
console.log('[ReactOnRails] Component registration logging enabled');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (Object.prototype.hasOwnProperty.call(newOptions, 'rootErrorHandlers')) {
|
|
102
|
-
// MUST SYNC: sibling implementation exists in packages/react-on-rails/src/capabilities/core.ts.
|
|
103
|
-
// Validates and merges the handlers per key (partial updates keep previously registered
|
|
104
|
-
// callbacks); warns when the React runtime cannot support them. Store the merged result so
|
|
105
|
-
// `option('rootErrorHandlers')` reflects the effective registration.
|
|
106
|
-
if (typeof rootErrorHandlers === 'undefined') {
|
|
107
|
-
resetRootErrorHandlers();
|
|
108
|
-
this.options.rootErrorHandlers = undefined;
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
setRootErrorHandlers(rootErrorHandlers);
|
|
112
|
-
this.options.rootErrorHandlers = getRootErrorHandlers();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (Object.keys(rest).length > 0) {
|
|
116
|
-
throw new Error(`Invalid options passed to ReactOnRails.options: ${JSON.stringify(rest)}`);
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
option(key) {
|
|
120
|
-
return this.options[key];
|
|
121
|
-
},
|
|
122
|
-
buildConsoleReplay() {
|
|
123
|
-
return buildConsoleReplay();
|
|
124
|
-
},
|
|
125
|
-
getConsoleReplayScript() {
|
|
126
|
-
return consoleReplay();
|
|
127
|
-
},
|
|
128
|
-
resetOptions() {
|
|
129
|
-
this.options = { ...DEFAULT_OPTIONS };
|
|
130
|
-
resetRootErrorHandlers();
|
|
131
|
-
},
|
|
132
|
-
// ===================================================================
|
|
133
|
-
// REGISTRY METHOD IMPLEMENTATIONS - Using provided registries
|
|
134
|
-
// ===================================================================
|
|
135
|
-
register(components) {
|
|
136
|
-
if (this.options.debugMode || this.options.logComponentRegistration) {
|
|
137
|
-
// Use performance.now() if available, otherwise fallback to Date.now()
|
|
138
|
-
const perf = typeof performance !== 'undefined' ? performance : { now: () => Date.now() };
|
|
139
|
-
const startTime = perf.now();
|
|
140
|
-
const componentNames = Object.keys(components);
|
|
141
|
-
console.log(`[ReactOnRails] Registering ${componentNames.length} component(s): ${componentNames.join(', ')}`);
|
|
142
|
-
ComponentRegistry.register(components);
|
|
143
|
-
const endTime = perf.now();
|
|
144
|
-
console.log(`[ReactOnRails] Component registration completed in ${(endTime - startTime).toFixed(2)}ms`);
|
|
145
|
-
// Log individual component details if in full debug mode
|
|
146
|
-
if (this.options.debugMode) {
|
|
147
|
-
componentNames.forEach((name) => {
|
|
148
|
-
const component = components[name];
|
|
149
|
-
const registrationMetric = componentRegistrationMetric(component);
|
|
150
|
-
console.log(`[ReactOnRails] ✅ Registered: ${name} (${registrationMetric.value} ${registrationMetric.label})`);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
ComponentRegistry.register(components);
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
registerStore(stores) {
|
|
159
|
-
this.registerStoreGenerators(stores);
|
|
160
|
-
},
|
|
161
|
-
registerStoreGenerators(storeGenerators) {
|
|
162
|
-
if (!storeGenerators) {
|
|
163
|
-
throw new Error('Called ReactOnRails.registerStoreGenerators with a null or undefined, rather than ' +
|
|
164
|
-
'an Object with keys being the store names and the values are the store generators.');
|
|
165
|
-
}
|
|
166
|
-
StoreRegistry.register(storeGenerators);
|
|
167
|
-
},
|
|
168
|
-
getStore(name, throwIfMissing = true) {
|
|
169
|
-
return StoreRegistry.getStore(name, throwIfMissing);
|
|
170
|
-
},
|
|
171
|
-
getStoreGenerator(name) {
|
|
172
|
-
return StoreRegistry.getStoreGenerator(name);
|
|
173
|
-
},
|
|
174
|
-
setStore(name, store) {
|
|
175
|
-
StoreRegistry.setStore(name, store);
|
|
176
|
-
},
|
|
177
|
-
clearHydratedStores() {
|
|
178
|
-
StoreRegistry.clearHydratedStores();
|
|
179
|
-
},
|
|
180
|
-
getComponent(name) {
|
|
181
|
-
return ComponentRegistry.get(name);
|
|
182
|
-
},
|
|
183
|
-
registeredComponents() {
|
|
184
|
-
return ComponentRegistry.components();
|
|
185
|
-
},
|
|
186
|
-
storeGenerators() {
|
|
187
|
-
return StoreRegistry.storeGenerators();
|
|
188
|
-
},
|
|
189
|
-
stores() {
|
|
190
|
-
return StoreRegistry.stores();
|
|
191
|
-
},
|
|
192
|
-
render(name, props, domNodeId, hydrate) {
|
|
193
|
-
const componentObj = ComponentRegistry.get(name);
|
|
194
|
-
const reactElement = createReactOutput({ componentObj, props, domNodeId });
|
|
195
|
-
return reactHydrateOrRender(document.getElementById(domNodeId), reactElement, hydrate, buildRootErrorCallbackOptions({ componentName: name || undefined, domNodeId: domNodeId || undefined }, hydrate));
|
|
196
|
-
},
|
|
197
|
-
// ===================================================================
|
|
198
|
-
// CLIENT-SIDE RENDERING STUBS - To be overridden by createReactOnRails
|
|
199
|
-
// ===================================================================
|
|
200
|
-
reactOnRailsPageLoaded() {
|
|
201
|
-
throw new Error('ReactOnRails.reactOnRailsPageLoaded is not initialized. This is a bug in react-on-rails.');
|
|
202
|
-
},
|
|
203
|
-
reactOnRailsComponentLoaded(domId) {
|
|
204
|
-
void domId; // Mark as used
|
|
205
|
-
throw new Error('ReactOnRails.reactOnRailsComponentLoaded is not initialized. This is a bug in react-on-rails.');
|
|
206
|
-
},
|
|
207
|
-
// ===================================================================
|
|
208
|
-
// SSR STUBS - Will throw errors in client bundle, overridden in full
|
|
209
|
-
// ===================================================================
|
|
210
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
|
-
serverRenderReactComponent(...args) {
|
|
212
|
-
void args; // Mark as used
|
|
213
|
-
throw new Error('serverRenderReactComponent is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
|
|
214
|
-
},
|
|
215
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
-
handleError(...args) {
|
|
217
|
-
void args; // Mark as used
|
|
218
|
-
throw new Error('handleError is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
|
|
219
|
-
},
|
|
220
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
-
prepareRenderResult(...args) {
|
|
222
|
-
void args; // Mark as used
|
|
223
|
-
throw new Error('prepareRenderResult is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
|
|
224
|
-
},
|
|
67
|
+
const assertProOnlyMethodsMatchOmit = true;
|
|
68
|
+
void assertProOnlyMethodsMatchOmit;
|
|
69
|
+
// Delegate the core method implementations to `createCoreCapability` (the canonical source),
|
|
70
|
+
// then re-shape to the historical base surface: strip the Pro-only stubs (using the shared
|
|
71
|
+
// `PRO_ONLY_METHODS` list that the drift guard above pins to `BaseClientObjectType`, so runtime
|
|
72
|
+
// and type can never drift) and add the client-side lifecycle stubs that `createReactOnRails`
|
|
73
|
+
// overrides at initialization.
|
|
74
|
+
const proOnlyMethods = new Set(PRO_ONLY_METHODS);
|
|
75
|
+
const core = createCoreCapability(registries);
|
|
76
|
+
const obj = Object.fromEntries(Object.entries(core).filter(([key]) => !proOnlyMethods.has(key)));
|
|
77
|
+
// ===================================================================
|
|
78
|
+
// CLIENT-SIDE RENDERING STUBS - To be overridden by createReactOnRails
|
|
79
|
+
// ===================================================================
|
|
80
|
+
obj.reactOnRailsPageLoaded = () => {
|
|
81
|
+
throw new Error('ReactOnRails.reactOnRailsPageLoaded is not initialized. This is a bug in react-on-rails.');
|
|
82
|
+
};
|
|
83
|
+
obj.reactOnRailsComponentLoaded = (domId) => {
|
|
84
|
+
void domId; // Mark as used
|
|
85
|
+
throw new Error('ReactOnRails.reactOnRailsComponentLoaded is not initialized. This is a bug in react-on-rails.');
|
|
225
86
|
};
|
|
226
87
|
// Cache the object and registries
|
|
227
88
|
cachedObject = obj;
|
package/lib/base/full.js
CHANGED
|
@@ -2,35 +2,17 @@
|
|
|
2
2
|
* @deprecated Use `capabilities/ssr.ts` instead. This file is kept for backward compatibility
|
|
3
3
|
* with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/full`.
|
|
4
4
|
*/
|
|
5
|
+
// This is a thin compatibility shim: the SSR implementations live in `capabilities/ssr.ts`
|
|
6
|
+
// (`createSSRCapability`). This file composes them onto the base client object, preserving
|
|
7
|
+
// the historical `createBaseFullObject` surface old Pro consumers rely on.
|
|
5
8
|
import { createBaseClientObject } from "./client.js";
|
|
6
|
-
import
|
|
7
|
-
import serverRenderReactComponent from "../serverRenderReactComponent.js";
|
|
8
|
-
import { buildLengthPrefixedResult } from "../serverRenderUtils.js";
|
|
9
|
-
// Warn about bundle size when included in browser bundles
|
|
10
|
-
if (typeof window !== 'undefined') {
|
|
11
|
-
console.warn('Optimization opportunity: "react-on-rails" includes ~14KB of server-rendering code. ' +
|
|
12
|
-
'Browsers may not need it. See https://forum.shakacode.com/t/how-to-use-different-versions-of-a-file-for-client-and-server-rendering/1352 ' +
|
|
13
|
-
'(Requires creating a free account). Click this for the stack trace.');
|
|
14
|
-
}
|
|
9
|
+
import { createSSRCapability } from "../capabilities/ssr.js";
|
|
15
10
|
export function createBaseFullObject(registries, currentObject = null) {
|
|
16
11
|
// Get or create client object (with caching logic)
|
|
17
12
|
const clientObject = createBaseClientObject(registries, currentObject);
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const reactOnRailsFullSpecificFunctions =
|
|
21
|
-
handleError(options) {
|
|
22
|
-
return handleError(options);
|
|
23
|
-
},
|
|
24
|
-
serverRenderReactComponent(options) {
|
|
25
|
-
return serverRenderReactComponent(options);
|
|
26
|
-
},
|
|
27
|
-
prepareRenderResult(html, consoleReplayScript, hasErrors, renderingError) {
|
|
28
|
-
return buildLengthPrefixedResult(html, consoleReplayScript, {
|
|
29
|
-
hasErrors,
|
|
30
|
-
error: renderingError ?? undefined,
|
|
31
|
-
});
|
|
32
|
-
},
|
|
33
|
-
};
|
|
13
|
+
// Delegate the SSR-specific functions to `createSSRCapability` (the canonical source).
|
|
14
|
+
// Typed to ReactOnRailsFullSpecificFunctions so we add exactly the SSR surface, nothing more.
|
|
15
|
+
const reactOnRailsFullSpecificFunctions = createSSRCapability();
|
|
34
16
|
// Type assertion is safe here because:
|
|
35
17
|
// 1. We start with BaseClientObjectType (from createBaseClientObject)
|
|
36
18
|
// 2. We add exactly the methods defined in ReactOnRailsFullSpecificFunctions
|