Skip to content

Commit f0b0b93

Browse files
committed
Improve frame communication in html environment
1 parent 8390f9f commit f0b0b93

File tree

5 files changed

+188
-84
lines changed

5 files changed

+188
-84
lines changed

src/components/player/ConsoleProxy.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,25 @@ import {
66
LogVisibility,
77
} from '../../types/Messages'
88

9-
const consoleProxy = ({ id: '0' } as unknown) as typeof window.console & {
10-
id: string
11-
_rnwp_log?: typeof window.console.log
9+
export type ConsoleProxy = Console & { _rnwp_log: Console['log'] }
10+
11+
function attachConsoleMethodsToProxy(self: { console: Console }) {
12+
// I don't think this can fail, but the console object can be strange...
13+
// If it fails, we won't proxy all the methods (which is likely fine)
14+
try {
15+
for (let key in self.console) {
16+
let f = (self.console as any)[key]
17+
18+
if (typeof f === 'function') {
19+
;(consoleProxy as any)[key] = f.bind(self.console)
20+
}
21+
}
22+
} catch (e) {}
1223
}
1324

14-
// I don't think this can fail, but the console object can be strange...
15-
// If it fails, we won't proxy all the methods (which is likely fine)
16-
try {
17-
for (let key in window.console) {
18-
let f = (window.console as any)[key]
25+
const consoleProxy = {} as ConsoleProxy
1926

20-
if (typeof f === 'function') {
21-
;(consoleProxy as any)[key] = f.bind(window.console)
22-
}
23-
}
24-
} catch (e) {}
27+
attachConsoleMethodsToProxy(window)
2528

2629
let consoleMessageIndex = 0
2730

src/components/workspace/Workspace.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,16 @@ export default function Workspace(props: Props) {
456456
runTsc(filename, code)
457457
break
458458
case 'babel':
459-
default:
460-
runBabel(filename, code)
459+
default: {
460+
if (/(j|t)sx?$/.test(filename)) {
461+
runBabel(filename, code)
462+
} else {
463+
// We don't actually "compile" non-JS/TS files, but it's simpler
464+
// to mark them as compiled so we only have to check file extensions
465+
// in one place
466+
dispatch(compiled(filename, code))
467+
}
468+
}
461469
}
462470
}
463471

