Skip to content

Commit 5e196bf

Browse files
committed
Add registerBundledModules option
1 parent a242d20 commit 5e196bf

File tree

11 files changed

+108
-66
lines changed

11 files changed

+108
-66
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ The sandbox accepts the following props/parameters.
103103
| **`entry`** | The filename of the file that runs first. This is only relevant when showing multiple files with the `files` parameter. Defaults to `index.js`, or `index.tsx` if TypeScript is enabled. | `'index.js'` or `'index.tsx'` |
104104
| **`initialTab`** | The filename of the tab to show by default. This is only relevant when showing multiple files with the `files` parameter. Defaults to the value of `entry`. | `entry` |
105105
| **`modules`** | An array of external modules to make available to the sandbox. Each object in the array should be an object containing a `name` and `url`. As a shorthand, pass a string name to load a module from unpkg (`https://unpkg.com/${name}`). More detail below. | `[]` |
106+
| **`registerBundledModules`** | Should the player share its bundled React, React DOM, and React Native implementations with the running app? Disable this if you plan to pass a `react`, `react-dom` or `react-native` module url. | `true` |
106107
| **`css`** | An optional CSS string to apply within the workspace `iframe`. | `''` |
107108
| **`styles`** | An map of inline style objects, applied to various elements to customize the style of the UI. Example: `{ header: { backgroundColor: 'red' } }` | `{}` |
108109
| **`strings`** | A map of strings that appear in the UI. Example: `{ loading: 'Loading dependencies...' }` | `{}` |

