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 +8 -0
- package/lib/ReactOnRails.client.js +14 -2
- package/lib/ReactOnRails.full.js +12 -2
- package/lib/RenderUtils.js +2 -5
- package/lib/base/client.d.ts +5 -1
- package/lib/base/client.js +9 -0
- package/lib/base/full.d.ts +5 -1
- package/lib/base/full.js +11 -0
- package/lib/base/full.rsc.d.ts +4 -0
- package/lib/base/full.rsc.js +7 -0
- package/lib/capabilities/core.d.ts +58 -0
- package/lib/capabilities/core.js +188 -0
- package/lib/capabilities/lifecycle.d.ts +9 -0
- package/lib/capabilities/lifecycle.js +19 -0
- package/lib/capabilities/ssr.d.ts +11 -0
- package/lib/capabilities/ssr.js +31 -0
- package/lib/capabilities/ssr.rsc.d.ts +10 -0
- package/lib/capabilities/ssr.rsc.js +19 -0
- package/lib/createReactOnRails.d.ts +18 -5
- package/lib/createReactOnRails.js +97 -63
- package/lib/pageLifecycle.js +23 -11
- package/lib/sanitizeNonce.d.ts +15 -0
- package/lib/sanitizeNonce.js +18 -0
- package/lib/scriptSanitizedVal.d.ts +2 -2
- package/lib/scriptSanitizedVal.js +2 -1
- package/lib/serverRenderReactComponent.d.ts +2 -2
- package/lib/serverRenderReactComponent.js +4 -4
- package/lib/serverRenderUtils.d.ts +22 -2
- package/lib/serverRenderUtils.js +89 -4
- package/lib/types/index.d.ts +37 -1
- package/package.json +11 -3
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 {
|
|
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(
|
|
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
|
package/lib/ReactOnRails.full.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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
|
package/lib/RenderUtils.js
CHANGED
|
@@ -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
|
-
|
|
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}>
|
package/lib/base/client.d.ts
CHANGED
|
@@ -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
|
package/lib/base/client.js
CHANGED
|
@@ -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;
|
package/lib/base/full.d.ts
CHANGED
|
@@ -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)
|
package/lib/base/full.rsc.d.ts
CHANGED
|
@@ -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';
|
package/lib/base/full.rsc.js
CHANGED
|
@@ -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 {
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
package/lib/pageLifecycle.js
CHANGED
|
@@ -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
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
-
|
|
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
|
|
2
|
-
export default
|
|
1
|
+
declare const scriptSanitizedVal: (val: string) => string;
|
|
2
|
+
export default scriptSanitizedVal;
|
|
3
3
|
//# sourceMappingURL=scriptSanitizedVal.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { RenderParams
|
|
2
|
-
declare function serverRenderReactComponentInternal(options: RenderParams): null | string | Promise<
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
2
|
-
|
|
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
|
package/lib/serverRenderUtils.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
export function
|
|
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
|
-
|
|
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) {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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<
|
|
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.
|
|
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://
|
|
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",
|