Skip to content

Commit 0fadd85

Browse files
authored
Merge pull request #57 from dabbott/codesandbox
Add CodeSandbox integration
2 parents c7466c6 + ff430f1 commit 0fadd85

File tree

14 files changed

+1498
-69
lines changed

14 files changed

+1498
-69
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"babel-preset-stage-1": "^6.3.13",
5454
"babel-runtime": "^6.3.19",
5555
"codemirror": "^5.54.0",
56+
"codesandbox": "^2.2.1",
5657
"css-loader": "^3.5.3",
5758
"diff": "^4.0.1",
5859
"eslint": "^1.10.3",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { getParameters } from 'codesandbox/lib/api/define'
2+
import React, { memo, useMemo } from 'react'
3+
import { useOptions } from '../../contexts/OptionsContext'
4+
import { entries, fromEntries } from '../../utils/Object'
5+
import { prefixObject } from '../../utils/Styles'
6+
import HeaderLink from './HeaderLink'
7+
8+
interface Props {
9+
files: Record<string, string>
10+
children?: React.ReactNode
11+
}
12+
13+
const styles = prefixObject({
14+
form: {
15+
display: 'flex',
16+
},
17+
// Reset button CSS: https://gist.github.com/MoOx/9137295
18+
button: {
19+
border: 'none',
20+
margin: '0',
21+
padding: '0',
22+
width: 'auto',
23+
overflow: 'visible',
24+
background: 'transparent',
25+
color: 'inherit',
26+
font: 'inherit',
27+
lineHeight: 'normal',
28+
WebkitFontSmoothing: 'inherit',
29+
MozOsxFontSmoothing: 'inherit',
30+
WebkitAppearance: 'none',
31+
},
32+
})
33+
34+
const scriptTargetMap: Record<number, string> = {
35+
0: 'ES3',
36+
1: 'ES5',
37+
2: 'ES2015',
38+
3: 'ES2016',
39+
4: 'ES2017',
40+
5: 'ES2018',
41+
6: 'ES2019',
42+
7: 'ES2020',
43+
99: 'ESNext',
44+
100: 'JSON',
45+
}
46+
47+
const moduleKindMap: Record<number, string> = {
48+
0: 'None',
49+
1: 'CommonJS',
50+
2: 'AMD',
51+
3: 'UMD',
52+
4: 'System',
53+
5: 'ES2015',
54+
6: 'ES2020',
55+
99: 'ESNext',
56+
}
57+
58+
const jsxEmitMap: Record<number, string> = {
59+
0: 'none',
60+
1: 'preserve',
61+
2: 'react',
62+
3: 'react-native',
63+
}
64+
65+
export const CodeSandboxButton = memo(function CodeSandboxButton({
66+
files,
67+
children,
68+
}: Props) {
69+
const internalOptions = useOptions()
70+
71+
const parameters = useMemo(() => {
72+
const { typescript, title, initialTab: main } = internalOptions
73+
const compilerOptions = typescript.compilerOptions || {}
74+
75+
const allFiles = {
76+
...files,
77+
...(typescript.enabled && {
78+
'tsconfig.json': JSON.stringify(
79+
{
80+
compilerOptions: {
81+
...compilerOptions,
82+
...('target' in compilerOptions && {
83+
target: scriptTargetMap[compilerOptions.target as number],
84+
}),
85+
...('module' in compilerOptions && {
86+
module: moduleKindMap[compilerOptions.module as number],
87+
}),
88+
...('jsx' in compilerOptions && {
89+
jsx: jsxEmitMap[compilerOptions.jsx as number],
90+
}),
91+
lib: (compilerOptions.lib || typescript.libs || [])
92+
.map((name) => (name.startsWith('lib.') ? name.slice(4) : name))
93+
.filter((name) => name !== 'lib'),
94+
},
95+
},
96+
null,
97+
2
98+
),
99+
}),
100+
}
101+
102+
return getParameters({
103+
files: {
104+
'package.json': {
105+
isBinary: false,
106+
content: {
107+
name: title,
108+
version: '1.0.0',
109+
main,
110+
scripts: {
111+
start: `parcel ${main} --open`,
112+
build: `parcel build ${main}`,
113+
},
114+
dependencies: {},
115+
devDependencies: {
116+
'parcel-bundler': '^1.6.1',
117+
},
118+
} as any,
119+
},
120+
...fromEntries(
121+
entries(allFiles).map(([name, code]) => [
122+
name,
123+
{ isBinary: false, content: code },
124+
])
125+
),
126+
},
127+
})
128+
}, [files, internalOptions])
129+
130+
return (
131+
<form
132+
style={styles.form}
133+
action="https://codesandbox.io/api/v1/sandboxes/define"
134+
method="POST"
135+
target="_blank"
136+
>
137+
<input type="hidden" name="parameters" value={parameters} />
138+
<button style={styles.button} type="submit">
139+
<HeaderLink>{children}</HeaderLink>
140+
</button>
141+
</form>
142+
)
143+
})
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { CSSProperties, memo } from 'react'
2-
import screenfull from 'screenfull'
32
import { mergeStyles, prefixObject } from '../../utils/Styles'
43

