Skip to content

Commit fd56211

Browse files
authored
React Typescript (codesandbox#192)
* React Typescript * Add typescript validation * Tweaks * Auto add @types to dependencies * Fix typing fetching for embeds
1 parent 49fd948 commit fd56211

File tree

31 files changed

+343
-66
lines changed

31 files changed

+343
-66
lines changed

config/webpack.common.js

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -233,26 +233,32 @@ module.exports = {
233233
// See https://github.com/facebookincubator/create-react-app/issues/186
234234
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
235235
// Make the monaco editor work
236-
new CopyWebpackPlugin([
237-
{
238-
from: __DEV__
239-
? 'node_modules/monaco-editor/dev/vs'
240-
: 'node_modules/monaco-editor/min/vs',
241-
to: 'public/vs',
242-
},
243-
{
244-
from: 'node_modules/monaco-vue/release/min',
245-
to: 'public/vs/language/vue',
246-
},
247-
{
248-
from: 'static',
249-
to: 'static',
250-
},
251-
{
252-
from: 'src/homepage/static',
253-
to: 'static',
254-
},
255-
]),
236+
new CopyWebpackPlugin(
237+
[
238+
{
239+
from: __DEV__
240+
? 'node_modules/monaco-editor/dev/vs'
241+
: 'node_modules/monaco-editor/min/vs',
242+
to: 'public/vs',
243+
},
244+
__PROD__ && {
245+
from: 'node_modules/monaco-editor/min-maps',
246+
to: 'public/min-maps',
247+
},
248+
{
249+
from: 'node_modules/monaco-vue/release/min',
250+
to: 'public/vs/language/vue',
251+
},
252+
{
253+
from: 'static',
254+
to: 'static',
255+
},
256+
{
257+
from: 'src/homepage/static',
258+
to: 'static',
259+
},
260+
].filter(x => x)
261+
),
256262
// Try to dedupe duplicated modules, if any:
257263
new webpack.optimize.CommonsChunkPlugin({
258264
name: 'common',
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import IconBase from 'react-icons/IconBase';
3+
4+
export default () => (
5+
<IconBase viewBox="0 0 32 32">
6+
<title>file_type_typescript</title>
7+
<path d="M23.827,8.243A4.424,4.424,0,0,1,26.05,9.524a5.853,5.853,0,0,1,.852,1.143c.011.045-1.534,1.083-2.471,1.662-.034.023-.169-.124-.322-.35a2.014,2.014,0,0,0-1.67-1c-1.077-.074-1.771.49-1.766,1.433a1.3,1.3,0,0,0,.153.666c.237.49.677.784,2.059,1.383,2.544,1.095,3.636,1.817,4.31,2.843a5.158,5.158,0,0,1,.416,4.333,4.764,4.764,0,0,1-3.932,2.815,10.9,10.9,0,0,1-2.708-.028,6.531,6.531,0,0,1-3.616-1.884,6.278,6.278,0,0,1-.926-1.371,2.655,2.655,0,0,1,.327-.208c.158-.09.756-.434,1.32-.761L19.1,19.6l.214.312a4.771,4.771,0,0,0,1.35,1.292,3.3,3.3,0,0,0,3.458-.175,1.545,1.545,0,0,0,.2-1.974c-.276-.395-.84-.727-2.443-1.422a8.8,8.8,0,0,1-3.349-2.055,4.687,4.687,0,0,1-.976-1.777,7.116,7.116,0,0,1-.062-2.268,4.332,4.332,0,0,1,3.644-3.374A9,9,0,0,1,23.827,8.243ZM15.484,9.726l.011,1.454h-4.63V24.328H7.6V11.183H2.97V9.755A13.986,13.986,0,0,1,3.01,8.289c.017-.023,2.832-.034,6.245-.028l6.211.017Z" />
8+
</IconBase>
9+
);

src/app/components/sandbox/CodeEditor/CodeMirror.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
436436
const { id, title, preferences } = this.props;
437437
const code = this.getCode();
438438
const mode = await this.getMode(title);
439-
if (mode === 'jsx' || mode === 'css') {
439+
if (mode === 'jsx' || mode === 'typescript' || mode === 'css') {
440440
try {
441441
const prettify = await import('app/utils/codemirror/prettify');
442442
const newCode = await prettify.default(

src/app/components/sandbox/CodeEditor/Monaco.js

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Preferences, ModuleError, Module, Directory } from 'common/types';
66
import { getModulePath } from 'app/store/entities/sandboxes/modules/selectors';
77

88
import theme from 'common/theme';
9+
import getTemplate from 'common/templates';
910

1011
/* eslint-disable import/no-webpack-loader-syntax */
1112
import SyntaxHighlightWorker from 'worker-loader!./monaco/workers/syntax-highlighter';
@@ -40,6 +41,8 @@ type Props = {
4041
directories: Array<Directory>,
4142
dependencies: ?Object,
4243
setCurrentModule: ?(sandboxId: string, moduleId: string) => void,
44+
template: string,
45+
addDependency: ?(sandboxId: string, dependency: string) => void,
4346
};
4447

4548
const Container = styled.div`
@@ -199,6 +202,18 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
199202
this.typingsFetcherWorker.addEventListener('message', event => {
200203
const { path, typings } = event.data;
201204

205+
if (
206+
path.startsWith('node_modules/@types') &&
207+
this.hasNativeTypescript() &&
208+
this.props.addDependency != null
209+
) {
210+
const dependency = path.match(/node_modules\/(@types\/.*)\//)[1];
211+
212+
if (!Object.keys(this.props.dependencies).includes(dependency)) {
213+
this.props.addDependency(this.props.sandboxId, dependency);
214+
}
215+
}
216+
202217
if (
203218
!this.monaco.languages.typescript.typescriptDefaults.getExtraLibs()[
204219
`file:///${path}`
@@ -235,12 +250,15 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
235250
);
236251
};
237252

238-
updateLintWarnings = (markers: Array<Object>) => {
239-
this.monaco.editor.setModelMarkers(
240-
this.editor.getModel(),
241-
'eslint',
242-
markers
243-
);
253+
updateLintWarnings = async (markers: Array<Object>) => {
254+
const mode = await this.getMode(this.props.title);
255+
if (mode === 'javascript') {
256+
this.monaco.editor.setModelMarkers(
257+
this.editor.getModel(),
258+
'eslint',
259+
markers
260+
);
261+
}
244262
};
245263

246264
shouldComponentUpdate(nextProps: Props, nextState: State) {
@@ -493,6 +511,11 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
493511
});
494512
};
495513

514+
hasNativeTypescript = () => {
515+
const template = getTemplate(this.props.template);
516+
return template.sourceConfig && template.sourceConfig.typescript;
517+
};
518+
496519
configureEditor = async (editor, monaco) => {
497520
this.editor = editor;
498521
this.monaco = monaco;
@@ -505,18 +528,28 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
505528

506529
this.setupWorkers();
507530

531+
const hasNativeTypescript = this.hasNativeTypescript();
532+
508533
const compilerDefaults = {
509534
jsxFactory: 'React.createElement',
510535
reactNamespace: 'React',
511536
jsx: monaco.languages.typescript.JsxEmit.React,
512537
target: monaco.languages.typescript.ScriptTarget.ES2016,
513-
allowNonTsExtensions: true,
538+
allowNonTsExtensions: !hasNativeTypescript,
514539
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
515-
module: monaco.languages.typescript.ModuleKind.System,
516-
experimentalDecorators: true,
540+
module: monaco.languages.typescript.ModuleKind.ES2015,
541+
experimentalDecorators: !hasNativeTypescript,
517542
noEmit: true,
518543
allowJs: true,
519544
typeRoots: ['node_modules/@types'],
545+
546+
forceConsistentCasingInFileNames: hasNativeTypescript,
547+
noImplicitReturns: hasNativeTypescript,
548+
noImplicitThis: hasNativeTypescript,
549+
noImplicitAny: hasNativeTypescript,
550+
strictNullChecks: hasNativeTypescript,
551+
suppressImplicitAnyIndexErrors: hasNativeTypescript,
552+
noUnusedLocals: hasNativeTypescript,
520553
};
521554

522555
monaco.languages.typescript.typescriptDefaults.setMaximunWorkerIdleTime(-1);
@@ -530,7 +563,7 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
530563

531564
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
532565
noSemanticValidation: false,
533-
noSyntaxValidation: true,
566+
noSyntaxValidation: !hasNativeTypescript,
534567
});
535568

536569
await this.initializeModules();
@@ -615,7 +648,9 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
615648
};
616649

617650
initializeModules = (modules = this.props.modules) =>
618-
Promise.all(modules.map(module => this.createModel(module, modules)));
651+
Promise.all(
652+
modules.reverse().map(module => this.createModel(module, modules))
653+
);
619654

620655
resizeEditor = () => {
621656
this.editor.layout();
@@ -737,7 +772,7 @@ export default class CodeEditor extends React.PureComponent<Props, State> {
737772
const code = this.getCode();
738773
const mode = await this.getMode(title);
739774

740-
if (mode === 'javascript' || mode === 'css') {
775+
if (mode === 'javascript' || mode === 'typescript' || mode === 'css') {
741776
try {
742777
const prettify = await import('app/utils/codemirror/prettify');
743778
const newCode = await prettify.default(

src/app/components/sandbox/Preview/index.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import styled from 'styled-components';
44

55
import { debounce } from 'lodash';
66

7-
import type { Preferences } from 'app/store/preferences/reducer';
8-
import type { Module, Sandbox, Directory } from 'common/types';
7+
import type { Module, Sandbox, Preferences, Directory } from 'common/types';
98

109
import { frameUrl } from 'app/utils/url-generator';
1110
import { findMainModule } from 'app/store/entities/sandboxes/modules/selectors';
@@ -215,8 +214,8 @@ export default class Preview extends React.PureComponent<Props, State> {
215214
};
216215

217216
getRenderedModule = () => {
218-
const { modules, module, isInProjectView } = this.props;
219-
return isInProjectView ? findMainModule(modules) : module;
217+
const { modules, module, template, isInProjectView } = this.props;
218+
return isInProjectView ? findMainModule(modules, template) : module;
220219
};
221220

222221
executeCodeImmediately = () => {

src/app/pages/Profile/Showcase/ShowcasePreview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class ShowcasePreview extends React.PureComponent {
5454
directories,
5555
} = this.props;
5656

57-
const mainModule = findMainModule(modules);
57+
const mainModule = findMainModule(modules, sandbox.template);
5858

5959
return (
6060
<Container>

src/app/pages/Sandbox/Editor/Content/Header/ShareView.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class ShareView extends React.PureComponent {
177177
clearDefaultModule = () => this.setState({ defaultModule: null });
178178

179179
getOptionsUrl = () => {
180-
const { modules, directories } = this.props;
180+
const { sandbox, modules, directories } = this.props;
181181
const {
182182
defaultModule,
183183
showEditor,
@@ -191,7 +191,7 @@ class ShareView extends React.PureComponent {
191191

192192
const options = {};
193193

194-
const mainModuleId = findMainModule(modules).id;
194+
const mainModuleId = findMainModule(modules, sandbox.template).id;
195195
if (defaultModule && defaultModule !== mainModuleId) {
196196
const modulePath = getModulePath(modules, directories, defaultModule);
197197
options.module = modulePath;
@@ -295,7 +295,7 @@ class ShareView extends React.PureComponent {
295295
} = this.state;
296296

297297
const defaultModule =
298-
this.state.defaultModule || findMainModule(modules).id;
298+
this.state.defaultModule || findMainModule(modules, sandbox.template).id;
299299

300300
return (
301301
<Container>

src/app/pages/Sandbox/Editor/Content/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class EditorPreview extends React.PureComponent<Props, State> {
109109
saveCode = () => {
110110
const { sandbox, modules, sandboxActions } = this.props;
111111

112-
const mainModule = findMainModule(modules);
112+
const mainModule = findMainModule(modules, sandbox.template);
113113
const { currentModule } = sandbox;
114114

115115
// $FlowIssue
@@ -139,7 +139,7 @@ class EditorPreview extends React.PureComponent<Props, State> {
139139
previewApiActions,
140140
} = this.props;
141141

142-
const mainModule = findMainModule(modules);
142+
const mainModule = findMainModule(modules, sandbox.template);
143143
if (!mainModule) throw new Error('Cannot find main module');
144144

145145
const { currentModule: currentModuleId } = sandbox;
@@ -173,6 +173,8 @@ class EditorPreview extends React.PureComponent<Props, State> {
173173
sandboxId={sandbox.id}
174174
dependencies={sandbox.npmDependencies}
175175
setCurrentModule={sandboxActions.setCurrentModule}
176+
addDependency={sandboxActions.addNPMDependency}
177+
template={sandbox.template}
176178
/>
177179
</FullSize>
178180
);

src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/DirectoryChildren.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Props = {
2323
parentShortid: string,
2424
isInProjectView: boolean,
2525
parentShortid: string,
26+
sandboxTemplate: string,
2627
};
2728

2829
export default class DirectoryChildren extends React.PureComponent<Props> {
@@ -40,6 +41,7 @@ export default class DirectoryChildren extends React.PureComponent<Props> {
4041
directories,
4142
parentShortid,
4243
sandboxId,
44+
sandboxTemplate,
4345
modules,
4446
deleteEntry,
4547
currentModuleId,
@@ -59,6 +61,7 @@ export default class DirectoryChildren extends React.PureComponent<Props> {
5961
shortid={dir.shortid}
6062
title={dir.title}
6163
sandboxId={sandboxId}
64+
sandboxTemplate={sandboxTemplate}
6265
modules={modules}
6366
directories={directories}
6467
currentModuleId={currentModuleId}
@@ -67,7 +70,7 @@ export default class DirectoryChildren extends React.PureComponent<Props> {
6770
))}
6871
{modules.filter(x => x.directoryShortid === parentShortid).map(m => {
6972
const isActive = m.id === currentModuleId;
70-
const mainModule = isMainModule(m);
73+
const mainModule = isMainModule(m, sandboxTemplate);
7174
const type = getType(m);
7275

7376
const hasError = m && m.errors.length;

src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/Entry/EntryIcons.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import HTMLIcon from 'react-icons/lib/fa/html5';
1111
import ErrorIcon from 'react-icons/lib/md/error';
1212
import RawIcon from 'react-icons/lib/go/file-text';
1313
import ReactIcon from 'app/components/ReactIcon';
14+
import TypeScriptIcon from 'app/components/TypeScriptIcon';
1415

1516
const NotSyncedIconWithMargin = styled(NotSyncedIcon)`
1617
margin-left: -20px;
@@ -53,6 +54,8 @@ const getIcon = (type, error, root) => {
5354
return <DirectoryIcon />;
5455
case 'css':
5556
return <CSSIcon />;
57+
case 'ts':
58+
return <TypeScriptIcon />;
5659
case 'html':
5760
return <HTMLIcon />;
5861
default:

0 commit comments

Comments
 (0)