Skip to content

Commit 9c222ef

Browse files
committed
Fix repl can be called as a side effect causing infinite recursion
1 parent 9423a35 commit 9c222ef

File tree

2 files changed

+80
-2
lines changed

2 files changed

+80
-2
lines changed

packages/jsrepl/tests/playwright/repl-eval.test.ts

+59
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,62 @@ test('multiline decor', async ({ page }) => {
336336
`
337337
)
338338
})
339+
340+
test(
341+
'repl is not called as a side effect of transformPayloadResult',
342+
{
343+
annotation: {
344+
type: 'issue',
345+
description: 'https://github.com/jsrepl/jsrepl.io/issues/3',
346+
},
347+
},
348+
async ({ page }) => {
349+
await visitPlayground(page, {
350+
openedModels: ['/test.ts'],
351+
activeModel: '/test.ts',
352+
showPreview: false,
353+
fs: new ReplFS.FS({
354+
kind: ReplFS.Kind.Directory,
355+
children: {
356+
'test.ts': {
357+
kind: ReplFS.Kind.File,
358+
content: dedent`
359+
const obj2 = {
360+
get foo() {
361+
return obj2;
362+
},
363+
};
364+
365+
const obj = { a: 20 };
366+
367+
const proxy = new Proxy(obj, {
368+
get(target, p) {
369+
370+
}
371+
})
372+
`,
373+
},
374+
},
375+
}),
376+
})
377+
378+
await assertMonacoContentsWithDecors(
379+
page,
380+
dedent`
381+
const obj2 = { // → obj2 = [ref *1] {foo: [Circular *1]}
382+
get foo() {
383+
return obj2;
384+
},
385+
};
386+
387+
const obj = { a: 20 }; // → obj = {a: 20}
388+
389+
const proxy = new Proxy(obj, { // → proxy = {a: undefined}
390+
get(target, p) {
391+
392+
}
393+
})
394+
`
395+
)
396+
}
397+
)

packages/preview-entry/src/repl.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import { transformPayloadResult } from './payload'
1010
import { postMessage } from './post-message'
1111
import type { PreviewWindow } from './types'
1212

13+
// Traversing the object props in `transformPayloadResult` can cause calling `repl`
14+
// again in some cases, which is not desired and may cause infinite recursion which
15+
// is not handled by circular reference prevention mechanism in `transformPayloadResult`.
16+
// Although that mechanism can be extended to the entire event loop stack, these
17+
// side-effects are not the intended behavior anyway: `repl` is intended to be
18+
// called by user-code only, and not during traversing the object props within `transformPayloadResult`.
19+
// See https://github.com/jsrepl/jsrepl.io/issues/3 for the reference.
20+
let skipReplAsSideEffect = false
21+
1322
export function setupRepl(win: PreviewWindow, token: number) {
1423
win[identifierNameRepl] = repl.bind({ token, win })
1524
win[identifierNameFunctionMeta] = () => {}
@@ -20,8 +29,18 @@ export function setupRepl(win: PreviewWindow, token: number) {
2029
}
2130

2231
function repl(this: { token: number; win: PreviewWindow }, ctxId: string | number, value: unknown) {
23-
const { token, win } = this
24-
postMessageRepl(token, win, value, false, ctxId)
32+
if (!skipReplAsSideEffect) {
33+
skipReplAsSideEffect = true
34+
try {
35+
const { token, win } = this
36+
postMessageRepl(token, win, value, false, ctxId)
37+
} catch (err) {
38+
console.error('JSRepl Error: repl failed', err)
39+
} finally {
40+
skipReplAsSideEffect = false
41+
}
42+
}
43+
2544
return value
2645
}
2746

0 commit comments

Comments
 (0)