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 CHANGED
@@ -81,11 +81,12 @@ ReactOnRails.register({
81
81
 
82
82
  ## Exports
83
83
 
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/types` | TypeScript type exports |
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
 
@@ -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
- * MUST SYNC: A sibling helper exists in packages/react-on-rails-pro/src/ClientSideRenderer.ts. If you
20
- * change the error-handling logic or log format here, update that copy too.
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/helper.rb
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 and it's still connected to the document.
177
- // If the node was replaced (e.g., via innerHTML or Turbo), we need to unmount the old
178
- // root and re-render to the new node to prevent memory leaks and ensure rendering works.
179
- const sameNode = existing.domNode === domNode && existing.domNode.isConnected;
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
- const label = existing.kind === 'renderer'
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
- // Hydrate if the DOM node has content (server-rendered HTML)
211
- // Since we skip already-rendered components above, this check now correctly
212
- // identifies only server-rendered content, not previously client-rendered content
213
- const shouldHydrate = !!domNode.innerHTML;
214
- const reactElementOrRouterResult = createReactOutput({
215
- componentObj,
216
- props,
217
- domNodeId,
218
- trace,
219
- railsContext,
220
- shouldHydrate,
221
- });
222
- if (isServerRenderHash(reactElementOrRouterResult)) {
223
- throw new Error(`\
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
- const error = e;
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
- // Use the same label as the async-rejection path so renderer-teardown failures are greppable
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();
@@ -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.
@@ -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
- if (throwIfMissing) {
46
- console.log('storeKeys', storeKeys);
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.
@@ -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
  };
@@ -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 * as Authenticity from "../Authenticity.js";
6
- import buildConsoleReplay, { consoleReplay } from "../buildConsoleReplay.js";
7
- import reactHydrateOrRender from "../reactHydrateOrRender.js";
8
- import createReactOutput from "../createReactOutput.js";
9
- import componentRegistrationMetric from "../componentRegistrationMetric.js";
10
- import { buildRootErrorCallbackOptions, getRootErrorHandlers, resetRootErrorHandlers, setRootErrorHandlers, } from "../rootErrorHandlers.js";
11
- const DEFAULT_OPTIONS = {
12
- traceTurbolinks: false,
13
- turbo: false,
14
- debugMode: false,
15
- logComponentRegistration: false,
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
- // Create and return new object
64
- const obj = {
65
- options: {},
66
- isRSCBundle: false,
67
- // ===================================================================
68
- // STABLE METHOD IMPLEMENTATIONS - Core package implementations
69
- // ===================================================================
70
- authenticityToken() {
71
- return Authenticity.authenticityToken();
72
- },
73
- authenticityHeaders(otherHeaders = {}) {
74
- return Authenticity.authenticityHeaders(otherHeaders);
75
- },
76
- reactHydrateOrRender(domNode, reactElement, hydrate) {
77
- // The component name is unknown on this low-level path; the dom id still ties errors to a mount.
78
- const rootErrorCallbackOptions = buildRootErrorCallbackOptions({ domNodeId: domNode.id || undefined }, hydrate);
79
- return reactHydrateOrRender(domNode, reactElement, hydrate, rootErrorCallbackOptions);
80
- },
81
- setOptions(newOptions) {
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 handleError from "../handleError.js";
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
- // Define SSR-specific functions with proper types
19
- // This object acts as a type-safe specification of what we're adding to the base object
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