src/environments/html-environment.tsx

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,61 @@
11
import { bundle } from 'packly'
2+
import consoleProxy, { ConsoleProxy } from '../components/player/ConsoleProxy'
23
import * as path from '../utils/path'
3-
import { initializeCommunication } from '../utils/playerCommunication'
4+
import {
5+
bindConsoleLogMethods,
6+
createWindowErrorHandler,
7+
initializeCommunication,
8+
} from '../utils/playerCommunication'
49
import { createAppLayout } from '../utils/PlayerUtils'
5-
import { EnvironmentOptions, IEnvironment } from './IEnvironment'
10+
import {
11+
EnvironmentOptions,
12+
EvaluationContext,
13+
IEnvironment,
14+
} from './IEnvironment'
15+
16+
// Inline stylesheets and scripts
17+
function generateBundle(context: EvaluationContext) {
18+
return bundle({
19+
entry: context.entry,
20+
request({ origin, url }) {
21+
if (origin === undefined) return context.fileMap[url]
22+
23+
// Don't inline (external) urls starting with http://, https://, or //
24+
if (/^(https?)?\/\//.test(url)) return undefined
25+
26+
// Inline absolute urls
27+
if (url.startsWith('/')) return context.fileMap[url.slice(1)]
28+
29+
// Inline relative urls
30+
const lookup = path.join(path.dirname(origin), url)
31+
32+
return context.fileMap[lookup]
33+
},
34+
})
35+
}
36+
37+
function bindIframeCommunication(
38+
iframe: HTMLIFrameElement,
39+
{ id, codeVersion }: { id: string; codeVersion: number }
40+
) {
41+
const iframeWindow = iframe.contentWindow! as Window & {
42+
console: ConsoleProxy
43+
}
44+
45+
bindConsoleLogMethods({
46+
consoleProxy: iframeWindow.console,
47+
codeVersion,
48+
id,
49+
prefixLineCount: 0,
50+
sharedEnvironment: false,
51+
})
52+
53+
iframeWindow.onerror = createWindowErrorHandler({
54+
codeVersion,
55+
id,
56+
prefixLineCount: 0,
57+
})
58+
}
659

760
export class HTMLEnvironment implements IEnvironment {
861
async initialize({
@@ -21,24 +74,9 @@ export class HTMLEnvironment implements IEnvironment {
2174
id,
2275
prefixLineCount: 0,
2376
sharedEnvironment,
77+
consoleProxy,
2478
onRunApplication: (context) => {
25-
const html = bundle({
26-
entry: context.entry,
27-
request({ origin, url }) {
28-
if (origin === undefined) return context.fileMap[url]
29-
30-
// Don't inline (external) urls starting with http://, https://, or //
31-
if (/^(https?)?\/\//.test(url)) return undefined
32-
33-
// Inline absolute urls
34-
if (url.startsWith('/')) return context.fileMap[url.slice(1)]
35-
36-
// Inline relative urls
37-
const lookup = path.join(path.dirname(origin), url)
38-
39-
return context.fileMap[lookup]
40-
},
41-
})
79+
const html = generateBundle(context)
4280

4381
const document = iframe.contentDocument
4482

@@ -47,6 +85,12 @@ export class HTMLEnvironment implements IEnvironment {
4785
// https://stackoverflow.com/questions/5784638/replace-entire-content-of-iframe
4886
document.close()
4987
document.open()
88+
89+
bindIframeCommunication(iframe, {
90+
id,
91+
codeVersion: context.codeVersion,
92+
})
93+
5094
document.write(html)
5195
document.close()
5296
},

src/environments/javascript-environment.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import VendorComponents, {
77
} from '../components/player/VendorComponents'
88
import formatError from '../utils/formatError'
99
import * as path from '../utils/path'
10-
import { initializeCommunication } from '../utils/playerCommunication'
10+
import {
11+
initializeCommunication,
12+
sendError,
13+
} from '../utils/playerCommunication'
1114
import { createAppLayout } from '../utils/PlayerUtils'
1215
import { prefixAndApply } from '../utils/Styles'
1316
import type {
@@ -214,8 +217,9 @@ export class JavaScriptEnvironment implements IEnvironment {
214217
wrapperElement.appendChild(statusBarElement)
215218
}
216219

217-
const { sendError } = initializeCommunication({
220+
initializeCommunication({
218221
id,
222+
consoleProxy,
219223
prefixLineCount: this.prefixLineCount,
220224
sharedEnvironment,
221225
onRunApplication: (context) => {
@@ -229,7 +233,7 @@ export class JavaScriptEnvironment implements IEnvironment {
229233
prelude,
230234
(codeVersion, error) => {
231235
const message = formatError(error, this.prefixLineCount)
232-
sendError(codeVersion, message)
236+
sendError(id, codeVersion, message)
233237
}
234238
)
235239

src/utils/playerCommunication.ts

Lines changed: 92 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,104 @@
11
import { isValidElement } from 'react'
22
import ReactDOM from 'react-dom'
3-
import { Message } from '../types/Messages'
4-
import * as ExtendedJSON from './ExtendedJSON'
5-
import consoleProxy, {
3+
import {
64
consoleClear,
75
consoleLog,
86
consoleLogRNWP,
7+
ConsoleProxy,
98
} from '../components/player/ConsoleProxy'
109
import { EvaluationContext } from '../environments/IEnvironment'
10+
import { Message } from '../types/Messages'
11+
import * as ExtendedJSON from './ExtendedJSON'
12+
13+
function post(message: Message) {
14+
try {
15+
parent.postMessage(ExtendedJSON.stringify(message), '*')
16+
} catch {}
17+
}
18+
19+
export function sendMessage(sharedEnvironment: boolean, message: Message) {
20+
if (sharedEnvironment) {
21+
enhanceConsoleLogs(message)
22+
parent.__message(message)
23+
} else {
24+
post(message)
25+
}
26+
}
27+
28+
export function sendError(
29+
id: string,
30+
codeVersion: number,
31+
errorMessage: string
32+
) {
33+
post({ id, codeVersion, type: 'error', payload: errorMessage })
34+
}
35+
36+
export function createWindowErrorHandler({
37+
codeVersion,
38+
id,
39+
prefixLineCount,
40+
}: {
41+
codeVersion: number
42+
id: string
43+
prefixLineCount: number
44+
}) {
45+
return (message: Event | string, _?: string, line?: number) => {
46+
const editorLine = (line || 0) - prefixLineCount
47+
sendError(id, codeVersion, `${message} (${editorLine})`)
48+
return true
49+
}
50+
}
51+
52+
export function bindConsoleLogMethods(options: {
53+
codeVersion: number
54+
consoleProxy: ConsoleProxy
55+
sharedEnvironment: boolean
56+
id: string
57+
prefixLineCount: number
58+
}) {
59+
const { codeVersion, consoleProxy, sharedEnvironment, id } = options
60+
61+
consoleProxy._rnwp_log = consoleLogRNWP.bind(
62+
consoleProxy,
63+
sendMessage.bind(null, sharedEnvironment),
64+
id,
65+
codeVersion
66+
)
67+
68+
consoleProxy.log = consoleLog.bind(
69+
consoleProxy,
70+
sendMessage.bind(null, sharedEnvironment),
71+
id,
72+
codeVersion,
73+
'visible'
74+
)
1175

76+
consoleProxy.clear = consoleClear.bind(
77+
consoleProxy,
78+
sendMessage.bind(null, sharedEnvironment),
79+
id,
80+
codeVersion
81+
)
82+
}
83+
84+
/**
85+
* Every time we run the application, we re-bind all the logging and error message
86+
* handlers with a new `codeVersion`. This ensures that logs aren't stale. We also
87+
* include the iframe's id to handle the case of multiple preview iframes
88+
*/
1289
export function initializeCommunication({
1390
id,
1491
sharedEnvironment,
1592
prefixLineCount,
93+
consoleProxy,
1694
onRunApplication,
1795
}: {
1896
id: string
1997
sharedEnvironment: boolean
2098
prefixLineCount: number
99+
consoleProxy: ConsoleProxy
21100
onRunApplication: (context: EvaluationContext) => void
22101
}) {
23-
function post(message: Message) {
24-
try {
25-
parent.postMessage(ExtendedJSON.stringify(message), '*')
26-
} catch {}
27-
}
28-
29-
function sendError(codeVersion: number, errorMessage: string) {
30-
post({ id, codeVersion, type: 'error', payload: errorMessage })
31-
}
32-
33-
function sendMessage(message: Message) {
34-
if (sharedEnvironment) {
35-
enhanceConsoleLogs(message)
36-
parent.__message(message)
37-
} else {
38-
post(message)
39-
}
40-
}
41-
42102
window.onmessage = (e: MessageEvent) => {
43103
if (!e.data || e.data.source !== 'rnwp') return
44104

@@ -48,43 +108,28 @@ export function initializeCommunication({
48108
codeVersion: number
49109
}
50110

51-
consoleProxy._rnwp_log = consoleLogRNWP.bind(
111+
bindConsoleLogMethods({
112+
codeVersion,
52113
consoleProxy,
53-
sendMessage,
114+
sharedEnvironment,
54115
id,
55-
codeVersion
56-
)
57-
consoleProxy.log = consoleLog.bind(
58-
consoleProxy,
59-
sendMessage,
116+
prefixLineCount,
117+
})
118+
119+
window.onerror = createWindowErrorHandler({
120+
prefixLineCount,
60121
id,
61122
codeVersion,
62-
'visible'
63-
)
64-
consoleProxy.clear = consoleClear.bind(
65-
consoleProxy,
66-
sendMessage,
67-
id,
68-
codeVersion
69-
)
70-
window.onerror = (message: Event | string, _?: string, line?: number) => {
71-
const editorLine = (line || 0) - prefixLineCount
72-
sendError(codeVersion, `${message} (${editorLine})`)
73-
return true
74-
}
123+
})
75124

76125
onRunApplication({ entry, fileMap, codeVersion, requireCache: {} })
77126
}
78-
79-
return {
80-
sendError,
81-
}
82127
}
83128

84129
/**
85130
* Enhance console logs to allow React elements to be rendered in the parent frame
86131
*/
87-
function enhanceConsoleLogs(message: Message) {
132+
export function enhanceConsoleLogs(message: Message) {
88133
if (message.type === 'console' && message.payload.command === 'log') {
89134
message.payload.data = message.payload.data.map((log) => {
90135
if (isValidElement(log as any)) {

0 commit comments

Comments
 (0)