react-on-rails 16.6.0 → 16.7.0-rc.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/README.md CHANGED
@@ -14,6 +14,14 @@ pnpm add react-on-rails
14
14
 
15
15
  **Using React on Rails Pro?** Install [`react-on-rails-pro`](https://www.npmjs.com/package/react-on-rails-pro) instead. The Pro package re-exports everything from this package plus Pro-exclusive features. The `react_on_rails_pro` gem requires the Pro npm package.
16
16
 
17
+ ## Need More Than OSS?
18
+
19
+ If you want React Server Components, streaming SSR, fragment caching, or faster Node-based SSR, try [`react-on-rails-pro`](https://www.npmjs.com/package/react-on-rails-pro). It is free to evaluate in development, CI/CD, and staging; production licenses are required only for production deployments.
20
+
21
+ - [Compare OSS vs Pro](https://reactonrails.com/docs/getting-started/oss-vs-pro/)
22
+ - [Pro quick start](https://reactonrails.com/docs/getting-started/pro-quick-start/)
23
+ - [React on Rails Pro overview](https://reactonrails.com/docs/pro/)
24
+
17
25
  ## Quick Start
18
26
 
19
27
  ### Register Components
@@ -1,7 +1,19 @@
1
- import { createBaseClientObject } from "./base/client.js";
1
+ import { createCoreCapability } from "./capabilities/core.js";
2
+ import { createLifecycleCapability } from "./capabilities/lifecycle.js";
2
3
  import createReactOnRails from "./createReactOnRails.js";
4
+ import ComponentRegistry from "./ComponentRegistry.js";
5
+ import StoreRegistry from "./StoreRegistry.js";
6
+ import { clientStartup } from "./clientStartup.js";
7
+ const registries = { ComponentRegistry, StoreRegistry };
3
8
  const currentGlobal = globalThis.ReactOnRails || null;
4
- const ReactOnRails = createReactOnRails(createBaseClientObject, currentGlobal);
9
+ const ReactOnRails = createReactOnRails([createCoreCapability(registries), createLifecycleCapability()], {
10
+ currentGlobal,
11
+ // Defer startup to the next tick so all synchronous <script> tags finish evaluating
12
+ // before we scan the DOM for components. Pro's proClientStartup runs synchronously
13
+ // because streaming hydration needs to attach listeners before the first paint.
14
+ startup: typeof window !== 'undefined' ? () => setTimeout(() => clientStartup(), 0) : null,
15
+ registries,
16
+ });
5
17
  export * from "./types/index.js";
6
18
  export default ReactOnRails;
7
19
  //# sourceMappingURL=ReactOnRails.client.js.map
@@ -1,7 +1,17 @@
1
- import { createBaseFullObject } from "./base/full.js";
1
+ import { createCoreCapability } from "./capabilities/core.js";
2
+ import { createLifecycleCapability } from "./capabilities/lifecycle.js";
3
+ import { createSSRCapability } from "./capabilities/ssr.js";
2
4
  import createReactOnRails from "./createReactOnRails.js";
5
+ import ComponentRegistry from "./ComponentRegistry.js";
6
+ import StoreRegistry from "./StoreRegistry.js";
7
+ import { clientStartup } from "./clientStartup.js";
8
+ const registries = { ComponentRegistry, StoreRegistry };
3
9
  const currentGlobal = globalThis.ReactOnRails || null;
4
- const ReactOnRails = createReactOnRails(createBaseFullObject, currentGlobal);
10
+ const ReactOnRails = createReactOnRails([createCoreCapability(registries), createLifecycleCapability(), createSSRCapability()], {
11
+ currentGlobal,
12
+ startup: typeof window !== 'undefined' ? () => setTimeout(() => clientStartup(), 0) : null,
13
+ registries,
14
+ });
5
15
  export * from "./types/index.js";
6
16
  export default ReactOnRails;
7
17
  //# sourceMappingURL=ReactOnRails.full.js.map
@@ -1,13 +1,10 @@
1
+ import sanitizeNonce from "./sanitizeNonce.js";
1
2
  // eslint-disable-next-line import/prefer-default-export -- only one export for now, but others may be added later
2
3
  export function wrapInScriptTags(scriptId, scriptBody, nonce) {
3
4
  if (!scriptBody) {
4
5
  return '';
5
6
  }
6
- // Sanitize nonce to prevent attribute injection attacks.
7
- // CSP nonces should be base64/base64url-like strings with optional trailing padding.
8
- // NOTE: keep this logic in sync with sanitizeNonce() in react-on-rails-pro/src/utils.ts
9
- const nonceWithAllowedCharsOnly = nonce?.replace(/[^a-zA-Z0-9+/=_-]/g, '');
10
- const sanitizedNonce = nonceWithAllowedCharsOnly?.match(/^[a-zA-Z0-9+/_-]+={0,2}$/)?.[0];
7
+ const sanitizedNonce = sanitizeNonce(nonce);
11
8
  const nonceAttr = sanitizedNonce ? ` nonce="${sanitizedNonce}"` : '';
12
9
  return `
13
10
  <script id="${scriptId}"${nonceAttr}>
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @deprecated Use `capabilities/core.ts` instead. This file is kept for backward compatibility
3
+ * with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/client`.
4
+ */
1
5
  import type { RegisteredComponent, ReactComponentOrRenderFunction, Store, StoreGenerator, ReactOnRailsInternal } from '../types/index.ts';
2
6
  interface Registries {
3
7
  ComponentRegistry: {
@@ -19,7 +23,7 @@ interface Registries {
19
23
  * Base client object type that includes all core ReactOnRails methods except Pro-specific ones.
20
24
  * Derived from ReactOnRailsInternal by omitting Pro-only methods.
21
25
  */
22
- export type BaseClientObjectType = Omit<ReactOnRailsInternal, 'getOrWaitForComponent' | 'getOrWaitForStore' | 'getOrWaitForStoreGenerator' | 'reactOnRailsStoreLoaded' | 'streamServerRenderedReactComponent' | 'serverRenderRSCReactComponent'>;
26
+ export type BaseClientObjectType = Omit<ReactOnRailsInternal, 'getOrWaitForComponent' | 'getOrWaitForStore' | 'getOrWaitForStoreGenerator' | 'reactOnRailsStoreLoaded' | 'streamServerRenderedReactComponent' | 'serverRenderRSCReactComponent' | 'addAsyncPropsCapabilityToComponentProps' | 'getOrCreateAsyncPropsManager'>;
23
27
  export declare function createBaseClientObject(registries: Registries, currentObject?: BaseClientObjectType | null): BaseClientObjectType;
24
28
  export {};
25
29
  //# sourceMappingURL=client.d.ts.map
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @deprecated Use `capabilities/core.ts` instead. This file is kept for backward compatibility
3
+ * with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/client`.
4
+ */
1
5
  import * as Authenticity from "../Authenticity.js";
2
6
  import buildConsoleReplay, { consoleReplay } from "../buildConsoleReplay.js";
3
7
  import reactHydrateOrRender from "../reactHydrateOrRender.js";
@@ -201,6 +205,11 @@ Fix: Use only react-on-rails OR react-on-rails-pro, not both.`);
201
205
  void args; // Mark as used
202
206
  throw new Error('handleError is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
203
207
  },
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ prepareRenderResult(...args) {
210
+ void args; // Mark as used
211
+ throw new Error('prepareRenderResult is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
212
+ },
204
213
  };
205
214
  // Cache the object and registries
206
215
  cachedObject = obj;
@@ -1,10 +1,14 @@
1
+ /**
2
+ * @deprecated Use `capabilities/ssr.ts` instead. This file is kept for backward compatibility
3
+ * with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/full`.
4
+ */
1
5
  import { createBaseClientObject, type BaseClientObjectType } from './client.ts';
2
6
  import type { ReactOnRailsInternal } from '../types/index.ts';
3
7
  /**
4
8
  * SSR-specific functions that extend the base client object to create a full object.
5
9
  * Typed explicitly to ensure type safety when mutating the base object.
6
10
  */
7
- export type ReactOnRailsFullSpecificFunctions = Pick<ReactOnRailsInternal, 'handleError' | 'serverRenderReactComponent'>;
11
+ export type ReactOnRailsFullSpecificFunctions = Pick<ReactOnRailsInternal, 'handleError' | 'serverRenderReactComponent' | 'prepareRenderResult'>;
8
12
  /**
9
13
  * Full object type that includes all base methods plus real SSR implementations.
10
14
  * Derived from ReactOnRailsInternal by picking base methods and SSR methods.
package/lib/base/full.js CHANGED
@@ -1,6 +1,11 @@
1
+ /**
2
+ * @deprecated Use `capabilities/ssr.ts` instead. This file is kept for backward compatibility
3
+ * with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/full`.
4
+ */
1
5
  import { createBaseClientObject } from "./client.js";
2
6
  import handleError from "../handleError.js";
3
7
  import serverRenderReactComponent from "../serverRenderReactComponent.js";
8
+ import { buildLengthPrefixedResult } from "../serverRenderUtils.js";
4
9
  // Warn about bundle size when included in browser bundles
5
10
  if (typeof window !== 'undefined') {
6
11
  console.warn('Optimization opportunity: "react-on-rails" includes ~14KB of server-rendering code. ' +
@@ -19,6 +24,12 @@ export function createBaseFullObject(registries, currentObject = null) {
19
24
  serverRenderReactComponent(options) {
20
25
  return serverRenderReactComponent(options);
21
26
  },
27
+ prepareRenderResult(html, consoleReplayScript, hasErrors, renderingError) {
28
+ return buildLengthPrefixedResult(html, consoleReplayScript, {
29
+ hasErrors,
30
+ error: renderingError ?? undefined,
31
+ });
32
+ },
22
33
  };
23
34
  // Type assertion is safe here because:
24
35
  // 1. We start with BaseClientObjectType (from createBaseClientObject)
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @deprecated Use `capabilities/ssr.rsc.ts` instead. This file is kept for backward compatibility
3
+ * with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/full`.
4
+ */
1
5
  import { createBaseClientObject, type BaseClientObjectType } from './client.ts';
2
6
  import type { BaseFullObjectType } from './full.ts';
3
7
  export type * from './full.ts';
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @deprecated Use `capabilities/ssr.rsc.ts` instead. This file is kept for backward compatibility
3
+ * with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/full`.
4
+ */
1
5
  import { createBaseClientObject } from "./client.js";
2
6
  export function createBaseFullObject(registries, currentObject = null) {
3
7
  // Get or create client object (with caching logic)
@@ -11,6 +15,9 @@ export function createBaseFullObject(registries, currentObject = null) {
11
15
  serverRenderReactComponent() {
12
16
  throw new Error('"serverRenderReactComponent" function is not supported in RSC bundle');
13
17
  },
18
+ prepareRenderResult() {
19
+ throw new Error('"prepareRenderResult" function is not supported in RSC bundle');
20
+ },
14
21
  };
15
22
  // Type assertion is safe here because:
16
23
  // 1. We start with BaseClientObjectType (from createBaseClientObject)
@@ -0,0 +1,58 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { RegisteredComponent, RenderReturnType, ReactComponentOrRenderFunction, AuthenticityHeaders, Store, StoreGenerator, ReactOnRailsOptions } from '../types/index.ts';
3
+ export interface Registries {
4
+ ComponentRegistry: {
5
+ register: (components: Record<string, ReactComponentOrRenderFunction>) => void;
6
+ get: (name: string) => RegisteredComponent;
7
+ components: () => Map<string, RegisteredComponent>;
8
+ };
9
+ StoreRegistry: {
10
+ register: (storeGenerators: Record<string, StoreGenerator>) => void;
11
+ getStore: (name: string, throwIfMissing?: boolean) => Store | undefined;
12
+ getStoreGenerator: (name: string) => StoreGenerator;
13
+ setStore: (name: string, store: Store) => void;
14
+ clearHydratedStores: () => void;
15
+ storeGenerators: () => Map<string, StoreGenerator>;
16
+ stores: () => Map<string, Store>;
17
+ };
18
+ }
19
+ /**
20
+ * Creates the core capability containing all base ReactOnRails methods.
21
+ * These are the methods that exist in every bundle variant (client, full, node, RSC).
22
+ */
23
+ export declare function createCoreCapability(registries: Registries): {
24
+ options: Partial<ReactOnRailsOptions>;
25
+ isRSCBundle: boolean;
26
+ authenticityToken(): string | null;
27
+ authenticityHeaders(otherHeaders?: Record<string, string>): AuthenticityHeaders;
28
+ reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType;
29
+ setOptions(newOptions: Partial<ReactOnRailsOptions>): void;
30
+ option<K extends keyof ReactOnRailsOptions>(key: K): ReactOnRailsOptions[K];
31
+ buildConsoleReplay(): string;
32
+ getConsoleReplayScript(): string;
33
+ resetOptions(): void;
34
+ register(components: Record<string, ReactComponentOrRenderFunction>): void;
35
+ registerStore(stores: Record<string, StoreGenerator>): void;
36
+ registerStoreGenerators(storeGenerators: Record<string, StoreGenerator>): void;
37
+ getStore(name: string, throwIfMissing?: boolean): Store | undefined;
38
+ getStoreGenerator(name: string): StoreGenerator;
39
+ setStore(name: string, store: Store): void;
40
+ clearHydratedStores(): void;
41
+ getComponent(name: string): RegisteredComponent;
42
+ registeredComponents(): Map<string, RegisteredComponent>;
43
+ storeGenerators(): Map<string, StoreGenerator>;
44
+ stores(): Map<string, Store>;
45
+ render(name: string, props: Record<string, string>, domNodeId: string, hydrate: boolean): RenderReturnType;
46
+ serverRenderReactComponent(...args: any[]): any;
47
+ handleError(...args: any[]): any;
48
+ prepareRenderResult(...args: any[]): any;
49
+ getOrWaitForComponent(): Promise<RegisteredComponent>;
50
+ getOrWaitForStore(): Promise<Store>;
51
+ getOrWaitForStoreGenerator(): Promise<StoreGenerator>;
52
+ reactOnRailsStoreLoaded(): Promise<void>;
53
+ streamServerRenderedReactComponent(...args: any[]): any;
54
+ serverRenderRSCReactComponent(...args: any[]): any;
55
+ addAsyncPropsCapabilityToComponentProps(...args: any[]): any;
56
+ getOrCreateAsyncPropsManager(...args: any[]): any;
57
+ };
58
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1,188 @@
1
+ import * as Authenticity from "../Authenticity.js";
2
+ import buildConsoleReplay, { consoleReplay } from "../buildConsoleReplay.js";
3
+ import reactHydrateOrRender from "../reactHydrateOrRender.js";
4
+ import createReactOutput from "../createReactOutput.js";
5
+ const DEFAULT_OPTIONS = {
6
+ traceTurbolinks: false,
7
+ turbo: false,
8
+ debugMode: false,
9
+ logComponentRegistration: false,
10
+ };
11
+ /**
12
+ * Creates the core capability containing all base ReactOnRails methods.
13
+ * These are the methods that exist in every bundle variant (client, full, node, RSC).
14
+ */
15
+ export function createCoreCapability(registries) {
16
+ const { ComponentRegistry, StoreRegistry } = registries;
17
+ return {
18
+ options: {},
19
+ isRSCBundle: false,
20
+ // ===================================================================
21
+ // STABLE METHOD IMPLEMENTATIONS - Core package implementations
22
+ // ===================================================================
23
+ authenticityToken() {
24
+ return Authenticity.authenticityToken();
25
+ },
26
+ authenticityHeaders(otherHeaders = {}) {
27
+ return Authenticity.authenticityHeaders(otherHeaders);
28
+ },
29
+ reactHydrateOrRender(domNode, reactElement, hydrate) {
30
+ return reactHydrateOrRender(domNode, reactElement, hydrate);
31
+ },
32
+ setOptions(newOptions) {
33
+ const { traceTurbolinks, turbo, debugMode, logComponentRegistration, ...rest } = newOptions;
34
+ if (typeof traceTurbolinks !== 'undefined') {
35
+ this.options.traceTurbolinks = traceTurbolinks;
36
+ }
37
+ if (typeof turbo !== 'undefined') {
38
+ this.options.turbo = turbo;
39
+ }
40
+ if (typeof debugMode !== 'undefined') {
41
+ this.options.debugMode = debugMode;
42
+ if (debugMode) {
43
+ console.log('[ReactOnRails] Debug mode enabled');
44
+ }
45
+ }
46
+ if (typeof logComponentRegistration !== 'undefined') {
47
+ this.options.logComponentRegistration = logComponentRegistration;
48
+ if (logComponentRegistration) {
49
+ console.log('[ReactOnRails] Component registration logging enabled');
50
+ }
51
+ }
52
+ if (Object.keys(rest).length > 0) {
53
+ throw new Error(`Invalid options passed to ReactOnRails.options: ${JSON.stringify(rest)}`);
54
+ }
55
+ },
56
+ option(key) {
57
+ return this.options[key];
58
+ },
59
+ buildConsoleReplay() {
60
+ return buildConsoleReplay();
61
+ },
62
+ getConsoleReplayScript() {
63
+ return consoleReplay();
64
+ },
65
+ resetOptions() {
66
+ this.options = { ...DEFAULT_OPTIONS };
67
+ },
68
+ // ===================================================================
69
+ // REGISTRY METHOD IMPLEMENTATIONS - Using provided registries
70
+ // ===================================================================
71
+ register(components) {
72
+ if (this.options.debugMode || this.options.logComponentRegistration) {
73
+ // Use performance.now() if available, otherwise fallback to Date.now()
74
+ const perf = typeof performance !== 'undefined' ? performance : { now: () => Date.now() };
75
+ const startTime = perf.now();
76
+ const componentNames = Object.keys(components);
77
+ console.log(`[ReactOnRails] Registering ${componentNames.length} component(s): ${componentNames.join(', ')}`);
78
+ ComponentRegistry.register(components);
79
+ const endTime = perf.now();
80
+ console.log(`[ReactOnRails] Component registration completed in ${(endTime - startTime).toFixed(2)}ms`);
81
+ // Log individual component details if in full debug mode
82
+ if (this.options.debugMode) {
83
+ componentNames.forEach((name) => {
84
+ const component = components[name];
85
+ const size = component.toString().length;
86
+ console.log(`[ReactOnRails] ✅ Registered: ${name} (${size} chars)`);
87
+ });
88
+ }
89
+ }
90
+ else {
91
+ ComponentRegistry.register(components);
92
+ }
93
+ },
94
+ registerStore(stores) {
95
+ this.registerStoreGenerators(stores);
96
+ },
97
+ registerStoreGenerators(storeGenerators) {
98
+ if (!storeGenerators) {
99
+ throw new Error('Called ReactOnRails.registerStoreGenerators with a null or undefined, rather than ' +
100
+ 'an Object with keys being the store names and the values are the store generators.');
101
+ }
102
+ StoreRegistry.register(storeGenerators);
103
+ },
104
+ getStore(name, throwIfMissing = true) {
105
+ return StoreRegistry.getStore(name, throwIfMissing);
106
+ },
107
+ getStoreGenerator(name) {
108
+ return StoreRegistry.getStoreGenerator(name);
109
+ },
110
+ setStore(name, store) {
111
+ StoreRegistry.setStore(name, store);
112
+ },
113
+ clearHydratedStores() {
114
+ StoreRegistry.clearHydratedStores();
115
+ },
116
+ getComponent(name) {
117
+ return ComponentRegistry.get(name);
118
+ },
119
+ registeredComponents() {
120
+ return ComponentRegistry.components();
121
+ },
122
+ storeGenerators() {
123
+ return StoreRegistry.storeGenerators();
124
+ },
125
+ stores() {
126
+ return StoreRegistry.stores();
127
+ },
128
+ render(name, props, domNodeId, hydrate) {
129
+ const componentObj = ComponentRegistry.get(name);
130
+ const reactElement = createReactOutput({ componentObj, props, domNodeId });
131
+ return this.reactHydrateOrRender(document.getElementById(domNodeId), reactElement, hydrate);
132
+ },
133
+ // ===================================================================
134
+ // SSR STUBS — overridden by createSSRCapability() in full/node bundles
135
+ // ===================================================================
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ serverRenderReactComponent(...args) {
138
+ void args;
139
+ throw new Error('serverRenderReactComponent is not available in the client bundle. Import "react-on-rails" server-side.');
140
+ },
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ handleError(...args) {
143
+ void args;
144
+ throw new Error('handleError is not available in the client bundle. Import "react-on-rails" server-side.');
145
+ },
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ prepareRenderResult(...args) {
148
+ void args;
149
+ throw new Error('prepareRenderResult is not available in the client bundle. Import "react-on-rails" server-side.');
150
+ },
151
+ // ===================================================================
152
+ // PRO STUBS — overridden by Pro capabilities in react-on-rails-pro
153
+ // ===================================================================
154
+ getOrWaitForComponent() {
155
+ throw new Error('getOrWaitForComponent requires the react-on-rails-pro package.');
156
+ },
157
+ getOrWaitForStore() {
158
+ throw new Error('getOrWaitForStore requires the react-on-rails-pro package.');
159
+ },
160
+ getOrWaitForStoreGenerator() {
161
+ throw new Error('getOrWaitForStoreGenerator requires the react-on-rails-pro package.');
162
+ },
163
+ reactOnRailsStoreLoaded() {
164
+ throw new Error('reactOnRailsStoreLoaded requires the react-on-rails-pro package.');
165
+ },
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ streamServerRenderedReactComponent(...args) {
168
+ void args;
169
+ throw new Error('streamServerRenderedReactComponent requires the react-on-rails-pro package.');
170
+ },
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ serverRenderRSCReactComponent(...args) {
173
+ void args;
174
+ throw new Error('serverRenderRSCReactComponent requires the react-on-rails-pro package.');
175
+ },
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ addAsyncPropsCapabilityToComponentProps(...args) {
178
+ void args;
179
+ throw new Error('addAsyncPropsCapabilityToComponentProps requires the react-on-rails-pro package.');
180
+ },
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ getOrCreateAsyncPropsManager(...args) {
183
+ void args;
184
+ throw new Error('getOrCreateAsyncPropsManager requires the react-on-rails-pro package.');
185
+ },
186
+ };
187
+ }
188
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Core lifecycle capability.
3
+ * Provides the core (non-Pro) implementations for page/component loaded callbacks.
4
+ */
5
+ export declare function createLifecycleCapability(): {
6
+ reactOnRailsPageLoaded(): Promise<void>;
7
+ reactOnRailsComponentLoaded(domId: string): Promise<void>;
8
+ };
9
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1,19 @@
1
+ /* eslint-disable import/prefer-default-export -- named export for consistency with capability API */
2
+ import { reactOnRailsPageLoaded } from "../clientStartup.js";
3
+ import { reactOnRailsComponentLoaded } from "../ClientRenderer.js";
4
+ /**
5
+ * Core lifecycle capability.
6
+ * Provides the core (non-Pro) implementations for page/component loaded callbacks.
7
+ */
8
+ export function createLifecycleCapability() {
9
+ return {
10
+ reactOnRailsPageLoaded() {
11
+ reactOnRailsPageLoaded();
12
+ return Promise.resolve();
13
+ },
14
+ reactOnRailsComponentLoaded(domId) {
15
+ return reactOnRailsComponentLoaded(domId);
16
+ },
17
+ };
18
+ }
19
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1,11 @@
1
+ import type { RenderParams, ErrorOptions, RenderingError } from '../types/index.ts';
2
+ /**
3
+ * SSR capability.
4
+ * Provides server-side rendering methods (serverRenderReactComponent, handleError).
5
+ */
6
+ export declare function createSSRCapability(): {
7
+ handleError(options: ErrorOptions): string | undefined;
8
+ serverRenderReactComponent(options: RenderParams): null | string | Promise<string>;
9
+ prepareRenderResult(html: string, consoleReplayScript: string, hasErrors: boolean, renderingError: RenderingError | null): string;
10
+ };
11
+ //# sourceMappingURL=ssr.d.ts.map
@@ -0,0 +1,31 @@
1
+ /* eslint-disable import/prefer-default-export -- named export for consistency with capability API */
2
+ import handleError from "../handleError.js";
3
+ import serverRenderReactComponent from "../serverRenderReactComponent.js";
4
+ import { buildLengthPrefixedResult } from "../serverRenderUtils.js";
5
+ // Warn about bundle size when included in browser bundles
6
+ if (typeof window !== 'undefined') {
7
+ console.warn('Optimization opportunity: this bundle includes ~14KB of server-rendering code that browsers may not need. ' +
8
+ 'See https://forum.shakacode.com/t/how-to-use-different-versions-of-a-file-for-client-and-server-rendering/1352 ' +
9
+ '(Requires creating a free account). Click this for the stack trace.');
10
+ }
11
+ /**
12
+ * SSR capability.
13
+ * Provides server-side rendering methods (serverRenderReactComponent, handleError).
14
+ */
15
+ export function createSSRCapability() {
16
+ return {
17
+ handleError(options) {
18
+ return handleError(options);
19
+ },
20
+ serverRenderReactComponent(options) {
21
+ return serverRenderReactComponent(options);
22
+ },
23
+ prepareRenderResult(html, consoleReplayScript, hasErrors, renderingError) {
24
+ return buildLengthPrefixedResult(html, consoleReplayScript, {
25
+ hasErrors,
26
+ error: renderingError ?? undefined,
27
+ });
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=ssr.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SSR capability for RSC bundles.
3
+ * SSR methods throw because they are not supported in the RSC bundle.
4
+ */
5
+ export declare function createSSRCapability(): {
6
+ handleError(): never;
7
+ serverRenderReactComponent(): never;
8
+ prepareRenderResult(): never;
9
+ };
10
+ //# sourceMappingURL=ssr.rsc.d.ts.map
@@ -0,0 +1,19 @@
1
+ /* eslint-disable import/prefer-default-export -- named export for consistency with capability API */
2
+ /**
3
+ * SSR capability for RSC bundles.
4
+ * SSR methods throw because they are not supported in the RSC bundle.
5
+ */
6
+ export function createSSRCapability() {
7
+ return {
8
+ handleError() {
9
+ throw new Error('"handleError" function is not supported in RSC bundle');
10
+ },
11
+ serverRenderReactComponent() {
12
+ throw new Error('"serverRenderReactComponent" function is not supported in RSC bundle');
13
+ },
14
+ prepareRenderResult() {
15
+ throw new Error('"prepareRenderResult" function is not supported in RSC bundle');
16
+ },
17
+ };
18
+ }
19
+ //# sourceMappingURL=ssr.rsc.js.map
@@ -1,7 +1,20 @@
1
- import { createBaseClientObject, type BaseClientObjectType } from './base/client.ts';
2
- import { createBaseFullObject } from './base/full.ts';
1
+ import type { Registries } from './capabilities/core.ts';
3
2
  import type { ReactOnRailsInternal } from './types/index.ts';
4
- type BaseObjectCreator = typeof createBaseClientObject | typeof createBaseFullObject;
5
- export default function createReactOnRails(baseObjectCreator: BaseObjectCreator, currentGlobal?: BaseClientObjectType | null): ReactOnRailsInternal;
6
- export {};
3
+ export type { Registries };
4
+ /**
5
+ * Assembles the ReactOnRails global object from an array of capabilities.
6
+ *
7
+ * Each capability is a partial implementation of ReactOnRailsInternal.
8
+ * Capabilities are merged in array order (last wins for overlapping keys).
9
+ *
10
+ * @param capabilities - Array of capability objects to merge.
11
+ * @param options.currentGlobal - Current globalThis.ReactOnRails value (for misconfiguration detection).
12
+ * @param options.startup - Callback invoked once on first initialization, after the global is set.
13
+ * @param options.registries - The registries used by the core capability (for mixing detection).
14
+ */
15
+ export default function createReactOnRails(capabilities: Partial<ReactOnRailsInternal>[], options: {
16
+ currentGlobal: ReactOnRailsInternal | null;
17
+ startup: (() => void) | null;
18
+ registries: Registries;
19
+ }): ReactOnRailsInternal;
7
20
  //# sourceMappingURL=createReactOnRails.d.ts.map
@@ -1,68 +1,102 @@
1
- import { clientStartup, reactOnRailsPageLoaded } from "./clientStartup.js";
2
- import { reactOnRailsComponentLoaded } from "./ClientRenderer.js";
3
- import ComponentRegistry from "./ComponentRegistry.js";
4
- import StoreRegistry from "./StoreRegistry.js";
5
- export default function createReactOnRails(baseObjectCreator, currentGlobal = null) {
6
- // Create base object with core registries, passing currentGlobal for caching/validation
7
- const baseObject = baseObjectCreator({
8
- ComponentRegistry,
9
- StoreRegistry,
10
- }, currentGlobal);
11
- // Define core-specific functions with proper types
12
- // This object acts as a type-safe specification of what we're adding/overriding on the base object
13
- const reactOnRailsCoreSpecificFunctions = {
14
- // Override base stubs with core implementations
15
- reactOnRailsPageLoaded() {
16
- reactOnRailsPageLoaded();
17
- return Promise.resolve();
18
- },
19
- reactOnRailsComponentLoaded(domId) {
20
- return reactOnRailsComponentLoaded(domId);
21
- },
22
- // Pro-only stubs (throw errors in core package)
23
- getOrWaitForComponent() {
24
- throw new Error('getOrWaitForComponent requires react-on-rails-pro package');
25
- },
26
- getOrWaitForStore() {
27
- throw new Error('getOrWaitForStore requires react-on-rails-pro package');
28
- },
29
- getOrWaitForStoreGenerator() {
30
- throw new Error('getOrWaitForStoreGenerator requires react-on-rails-pro package');
31
- },
32
- reactOnRailsStoreLoaded() {
33
- throw new Error('reactOnRailsStoreLoaded requires react-on-rails-pro package');
34
- },
35
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
- streamServerRenderedReactComponent() {
37
- throw new Error('streamServerRenderedReactComponent requires react-on-rails-pro package');
38
- },
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- serverRenderRSCReactComponent() {
41
- throw new Error('serverRenderRSCReactComponent requires react-on-rails-pro package');
42
- },
43
- };
44
- // Type assertion is safe here because:
45
- // 1. We start with BaseClientObjectType or BaseFullObjectType (from baseObjectCreator)
46
- // 2. We add exactly the methods defined in ReactOnRailsCoreSpecificFunctions
47
- // 3. ReactOnRailsInternal = Base + ReactOnRailsCoreSpecificFunctions
48
- // TypeScript can't track the mutation, but we ensure type safety by explicitly typing
49
- // the functions object above
50
- const reactOnRails = baseObject;
51
- // Assign core-specific functions to the ReactOnRails object using Object.assign
52
- // This pattern ensures we add exactly what's defined in the type, nothing more, nothing less
53
- Object.assign(reactOnRails, reactOnRailsCoreSpecificFunctions);
54
- // Assign to global if not already assigned
55
- if (!globalThis.ReactOnRails) {
56
- globalThis.ReactOnRails = reactOnRails;
57
- // Reset options to defaults (only on first initialization)
58
- reactOnRails.resetOptions();
59
- // Run client startup (only on first initialization)
60
- if (typeof window !== 'undefined') {
61
- setTimeout(() => {
62
- clientStartup();
63
- }, 0);
1
+ // Module-level cache for singleton enforcement and validation
2
+ let cachedObject = null;
3
+ let cachedRegistries = null;
4
+ /**
5
+ * Assembles the ReactOnRails global object from an array of capabilities.
6
+ *
7
+ * Each capability is a partial implementation of ReactOnRailsInternal.
8
+ * Capabilities are merged in array order (last wins for overlapping keys).
9
+ *
10
+ * @param capabilities - Array of capability objects to merge.
11
+ * @param options.currentGlobal - Current globalThis.ReactOnRails value (for misconfiguration detection).
12
+ * @param options.startup - Callback invoked once on first initialization, after the global is set.
13
+ * @param options.registries - The registries used by the core capability (for mixing detection).
14
+ */
15
+ export default function createReactOnRails(capabilities, options) {
16
+ const { currentGlobal, startup, registries } = options;
17
+ // ===================================================================
18
+ // VALIDATION — preserved from base/client.ts
19
+ // ===================================================================
20
+ // Webpack misconfiguration detection: currentGlobal is null but we have a cached object.
21
+ // This indicates webpack's optimization.runtimeChunk is set to "true" or "multiple".
22
+ if (currentGlobal === null && cachedObject !== null) {
23
+ throw new Error(`\
24
+ ReactOnRails was already initialized, but a new initialization was attempted without passing the existing global.
25
+ This usually means Webpack's optimization.runtimeChunk is set to "true" or "multiple" instead of "single".
26
+
27
+ Fix: Set optimization.runtimeChunk to "single" in your webpack configuration.
28
+ See: https://github.com/shakacode/react_on_rails/issues/1558`);
29
+ }
30
+ // Cross-runtime detection: another bundle/runtime already initialized the global,
31
+ // but this module instance hasn't cached anything yet.
32
+ // In development with HMR/fast-refresh, webpack invalidates the module cache
33
+ // (resetting cachedObject to null) while globalThis.ReactOnRails persists from the
34
+ // previous load. The old base/client.ts code handled this gracefully by returning
35
+ // the existing global. We preserve that behavior in development and only throw in
36
+ // production where this genuinely indicates misconfiguration (multiple runtimes or
37
+ // mixed core/pro bundles).
38
+ if (currentGlobal !== null && cachedObject === null) {
39
+ if (process.env.NODE_ENV === 'production') {
40
+ throw new Error(`\
41
+ ReactOnRails was already initialized by another bundle or runtime instance.
42
+ This usually means multiple webpack runtimes or mixed core/pro bundles were loaded on the same page.
43
+
44
+ Fix: Ensure only one ReactOnRails bundle initializes per page, and set optimization.runtimeChunk to "single".`);
45
+ }
46
+ // In development (HMR), accept the pre-existing global and re-cache it.
47
+ cachedObject = currentGlobal;
48
+ cachedRegistries = registries;
49
+ return cachedObject;
50
+ }
51
+ // Global contamination detection: currentGlobal exists but doesn't match cached object.
52
+ if (currentGlobal !== null && cachedObject !== null && currentGlobal !== cachedObject) {
53
+ throw new Error(`\
54
+ ReactOnRails global object mismatch detected.
55
+ The current global ReactOnRails object is different from the one created by this package.
56
+
57
+ This usually means:
58
+ 1. You're mixing react-on-rails (core) with react-on-rails-pro
59
+ 2. Another library is interfering with the global ReactOnRails object
60
+
61
+ Fix: Use only one package (core OR pro) consistently throughout your application.`);
62
+ }
63
+ // Registry mixing detection: different registries indicate core/pro package mixing.
64
+ if (cachedRegistries !== null) {
65
+ if (registries.ComponentRegistry !== cachedRegistries.ComponentRegistry ||
66
+ registries.StoreRegistry !== cachedRegistries.StoreRegistry) {
67
+ throw new Error(`\
68
+ Cannot mix react-on-rails (core) with react-on-rails-pro.
69
+ Different registries detected - the packages use incompatible registries.
70
+
71
+ Fix: Use only react-on-rails OR react-on-rails-pro, not both.`);
64
72
  }
65
73
  }
74
+ // Return cached object if already initialized (all validation passed above).
75
+ // Merge only additive capabilities onto the existing object.
76
+ // The first capability is the core baseline, which includes client/pro stubs.
77
+ // Re-applying it during a later initialization can downgrade already-added methods
78
+ // (e.g., SSR/streaming/RSC) back to stubs.
79
+ if (cachedObject !== null) {
80
+ Object.assign(cachedObject, ...capabilities.slice(1));
81
+ return cachedObject;
82
+ }
83
+ // ===================================================================
84
+ // ASSEMBLY — merge all capabilities in order
85
+ // ===================================================================
86
+ const reactOnRails = Object.assign({}, ...capabilities);
87
+ // Cache the object and registries
88
+ cachedObject = reactOnRails;
89
+ cachedRegistries = registries;
90
+ // ===================================================================
91
+ // GLOBAL ASSIGNMENT — only on first initialization
92
+ // ===================================================================
93
+ globalThis.ReactOnRails = reactOnRails;
94
+ // Reset options to defaults
95
+ reactOnRails.resetOptions();
96
+ // Run startup callback
97
+ if (startup) {
98
+ startup();
99
+ }
66
100
  return reactOnRails;
67
101
  }
68
102
  //# sourceMappingURL=createReactOnRails.js.map
@@ -50,21 +50,33 @@ function initializePageEventListeners() {
50
50
  return;
51
51
  }
52
52
  isPageLifecycleInitialized = true;
53
- // Important: replacing this condition with `document.readyState !== 'loading'` is not valid
54
- // As the core ReactOnRails needs to ensure that all component bundles are loaded and executed before hydrating them
55
- // If the `document.readyState === 'interactive'`, it doesn't guarantee that deferred scripts are executed
56
- // the `readyState` can be `'interactive'` while the deferred scripts are still being executed
57
- // Which will lead to the error `"Could not find component registered with name <component name>"`
58
- // It will happen if this line is reached before the component chunk is executed on browser and reached the line
59
- // ReactOnRails.register({ Component });
60
- // ReactOnRailsPro is resellient against that type of race conditions, but it won't wait for that state anyway
61
- // As it immediately hydrates the components at the page as soon as its html and bundle is loaded on the browser
62
- // See pageLifecycle.test.js for unit tests validating this logic
53
+ // Important: replacing this condition with `document.readyState !== 'loading'` is not valid for
54
+ // the core page-load sweep. During `interactive`, deferred/module scripts may still be executing,
55
+ // and a component chunk may not yet have reached `ReactOnRails.register({ Component })`.
56
+ // Starting hydration too early can trigger "Could not find component registered" errors.
57
+ //
58
+ // However, async or dynamically-injected scripts can start after DOMContentLoaded has already fired
59
+ // while the document is still `interactive`. In that case, waiting only for DOMContentLoaded can miss
60
+ // initialization entirely, so we recover on the later `complete` transition.
61
+ //
62
+ // ReactOnRailsPro's early hydration path is more resilient to the registration race because it can
63
+ // hydrate components as their HTML and bundles arrive, but this page lifecycle still powers the
64
+ // fallback page-load sweep. See pageLifecycle.test.js for regression coverage of both cases.
63
65
  if (document.readyState === 'complete') {
64
66
  setupPageNavigationListeners();
65
67
  }
66
68
  else {
67
- document.addEventListener('DOMContentLoaded', setupPageNavigationListeners);
69
+ const initialPageLoadHandler = (event) => {
70
+ if (event.type === 'readystatechange' && document.readyState !== 'complete')
71
+ return;
72
+ document.removeEventListener('DOMContentLoaded', initialPageLoadHandler);
73
+ document.removeEventListener('readystatechange', initialPageLoadHandler);
74
+ setupPageNavigationListeners();
75
+ };
76
+ document.addEventListener('DOMContentLoaded', initialPageLoadHandler);
77
+ if (document.readyState === 'interactive') {
78
+ document.addEventListener('readystatechange', initialPageLoadHandler);
79
+ }
68
80
  }
69
81
  }
70
82
  export function onPageLoaded(callback) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Sanitizes a CSP nonce to prevent attribute injection attacks.
3
+ *
4
+ * Policy: sanitize-then-validate. Characters outside the base64/base64url alphabet
5
+ * are stripped first, then the result is validated against the expected nonce pattern.
6
+ * If the sanitized value does not match, `undefined` is returned and no nonce
7
+ * attribute will be emitted — the render proceeds without a nonce rather than
8
+ * failing or logging potentially sensitive values. Note that if stripping yields
9
+ * a string that still matches the base64/base64url pattern, that stripped value
10
+ * is returned.
11
+ *
12
+ * CSP nonces should be base64 or base64url strings with optional trailing `=` padding.
13
+ */
14
+ export default function sanitizeNonce(nonce?: string): string | undefined;
15
+ //# sourceMappingURL=sanitizeNonce.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Sanitizes a CSP nonce to prevent attribute injection attacks.
3
+ *
4
+ * Policy: sanitize-then-validate. Characters outside the base64/base64url alphabet
5
+ * are stripped first, then the result is validated against the expected nonce pattern.
6
+ * If the sanitized value does not match, `undefined` is returned and no nonce
7
+ * attribute will be emitted — the render proceeds without a nonce rather than
8
+ * failing or logging potentially sensitive values. Note that if stripping yields
9
+ * a string that still matches the base64/base64url pattern, that stripped value
10
+ * is returned.
11
+ *
12
+ * CSP nonces should be base64 or base64url strings with optional trailing `=` padding.
13
+ */
14
+ export default function sanitizeNonce(nonce) {
15
+ const nonceWithAllowedCharsOnly = nonce?.replace(/[^a-zA-Z0-9+/=_-]/g, '');
16
+ return nonceWithAllowedCharsOnly?.match(/^[a-zA-Z0-9+/_-]+={0,2}$/)?.[0];
17
+ }
18
+ //# sourceMappingURL=sanitizeNonce.js.map
@@ -1,3 +1,3 @@
1
- declare const _default: (val: string) => string;
2
- export default _default;
1
+ declare const scriptSanitizedVal: (val: string) => string;
2
+ export default scriptSanitizedVal;
3
3
  //# sourceMappingURL=scriptSanitizedVal.d.ts.map
@@ -1,6 +1,7 @@
1
- export default (val) => {
1
+ const scriptSanitizedVal = (val) => {
2
2
  // Replace closing
3
3
  const re = /<\/\W*script/gi;
4
4
  return val.replace(re, '(/script');
5
5
  };
6
+ export default scriptSanitizedVal;
6
7
  //# sourceMappingURL=scriptSanitizedVal.js.map
@@ -1,5 +1,5 @@
1
- import type { RenderParams, RenderResult } from './types/index.ts';
2
- declare function serverRenderReactComponentInternal(options: RenderParams): null | string | Promise<RenderResult>;
1
+ import type { RenderParams } from './types/index.ts';
2
+ declare function serverRenderReactComponentInternal(options: RenderParams): null | string | Promise<string>;
3
3
  declare const serverRenderReactComponent: typeof serverRenderReactComponentInternal;
4
4
  export default serverRenderReactComponent;
5
5
  //# sourceMappingURL=serverRenderReactComponent.d.ts.map
@@ -5,7 +5,7 @@ import { isPromise, isServerRenderHash } from "./isServerRenderResult.js";
5
5
  import { consoleReplay } from "./buildConsoleReplay.js";
6
6
  import handleError from "./handleError.js";
7
7
  import { renderToString } from "./ReactDOMServer.cjs";
8
- import { createResultObject, convertToError, validateComponent } from "./serverRenderUtils.js";
8
+ import { buildLengthPrefixedResult, convertToError, validateComponent } from "./serverRenderUtils.js";
9
9
  function processServerRenderHash(result, options) {
10
10
  const { redirectLocation, routeError } = result;
11
11
  const hasErrors = !!routeError;
@@ -88,12 +88,12 @@ async function createPromiseResult(renderState, options, throwJsErrors) {
88
88
  ? processServerRenderHash(asyncResult, options)
89
89
  : { ...renderState, result: asyncResult };
90
90
  const consoleReplayScript = consoleReplay(consoleHistory);
91
- return createResultObject(finalRenderState.result, consoleReplayScript, finalRenderState);
91
+ return buildLengthPrefixedResult(finalRenderState.result, consoleReplayScript, finalRenderState);
92
92
  }
93
93
  catch (e) {
94
94
  const errorRenderState = handleRenderingError(e, { componentName: options.componentName, throwJsErrors });
95
95
  const consoleReplayScript = consoleReplay(consoleHistory);
96
- return createResultObject(errorRenderState.result, consoleReplayScript, errorRenderState);
96
+ return buildLengthPrefixedResult(errorRenderState.result, consoleReplayScript, errorRenderState);
97
97
  }
98
98
  }
99
99
  function createFinalResult(renderState, options, throwJsErrors) {
@@ -102,7 +102,7 @@ function createFinalResult(renderState, options, throwJsErrors) {
102
102
  return createPromiseResult({ ...renderState, result }, options, throwJsErrors);
103
103
  }
104
104
  const consoleReplayScript = consoleReplay();
105
- return JSON.stringify(createResultObject(result, consoleReplayScript, renderState));
105
+ return buildLengthPrefixedResult(result, consoleReplayScript, renderState);
106
106
  }
107
107
  function serverRenderReactComponentInternal(options) {
108
108
  const { name: componentName, domNodeId, trace, props, railsContext, renderingReturnsPromises, throwJsErrors, } = options;
@@ -1,5 +1,25 @@
1
- import type { RegisteredComponent, RenderResult, RenderState, StreamRenderState, FinalHtmlResult } from './types/index.ts';
2
- export declare function createResultObject(html: FinalHtmlResult | null, consoleReplayScript: string, renderState: RenderState | StreamRenderState): RenderResult;
1
+ import type { RegisteredComponent, RenderingError, FinalHtmlResult } from './types/index.ts';
2
+ /**
3
+ * Builds the metadata object for the length-prefixed streaming protocol.
4
+ * This is the shared metadata builder used by both streaming and non-streaming paths.
5
+ * It contains everything EXCEPT the html content, which travels as raw bytes.
6
+ */
7
+ type RenderMetadataSource = {
8
+ clientProps?: Record<string, unknown>;
9
+ hasErrors?: boolean;
10
+ error?: RenderingError;
11
+ isShellReady?: boolean;
12
+ };
13
+ export declare function buildRenderMetadata(consoleReplayScript: string, renderState: RenderMetadataSource): Record<string, unknown>;
14
+ /**
15
+ * Builds a length-prefixed result string from html content and render state.
16
+ * Format: <metadata JSON>\t<content byte length hex>\n<raw html content>
17
+ *
18
+ * Used by the non-streaming rendering path. The streaming path uses
19
+ * buildRenderMetadata directly with Buffer operations for efficiency.
20
+ */
21
+ export declare function buildLengthPrefixedResult(html: FinalHtmlResult | null, consoleReplayScript: string, renderState: RenderMetadataSource): string;
3
22
  export declare function convertToError(e: unknown): Error;
4
23
  export declare function validateComponent(componentObj: RegisteredComponent, componentName: string): void;
24
+ export {};
5
25
  //# sourceMappingURL=serverRenderUtils.d.ts.map
@@ -1,8 +1,7 @@
1
- export function createResultObject(html, consoleReplayScript, renderState) {
1
+ export function buildRenderMetadata(consoleReplayScript, renderState) {
2
2
  return {
3
- html,
4
- clientProps: renderState.clientProps,
5
3
  consoleReplayScript,
4
+ clientProps: renderState.clientProps,
6
5
  hasErrors: renderState.hasErrors,
7
6
  renderingError: renderState.error && {
8
7
  message: renderState.error.message,
@@ -11,8 +10,94 @@ export function createResultObject(html, consoleReplayScript, renderState) {
11
10
  isShellReady: 'isShellReady' in renderState ? renderState.isShellReady : undefined,
12
11
  };
13
12
  }
13
+ function isCrossRealmError(e) {
14
+ return typeof e === 'object' && e !== null && Object.prototype.toString.call(e) === '[object Error]';
15
+ }
16
+ function stringifyThrownValue(e) {
17
+ if (isCrossRealmError(e)) {
18
+ return typeof e.message === 'string' ? e.message : Object.prototype.toString.call(e);
19
+ }
20
+ if (typeof e === 'object' && e !== null) {
21
+ try {
22
+ // JSON.stringify can return undefined without throwing, for example when toJSON returns undefined.
23
+ return JSON.stringify(e) ?? Object.prototype.toString.call(e);
24
+ }
25
+ catch {
26
+ return Object.prototype.toString.call(e);
27
+ }
28
+ }
29
+ return String(e);
30
+ }
31
+ /**
32
+ * Returns the UTF-8 byte length of a string.
33
+ * Uses native Buffer.byteLength when available (Node.js, Pro node renderer).
34
+ * Falls back to a pure-JS implementation for environments without Buffer (mini_racer).
35
+ */
36
+ function utf8ByteLength(str) {
37
+ if (typeof Buffer !== 'undefined' && typeof Buffer.byteLength === 'function') {
38
+ return Buffer.byteLength(str, 'utf-8');
39
+ }
40
+ let bytes = 0;
41
+ for (let i = 0; i < str.length; i += 1) {
42
+ const code = str.charCodeAt(i);
43
+ if (code <= 0x7f) {
44
+ bytes += 1;
45
+ }
46
+ else if (code <= 0x7ff) {
47
+ bytes += 2;
48
+ }
49
+ else if (code >= 0xd800 && code <= 0xdbff) {
50
+ const next = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
51
+ if (next >= 0xdc00 && next <= 0xdfff) {
52
+ bytes += 4; // valid surrogate pair
53
+ i += 1;
54
+ }
55
+ else {
56
+ bytes += 3; // lone high surrogate → U+FFFD
57
+ }
58
+ }
59
+ else {
60
+ bytes += 3; // BMP char (0x800-0xFFFF) or lone low surrogate
61
+ }
62
+ }
63
+ return bytes;
64
+ }
65
+ /**
66
+ * Builds a length-prefixed result string from html content and render state.
67
+ * Format: <metadata JSON>\t<content byte length hex>\n<raw html content>
68
+ *
69
+ * Used by the non-streaming rendering path. The streaming path uses
70
+ * buildRenderMetadata directly with Buffer operations for efficiency.
71
+ */
72
+ export function buildLengthPrefixedResult(html, consoleReplayScript, renderState) {
73
+ // payloadType tells Ruby how to interpret the content bytes:
74
+ // "string" — raw HTML, use as-is (the common case)
75
+ // "object" — JSON-serialized value, needs JSON.parse (ServerRenderHash or null)
76
+ const metadataObj = buildRenderMetadata(consoleReplayScript, renderState);
77
+ let htmlStr;
78
+ if (typeof html === 'string') {
79
+ metadataObj.payloadType = 'string';
80
+ htmlStr = html;
81
+ }
82
+ else {
83
+ // Handles null, ServerRenderHashRenderedHtml objects, etc.
84
+ // JSON.stringify(null) → "null", which Ruby will JSON.parse back.
85
+ metadataObj.payloadType = 'object';
86
+ htmlStr = JSON.stringify(html);
87
+ }
88
+ const metadata = JSON.stringify(metadataObj);
89
+ const byteLength = utf8ByteLength(htmlStr);
90
+ return `${metadata}\t${byteLength.toString(16).padStart(8, '0')}\n${htmlStr}`;
91
+ }
14
92
  export function convertToError(e) {
15
- return e instanceof Error ? e : new Error(String(e));
93
+ if (e instanceof Error) {
94
+ return e;
95
+ }
96
+ const message = stringifyThrownValue(e);
97
+ // tsconfig uses es2020 libs, which do not type Error.cause even though supported runtimes provide it.
98
+ const error = new Error(message);
99
+ error.cause = e;
100
+ return error;
16
101
  }
17
102
  export function validateComponent(componentObj, componentName) {
18
103
  if (componentObj.isRenderer) {
@@ -77,6 +77,11 @@ type RenderFunctionSyncResult = ReactComponent | ServerRenderResult;
77
77
  type RenderFunctionAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactComponent | ServerRenderResult>;
78
78
  type RenderFunctionResult = RenderFunctionSyncResult | RenderFunctionAsyncResult;
79
79
  type StreamableComponentResult = ReactElement | Promise<ReactElement | string>;
80
+ type AsyncPropsManager = {
81
+ getProp: (propName: string) => Promise<unknown>;
82
+ setProp: (propName: string, propValue: unknown) => void;
83
+ endStream: () => void;
84
+ };
80
85
  /**
81
86
  * Render-functions are used to create dynamic React components or server-rendered HTML with side effects.
82
87
  * They receive two arguments: props and railsContext.
@@ -119,11 +124,13 @@ export interface RegisteredComponent {
119
124
  isRenderer: boolean;
120
125
  }
121
126
  export type ItemRegistrationCallback<T> = (component: T) => void;
127
+ export type GenerateRSCPayloadFunction = (componentName: string, props: unknown, railsContext: RailsContextWithServerComponentMetadata) => Promise<NodeJS.ReadableStream>;
122
128
  interface Params {
123
129
  props?: Record<string, unknown>;
124
130
  railsContext?: RailsContext;
125
131
  domNodeId?: string;
126
132
  trace?: boolean;
133
+ generateRSCPayload?: GenerateRSCPayloadFunction;
127
134
  }
128
135
  export interface RenderParams extends Params {
129
136
  name: string;
@@ -245,6 +252,9 @@ export type RSCPayloadStreamInfo = {
245
252
  componentName: string;
246
253
  };
247
254
  export type RSCPayloadCallback = (streamInfo: RSCPayloadStreamInfo) => void;
255
+ export type WithAsyncProps<AsyncPropsType extends Record<string, unknown>, PropsType extends Record<string, unknown>> = PropsType & {
256
+ getReactOnRailsAsyncProp: <PropName extends keyof AsyncPropsType>(propName: PropName) => Promise<AsyncPropsType[PropName]>;
257
+ };
248
258
  /** Contains the parts of the `ReactOnRails` API intended for internal use only. */
249
259
  export interface ReactOnRailsInternal extends ReactOnRails {
250
260
  /**
@@ -312,7 +322,7 @@ export interface ReactOnRailsInternal extends ReactOnRails {
312
322
  /**
313
323
  * Used by server rendering by Rails
314
324
  */
315
- serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>;
325
+ serverRenderReactComponent(options: RenderParams): null | string | Promise<string>;
316
326
  /**
317
327
  * Used by server rendering by Rails
318
328
  */
@@ -325,6 +335,11 @@ export interface ReactOnRailsInternal extends ReactOnRails {
325
335
  * Used by Rails to catch errors in rendering
326
336
  */
327
337
  handleError(options: ErrorOptions): string | undefined;
338
+ /**
339
+ * Prepares a rendering result in the length-prefixed wire format for transport to Ruby.
340
+ * Used by the server_render_js Rails helper to format arbitrary JS evaluation results.
341
+ */
342
+ prepareRenderResult(html: string, consoleReplayScript: string, hasErrors: boolean, renderingError: RenderingError | null): string;
328
343
  /**
329
344
  * Used by Rails server rendering to replay console messages.
330
345
  * Returns the console replay script wrapped in script tags.
@@ -359,6 +374,27 @@ export interface ReactOnRailsInternal extends ReactOnRails {
359
374
  * Indicates if the RSC bundle is being used.
360
375
  */
361
376
  isRSCBundle: boolean;
377
+ /**
378
+ * Adds the getAsyncProp function to the component props object.
379
+ * Uses getOrCreateAsyncPropsManager internally to handle race conditions
380
+ * between initial render and update chunks.
381
+ *
382
+ * @param props - The component props to enhance
383
+ * @param sharedExecutionContext - Map scoped to the current HTTP request
384
+ * @returns An object containing the component props with getReactOnRailsAsyncProp added
385
+ */
386
+ addAsyncPropsCapabilityToComponentProps: <AsyncPropsType extends Record<string, unknown>, PropsType extends Record<string, unknown>>(props: PropsType, sharedExecutionContext: Map<string, unknown>) => {
387
+ props: WithAsyncProps<AsyncPropsType, PropsType>;
388
+ };
389
+ /**
390
+ * Gets or creates an AsyncPropsManager from the shared execution context.
391
+ * Implements lazy initialization to handle race conditions between
392
+ * the initial render request and update chunks.
393
+ *
394
+ * @param sharedExecutionContext - Map scoped to the current HTTP request
395
+ * @returns The AsyncPropsManager instance (existing or newly created)
396
+ */
397
+ getOrCreateAsyncPropsManager: (sharedExecutionContext: Map<string, unknown>) => AsyncPropsManager;
362
398
  }
363
399
  export type RenderStateHtml = FinalHtmlResult | Promise<FinalHtmlResult | ServerRenderResult>;
364
400
  export type RenderState = {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "react-on-rails",
3
- "version": "16.6.0",
3
+ "version": "16.7.0-rc.0",
4
4
  "description": "react-on-rails JavaScript for react_on_rails Ruby gem",
5
5
  "main": "lib/ReactOnRails.full.js",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/shakacode/react_on_rails.git"
9
+ "url": "git+https://github.com/shakacode/react_on_rails.git",
10
+ "directory": "packages/react-on-rails"
10
11
  },
11
12
  "keywords": [
12
13
  "react",
@@ -40,10 +41,17 @@
40
41
  "./buildConsoleReplay": "./lib/buildConsoleReplay.js",
41
42
  "./ReactDOMServer": "./lib/ReactDOMServer.cjs",
42
43
  "./serverRenderReactComponent": "./lib/serverRenderReactComponent.js",
44
+ "./@internal/sanitizeNonce": "./lib/sanitizeNonce.js",
43
45
  "./@internal/base/client": "./lib/base/client.js",
44
46
  "./@internal/base/full": {
45
47
  "react-server": "./lib/base/full.rsc.js",
46
48
  "default": "./lib/base/full.js"
49
+ },
50
+ "./@internal/createReactOnRails": "./lib/createReactOnRails.js",
51
+ "./@internal/capabilities/core": "./lib/capabilities/core.js",
52
+ "./@internal/capabilities/ssr": {
53
+ "react-server": "./lib/capabilities/ssr.rsc.js",
54
+ "default": "./lib/capabilities/ssr.js"
47
55
  }
48
56
  },
49
57
  "peerDependencies": {
@@ -60,7 +68,7 @@
60
68
  "bugs": {
61
69
  "url": "https://github.com/shakacode/react_on_rails/issues"
62
70
  },
63
- "homepage": "https://github.com/shakacode/react_on_rails#readme",
71
+ "homepage": "https://reactonrails.com/docs/",
64
72
  "scripts": {
65
73
  "build": "pnpm run clean && tsc",
66
74
  "build-watch": "pnpm run clean && tsc --watch",