Skip to content

Commit adf6b41

Browse files
add runOnOtherBundle functionality to the vm context
1 parent ef997e9 commit adf6b41

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed

packages/node-renderer/src/worker/vm.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { ReactOnRails as ROR } from 'react-on-rails';
1515
import type { Context } from 'vm';
1616

1717
import SharedConsoleHistory from '../shared/sharedConsoleHistory';
18+
import { handleRenderRequest } from './handleRenderRequest';
1819
import log from '../shared/log';
1920
import { getConfig } from '../shared/configBuilder';
2021
import { formatExceptionMessage, smartTrim, isReadableStream } from '../shared/utils';
@@ -105,7 +106,16 @@ export async function buildVM(filePath: string) {
105106
const { supportModules, stubTimers, additionalContext } = getConfig();
106107
const additionalContextIsObject = additionalContext !== null && additionalContext.constructor === Object;
107108
const sharedConsoleHistory = new SharedConsoleHistory();
108-
const contextObject = { sharedConsoleHistory };
109+
110+
const runOnOtherBundle = async (bundleTimestamp: string | number, renderingRequest: string) => {
111+
const result = await handleRenderRequest({ renderingRequest, bundleTimestamp });
112+
if (result.status !== 200) {
113+
throw new Error(`Failed to render on other bundle ${bundleTimestamp}, error: ${result.data}`);
114+
}
115+
return result.data ?? result.stream;
116+
};
117+
118+
const contextObject = { sharedConsoleHistory, runOnOtherBundle };
109119

110120
if (supportModules) {
111121
// IMPORTANT: When adding anything to this object, update:
@@ -275,9 +285,14 @@ ${smartTrim(renderingRequest)}`);
275285
await writeFileAsync(debugOutputPathCode, renderingRequest);
276286
}
277287

278-
let result = sharedConsoleHistory.trackConsoleHistoryInRenderRequest(
279-
() => vm.runInContext(renderingRequest, context) as RenderCodeResult,
280-
);
288+
let result = sharedConsoleHistory.trackConsoleHistoryInRenderRequest(() => {
289+
context.renderingRequest = renderingRequest;
290+
try {
291+
return vm.runInContext(renderingRequest, context) as RenderCodeResult;
292+
} finally {
293+
context.renderingRequest = undefined;
294+
}
295+
});
281296

282297
if (isReadableStream(result)) {
283298
return result;

packages/node-renderer/tests/handleRenderRequest.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ const renderResult = {
3535
data: JSON.stringify({ html: 'Dummy Object' }),
3636
};
3737

38+
const renderResultFromBothBundles = {
39+
status: 200,
40+
headers: { 'Cache-Control': 'public, max-age=31536000' },
41+
data: JSON.stringify({
42+
mainBundleResult: { html: 'Dummy Object' },
43+
secondaryBundleResult: { html: 'Dummy Object from secondary bundle' },
44+
}),
45+
};
3846
describe(testName, () => {
3947
beforeEach(async () => {
4048
await resetForTest(testName);
@@ -224,4 +232,82 @@ describe(testName, () => {
224232

225233
expect(result).toEqual(renderResult);
226234
});
235+
236+
test('rendering request can call runOnOtherBundle', async () => {
237+
await createVmBundle(testName);
238+
await createSecondaryVmBundle(testName);
239+
240+
const renderingRequest = `
241+
runOnOtherBundle(${SECONDARY_BUNDLE_TIMESTAMP}, 'ReactOnRails.dummy').then((secondaryBundleResult) => ({
242+
mainBundleResult: ReactOnRails.dummy,
243+
secondaryBundleResult: JSON.parse(secondaryBundleResult),
244+
}));
245+
`;
246+
247+
const result = await handleRenderRequest({
248+
renderingRequest,
249+
bundleTimestamp: BUNDLE_TIMESTAMP,
250+
dependencyBundleTimestamps: [SECONDARY_BUNDLE_TIMESTAMP],
251+
});
252+
253+
expect(result).toEqual(renderResultFromBothBundles);
254+
// Both bundles should be in the VM context
255+
expect(
256+
hasVMContextForBundle(path.resolve(__dirname, `./tmp/${testName}/1495063024898/1495063024898.js`)),
257+
).toBeTruthy();
258+
expect(
259+
hasVMContextForBundle(path.resolve(__dirname, `./tmp/${testName}/1495063024899/1495063024899.js`)),
260+
).toBeTruthy();
261+
});
262+
263+
test('renderingRequest is globally accessible inside the VM', async () => {
264+
await createVmBundle(testName);
265+
266+
const renderingRequest = `
267+
renderingRequest;
268+
`;
269+
270+
const result = await handleRenderRequest({
271+
renderingRequest,
272+
bundleTimestamp: BUNDLE_TIMESTAMP,
273+
});
274+
275+
expect(result).toEqual({
276+
status: 200,
277+
headers: { 'Cache-Control': 'public, max-age=31536000' },
278+
data: renderingRequest,
279+
});
280+
});
281+
282+
// The renderingRequest variable is automatically reset after synchronous execution to prevent data leakage
283+
// between requests in the shared VM context. This means it will be undefined in any async callbacks.
284+
//
285+
// If you need to access renderingRequest in an async context, save it to a local variable first:
286+
//
287+
// const renderingRequest = `
288+
// const savedRequest = renderingRequest; // Save synchronously
289+
// Promise.resolve().then(() => {
290+
// return savedRequest; // Access async
291+
// });
292+
// `;
293+
test('renderingRequest is reset after the sync execution (not accessible from async functions)', async () => {
294+
await createVmBundle(testName);
295+
296+
// Since renderingRequest is undefined in async callbacks, we return the string 'undefined'
297+
// to demonstrate this behavior (as undefined cannot be returned from the VM)
298+
const renderingRequest = `
299+
Promise.resolve().then(() => renderingRequest ?? 'undefined');
300+
`;
301+
302+
const result = await handleRenderRequest({
303+
renderingRequest,
304+
bundleTimestamp: BUNDLE_TIMESTAMP,
305+
});
306+
307+
expect(result).toEqual({
308+
status: 200,
309+
headers: { 'Cache-Control': 'public, max-age=31536000' },
310+
data: JSON.stringify('undefined'),
311+
});
312+
});
227313
});

0 commit comments

Comments
 (0)