Skip to content

Commit ba8dd11

Browse files
Enhance ReactOnRails initialization and type safety
- Updated global declaration for ReactOnRails to allow undefined, improving type safety. - Refactored createReactOnRails and createBaseClientObject functions to accept current global state for better caching and validation. - Introduced type-safe specifications for core and Pro-specific functions, ensuring proper method overrides. - Enhanced error handling for global object initialization, preventing conflicts with multiple runtime chunks. These changes improve the robustness and maintainability of the React on Rails framework.
1 parent 0e49d15 commit ba8dd11

File tree

16 files changed

+285
-111
lines changed

16 files changed

+285
-111
lines changed

packages/react-on-rails-pro/src/ReactOnRails.client.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
*/
1414

1515
import { createBaseClientObject } from 'react-on-rails/@internal/base/client';
16-
import { createReactOnRailsPro } from './createReactOnRailsPro.ts';
16+
import createReactOnRailsPro from './createReactOnRailsPro.ts';
1717

18-
const ReactOnRails = createReactOnRailsPro(createBaseClientObject);
18+
const currentGlobal = globalThis.ReactOnRails || null;
19+
const ReactOnRails = createReactOnRailsPro(createBaseClientObject, currentGlobal);
1920

2021
export * from 'react-on-rails/types';
2122
export default ReactOnRails;

packages/react-on-rails-pro/src/ReactOnRails.full.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414

1515
import { createBaseFullObject } from 'react-on-rails/@internal/base/full';
16-
import { createReactOnRailsPro } from './createReactOnRailsPro.ts';
16+
import createReactOnRailsPro from './createReactOnRailsPro.ts';
1717

1818
// Warn about bundle size when included in browser bundles
1919
if (typeof window !== 'undefined') {
@@ -24,7 +24,8 @@ if (typeof window !== 'undefined') {
2424
);
2525
}
2626

27-
const ReactOnRails = createReactOnRailsPro(createBaseFullObject);
27+
const currentGlobal = globalThis.ReactOnRails || null;
28+
const ReactOnRails = createReactOnRailsPro(createBaseFullObject, currentGlobal);
2829

2930
export * from 'react-on-rails/types';
3031
export default ReactOnRails;

packages/react-on-rails-pro/src/ReactOnRails.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import ReactOnRails from './ReactOnRails.full.ts';
1616
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent.ts';
1717

1818
// Add Pro server-side streaming functionality
19+
1920
ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;
2021

2122
export * from './ReactOnRails.full.ts';

packages/react-on-rails-pro/src/createReactOnRailsPro.ts

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
1313
*/
1414