54
const styles = prefixObject({
@@ -10,22 +9,27 @@ const styles = prefixObject({
109
padding: '0 20px',
1110
display: 'flex',
1211
alignItems: 'center',
12+
cursor: 'pointer',
13+
textDecoration: 'underline',
1314
},
1415
})
1516

1617
interface Props {
17-
title: string
18+
children: React.ReactNode
1819
textStyle?: CSSProperties
20+
onClick?: () => void
1921
}
2022

21-
const toggleFullscreen = () => (screenfull as any).toggle()
22-
23-
export default memo(function Fullscreen({ textStyle, title }: Props) {
23+
export default memo(function Fullscreen({
24+
textStyle,
25+
onClick,
26+
children,
27+
}: Props) {
2428
const computedTextStyle = mergeStyles(styles.text, textStyle)
2529

2630
return (
27-
<div style={computedTextStyle} onClick={toggleFullscreen}>
28-
{title}
31+
<div style={computedTextStyle} onClick={onClick}>
32+
{children}
2933
</div>
3034
)
3135
})

src/components/workspace/panes/EditorPane.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { memo, useState } from 'react'
2+
import screenfull from 'screenfull'
23
import { LogCommand } from '../../../types/Messages'
34
import { EditorPaneOptions } from '../../../utils/Panes'
45
import { columnStyle, mergeStyles, prefixObject } from '../../../utils/Styles'
@@ -11,14 +12,18 @@ import {
1112
import About from '../About'
1213
import Button from '../Button'
1314
import Editor, { Props as EditorProps } from '../Editor'
14-
import Fullscreen from '../Fullscreen'
15+
import HeaderLink from '../HeaderLink'
1516
import Header from '../Header'
1617
import Overlay from '../Overlay'
1718
import Status from '../Status'
1819
import Tabs from '../Tabs'
1920
import { PlaygroundOptions, PublicError, ExternalStyles } from '../Workspace'
2021
import type { WorkspaceDiff } from '../App'
2122
import { TypeScriptOptions, UserInterfaceStrings } from '../../../utils/options'
23+
import { useOptions } from '../../../contexts/OptionsContext'
24+
import { CodeSandboxButton } from '../CodeSandboxButton'
25+
26+
const toggleFullscreen = () => (screenfull as any).toggle()
2227

2328
const styles = prefixObject({
2429
editorPane: columnStyle,
@@ -97,9 +102,11 @@ export default memo(function EditorPane({
97102
getTypeInfo,
98103
onClickTab,
99104
}: Props) {
105+
const internalOptions = useOptions()
106+
100107
const [showDetails, setShowDetails] = useState(false)
101108

102-
const { title } = options
109+
const title = options.title ?? internalOptions.title
103110

104111
const fileDiff = diff[activeFile] ? diff[activeFile].ranges : []
105112

@@ -108,6 +115,24 @@ export default memo(function EditorPane({
108115

109116
const style = mergeStyles(styles.editorPane, options.style)
110117

118+
const headerElements = (
119+
<>
120+
{fullscreen && (
121+
<HeaderLink
122+
textStyle={externalStyles.tabText}
123+
onClick={toggleFullscreen}
124+
>
125+
{strings.fullscreen}
126+
</HeaderLink>
127+
)}
128+
{internalOptions.codesandbox && (
129+
<CodeSandboxButton files={files}>
130+
{strings.codesandbox}
131+
</CodeSandboxButton>
132+
)}
133+
</>
134+
)
135+
111136
return (
112137
<div style={style}>
113138
{title && (
@@ -116,12 +141,7 @@ export default memo(function EditorPane({
116141
headerStyle={externalStyles.header}
117142
textStyle={externalStyles.headerText}
118143
>
119-
{fullscreen && (
120-
<Fullscreen
121-
title={strings.fullscreen}
122-
textStyle={externalStyles.headerText}
123-
/>
124-
)}
144+
{headerElements}
125145
</Header>
126146
)}
127147
{fileTabs.length > 1 && (
@@ -137,12 +157,7 @@ export default memo(function EditorPane({
137157
activeTextStyle={externalStyles.tabTextActive}
138158
changedTextStyle={externalStyles.tabTextChanged}
139159
>
140-
{fullscreen && !title && (
141-
<Fullscreen
142-
title={strings.fullscreen}
143-
textStyle={externalStyles.tabText}
144-
/>
145-
)}
160+
{!title && headerElements}
146161
</Tabs>
147162
)}
148163
<Editor

src/contexts/OptionsContext.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createContext, useContext } from 'react'
2+
import { InternalOptions } from '../utils/options'
3+
4+
const OptionsContext = createContext<InternalOptions | undefined>(undefined)
5+
6+
export const OptionsProvider = OptionsContext.Provider
7+
8+
export const useOptions = () => {
9+
const options = useContext(OptionsContext)
10+
11+
if (!options) {
12+
throw new Error(`Supply a Options component using OptionsContext.Provider`)
13+
}
14+
15+
return options
16+
}

src/environments/html-environment.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { appendCSS } from '../utils/CSS'
2+
import { entries } from '../utils/Object'
23
import { initializeCommunication } from '../utils/playerCommunication'
34
import { createAppLayout } from '../utils/PlayerUtils'
45
import { EnvironmentOptions, IEnvironment } from './IEnvironment'
@@ -31,7 +32,7 @@ export class HTMLEnvironment implements IEnvironment {
3132
document.write(entryFile)
3233
document.close()
3334

34-
const cssFiles = Object.entries(context.fileMap).filter(([name]) =>
35+
const cssFiles = entries(context.fileMap).filter(([name]) =>
3536
name.endsWith('.css')
3637
)
3738

src/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { prefixAndApply } from './utils/Styles'
1111
import { appendCSS } from './utils/CSS'
1212
import { normalize, PublicOptions, getFileExtensions } from './utils/options'
1313
import App from './components/workspace/App'
14+
import { OptionsProvider } from './contexts/OptionsContext'
1415

1516
const { data = '{}', preset } = getHashString()
1617

@@ -34,7 +35,12 @@ const mount = document.getElementById('player-root') as HTMLDivElement
3435
prefixAndApply({ display: 'flex' }, mount)
3536

3637
function render() {
37-
ReactDOM.render(<App onChange={onChange} {...rest} />, mount)
38+
ReactDOM.render(
39+
<OptionsProvider value={internalOptions}>
40+
<App onChange={onChange} {...rest} />
41+
</OptionsProvider>,
42+
mount
43+
)
3844
}
3945

4046
const extensions = getFileExtensions(internalOptions)

src/typescript-worker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as ts from 'typescript'
2+
import { entries } from './utils/Object'
23
import type {
34
TypeScriptErrorResponse,
45
TypeScriptRequest,
@@ -137,7 +138,7 @@ onmessage = function ({ data }) {
137138
compiler.then(({ host }) => {
138139
const { files } = command
139140

140-
Object.entries(files).forEach(([filename, code]) => {
141+
entries(files).forEach(([filename, code]) => {
141142
host.addFile(filename, code)
142143
})
143144
})

src/utils/Object.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export function hasOwnProperty<O extends object, K extends PropertyKey>(
2+
obj: O,
3+
key: K
4+
): obj is O & { [key in K]: unknown } {
5+
return Object.prototype.hasOwnProperty.call(obj, key)
6+
}
7+
8+
function isEnumerable(obj: object, key: PropertyKey) {
9+
return Object.prototype.propertyIsEnumerable.call(obj, key)
10+
}
11+
12+
export function fromEntries<T = any>(
13+
entries: Iterable<readonly [PropertyKey, T]>
14+
): { [k: string]: T } {
15+
return [...entries].reduce((obj: Record<PropertyKey, T>, [key, val]) => {
16+
obj[key as any] = val
17+
return obj
18+
}, {})
19+
}
20+
21+
export function entries<T>(
22+
obj: { [s: string]: T } | ArrayLike<T>
23+
): [string, T][] {
24+
if (obj == null) {
25+
throw new TypeError('Cannot convert undefined or null to object')
26+
}
27+
28+
const pairs: [string, T][] = []
29+
30+
for (let key in obj) {
31+
if (hasOwnProperty(obj, key) && isEnumerable(obj, key)) {
32+
pairs.push([key, obj[key]])
33+
}
34+
}
35+
36+
return pairs
37+
}

0 commit comments

Comments
 (0)