Skip to content

Commit 4489560

Browse files
authored
Refresh Iframe context on runCode (exercism#7883)
* Use srcdoc for iframe updating * Add code-run version number * Clear deps array * Tweak error location * Update error location, fix error widget
1 parent 5208035 commit 4489560

File tree

9 files changed

+80
-55
lines changed

9 files changed

+80
-55
lines changed

app/javascript/components/bootcamp/FrontendExercisePage/FrontendExercisePage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export default function FrontendExercisePage(data: FrontendExercisePageProps) {
2828
defaultCode,
2929
} = useSetupEditors(data.exercise.slug, data.code, actualIFrameRef)
3030

31+
const jsCodeRunId = React.useRef(0)
32+
3133
useRestoreIframeScrollAfterResize()
3234

3335
return (
@@ -39,6 +41,7 @@ export default function FrontendExercisePage(data: FrontendExercisePageProps) {
3941
htmlEditorRef: htmlEditorViewRef,
4042
cssEditorRef: cssEditorViewRef,
4143
jsEditorRef: jsEditorViewRef,
44+
jsCodeRunId,
4245
handleCssEditorDidMount,
4346
handleHtmlEditorDidMount,
4447
handleJsEditorDidMount,

app/javascript/components/bootcamp/FrontendExercisePage/FrontendExercisePageContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type FrontendExercisePageContextType = {
1515
htmlEditorRef: React.RefObject<EditorView>
1616
cssEditorRef: React.RefObject<EditorView>
1717
jsEditorRef: React.RefObject<EditorView>
18+
jsCodeRunId: React.MutableRefObject<number>
1819
handleHtmlEditorDidMount: (handler: Handler) => void
1920
handleCssEditorDidMount: (handler: Handler) => void
2021
handleJsEditorDidMount: (handler: Handler) => void

app/javascript/components/bootcamp/FrontendExercisePage/LHS/LHS.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function LHS() {
3636
exercise,
3737
code,
3838
links,
39+
jsCodeRunId,
3940
} = useContext(FrontendExercisePageContext)
4041

4142
const {
@@ -108,7 +109,7 @@ export function LHS() {
108109
},
109110
customFunctions: [],
110111
readonlyRanges: {
111-
html: cssReadonlyRanges,
112+
html: htmlReadonlyRanges,
112113
css: cssReadonlyRanges,
113114
js: jsReadonlyRanges,
114115
},
@@ -117,12 +118,18 @@ export function LHS() {
117118
const result = parseJS(jsView.state.doc.toString())
118119
switch (result.status) {
119120
case 'success':
120-
const fullScript = wrapJSCode(jsCode)
121-
const expectedScript = wrapJSCode(exercise.config.expected.js)
121+
setLogs([])
122+
123+
jsCodeRunId.current = (jsCodeRunId.current || 0) + 1
124+
const fullScript = wrapJSCode(jsCode, jsCodeRunId.current || 0)
125+
const expectedScript = wrapJSCode(
126+
exercise.config.expected.js,
127+
jsCodeRunId.current || 0
128+
)
122129
// we'll only run the JS code if:
123130
// 1. someone clicks the `Run Code` button and
124131
// 2. there are no parsing errors
125-
const runCode = updateIFrame(
132+
updateIFrame(
126133
actualIFrameRef,
127134
{
128135
script: fullScript,
@@ -132,15 +139,15 @@ export function LHS() {
132139
code
133140
)
134141

135-
const runRefCode = updateIFrame(
142+
updateIFrame(
136143
expectedIFrameRef,
137144
{
138145
...exercise.config.expected,
139146
script: expectedScript,
140147
},
141148
code
142149
)
143-
const runExpectedCode = updateIFrame(
150+
updateIFrame(
144151
expectedReferenceIFrameRef,
145152
{
146153
...exercise.config.expected,
@@ -152,10 +159,6 @@ export function LHS() {
152159
if (RHSActiveTab === 'instructions') {
153160
setRHSActiveTab('output')
154161
}
155-
setLogs([])
156-
runCode?.()
157-
runRefCode?.()
158-
runExpectedCode?.()
159162
break
160163
case 'error':
161164
setTab('javascript')

app/javascript/components/bootcamp/FrontendExercisePage/LHS/showJsError.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { EditorView } from 'codemirror'
2-
import { result } from 'lodash'
32
import {
43
showInfoWidgetEffect,
54
informationWidgetDataEffect,
@@ -18,7 +17,7 @@ export function showJsError(
1817
) {
1918
if (!view) return
2019

21-
scrollToLine(view, error.lineNumber)
20+
setTimeout(() => scrollToLine(view, error.lineNumber), 50)
2221

2322
const editorLength = view.state.doc.length
2423
const errorLine = view.state.doc.line(error.lineNumber)
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { scriptPrelude, scriptPostlude } from '../utils/updateIFrame'
22

3-
export function wrapJSCode(jsCode: string) {
3+
export function wrapJSCode(jsCode: string, jsCodeRunId: number) {
44
return `<script>
5-
${scriptPrelude}${jsCode || ''}${scriptPostlude}
5+
window.__runId__ = ${jsCodeRunId};
6+
${scriptPrelude} ${jsCode || ''} ${scriptPostlude}
7+
//# sourceURL=exercise.js
68
</script>`
79
}

app/javascript/components/bootcamp/FrontendExercisePage/RHS/Panels/ConsolePanel/Logger.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import React, { useEffect, useRef } from 'react'
1+
import React, { useContext, useEffect, useRef } from 'react'
22
import { renderLog } from '@/components/bootcamp/JikiscriptExercisePage/RHS/Logger/renderLog'
33
import { useHighlighting } from '@/utils/highlight'
44
import { useFrontendExercisePageStore } from '../../../store/frontendExercisePageStore'
5+
import { FrontendExercisePageContext } from '../../../FrontendExercisePageContext'
56

67
export function Logger() {
78
const containerRef = useRef<HTMLDivElement>(null)
89

910
const { logs, setLogs } = useFrontendExercisePageStore()
1011

12+
const { jsCodeRunId } = useContext(FrontendExercisePageContext)
13+
1114
const ref = useHighlighting<HTMLDivElement>()
1215

1316
useEffect(() => {
1417
const handleMessage = (event: MessageEvent) => {
1518
if (event.data?.type === 'iframe-log') {
1619
const newLogs = event.data.logs as unknown[][]
17-
setLogs((prev) => [...prev, ...newLogs])
20+
const logRunId = event.data.runId
21+
22+
if (logRunId === jsCodeRunId.current) {
23+
setLogs((prev) => [...prev, ...newLogs])
24+
} else {
25+
console.debug('Ignoring stale logs: ', newLogs)
26+
}
1827
}
1928
}
2029

app/javascript/components/bootcamp/FrontendExercisePage/utils/extractLineColFromStackMessage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export function extractLineAndColumnFromStack(stack?: string): {
66
return { line: 1, column: 1 }
77
}
88

9-
const match = stack.match(/<anonymous>:(\d+):(\d+)/)
9+
const match = stack.match(/(?:\()?(?:\w+\.js|<anonymous>):(\d+):(\d+)\)?/)
10+
1011
if (match) {
1112
return {
1213
line: parseInt(match[1], 10),

app/javascript/components/bootcamp/FrontendExercisePage/utils/updateIFrame.ts

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ window.log = function (...args) {
3434
3535
window.parent.postMessage({
3636
type: 'iframe-log',
37+
runId: window.__runId__,
3738
logs: [safeArgs],
3839
}, '*');
3940
};
@@ -54,15 +55,17 @@ export const scriptPostlude = `
5455
`
5556

5657
const jsPreludeLines = scriptPrelude.split('\n')
57-
export const jsLineOffset = jsPreludeLines.length
58+
// +1 offset because this line got added to the prelude:
59+
// window.__runId__ = ${jsCodeRunId};
60+
export const jsLineOffset = jsPreludeLines.length + 1
5861

5962
export function updateIFrame(
6063
iframeRef:
6164
| React.RefObject<HTMLIFrameElement>
6265
| React.ForwardedRef<HTMLIFrameElement>,
6366
{ html, css, script }: { html?: string; css?: string; script?: string },
6467
code: FrontendExercisePageCode
65-
): (() => void) | undefined {
68+
): void {
6669
let iframeElement: HTMLIFrameElement | null = null
6770

6871
if (iframeRef) {
@@ -76,30 +79,38 @@ export function updateIFrame(
7679

7780
if (!iframeElement) return
7881

79-
const iframeDoc =
80-
iframeElement.contentDocument || iframeElement.contentWindow?.document
81-
if (!iframeDoc) return
82-
8382
const iframeHtml = `
8483
<!DOCTYPE html>
8584
<html>
8685
<head>
8786
<style>
8887
${code.normalizeCss}
89-
${code.default.css}
88+
${code.default.css}
9089
${css || ''}
9190
</style>
92-
</head>
93-
<body>
91+
</head>
92+
<body>
9493
${html || ''}
9594
${script || ''}
9695
</body>
9796
</html>`
9897

9998
try {
100-
iframeDoc.open()
101-
iframeDoc.write(iframeHtml)
102-
iframeDoc.close()
99+
iframeElement.onload = () => {
100+
try {
101+
const runCode = (
102+
iframeElement.contentWindow as Window & { runCode?: () => void }
103+
)?.runCode
104+
if (typeof runCode === 'function') {
105+
runCode()
106+
} else {
107+
console.warn('runCode is not defined on iframe')
108+
}
109+
} catch (err) {
110+
console.error('Failed to execute runCode:', err)
111+
}
112+
}
113+
iframeElement.srcdoc = iframeHtml
103114
} catch (err) {
104115
window.postMessage(
105116
{
@@ -109,20 +120,5 @@ export function updateIFrame(
109120
},
110121
'*'
111122
)
112-
return
113-
}
114-
115-
return () => {
116-
try {
117-
// @ts-ignore
118-
const runCode = iframeElement?.contentWindow?.runCode
119-
if (typeof runCode === 'function') {
120-
runCode()
121-
} else {
122-
console.warn('runCode is not defined on iframe')
123-
}
124-
} catch (err) {
125-
console.error('Failed to execute runCode:', err)
126-
}
127123
}
128124
}

app/javascript/components/bootcamp/JikiscriptExercisePage/CodeMirror/extensions/end-line-information/information-widget.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export class InformationWidget extends WidgetType {
2121
private arrowElement: HTMLElement | null = null
2222
private observer: MutationObserver | null = null
2323
private autoUpdateCleanup: (() => void) | null = null
24-
private scrollContainer: HTMLElement | null = null
2524

2625
constructor(
2726
private readonly tooltipHtml: string,
@@ -213,29 +212,39 @@ export class InformationWidget extends WidgetType {
213212

214213
private handleScroll?: EventListener
215214
private handleStorage?: (e: StorageEvent) => void
215+
private scrollContainers: HTMLElement[] = []
216216

217217
private setupScrollListener() {
218-
const scrollContainer = document.querySelector('.cm-scroller')
219-
if (!scrollContainer) {
220-
return
221-
}
218+
const scrollContainers = document.querySelectorAll('.cm-scroller')
219+
if (!scrollContainers) return
222220

223221
this.handleScroll = this.positionTooltip.bind(this)
224222
this.handleStorage = (e: StorageEvent) => {
225-
if (e.key === 'solve-exercise-page-lhs') {
223+
if (
224+
e.key === 'solve-exercise-page-lhs' ||
225+
e.key === 'frontend-training-page-size'
226+
) {
226227
this.positionTooltip()
227228
}
228229
}
229230

230-
scrollContainer.addEventListener('scroll', this.handleScroll)
231-
window.addEventListener('storage', this.handleStorage)
231+
this.scrollContainers = []
232+
233+
for (const scrollContainer of scrollContainers) {
234+
if (scrollContainer instanceof HTMLElement) {
235+
scrollContainer.addEventListener('scroll', this.handleScroll)
236+
this.scrollContainers.push(scrollContainer)
237+
}
238+
}
232239

233-
this.scrollContainer = scrollContainer as HTMLElement
240+
window.addEventListener('storage', this.handleStorage)
234241
}
235242

236243
private cleanup() {
237-
if (this.scrollContainer && this.handleScroll) {
238-
this.scrollContainer.removeEventListener('scroll', this.handleScroll)
244+
if (this.handleScroll) {
245+
for (const container of this.scrollContainers) {
246+
container.removeEventListener('scroll', this.handleScroll)
247+
}
239248
}
240249

241250
if (this.handleStorage) {
@@ -256,5 +265,7 @@ export class InformationWidget extends WidgetType {
256265
this.observer.disconnect()
257266
this.observer = null
258267
}
268+
269+
this.scrollContainers = []
259270
}
260271
}

0 commit comments

Comments
 (0)