Skip to content

Commit 1fc6ce0

Browse files
alexeyr-ci2alexeyr
andauthoredMar 20, 2025··
Fix TypeScript issues (#1715)
* Fix some typescript-eslint issues * Give more precise type to Store * Fix promise issues * Use strict TypeScript-ESLint config * Remove unneeded spacing from an error message * Simplify logic * A trivial optimization * Avoid unnecessary await * Make renderOrHydrateComponent async * Make onPageLoaded/Unloaded sync again * Add a check for unmounted state again --------- Co-authored-by: Alexey Romanov <[email protected]>
1 parent 79b4941 commit 1fc6ce0

17 files changed

+139
-87
lines changed
 

‎eslint.config.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ const config = tsEslint.config([
8080
'function-paren-newline': 'off',
8181
'object-curly-newline': 'off',
8282
'no-restricted-syntax': ['error', 'SequenceExpression'],
83+
'no-void': [
84+
'error',
85+
{
86+
allowAsStatement: true,
87+
},
88+
],
8389

8490
'import/extensions': [
8591
'error',
@@ -134,25 +140,37 @@ const config = tsEslint.config([
134140
{
135141
files: ['**/*.ts', '**/*.tsx'],
136142

137-
extends: tsEslint.configs.recommended,
143+
extends: tsEslint.configs.strictTypeChecked,
138144

139145
languageOptions: {
140146
parserOptions: {
141147
projectService: {
142148
allowDefaultProject: ['eslint.config.ts', 'knip.ts'],
149+
// Needed because `import * as ... from` instead of `import ... from` doesn't work in this file
150+
// for some imports.
151+
defaultProject: 'tsconfig.eslint.json',
143152
},
144153
},
145154
},
146155

147156
rules: {
148157
'@typescript-eslint/no-namespace': 'off',
149158
'@typescript-eslint/no-shadow': 'error',
159+
'@typescript-eslint/no-confusing-void-expression': [
160+
'error',
161+
{
162+
ignoreArrowShorthand: true,
163+
},
164+
],
165+
// Too many false positives
166+
'@typescript-eslint/no-unnecessary-condition': 'off',
150167
'@typescript-eslint/no-unused-vars': [
151168
'error',
152169
{
153170
caughtErrorsIgnorePattern: '^_',
154171
},
155172
],
173+
'@typescript-eslint/restrict-template-expressions': 'off',
156174
},
157175
},
158176
// must be the last config in the array

‎node_package/src/ClientSideRenderer.ts

+38-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-classes-per-file */
2-
/* eslint-disable react/no-deprecated -- while we need to support React 16 */
2+
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated -- while we need to support React 16 */
33

44
import * as ReactDOM from 'react-dom';
55
import type { ReactElement } from 'react';
@@ -14,13 +14,13 @@ import { debugTurbolinks } from './turbolinksUtils';
1414

1515
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
1616

17-
function delegateToRenderer(
17+
async function delegateToRenderer(
1818
componentObj: RegisteredComponent,
19-
props: Record<string, string>,
19+
props: Record<string, unknown>,
2020
railsContext: RailsContext,
2121
domNodeId: string,
2222
trace: boolean,
23-
): boolean {
23+
): Promise<boolean> {
2424
const { name, component, isRenderer } = componentObj;
2525

2626
if (isRenderer) {
@@ -32,7 +32,7 @@ function delegateToRenderer(
3232
);
3333
}
3434

35-
(component as RenderFunction)(props, railsContext, domNodeId);
35+
await (component as RenderFunction)(props, railsContext, domNodeId);
3636
return true;
3737
}
3838

@@ -81,7 +81,7 @@ class ComponentRenderer {
8181
// This must match lib/react_on_rails/helper.rb
8282
const name = el.getAttribute('data-component-name') || '';
8383
const { domNodeId } = this;
84-
const props = el.textContent !== null ? JSON.parse(el.textContent) : {};
84+
const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {};
8585
const trace = el.getAttribute('data-trace') === 'true';
8686

8787
try {
@@ -92,7 +92,11 @@ class ComponentRenderer {
9292
return;
9393
}
9494

95-
if (delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) {
95+
if (
96+
(await delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) ||
97+
// @ts-expect-error The state can change while awaiting delegateToRenderer
98+
this.state === 'unmounted'
99+
) {
96100
return;
97101
}
98102

@@ -163,8 +167,8 @@ You should return a React.Component always for the client side entry point.`);
163167
}
164168

165169
waitUntilRendered(): Promise<void> {
166-
if (this.state === 'rendering') {
167-
return this.renderPromise!;
170+
if (this.state === 'rendering' && this.renderPromise) {
171+
return this.renderPromise;
168172
}
169173
return Promise.resolve();
170174
}
@@ -183,15 +187,18 @@ class StoreRenderer {
183187
}
184188

185189
const name = storeDataElement.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
186-
const props = storeDataElement.textContent !== null ? JSON.parse(storeDataElement.textContent) : {};
190+
const props =
191+
storeDataElement.textContent !== null
192+
? (JSON.parse(storeDataElement.textContent) as Record<string, unknown>)
193+
: {};
187194
this.hydratePromise = this.hydrate(context, railsContext, name, props);
188195
}
189196

190197
private async hydrate(
191198
context: Context,
192199
railsContext: RailsContext,
193200
name: string,
194-
props: Record<string, string>,
201+
props: Record<string, unknown>,
195202
) {
196203
const storeGenerator = await context.ReactOnRails.getOrWaitForStoreGenerator(name);
197204
if (this.state === 'unmounted') {
@@ -204,8 +211,8 @@ class StoreRenderer {
204211
}
205212

206213
waitUntilHydrated(): Promise<void> {
207-
if (this.state === 'hydrating') {
208-
return this.hydratePromise!;
214+
if (this.state === 'hydrating' && this.hydratePromise) {
215+
return this.hydratePromise;
209216
}
210217
return Promise.resolve();
211218
}
@@ -217,26 +224,30 @@ class StoreRenderer {
217224

218225
const renderedRoots = new Map<string, ComponentRenderer>();
219226

220-
export function renderOrHydrateComponent(domIdOrElement: string | Element): ComponentRenderer | undefined {
227+
export function renderOrHydrateComponent(domIdOrElement: string | Element) {
221228
const domId = getDomId(domIdOrElement);
222-
debugTurbolinks(`renderOrHydrateComponent ${domId}`);
229+
debugTurbolinks('renderOrHydrateComponent', domId);
223230
let root = renderedRoots.get(domId);
224231
if (!root) {
225232
root = new ComponentRenderer(domIdOrElement);
226233
renderedRoots.set(domId, root);
227234
}
228-
return root;
235+
return root.waitUntilRendered();
229236
}
230237

231-
export function renderOrHydrateForceLoadedComponents(): void {
232-
const els = document.querySelectorAll(`.js-react-on-rails-component[data-force-load="true"]`);
233-
els.forEach((el) => renderOrHydrateComponent(el));
238+
async function forAllElementsAsync(
239+
selector: string,
240+
callback: (el: Element) => Promise<void>,
241+
): Promise<void> {
242+
const els = document.querySelectorAll(selector);
243+
await Promise.all(Array.from(els).map(callback));
234244
}
235245

236-
export function renderOrHydrateAllComponents(): void {
237-
const els = document.querySelectorAll(`.js-react-on-rails-component`);
238-
els.forEach((el) => renderOrHydrateComponent(el));
239-
}
246+
export const renderOrHydrateForceLoadedComponents = () =>
247+
forAllElementsAsync('.js-react-on-rails-component[data-force-load="true"]', renderOrHydrateComponent);
248+
249+
export const renderOrHydrateAllComponents = () =>
250+
forAllElementsAsync('.js-react-on-rails-component', renderOrHydrateComponent);
240251

241252
function unmountAllComponents(): void {
242253
renderedRoots.forEach((root) => root.unmount());
@@ -267,15 +278,11 @@ export async function hydrateStore(storeNameOrElement: string | Element) {
267278
await storeRenderer.waitUntilHydrated();
268279
}
269280

270-
export async function hydrateForceLoadedStores(): Promise<void> {
271-
const els = document.querySelectorAll(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}][data-force-load="true"]`);
272-
await Promise.all(Array.from(els).map((el) => hydrateStore(el)));
273-
}
281+
export const hydrateForceLoadedStores = () =>
282+
forAllElementsAsync(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}][data-force-load="true"]`, hydrateStore);
274283

275-
export async function hydrateAllStores(): Promise<void> {
276-
const els = document.querySelectorAll(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}]`);
277-
await Promise.all(Array.from(els).map((el) => hydrateStore(el)));
278-
}
284+
export const hydrateAllStores = () =>
285+
forAllElementsAsync(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}]`, hydrateStore);
279286

280287
function unmountAllStores(): void {
281288
storeRenderers.forEach((storeRenderer) => storeRenderer.unmount());

‎node_package/src/ComponentRegistry.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type RegisteredComponent, type ReactComponentOrRenderFunction, type RenderFunction } from './types';
1+
import { type RegisteredComponent, type ReactComponentOrRenderFunction } from './types';
22
import isRenderFunction from './isRenderFunction';
33
import CallbackRegistry from './CallbackRegistry';
44

@@ -20,7 +20,7 @@ export default {
2020
}
2121

2222
const renderFunction = isRenderFunction(component);
23-
const isRenderer = renderFunction && (component as RenderFunction).length === 3;
23+
const isRenderer = renderFunction && component.length === 3;
2424

2525
componentRegistry.set(name, {
2626
name,

‎node_package/src/ReactOnRails.client.ts

+11-13
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ if (ctx === undefined) {
2525
}
2626

2727
if (ctx.ReactOnRails !== undefined) {
28-
throw new Error(`
29-
The ReactOnRails value exists in the ${ctx} scope, it may not be safe to overwrite it.
30-
31-
This could be caused by setting Webpack's optimization.runtimeChunk to "true" or "multiple," rather than "single." Check your Webpack configuration.
32-
33-
Read more at https://github.com/shakacode/react_on_rails/issues/1558.
34-
`);
28+
/* eslint-disable @typescript-eslint/no-base-to-string -- Window and Global both have useful toString() */
29+
throw new Error(`\
30+
The ReactOnRails value exists in the ${ctx} scope, it may not be safe to overwrite it.
31+
This could be caused by setting Webpack's optimization.runtimeChunk to "true" or "multiple," rather than "single."
32+
Check your Webpack configuration. Read more at https://github.com/shakacode/react_on_rails/issues/1558.`);
33+
/* eslint-enable @typescript-eslint/no-base-to-string */
3534
}
3635

3736
const DEFAULT_OPTIONS = {
@@ -149,12 +148,12 @@ ctx.ReactOnRails = {
149148
return ClientStartup.reactOnRailsPageLoaded();
150149
},
151150

152-
reactOnRailsComponentLoaded(domId: string): void {
153-
renderOrHydrateComponent(domId);
151+
reactOnRailsComponentLoaded(domId: string): Promise<void> {
152+
return renderOrHydrateComponent(domId);
154153
},
155154

156-
reactOnRailsStoreLoaded(storeName: string): void {
157-
hydrateStore(storeName);
155+
reactOnRailsStoreLoaded(storeName: string): Promise<void> {
156+
return hydrateStore(storeName);
158157
},
159158

160159
/**
@@ -201,11 +200,10 @@ ctx.ReactOnRails = {
201200

202201
/**
203202
* Allows saving the store populated by Rails form props. Used internally by ReactOnRails.
204-
* @param name
205203
* @returns Redux Store, possibly hydrated
206204
*/
207205
setStore(name: string, store: Store): void {
208-
return StoreRegistry.setStore(name, store);
206+
StoreRegistry.setStore(name, store);
209207
},
210208

211209
/**

‎node_package/src/ReactOnRailsRSC.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRender
4646
});
4747
pipeToTransform(rscStream);
4848
})
49-
.catch((e) => {
49+
.catch((e: unknown) => {
5050
const error = convertToError(e);
5151
renderState.hasErrors = true;
5252
renderState.error = error;

‎node_package/src/buildConsoleReplay.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function consoleReplay(
3737
val = 'undefined';
3838
}
3939
} catch (e) {
40+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- if we here, JSON.stringify didn't work
4041
val = `${(e as Error).message}: ${arg}`;
4142
}
4243

‎node_package/src/clientStartup.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function reactOnRailsPageUnloaded(): void {
1919
unmountAll();
2020
}
2121

22-
export async function clientStartup(context: Context): Promise<void> {
22+
export function clientStartup(context: Context) {
2323
// Check if server rendering
2424
if (!isWindow(context)) {
2525
return;
@@ -34,9 +34,11 @@ export async function clientStartup(context: Context): Promise<void> {
3434
// eslint-disable-next-line no-underscore-dangle
3535
context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__ = true;
3636

37-
// force loaded components and stores are rendered and hydrated immediately
38-
renderOrHydrateForceLoadedComponents();
39-
hydrateForceLoadedStores();
37+
// Force loaded components and stores are rendered and hydrated immediately.
38+
// The hydration process can handle the concurrent hydration of components and stores,
39+
// so awaiting this isn't necessary.
40+
void renderOrHydrateForceLoadedComponents();
41+
void hydrateForceLoadedStores();
4042

4143
// Other components and stores are rendered and hydrated when the page is fully loaded
4244
onPageLoaded(reactOnRailsPageLoaded);

‎node_package/src/context.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type Context = Window | typeof globalThis;
1818
/**
1919
* Get the context, be it window or global
2020
*/
21+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
2122
export default function context(this: void): Context | void {
2223
return (typeof window !== 'undefined' && window) || (typeof global !== 'undefined' && global) || this;
2324
}
@@ -53,7 +54,7 @@ export function getContextAndRailsContext(): { context: Context | null; railsCon
5354
}
5455

5556
try {
56-
currentRailsContext = JSON.parse(el.textContent);
57+
currentRailsContext = JSON.parse(el.textContent) as RailsContext;
5758
} catch (e) {
5859
console.error('Error parsing Rails context:', e);
5960
return { context: null, railsContext: null };

‎node_package/src/isRenderFunction.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function isRenderFunction(
1212
component: ReactComponentOrRenderFunction,
1313
): component is RenderFunction {
1414
// No for es5 or es6 React Component
15+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1516
if ((component as RenderFunction).prototype?.isReactComponent) {
1617
return false;
1718
}
+12-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import * as path from 'path';
22
import * as fs from 'fs/promises';
33

4-
const loadedReactClientManifests = new Map<string, Record<string, unknown>>();
4+
type ClientManifest = Record<string, unknown>;
5+
const loadedReactClientManifests = new Map<string, ClientManifest>();
56

67
export default async function loadReactClientManifest(reactClientManifestFileName: string) {
78
// React client manifest is uploaded to node renderer as an asset.
89
// Renderer copies assets to the same place as the server-bundle.js and rsc-bundle.js.
910
// Thus, the __dirname of this code is where we can find the manifest file.
1011
const manifestPath = path.resolve(__dirname, reactClientManifestFileName);
11-
if (!loadedReactClientManifests.has(manifestPath)) {
12-
// TODO: convert to async
13-
try {
14-
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
15-
loadedReactClientManifests.set(manifestPath, manifest);
16-
} catch (error) {
17-
throw new Error(`Failed to load React client manifest from ${manifestPath}: ${error}`);
18-
}
12+
const loadedReactClientManifest = loadedReactClientManifests.get(manifestPath);
13+
if (loadedReactClientManifest) {
14+
return loadedReactClientManifest;
1915
}
2016

21-
return loadedReactClientManifests.get(manifestPath)!;
17+
try {
18+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as ClientManifest;
19+
loadedReactClientManifests.set(manifestPath, manifest);
20+
return manifest;
21+
} catch (error) {
22+
throw new Error(`Failed to load React client manifest from ${manifestPath}: ${error}`);
23+
}
2224
}

‎node_package/src/pageLifecycle.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
turbolinksVersion5,
77
} from './turbolinksUtils';
88

9-
type PageLifecycleCallback = () => void;
9+
type PageLifecycleCallback = () => void | Promise<void>;
1010
type PageState = 'load' | 'unload' | 'initial';
1111

1212
const pageLoadedCallbacks = new Set<PageLifecycleCallback>();
@@ -16,12 +16,16 @@ let currentPageState: PageState = 'initial';
1616

1717
function runPageLoadedCallbacks(): void {
1818
currentPageState = 'load';
19-
pageLoadedCallbacks.forEach((callback) => callback());
19+
pageLoadedCallbacks.forEach((callback) => {
20+
void callback();
21+
});
2022
}
2123

2224
function runPageUnloadedCallbacks(): void {
2325
currentPageState = 'unload';
24-
pageUnloadedCallbacks.forEach((callback) => callback());
26+
pageUnloadedCallbacks.forEach((callback) => {
27+
void callback();
28+
});
2529
}
2630

2731
function setupTurbolinksEventListeners(): void {
@@ -71,15 +75,15 @@ function initializePageEventListeners(): void {
7175

7276
export function onPageLoaded(callback: PageLifecycleCallback): void {
7377
if (currentPageState === 'load') {
74-
callback();
78+
void callback();
7579
}
7680
pageLoadedCallbacks.add(callback);
7781
initializePageEventListeners();
7882
}
7983

8084
export function onPageUnloaded(callback: PageLifecycleCallback): void {
8185
if (currentPageState === 'unload') {
82-
callback();
86+
void callback();
8387
}
8488
pageUnloadedCallbacks.add(callback);
8589
initializePageEventListeners();

‎node_package/src/reactHydrateOrRender.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,39 @@ type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => Ren
77

88
// TODO: once React dependency is updated to >= 18, we can remove this and just
99
// import ReactDOM from 'react-dom/client';
10-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11-
let reactDomClient: any;
10+
let reactDomClient: typeof import('react-dom/client');
1211
if (supportsRootApi) {
1312
// This will never throw an exception, but it's the way to tell Webpack the dependency is optional
1413
// https://github.com/webpack/webpack/issues/339#issuecomment-47739112
1514
// Unfortunately, it only converts the error to a warning.
1615
try {
1716
// eslint-disable-next-line global-require,@typescript-eslint/no-require-imports
18-
reactDomClient = require('react-dom/client');
17+
reactDomClient = require('react-dom/client') as typeof import('react-dom/client');
1918
} catch (_e) {
2019
// We should never get here, but if we do, we'll just use the default ReactDOM
2120
// and live with the warning.
22-
reactDomClient = ReactDOM;
21+
reactDomClient = ReactDOM as unknown as typeof import('react-dom/client');
2322
}
2423
}
2524

26-
/* eslint-disable react/no-deprecated -- while we need to support React 16 */
25+
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion --
26+
* while we need to support React 16
27+
*/
2728
const reactHydrate: HydrateOrRenderType = supportsRootApi
28-
? reactDomClient.hydrateRoot
29+
? reactDomClient!.hydrateRoot
2930
: (domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode);
3031

3132
function reactRender(domNode: Element, reactElement: ReactElement): RenderReturnType {
3233
if (supportsRootApi) {
33-
const root = reactDomClient.createRoot(domNode);
34+
const root = reactDomClient!.createRoot(domNode);
3435
root.render(reactElement);
3536
return root;
3637
}
3738

3839
// eslint-disable-next-line react/no-render-return-value
3940
return ReactDOM.render(reactElement, domNode);
4041
}
41-
/* eslint-enable react/no-deprecated */
42+
/* eslint-enable react/no-deprecated,@typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion */
4243

4344
export default function reactHydrateOrRender(
4445
domNode: Element,

‎node_package/src/streamServerRenderedReactComponent.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const stringToStream = (str: string): Readable => {
1717
return stream;
1818
};
1919

20-
type BufferdEvent = {
20+
type BufferedEvent = {
2121
event: 'data' | 'error' | 'end';
2222
data: unknown;
2323
};
@@ -38,7 +38,7 @@ type BufferdEvent = {
3838
* - emitError: A function to manually emit errors into the stream
3939
*/
4040
const bufferStream = (stream: Readable) => {
41-
const bufferedEvents: BufferdEvent[] = [];
41+
const bufferedEvents: BufferedEvent[] = [];
4242
let startedReading = false;
4343

4444
const listeners = (['data', 'error', 'end'] as const).map((event) => {
@@ -58,7 +58,7 @@ const bufferStream = (stream: Readable) => {
5858

5959
// Remove initial listeners
6060
listeners.forEach(({ event, listener }) => stream.off(event, listener));
61-
const handleEvent = ({ event, data }: BufferdEvent) => {
61+
const handleEvent = ({ event, data }: BufferedEvent) => {
6262
if (event === 'data') {
6363
this.push(data);
6464
} else if (event === 'error') {
@@ -96,7 +96,8 @@ export const transformRenderStreamChunksToResultObject = (renderState: StreamRen
9696

9797
const transformStream = new PassThrough({
9898
transform(chunk, _, callback) {
99-
const htmlChunk = chunk.toString();
99+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
100+
const htmlChunk = chunk.toString() as string;
100101
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
101102
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
102103

‎node_package/src/transformRSCStreamAndReplayConsoleLogs.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export default function transformRSCStreamAndReplayConsoleLogs(stream: ReadableStream) {
1+
import { RenderResult } from './types';
2+
3+
export default function transformRSCStreamAndReplayConsoleLogs(stream: ReadableStream<Uint8Array>) {
24
return new ReadableStream({
35
async start(controller) {
46
const reader = stream.getReader();
@@ -16,7 +18,7 @@ export default function transformRSCStreamAndReplayConsoleLogs(stream: ReadableS
1618
.filter((line) => line.trim() !== '')
1719
.map((line) => {
1820
try {
19-
return JSON.parse(line);
21+
return JSON.parse(line) as RenderResult;
2022
} catch (error) {
2123
console.error('Error parsing JSON:', line, error);
2224
throw error;
@@ -25,7 +27,7 @@ export default function transformRSCStreamAndReplayConsoleLogs(stream: ReadableS
2527

2628
for (const jsonChunk of jsonChunks) {
2729
const { html, consoleReplayScript = '' } = jsonChunk;
28-
controller.enqueue(encoder.encode(html));
30+
controller.enqueue(encoder.encode(html ?? ''));
2931

3032
const replayConsoleCode = consoleReplayScript
3133
.trim()

‎node_package/src/turbolinksUtils.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ declare global {
88
}
99
}
1010

11-
export function debugTurbolinks(...msg: string[]): void {
11+
/**
12+
* Formats a message if the `traceTurbolinks` option is enabled.
13+
* Multiple arguments can be passed like to `console.log`,
14+
* except format specifiers aren't substituted (because it isn't used as the first argument).
15+
*/
16+
export function debugTurbolinks(...msg: unknown[]): void {
1217
if (!window) {
1318
return;
1419
}

‎node_package/src/types/index.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import type { Readable } from 'stream';
77
// See https://github.com/shakacode/react_on_rails/issues/1321
88
// and https://redux.js.org/api/store for the actual API.
99
/* eslint-disable @typescript-eslint/no-explicit-any */
10-
type Store = unknown;
10+
type Store = {
11+
getState(): unknown;
12+
};
1113

1214
type ReactComponent = ComponentType<any> | string;
1315

@@ -164,6 +166,7 @@ export interface Root {
164166
unmount(): void;
165167
}
166168

169+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- inherited from React 16/17, can't avoid here
167170
export type RenderReturnType = void | Element | Component | Root;
168171

169172
export interface ReactOnRails {
@@ -177,8 +180,8 @@ export interface ReactOnRails {
177180
setOptions(newOptions: { traceTurbolinks: boolean }): void;
178181
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType;
179182
reactOnRailsPageLoaded(): Promise<void>;
180-
reactOnRailsComponentLoaded(domId: string): void;
181-
reactOnRailsStoreLoaded(storeName: string): void;
183+
reactOnRailsComponentLoaded(domId: string): Promise<void>;
184+
reactOnRailsStoreLoaded(storeName: string): Promise<void>;
182185
authenticityToken(): string | null;
183186
authenticityHeaders(otherHeaders: Record<string, string>): AuthenticityHeaders;
184187
option(key: string): string | number | boolean | undefined;

‎tsconfig.eslint.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"allowSyntheticDefaultImports": true
5+
}
6+
}

0 commit comments

Comments
 (0)
Please sign in to comment.