react-on-rails-pro 16.7.0-rc.2 → 17.0.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.
@@ -1,4 +1,6 @@
1
1
  declare const ReactOnRails: import("react-on-rails/types").ReactOnRailsInternal;
2
2
  export * from 'react-on-rails/types';
3
+ export { unstable_cache, registerCacheHandler } from './cache/index.ts';
4
+ export type { CacheHandler, CacheEntry, UnstableCacheOptions } from './cache/index.ts';
3
5
  export default ReactOnRails;
4
6
  //# sourceMappingURL=ReactOnRailsRSC.d.ts.map
@@ -17,5 +17,6 @@ import createReactOnRailsPro from "./createReactOnRailsPro.js";
17
17
  const currentGlobal = globalThis.ReactOnRails || null;
18
18
  const ReactOnRails = createReactOnRailsPro([createSSRCapability(), createProRSCCapability()], currentGlobal);
19
19
  export * from 'react-on-rails/types';
20
+ export { unstable_cache, registerCacheHandler } from "./cache/index.js"; // eslint-disable-line camelcase -- matches Next.js API naming convention
20
21
  export default ReactOnRails;
21
22
  //# sourceMappingURL=ReactOnRailsRSC.js.map
@@ -0,0 +1,10 @@
1
+ export interface CacheEntry {
2
+ value: Buffer[];
3
+ revalidate: number;
4
+ timestamp: number;
5
+ }
6
+ export interface CacheHandler {
7
+ get(key: string): Promise<CacheEntry | null>;
8
+ set(key: string, entry: CacheEntry): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=CacheHandler.d.ts.map
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=CacheHandler.js.map
@@ -0,0 +1,9 @@
1
+ import type { CacheEntry, CacheHandler } from './CacheHandler.ts';
2
+ export declare class InMemoryLRUCacheHandler implements CacheHandler {
3
+ private cache;
4
+ private maxEntries;
5
+ constructor(maxEntries?: number);
6
+ get(key: string): Promise<CacheEntry | null>;
7
+ set(key: string, entry: CacheEntry): Promise<void>;
8
+ }
9
+ //# sourceMappingURL=InMemoryLRUCacheHandler.d.ts.map
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ // eslint-disable-next-line import/prefer-default-export -- designed for named import alongside the interface
15
+ export class InMemoryLRUCacheHandler {
16
+ constructor(maxEntries = 1000) {
17
+ this.cache = new Map();
18
+ this.maxEntries = maxEntries;
19
+ }
20
+ // eslint-disable-next-line @typescript-eslint/require-await -- CacheHandler interface is async for remote implementations
21
+ async get(key) {
22
+ const entry = this.cache.get(key);
23
+ if (!entry)
24
+ return null;
25
+ if (entry.revalidate > 0 && Date.now() - entry.timestamp > entry.revalidate * 1000) {
26
+ this.cache.delete(key);
27
+ return null;
28
+ }
29
+ // Move to end (most-recently-used) by re-inserting
30
+ this.cache.delete(key);
31
+ this.cache.set(key, entry);
32
+ return entry;
33
+ }
34
+ // eslint-disable-next-line @typescript-eslint/require-await
35
+ async set(key, entry) {
36
+ // If key already exists, remove it first so re-insert goes to end
37
+ if (this.cache.has(key)) {
38
+ this.cache.delete(key);
39
+ }
40
+ // Evict oldest (first) entry if at capacity
41
+ if (this.cache.size >= this.maxEntries) {
42
+ const oldestKey = this.cache.keys().next().value;
43
+ if (oldestKey !== undefined) {
44
+ this.cache.delete(oldestKey);
45
+ }
46
+ }
47
+ this.cache.set(key, entry);
48
+ }
49
+ }
50
+ //# sourceMappingURL=InMemoryLRUCacheHandler.js.map
@@ -0,0 +1,2 @@
1
+ export declare function buildCacheKey(buildId: string, id: string, args: unknown[]): string;
2
+ //# sourceMappingURL=buildCacheKey.d.ts.map
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ import { createHash } from 'crypto';
15
+ // eslint-disable-next-line import/prefer-default-export -- will grow with additional key utilities
16
+ export function buildCacheKey(buildId, id, args) {
17
+ const hash = createHash('sha256');
18
+ hash.update(buildId);
19
+ hash.update(':');
20
+ hash.update(id);
21
+ hash.update(':');
22
+ hash.update(JSON.stringify(args));
23
+ return `rorp:rsc-cache:${hash.digest('hex')}`;
24
+ }
25
+ //# sourceMappingURL=buildCacheKey.js.map
@@ -0,0 +1,3 @@
1
+ export declare function setBuildId(id: string): void;
2
+ export declare function getBuildId(): string;
3
+ //# sourceMappingURL=buildIdProvider.d.ts.map
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ let buildId;
15
+ export function setBuildId(id) {
16
+ buildId = id;
17
+ }
18
+ export function getBuildId() {
19
+ if (!buildId) {
20
+ throw new Error('BUILD_ID not set. Ensure unstable_cache is used within a React Server Component render context. ' +
21
+ 'The BUILD_ID is initialized from rscBundleHash during the first render request.');
22
+ }
23
+ return buildId;
24
+ }
25
+ //# sourceMappingURL=buildIdProvider.js.map
@@ -0,0 +1,4 @@
1
+ import type { CacheHandler } from './CacheHandler.ts';
2
+ export declare function registerCacheHandler(kind: string, handler: CacheHandler): void;
3
+ export declare function getCacheHandler(kind: string): CacheHandler;
4
+ //# sourceMappingURL=cacheHandlerRegistry.d.ts.map
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ import { InMemoryLRUCacheHandler } from "./InMemoryLRUCacheHandler.js";
15
+ const handlers = new Map();
16
+ handlers.set('default', new InMemoryLRUCacheHandler());
17
+ export function registerCacheHandler(kind, handler) {
18
+ handlers.set(kind, handler);
19
+ }
20
+ export function getCacheHandler(kind) {
21
+ const handler = handlers.get(kind);
22
+ if (!handler) {
23
+ throw new Error(`No CacheHandler registered for kind "${kind}". ` +
24
+ `Available kinds: ${[...handlers.keys()].join(', ')}. ` +
25
+ 'Register a handler with registerCacheHandler(kind, handler).');
26
+ }
27
+ return handler;
28
+ }
29
+ //# sourceMappingURL=cacheHandlerRegistry.js.map
@@ -0,0 +1,5 @@
1
+ export { unstable_cache } from './unstable_cache.ts';
2
+ export type { UnstableCacheOptions } from './unstable_cache.ts';
3
+ export type { CacheHandler, CacheEntry } from './CacheHandler.ts';
4
+ export { registerCacheHandler } from './cacheHandlerRegistry.ts';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ /* eslint-disable camelcase -- matches Next.js API naming convention */
15
+ export { unstable_cache } from "./unstable_cache.js";
16
+ export { registerCacheHandler } from "./cacheHandlerRegistry.js";
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stub module for non-react-server contexts (e.g. SSR server bundle).
3
+ *
4
+ * The auto-load system includes .server.tsx files in both the RSC bundle
5
+ * and the SSR server bundle. The SSR bundle never executes server component
6
+ * code (it delegates to the RSC bundle), but webpack still needs to resolve
7
+ * all imports at build time. This stub satisfies that requirement.
8
+ */
9
+ import type { ReactNode } from 'react';
10
+ import type { CacheHandler, CacheEntry } from './CacheHandler.ts';
11
+ export type { CacheHandler, CacheEntry };
12
+ export interface UnstableCacheOptions {
13
+ id: string;
14
+ revalidate?: number;
15
+ kind?: string;
16
+ }
17
+ export declare function unstable_cache<TArgs extends unknown[]>(originalFn: (...args: TArgs) => Promise<ReactNode> | ReactNode, options: UnstableCacheOptions): (...args: TArgs) => Promise<ReactNode>;
18
+ export declare function registerCacheHandler(kind: string, handler: CacheHandler): void;
19
+ //# sourceMappingURL=index.stub.d.ts.map
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ const STUB_ERROR = 'unstable_cache is only available in the react-server bundle. ' +
15
+ 'It should not be called from the SSR server bundle or client bundle.';
16
+ // eslint-disable-next-line camelcase -- matches Next.js API naming convention
17
+ export function unstable_cache(originalFn, options) {
18
+ return () => {
19
+ throw new Error(STUB_ERROR);
20
+ };
21
+ }
22
+ export function registerCacheHandler(kind, handler) {
23
+ throw new Error(STUB_ERROR);
24
+ }
25
+ //# sourceMappingURL=index.stub.js.map
@@ -0,0 +1,5 @@
1
+ import { buildClientRenderer } from 'react-on-rails-rsc/client.node';
2
+ export declare function setManifestFileNames(clientManifest: string, serverClientManifest: string): void;
3
+ export declare function getClientManifestFileName(): string | undefined;
4
+ export declare function getClientRenderer(): Promise<ReturnType<typeof buildClientRenderer>>;
5
+ //# sourceMappingURL=manifestLoader.d.ts.map
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ import { buildClientRenderer } from 'react-on-rails-rsc/client.node';
15
+ import loadJsonFile from "../loadJsonFile.js";
16
+ let clientManifestFileName;
17
+ let serverClientManifestFileName;
18
+ let clientRendererPromise;
19
+ export function setManifestFileNames(clientManifest, serverClientManifest) {
20
+ clientManifestFileName = clientManifest;
21
+ serverClientManifestFileName = serverClientManifest;
22
+ }
23
+ export function getClientManifestFileName() {
24
+ return clientManifestFileName;
25
+ }
26
+ export function getClientRenderer() {
27
+ if (!clientRendererPromise) {
28
+ if (!clientManifestFileName || !serverClientManifestFileName) {
29
+ throw new Error('Manifest file names not set. Ensure setManifestFileNames() is called before getClientRenderer(). ' +
30
+ 'This is done automatically during the first RSC render request.');
31
+ }
32
+ const clientFile = clientManifestFileName;
33
+ const serverFile = serverClientManifestFileName;
34
+ clientRendererPromise = Promise.all([
35
+ loadJsonFile(serverFile),
36
+ loadJsonFile(clientFile),
37
+ ])
38
+ .then(([reactServerManifest, reactClientManifest]) => buildClientRenderer(reactClientManifest, reactServerManifest))
39
+ .catch((err) => {
40
+ clientRendererPromise = undefined;
41
+ throw err;
42
+ });
43
+ }
44
+ return clientRendererPromise;
45
+ }
46
+ //# sourceMappingURL=manifestLoader.js.map
@@ -0,0 +1,3 @@
1
+ import { buildServerRenderer } from 'react-on-rails-rsc/server.node';
2
+ export declare function getServerRenderer(): Promise<ReturnType<typeof buildServerRenderer>>;
3
+ //# sourceMappingURL=manifestLoaderServer.d.ts.map
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ import { buildServerRenderer } from 'react-on-rails-rsc/server.node';
15
+ import loadJsonFile from "../loadJsonFile.js";
16
+ import { getClientManifestFileName } from "./manifestLoader.js";
17
+ let serverRendererPromise;
18
+ // eslint-disable-next-line import/prefer-default-export -- named export for consistency with manifestLoader
19
+ export function getServerRenderer() {
20
+ if (!serverRendererPromise) {
21
+ const clientManifest = getClientManifestFileName();
22
+ if (!clientManifest) {
23
+ throw new Error('Manifest file names not set. Ensure setManifestFileNames() is called before getServerRenderer(). ' +
24
+ 'This is done automatically during the first RSC render request.');
25
+ }
26
+ serverRendererPromise = loadJsonFile(clientManifest)
27
+ .then((reactClientManifest) => buildServerRenderer(reactClientManifest))
28
+ .catch((err) => {
29
+ serverRendererPromise = undefined;
30
+ throw err;
31
+ });
32
+ }
33
+ return serverRendererPromise;
34
+ }
35
+ //# sourceMappingURL=manifestLoaderServer.js.map
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface UnstableCacheOptions {
3
+ /** Stable identifier for this cached function. Required. */
4
+ id: string;
5
+ /** Time in seconds before the cache entry is considered stale. 0 = indefinite. */
6
+ revalidate?: number;
7
+ /** Cache handler kind to use. Defaults to 'default' (in-memory LRU). */
8
+ kind?: string;
9
+ }
10
+ export declare function unstable_cache<TArgs extends unknown[]>(originalFn: (...args: TArgs) => Promise<ReactNode> | ReactNode, options: UnstableCacheOptions): (...args: TArgs) => Promise<ReactNode>;
11
+ //# sourceMappingURL=unstable_cache.d.ts.map
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Copyright (c) 2025 Shakacode LLC
3
+ *
4
+ * This file is NOT licensed under the MIT (open source) license.
5
+ * It is part of the React on Rails Pro offering and is licensed separately.
6
+ *
7
+ * Unauthorized copying, modification, distribution, or use of this file,
8
+ * via any medium, is strictly prohibited without a valid license agreement
9
+ * from Shakacode LLC.
10
+ *
11
+ * For licensing terms, please see:
12
+ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
13
+ */
14
+ import { PassThrough } from 'stream';
15
+ import { getCacheHandler } from "./cacheHandlerRegistry.js";
16
+ import { buildCacheKey } from "./buildCacheKey.js";
17
+ import { getBuildId } from "./buildIdProvider.js";
18
+ import { getClientRenderer } from "./manifestLoader.js";
19
+ import { getServerRenderer } from "./manifestLoaderServer.js";
20
+ function chunksToNodeStream(chunks) {
21
+ const stream = new PassThrough();
22
+ for (const chunk of chunks) {
23
+ stream.push(chunk);
24
+ }
25
+ stream.push(null);
26
+ return stream;
27
+ }
28
+ function bufferNodeStream(stream) {
29
+ const chunks = [];
30
+ return new Promise((resolve, reject) => {
31
+ stream.on('data', (chunk) => chunks.push(chunk));
32
+ stream.on('end', () => resolve(chunks));
33
+ stream.on('error', reject);
34
+ });
35
+ }
36
+ // eslint-disable-next-line camelcase -- matches Next.js API naming convention
37
+ export function unstable_cache(originalFn, options) {
38
+ const { id, revalidate = 0, kind = 'default' } = options;
39
+ return async function cachedFn(...args) {
40
+ const handler = getCacheHandler(kind);
41
+ const cacheKey = buildCacheKey(getBuildId(), id, args);
42
+ // --- HIT path ---
43
+ const entry = await handler.get(cacheKey);
44
+ if (entry) {
45
+ const replayStream = chunksToNodeStream(entry.value);
46
+ const { createFromNodeStream } = await getClientRenderer();
47
+ return createFromNodeStream(replayStream);
48
+ }
49
+ // --- MISS path ---
50
+ const reactTree = await originalFn(...args);
51
+ const { renderToPipeableStream } = await getServerRenderer();
52
+ const rscPipeable = renderToPipeableStream(reactTree);
53
+ const source = new PassThrough();
54
+ const forCache = new PassThrough();
55
+ const forReturn = new PassThrough();
56
+ rscPipeable.pipe(source);
57
+ source.on('data', (chunk) => {
58
+ forCache.push(chunk);
59
+ forReturn.push(chunk);
60
+ });
61
+ source.on('end', () => {
62
+ forCache.push(null);
63
+ forReturn.push(null);
64
+ });
65
+ source.on('error', (err) => {
66
+ forCache.destroy(err);
67
+ forReturn.destroy(err);
68
+ });
69
+ bufferNodeStream(forCache)
70
+ .then((chunks) => {
71
+ const newEntry = {
72
+ value: chunks,
73
+ revalidate,
74
+ timestamp: Date.now(),
75
+ };
76
+ return handler.set(cacheKey, newEntry);
77
+ })
78
+ .catch((err) => {
79
+ console.error('unstable_cache: failed to store cache entry', err);
80
+ });
81
+ const { createFromNodeStream } = await getClientRenderer();
82
+ return createFromNodeStream(forReturn);
83
+ };
84
+ }
85
+ //# sourceMappingURL=unstable_cache.js.map
@@ -1,17 +1,64 @@
1
1
  /* eslint-disable import/prefer-default-export -- named export for consistency with capability API */
2
- import { buildServerRenderer } from 'react-on-rails-rsc/server.node';
3
2
  import { assertRailsContextWithServerStreamingCapabilities, } from 'react-on-rails/types';
4
3
  import { convertToError } from 'react-on-rails/serverRenderUtils';
5
4
  import handleError from "../handleErrorRSC.js";
6
5
  import { getOrCreateAsyncPropsManager } from "../AsyncPropsManager.js";
7
6
  import { streamServerRenderedComponent, transformRenderStreamChunksToResultObject, } from "../streamingUtils.js";
8
- import loadJsonFile from "../loadJsonFile.js";
9
- let serverRendererPromise;
7
+ import { setManifestFileNames } from "../cache/manifestLoader.js";
8
+ import { getServerRenderer } from "../cache/manifestLoaderServer.js";
9
+ import { setBuildId } from "../cache/buildIdProvider.js";
10
+ const CLIENT_HOOK_NAMES = [
11
+ 'useState',
12
+ 'useEffect',
13
+ 'useReducer',
14
+ 'useCallback',
15
+ 'useMemo',
16
+ 'useRef',
17
+ 'useLayoutEffect',
18
+ 'useImperativeHandle',
19
+ 'useContext',
20
+ 'useSyncExternalStore',
21
+ 'useTransition',
22
+ 'useDeferredValue',
23
+ 'useId',
24
+ 'useDebugValue',
25
+ 'useInsertionEffect',
26
+ 'useOptimistic',
27
+ 'useActionState',
28
+ ].join('|');
29
+ const CLIENT_HOOK_RUNTIME_ERROR_REGEX = new RegExp(`(?:(?:React\\.)|\\(0\\s*,\\s*[\\w$]+\\.)?(${CLIENT_HOOK_NAMES})\\)? is not a function\\b`);
30
+ const addRSCClientHookDiagnostic = (error, componentName) => {
31
+ const match = error.message.match(CLIENT_HOOK_RUNTIME_ERROR_REGEX);
32
+ if (!match)
33
+ return error;
34
+ const hookName = match[1];
35
+ const enhancedError = new Error(`[React on Rails Pro] Component "${componentName}" called client hook "${hookName}" while rendering in ` +
36
+ `the React Server Components runtime.\n\n` +
37
+ `Most likely cause: "${componentName}", or a component it imports, uses client-only APIs but is missing ` +
38
+ `the '"use client";' directive.\n\n` +
39
+ `Add '"use client";' as the first statement of the client component file, or move hooks, event handlers, ` +
40
+ `and class components into a separate client component.\n\n` +
41
+ `Note: .client/.server file suffixes only control bundle placement. The '"use client";' directive controls ` +
42
+ `RSC client/server classification.\n\n` +
43
+ `Original error: ${error.message}`);
44
+ enhancedError.name = error.name;
45
+ enhancedError.cause = error;
46
+ const enhancedStackFrames = enhancedError.stack?.split('\n').slice(1).join('\n');
47
+ enhancedError.stack = `${enhancedError.name}: ${enhancedError.message}${enhancedStackFrames ? `\n${enhancedStackFrames}` : ''}\nCaused by: ${error.stack || error.message}`;
48
+ return enhancedError;
49
+ };
10
50
  const streamRenderRSCComponent = (reactRenderingResult, options, streamingTrackers) => {
11
- const { throwJsErrors } = options;
51
+ const { name: componentName, throwJsErrors } = options;
12
52
  const { railsContext } = options;
13
53
  assertRailsContextWithServerStreamingCapabilities(railsContext);
14
- const { reactClientManifestFileName } = railsContext;
54
+ const { reactClientManifestFileName, reactServerClientManifestFileName } = railsContext;
55
+ // Initialize manifest loader and BUILD_ID on first render request.
56
+ // These are per-process constants that don't change between requests.
57
+ setManifestFileNames(reactClientManifestFileName, reactServerClientManifestFileName);
58
+ const rscPayloadParams = railsContext.serverSideRSCPayloadParameters;
59
+ if (rscPayloadParams?.rscBundleHash) {
60
+ setBuildId(rscPayloadParams.rscBundleHash);
61
+ }
15
62
  const renderState = {
16
63
  result: null,
17
64
  hasErrors: false,
@@ -19,23 +66,17 @@ const streamRenderRSCComponent = (reactRenderingResult, options, streamingTracke
19
66
  };
20
67
  const { pipeToTransform, readableStream, emitError } = transformRenderStreamChunksToResultObject(renderState);
21
68
  const reportError = (error) => {
22
- console.error('Error in RSC stream', error);
69
+ const diagnosticError = addRSCClientHookDiagnostic(error, componentName);
70
+ console.error('Error in RSC stream', diagnosticError);
23
71
  if (throwJsErrors) {
24
- emitError(error);
72
+ emitError(diagnosticError);
25
73
  }
26
74
  renderState.hasErrors = true;
27
- renderState.error = error;
75
+ renderState.error = diagnosticError;
76
+ return diagnosticError;
28
77
  };
29
78
  const initializeAndRender = async () => {
30
- if (!serverRendererPromise) {
31
- serverRendererPromise = loadJsonFile(reactClientManifestFileName)
32
- .then((reactClientManifest) => buildServerRenderer(reactClientManifest))
33
- .catch((err) => {
34
- serverRendererPromise = undefined;
35
- throw err;
36
- });
37
- }
38
- const { renderToPipeableStream } = await serverRendererPromise;
79
+ const { renderToPipeableStream } = await getServerRenderer();
39
80
  const rscStream = renderToPipeableStream(await reactRenderingResult, {
40
81
  onError: (err) => {
41
82
  const error = convertToError(err);
@@ -45,8 +86,7 @@ const streamRenderRSCComponent = (reactRenderingResult, options, streamingTracke
45
86
  pipeToTransform(rscStream);
46
87
  };
47
88
  initializeAndRender().catch((e) => {
48
- const error = convertToError(e);
49
- reportError(error);
89
+ const error = reportError(convertToError(e));
50
90
  const errorHtml = handleError({ e: error, name: options.name, serverSide: true });
51
91
  pipeToTransform(errorHtml);
52
92
  });