15-
import { createBaseClientObject } from 'react-on-rails/@internal/base/client';
15+
import { createBaseClientObject, type BaseClientObjectType } from 'react-on-rails/@internal/base/client';
1616
import { createBaseFullObject } from 'react-on-rails/@internal/base/full';
1717
import { onPageLoaded, onPageUnloaded } from 'react-on-rails/pageLifecycle';
1818
import { debugTurbolinks } from 'react-on-rails/turbolinksUtils';
19-
import type { Store, StoreGenerator, RegisteredComponent } from 'react-on-rails/types';
19+
import type { ReactOnRailsInternal, RegisteredComponent, Store, StoreGenerator } from 'react-on-rails/types';
2020
import * as ProComponentRegistry from './ComponentRegistry.ts';
2121
import * as ProStoreRegistry from './StoreRegistry.ts';
2222
import {
@@ -31,6 +31,22 @@ import {
3131

3232
type BaseObjectCreator = typeof createBaseClientObject | typeof createBaseFullObject;
3333

34+
/**
35+
* Pro-specific functions that override base/core stubs with real implementations.
36+
* Typed explicitly to ensure type safety when mutating the base object.
37+
*/
38+
type ReactOnRailsProSpecificFunctions = Pick<
39+
ReactOnRailsInternal,
40+
| 'reactOnRailsPageLoaded'
41+
| 'reactOnRailsComponentLoaded'
42+
| 'getOrWaitForComponent'
43+
| 'getOrWaitForStore'
44+
| 'getOrWaitForStoreGenerator'
45+
| 'reactOnRailsStoreLoaded'
46+
| 'streamServerRenderedReactComponent'
47+
| 'serverRenderRSCReactComponent'
48+
>;
49+
3450
// Pro client startup with immediate hydration support
3551
async function reactOnRailsPageLoaded() {
3652
debugTurbolinks('reactOnRailsPageLoaded [PRO]');
@@ -62,19 +78,23 @@ function clientStartup() {
6278
onPageUnloaded(reactOnRailsPageUnloaded);
6379
}
6480

65-
// eslint-disable-next-line import/prefer-default-export
66-
export function createReactOnRailsPro(baseObjectCreator: BaseObjectCreator) {
67-
// Create base object with Pro registries
68-
const baseObject = baseObjectCreator({
69-
ComponentRegistry: ProComponentRegistry,
70-
StoreRegistry: ProStoreRegistry,
71-
});
72-
73-
// Add Pro-specific implementations
74-
const ReactOnRails = {
75-
...baseObject,
81+
export default function createReactOnRailsPro(
82+
baseObjectCreator: BaseObjectCreator,
83+
currentGlobal: BaseClientObjectType | null = null,
84+
): ReactOnRailsInternal {
85+
// Create base object with Pro registries, passing currentGlobal for caching/validation
86+
const baseObject = baseObjectCreator(
87+
{
88+
ComponentRegistry: ProComponentRegistry,
89+
StoreRegistry: ProStoreRegistry,
90+
},
91+
currentGlobal,
92+
);
7693

77-
// Override client-side rendering stubs with Pro implementations
94+
// Define Pro-specific functions with proper types
95+
// This object acts as a type-safe specification of what we're adding/overriding on the base object
96+
const reactOnRailsProSpecificFunctions: ReactOnRailsProSpecificFunctions = {
97+
// Override core implementations with Pro implementations
7898
reactOnRailsPageLoaded(): Promise<void> {
7999
return reactOnRailsPageLoaded();
80100
},
@@ -83,11 +103,7 @@ export function createReactOnRailsPro(baseObjectCreator: BaseObjectCreator) {
83103
return renderOrHydrateComponent(domId);
84104
},
85105

86-
// ===================================================================
87-
// PRO-ONLY METHOD IMPLEMENTATIONS
88-
// These methods don't exist in base, add them here
89-
// ===================================================================
90-
106+
// Pro-only method implementations (override core stubs)
91107
getOrWaitForComponent(name: string): Promise<RegisteredComponent> {
92108
return ProComponentRegistry.getOrWaitForComponent(name);
93109
},
@@ -105,28 +121,42 @@ export function createReactOnRailsPro(baseObjectCreator: BaseObjectCreator) {
105121
},
106122

107123
// streamServerRenderedReactComponent is added in ReactOnRails.node.ts
108-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
109-
streamServerRenderedReactComponent(..._args: any[]): any {
124+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
125+
streamServerRenderedReactComponent(): any {
110126
throw new Error(
111127
'streamServerRenderedReactComponent requires importing from react-on-rails-pro in Node.js environment',
112128
);
113129
},
114130

115131
// serverRenderRSCReactComponent is added in ReactOnRailsRSC.ts
116-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
117-
serverRenderRSCReactComponent(..._args: any[]): any {
132+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133+
serverRenderRSCReactComponent(): any {
118134
throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only');
119135
},
120136
};
121137

122-
// Assign to global
123-
globalThis.ReactOnRails = ReactOnRails;
138+
// Type assertion is safe here because:
139+
// 1. We start with BaseClientObjectType or BaseFullObjectType (from baseObjectCreator)
140+
// 2. We add exactly the methods defined in ReactOnRailsProSpecificFunctions
141+
// 3. ReactOnRailsInternal = Base + ReactOnRailsProSpecificFunctions
142+
// TypeScript can't track the mutation, but we ensure type safety by explicitly typing
143+
// the functions object above
144+
const reactOnRailsPro = baseObject as unknown as ReactOnRailsInternal;
145+
146+
// Assign Pro-specific functions to the ReactOnRailsPro object using Object.assign
147+
// This pattern ensures we add exactly what's defined in the type, nothing more, nothing less
148+
Object.assign(reactOnRailsPro, reactOnRailsProSpecificFunctions);
124149

125-
// Reset options to defaults
126-
ReactOnRails.resetOptions();
150+
// Assign to global if not already assigned
151+
if (!globalThis.ReactOnRails) {
152+
globalThis.ReactOnRails = reactOnRailsPro;
127153

128-
// Run Pro client startup with immediate hydration support
129-
clientStartup();
154+
// Reset options to defaults (only on first initialization)
155+
reactOnRailsPro.resetOptions();
156+
157+
// Run Pro client startup with immediate hydration support (only on first initialization)
158+
clientStartup();
159+
}
130160

131-
return ReactOnRails;
161+
return reactOnRailsPro;
132162
}

packages/react-on-rails-pro/src/registerServerComponent/client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const registerServerComponent = (...componentNames: string[]) => {
5555
<RSCRoute componentName={name} componentProps={props} />
5656
));
5757
}
58+
5859
ReactOnRails.register(componentsWrappedInRSCRoute);
5960
};
6061

packages/react-on-rails-pro/src/registerServerComponent/server.rsc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import { ReactComponent, RenderFunction } from 'react-on-rails/types';
3333
* });
3434
* ```
3535
*/
36-
const registerServerComponent = (components: { [id: string]: ReactComponent | RenderFunction }) =>
36+
const registerServerComponent = (components: { [id: string]: ReactComponent | RenderFunction }) => {
3737
ReactOnRails.register(components);
38+
};
3839

3940
export default registerServerComponent;

packages/react-on-rails-pro/src/registerServerComponent/server.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const registerServerComponent = (components: Record<string, ReactComponent>) =>
4343
<RSCRoute componentName={componentName} componentProps={props} />
4444
));
4545
}
46+
4647
ReactOnRails.register(componentsWrappedInRSCRoute);
4748
};
4849

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createBaseClientObject } from './base/client.ts';
2-
import { createReactOnRails } from './createReactOnRails.ts';
2+
import createReactOnRails from './createReactOnRails.ts';
33

4-
const ReactOnRails = createReactOnRails(createBaseClientObject);
4+
const currentGlobal = globalThis.ReactOnRails || null;
5+
const ReactOnRails = createReactOnRails(createBaseClientObject, currentGlobal);
56

67
export * from './types/index.ts';
78
export default ReactOnRails;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createBaseFullObject } from './base/full.ts';
2-
import { createReactOnRails } from './createReactOnRails.ts';
2+
import createReactOnRails from './createReactOnRails.ts';
33

4-
const ReactOnRails = createReactOnRails(createBaseFullObject);
4+
const currentGlobal = globalThis.ReactOnRails || null;
5+
const ReactOnRails = createReactOnRails(createBaseFullObject, currentGlobal);
56

67
export * from './types/index.ts';
78
export default ReactOnRails;

packages/react-on-rails/src/ReactOnRails.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ReactOnRails from './ReactOnRails.full.ts';
22

33
// Pro-only functionality - provide stub that directs users to upgrade
4+
45
ReactOnRails.streamServerRenderedReactComponent = () => {
56
throw new Error('streamServerRenderedReactComponent requires react-on-rails-pro package');
67
};

0 commit comments

Comments
 (0)