src/components/workspace/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react'
1+
import React, { useState } from 'react'
22
import screenfull from 'screenfull'
33
import diff, { DiffRange } from '../../utils/Diff'
44
import { InternalOptions, WorkspaceStep } from '../../utils/options'
@@ -65,6 +65,7 @@ export default function App({
6565
workspaces,
6666
typescript,
6767
detectedModules,
68+
registerBundledModules,
6869
compiler,
6970
onChange,
7071
}: Props) {
@@ -100,6 +101,7 @@ export default function App({
100101
activeStepIndex={activeStepIndex}
101102
onChangeActiveStepIndex={setActiveStepIndex}
102103
detectedModules={detectedModules}
104+
registerBundledModules={registerBundledModules}
103105
diff={diff}
104106
// Merge props from the current workspace step
105107
{...(workspaces.length > 0

src/components/workspace/PlayerFrame.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, { PureComponent } from 'react'
2+
import { ConsoleCommand, Message } from '../../types/Messages'
23
import * as ExtendedJSON from '../../utils/ExtendedJSON'
4+
import { encode } from '../../utils/queryString'
35
import { prefixObject } from '../../utils/Styles'
6+
import type { ExternalModule } from '../player/VendorComponents'
47
import Phone from './Phone'
5-
import { Message, ConsoleCommand } from '../../types/Messages'
6-
import { encode } from '../../utils/queryString'
78
import { ExternalStyles } from './Workspace'
8-
import type { ExternalModule } from '../player/VendorComponents'
99

1010
const styles = prefixObject({
1111
iframe: {
@@ -27,6 +27,7 @@ interface Props {
2727
sharedEnvironment: boolean
2828
detectedModules: ExternalModule[]
2929
modules: ExternalModule[]
30+
registerBundledModules: boolean
3031
styleSheet: string
3132
css: string
3233
prelude: string
@@ -149,6 +150,7 @@ export default class extends PureComponent<Props, State> {
149150
externalStyles,
150151
environmentName,
151152
assetRoot,
153+
registerBundledModules,
152154
detectedModules,
153155
modules,
154156
styleSheet,
@@ -169,6 +171,7 @@ export default class extends PureComponent<Props, State> {
169171
assetRoot,
170172
detectedModules: JSON.stringify(detectedModules),
171173
modules: JSON.stringify(modules),
174+
registerBundledModules,
172175
styleSheet,
173176
css,
174177
statusBarColor,

src/components/workspace/Workspace.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
import React, {
22
CSSProperties,
3+
memo,
34
ReactNode,
5+
useCallback,
46
useEffect,
7+
useMemo,
58
useReducer,
69
useRef,
7-
useMemo,
8-
useCallback,
9-
memo,
1010
useState,
1111
} from 'react'
1212
import type * as ts from 'typescript'
13+
import { useOptions } from '../../contexts/OptionsContext'
14+
import useRerenderEffect from '../../hooks/useRerenderEffect'
15+
import useResponsiveBreakpoint from '../../hooks/useResponsiveBreakpoint'
1316
import * as workspace from '../../reducers/workspace'
1417
import { ConsoleCommand, LogCommand } from '../../types/Messages'
1518
import babelRequest, { BabelResponse } from '../../utils/BabelRequest'
16-
import { PaneOptions, containsPane } from '../../utils/Panes'
19+
import {
20+
CompilerOptions,
21+
TypeScriptOptions,
22+
UserInterfaceStrings,
23+
WorkspaceStep,
24+
} from '../../utils/options'
25+
import { containsPane, PaneOptions } from '../../utils/Panes'
1726
import { prefixObject, rowStyle } from '../../utils/Styles'
1827
import { Tab } from '../../utils/Tab'
19-
import typeScriptRequest, {
20-
TypeScriptCompileRequest,
21-
} from '../../utils/TypeScriptRequest'
28+
import typeScriptRequest from '../../utils/TypeScriptRequest'
29+
import type { ExternalModule } from '../player/VendorComponents'
30+
import { WorkspaceDiff } from './App'
2231
import { Props as EditorProps } from './Editor'
2332
import ConsolePane from './panes/ConsolePane'
2433
import EditorPane from './panes/EditorPane'
@@ -27,18 +36,6 @@ import StackPane from './panes/StackPane'
2736
import TranspilerPane from './panes/TranspilerPane'
2837
import WorkspacesPane from './panes/WorkspacesPane'
2938
import PlayerFrame from './PlayerFrame'
30-
import useResponsiveBreakpoint from '../../hooks/useResponsiveBreakpoint'
31-
import {
32-
WorkspaceStep,
33-
UserInterfaceStrings,
34-
CompilerOptions,
35-
TypeScriptOptions,
36-
} from '../../utils/options'
37-
import { WorkspaceDiff } from './App'
38-
import useRerenderEffect from '../../hooks/useRerenderEffect'
39-
import type { ExternalModule } from '../player/VendorComponents'
40-
import { basename, extname } from '../../utils/path'
41-
import { useOptions } from '../../contexts/OptionsContext'
4239

4340
const {
4441
reducer,
@@ -144,6 +141,7 @@ export interface Props {
144141
activeStepIndex: number
145142
onChangeActiveStepIndex: (index: number) => void
146143
detectedModules: ExternalModule[]
144+
registerBundledModules: boolean
147145
}
148146

149147
type WorkspacePaneProps = {
@@ -161,6 +159,7 @@ type WorkspacePaneProps = {
161159
typescriptOptions: TypeScriptOptions
162160
workspaces: WorkspaceStep[]
163161
detectedModules: ExternalModule[]
162+
registerBundledModules: boolean
164163
diff: Record<string, WorkspaceDiff>
165164
activeStepIndex: number
166165
onChangeActiveStepIndex: (index: number) => void
@@ -197,6 +196,7 @@ const WorkspacePane = memo((props: WorkspacePaneProps) => {
197196
sharedEnvironment,
198197
workspaces,
199198
detectedModules,
199+
registerBundledModules,
200200
activeStepIndex,
201201
onChangeActiveStepIndex,
202202
fullscreen,
@@ -265,6 +265,7 @@ const WorkspacePane = memo((props: WorkspacePaneProps) => {
265265
onCreatePlayer(options.id, player)
266266
}}
267267
detectedModules={detectedModules}
268+
registerBundledModules={registerBundledModules}
268269
options={options}
269270
externalStyles={externalStyles}
270271
environmentName={environmentName}
@@ -635,6 +636,7 @@ export default function Workspace(props: Props) {
635636
typescriptOptions={props.typescriptOptions}
636637
workspaces={props.workspaces}
637638
detectedModules={props.detectedModules}
639+
registerBundledModules={props.registerBundledModules}
638640
diff={props.diff}
639641
activeStepIndex={props.activeStepIndex}
640642
onChangeActiveStepIndex={props.onChangeActiveStepIndex}

src/components/workspace/panes/PlayerPane.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import React, { CSSProperties, memo, useState } from 'react'
1+
import React, { memo, useState } from 'react'
2+
import { useOptions } from '../../../contexts/OptionsContext'
23
import { ConsoleCommand, LogCommand } from '../../../types/Messages'
4+
import { PlayerPaneOptions } from '../../../utils/Panes'
35
import {
46
columnStyle,
57
mergeStyles,
68
prefixObject,
79
rowStyle,
810
} from '../../../utils/Styles'
11+
import type { ExternalModule } from '../../player/VendorComponents'
912
import Button from '../Button'
1013
import Console from '../Console'
1114
import Header from '../Header'
15+
import HeaderLink from '../HeaderLink'
16+
import { ReloadIcon } from '../Icons'
1217
import PlayerFrame from '../PlayerFrame'
18+
import { HorizontalSpacer } from '../Spacer'
1319
import Status from '../Status'
14-
import { PlayerPaneOptions } from '../../../utils/Panes'
1520
import { ExternalStyles } from '../Workspace'
16-
import type { ExternalModule } from '../../player/VendorComponents'
17-
import { ReloadIcon } from '../Icons'
18-
import HeaderLink from '../HeaderLink'
19-
import { HorizontalSpacer } from '../Spacer'
20-
import { useOptions } from '../../../contexts/OptionsContext'
2121

2222
const styles = prefixObject({
2323
playerPane: mergeStyles(columnStyle, { flex: '0 0 auto' }),
@@ -32,6 +32,7 @@ export interface Props {
3232
sharedEnvironment: boolean
3333
files: Record<string, string>
3434
detectedModules: ExternalModule[]
35+
registerBundledModules: boolean
3536
logs: LogCommand[]
3637
onPlayerRun: () => void
3738
onPlayerReady: () => void
@@ -55,6 +56,7 @@ const PlayerPane = memo(
5556
onPlayerError,
5657
onPlayerConsole,
5758
detectedModules,
59+
registerBundledModules,
5860
},
5961
ref
6062
) {
@@ -112,6 +114,7 @@ const PlayerPane = memo(
112114
assetRoot={assetRoot}
113115
detectedModules={detectedModules}
114116
modules={modules}
117+
registerBundledModules={registerBundledModules}
115118
styleSheet={styleSheet}
116119
css={css}
117120
prelude={prelude}

src/environments/IEnvironment.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface EnvironmentOptions {
2626
styles: PlayerStyles
2727
modules: ExternalModuleDescription[]
2828
detectedModules: ExternalModuleDescription[]
29+
registerBundledModules: boolean
2930
}
3031

3132
export interface IEnvironment {

src/environments/javascript-environment.tsx

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface BeforeEvaluateOptions {
2626
export interface AfterEvaluateOptions {
2727
context: EvaluationContext
2828
host: HTMLDivElement
29+
require: (name: string) => unknown
2930
}
3031

3132
class JavaScriptSandbox {
@@ -54,10 +55,6 @@ class JavaScriptSandbox {
5455
const { fileMap, entry, requireCache } = context
5556
let { environment, assetRoot } = this
5657

57-
if (environment.hasModule(name)) {
58-
return environment.requireModule(name)
59-
}
60-
6158
// If name begins with . or ..
6259
if (name.match(/^\.{1,2}\//)) {
6360
const lookup = path.join(path.dirname(requirerName), name)
@@ -103,6 +100,8 @@ class JavaScriptSandbox {
103100
}
104101

105102
return requireCache[name]
103+
} else if (environment.hasModule(name)) {
104+
return environment.requireModule(name)
106105
} else {
107106
throw new Error(`Failed to resolve module ${name}`)
108107
}
@@ -133,7 +132,11 @@ class JavaScriptSandbox {
133132

134133
this.evaluate(entry, fileMap[entry], context)
135134

136-
environment.afterEvaluate({ context, host })
135+
environment.afterEvaluate({
136+
context,
137+
host,
138+
require: (name: string) => this.require(context, name, entry),
139+
})
137140
} catch (e) {
138141
onError(codeVersion, e as Error)
139142
}
@@ -182,23 +185,26 @@ export class JavaScriptEnvironment implements IEnvironment {
182185
styles,
183186
modules,
184187
detectedModules,
188+
registerBundledModules,
185189
}: EnvironmentOptions) {
186-
// Since these are already loaded anyway, there's no real cost to exposing them.
187-
// Always register them even for pure JS
188-
this.nodeModules['react'] = React
189-
this.nodeModules['react-dom'] = ReactDOM
190-
this.nodeModules['prop-types'] = PropTypes
191-
192-
Object.assign(window, {
193-
React,
194-
ReactDOM,
195-
PropTypes,
196-
})
190+
if (registerBundledModules) {
191+
// Since these are already loaded anyway, there's no real cost to exposing them.
192+
// Always register them even for pure JS
193+
this.nodeModules['react'] = React
194+
this.nodeModules['react-dom'] = ReactDOM
195+
this.nodeModules['prop-types'] = PropTypes
196+
197+
Object.assign(window, {
198+
React,
199+
ReactDOM,
200+
PropTypes,
201+
})
202+
}
197203

198204
return this.loadExternalModules({
199205
modules,
200206
detectedModules,
201-
hasModule: this.hasModule,
207+
hasModule: registerBundledModules ? this.hasModule : () => false,
202208
}).then(() => {
203209
const { appElement, wrapperElement } = createAppLayout(document, styles)
204210

@@ -263,9 +269,9 @@ export class JavaScriptEnvironment implements IEnvironment {
263269
const normalizedModules = modules.filter(({ name }) => !hasModule(name))
264270

265271
// Only download detected modules that aren't also listed as vendor components
266-
const detectedModulesToDownload = detectedModules.filter(
267-
({ name }) => !normalizedModules.some((m) => m.name === name)
268-
)
272+
const detectedModulesToDownload = detectedModules
273+
.filter(({ name }) => !hasModule(name))
274+
.filter(({ name }) => !normalizedModules.some((m) => m.name === name))
269275

270276
return VendorComponents.load([
271277
...normalizedModules,

src/environments/react-environment.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import ReactDOM from 'react-dom'
2+
import type ReactDOM from 'react-dom'
33
import hasProperty from '../utils/hasProperty'
44
import {
55
AfterEvaluateOptions,
@@ -8,11 +8,16 @@ import {
88
} from './javascript-environment'
99

1010
class ReactEnvironment extends JavaScriptEnvironment {
11+
lastReactDOM: typeof ReactDOM | null = null
12+
1113
beforeEvaluate({ host }: BeforeEvaluateOptions) {
12-
ReactDOM.unmountComponentAtNode(host)
14+
this.lastReactDOM?.unmountComponentAtNode(host)
1315
}
1416

15-
afterEvaluate({ context, host }: AfterEvaluateOptions) {
17+
afterEvaluate({ context, host, require }: AfterEvaluateOptions) {
18+
const currentReactDOM = require('react-dom') as typeof ReactDOM
19+
this.lastReactDOM = currentReactDOM
20+
1621
const EntryComponent = context.requireCache[context.entry]
1722

1823
if (
@@ -21,7 +26,7 @@ class ReactEnvironment extends JavaScriptEnvironment {
2126
hasProperty(EntryComponent, 'default')
2227
) {
2328
const Component = EntryComponent.default as React.FunctionComponent
24-
ReactDOM.render(<Component />, host)
29+
currentReactDOM.render(<Component />, host)
2530
}
2631

2732
const renderedElement = host.firstElementChild as HTMLElement | undefined

0 commit comments

Comments
 (0)