Repository: satya164/monaco-editor-boilerplate
Branch: master
Commit: 3237471d2efb
Files: 22
Total size: 3.2 MB
Directory structure:
gitextract_lphjetof/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .flowconfig
├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── serve.config.js
├── src/
│ ├── App.js
│ ├── Editor.css
│ ├── Editor.js
│ ├── config/
│ │ └── eslint.json
│ ├── index.js
│ ├── themes/
│ │ ├── dark.js
│ │ └── light.js
│ ├── vendor/
│ │ └── eslint.bundle.js
│ └── workers/
│ ├── eslint.worker.js
│ └── typings.worker.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [["env", { "modules": false }], "react", "stage-2"]
}
================================================
FILE: .editorconfig
================================================
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2
================================================
FILE: .eslintignore
================================================
**/dist/*
**/node_modules/*
================================================
FILE: .eslintrc
================================================
{
"extends": "eslint-config-satya164",
"env": {
"browser": true,
"node": true,
"es6": true,
}
}
================================================
FILE: .flowconfig
================================================
[ignore]
[include]
[libs]
[options]
munge_underscores=true
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
module.name_mapper='^monaco-editor$' -> 'monaco-editor/esm/vs/editor/editor.main.js'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf|ttf|otf\)$' -> 'ImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
================================================
FILE: .gitignore
================================================
*~
.DS_Store
.sass-cache
npm-debug.log
node_modules/
bower_components/
dist/
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Satyajit Sahoo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
Monaco Editor boilerplate
=========================
A simple boilerplate for Monaco editor with React.
================================================
FILE: index.html
================================================
Monaco Editor Sample
================================================
FILE: package.json
================================================
{
"name": "monaco-editor-boilerplate",
"version": "1.0.0",
"private": true,
"description": "Boilerplate for projects using ES2015 and React",
"main": "index.js",
"repository": {
"type": "git",
"url": "git://github.com/satya164/react-boilerplate.git"
},
"scripts": {
"start": "cross-env NODE_ENV=development webpack-serve",
"flow": "flow",
"lint": "eslint .",
"build": "cross-env NODE_ENV=production webpack -p",
"prebuild": "del dist"
},
"author": "Satyajit Sahoo (https://github.com/satya164/)",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^9.0.0",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del-cli": "^1.1.0",
"eslint": "^5.5.0",
"eslint-config-satya164": "^2.0.1",
"flow-bin": "^0.80.0",
"mini-css-extract-plugin": "^0.4.2",
"style-loader": "^0.23.0",
"webpack": "^4.17.2",
"webpack-command": "^0.4.1",
"webpack-serve": "^2.0.2",
"worker-loader": "^2.0.0"
},
"dependencies": {
"babel-polyfill": "^6.26.0",
"dedent": "^0.7.0",
"idb-keyval": "^3.1.0",
"lodash": "^4.17.10",
"monaco-editor": "^0.14.3",
"prettier": "^1.14.2",
"react": "^16.5.0",
"react-dom": "^16.5.0"
}
}
================================================
FILE: serve.config.js
================================================
/* eslint-disable import/no-commonjs */
module.exports = {
port: 3000,
devMiddleware: { publicPath: '/dist/' },
};
================================================
FILE: src/App.js
================================================
/* @flow */
import 'babel-polyfill';
import * as React from 'react';
import dedent from 'dedent';
import Editor from './Editor';
const files = {
'App.js': dedent`import React, { Component } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { Constants } from 'expo';
import AssetExample from './AssetExample';
export default class App extends Component {
render() {
return (
Change code in the editor and watch it change on your phone!
Save to get a shareable url.
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
color: '#34495e',
},
});`,
'AssetExample.js': dedent`import React, { Component } from 'react';
import { Text, View, StyleSheet, Image } from 'react-native';
export default class AssetExample extends Component {
render() {
return (
Local files and assets can be imported by dragging and dropping them into the editor
);
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
paragraph: {
margin: 24,
marginTop: 0,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
color: '#34495e',
},
logo: {
backgroundColor: "#056ecf",
height: 128,
width: 128,
}
});`,
};
type State = {
files: {
[name: string]: string,
},
current: string,
};
export default class App extends React.Component<{}, State> {
state = {
files,
current: 'App.js',
};
_handleValueChange = code =>
this.setState(state => ({
files: {
...state.files,
[state.current]: code,
},
}));
_handleOpenPath = path => this.setState({ current: path });
render() {
return (
{Object.keys(this.state.files).map(name => (
this._handleOpenPath(name)}
>
{name}
))}
);
}
}
================================================
FILE: src/Editor.css
================================================
/* Common overrides */
.monaco-editor .line-numbers {
color: currentColor;
opacity: .5;
}
/* Light theme overrides */
.ayu-light .JsxText {
color: #5c6773;
}
.ayu-light .JsxSelfClosingElement,
.ayu-light .JsxOpeningElement,
.ayu-light .JsxClosingElement,
.ayu-light .tagName-of-JsxOpeningElement,
.ayu-light .tagName-of-JsxClosingElement,
.ayu-light .tagName-of-JsxSelfClosingElement {
color: #41a6d9;
}
.ayu-light .name-of-JsxAttribute {
color: #f08c36;
}
.ayu-light .name-of-PropertyAssignment {
color: #86b300;
}
.ayu-light .name-of-PropertyAccessExpression {
color: #f08c36;
}
/* Dark theme overrides */
.ayu-dark .JsxText {
color: #d9d7ce;
}
.ayu-dark .JsxSelfClosingElement,
.ayu-dark .JsxOpeningElement,
.ayu-dark .JsxClosingElement,
.ayu-dark .tagName-of-JsxOpeningElement,
.ayu-dark .tagName-of-JsxClosingElement,
.ayu-dark .tagName-of-JsxSelfClosingElement {
color: #5ccfe6;
}
.ayu-dark .name-of-JsxAttribute {
color: #ffcf71;
}
.ayu-dark .name-of-PropertyAssignment {
color: #bae67e;
}
.ayu-dark .name-of-PropertyAccessExpression {
color: #ffcf71;
}
================================================
FILE: src/Editor.js
================================================
/* @flow */
import * as monaco from 'monaco-editor/esm/vs/editor/editor.main';
import { SimpleEditorModelResolverService } from 'monaco-editor/esm/vs/editor/standalone/browser/simpleServices';
import { StaticServices } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices';
import * as React from 'react';
import debounce from 'lodash/debounce';
import TypingsWorker from './workers/typings.worker';
import ESLintWorker from './workers/eslint.worker';
import light from './themes/light';
import dark from './themes/dark';
import './Editor.css';
/**
* Monkeypatch to make 'Find All References' work across multiple files
* https://github.com/Microsoft/monaco-editor/issues/779#issuecomment-374258435
*/
SimpleEditorModelResolverService.prototype.findModel = function(
editor,
resource
) {
return monaco.editor
.getModels()
.find(model => model.uri.toString() === resource.toString());
};
global.MonacoEnvironment = {
getWorker(moduleId, label) {
let MonacoWorker;
switch (label) {
case 'json':
/* $FlowFixMe */
MonacoWorker = require('worker-loader!monaco-editor/esm/vs/language/json/json.worker');
break;
case 'typescript':
case 'javascript':
/* $FlowFixMe */
MonacoWorker = require('worker-loader!monaco-editor/esm/vs/language/typescript/ts.worker');
break;
default:
/* $FlowFixMe */
MonacoWorker = require('worker-loader!monaco-editor/esm/vs/editor/editor.worker');
}
return new MonacoWorker();
},
};
monaco.editor.defineTheme('ayu-light', light);
monaco.editor.defineTheme('ayu-dark', dark);
/**
* Disable typescript's diagnostics for JavaScript files.
* This suppresses errors when using Flow syntax.
* It's also unnecessary since we use ESLint for error checking.
*/
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: true,
});
/**
* Use prettier to format JavaScript code.
* This will replace the default formatter.
*/
monaco.languages.registerDocumentFormattingEditProvider('javascript', {
async provideDocumentFormattingEdits(model) {
const prettier = await import('prettier/standalone');
const babylon = await import('prettier/parser-babylon');
const text = prettier.format(model.getValue(), {
parser: 'babylon',
plugins: [babylon],
singleQuote: true,
});
return [
{
range: model.getFullModelRange(),
text,
},
];
},
});
/**
* Sync all the models to the worker eagerly.
* This enables intelliSense for all files without needing an `addExtraLib` call.
*/
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
/**
* Configure the typescript compiler to detect JSX and load type definitions
*/
const compilerOptions = {
allowJs: true,
allowSyntheticDefaultImports: true,
alwaysStrict: true,
jsx: 'React',
jsxFactory: 'React.createElement',
};
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
compilerOptions
);
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
compilerOptions
);
type Props = {
files: { [path: string]: string },
path: string,
value: string,
onOpenPath: (path: string) => mixed,
onValueChange: (value: string) => mixed,
lineNumbers?: 'on' | 'off',
wordWrap: 'off' | 'on' | 'wordWrapColumn' | 'bounded',
scrollBeyondLastLine?: boolean,
minimap?: {
enabled?: boolean,
maxColumn?: number,
renderCharacters?: boolean,
showSlider?: 'always' | 'mouseover',
side?: 'right' | 'left',
},
theme: 'ayu-light' | 'ayu-dark',
};
// Store editor states such as cursor position, selection and scroll position for each model
const editorStates = new Map();
// Store details about typings we have loaded
const extraLibs = new Map();
const codeEditorService = StaticServices.codeEditorService.get();
export default class Editor extends React.Component {
static defaultProps = {
lineNumbers: 'on',
wordWrap: 'on',
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
theme: 'ayu-light',
};
static removePath(path: string) {
// Remove editor states
editorStates.delete(path);
// Remove associated models
const model = monaco.editor
.getModels()
.find(model => model.uri.path === path);
model && model.dispose();
}
static renamePath(oldPath: string, newPath: string) {
const selection = editorStates.get(oldPath);
editorStates.delete(oldPath);
editorStates.set(newPath, selection);
this.removePath(oldPath);
}
componentDidMount() {
// Intialize the linter
this._linterWorker = new ESLintWorker();
this._linterWorker.addEventListener('message', ({ data }: any) =>
this._updateMarkers(data)
);
// Intialize the type definitions worker
this._typingsWorker = new TypingsWorker();
this._typingsWorker.addEventListener('message', ({ data }: any) =>
this._addTypings(data)
);
// Fetch some definitions
const dependencies = {
expo: '29.0.0',
react: '16.3.1',
'react-native': '0.55.4',
};
Object.keys(dependencies).forEach(name =>
this._typingsWorker.postMessage({
name,
version: dependencies[name],
})
);
const { path, value, ...rest } = this.props;
this._editor = monaco.editor.create(this._node, rest, {
codeEditorService: Object.assign(Object.create(codeEditorService), {
openCodeEditor: async ({ resource, options }, editor) => {
// Open the file with this path
// This should set the model with the path and value
this.props.onOpenPath(resource.path);
// Move cursor to the desired position
editor.setSelection(options.selection);
// Scroll the editor to bring the desired line into focus
editor.revealLine(options.selection.startLineNumber);
return Promise.resolve({
getControl: () => editor,
});
}
}),
});
Object.keys(this.props.files).forEach(path =>
this._initializeFile(path, this.props.files[path])
);
this._openFile(path, value);
this._phantom.contentWindow.addEventListener('resize', this._handleResize);
}
componentDidUpdate(prevProps: Props) {
const { path, value, ...rest } = this.props;
this._editor.updateOptions(rest);
if (path !== prevProps.path) {
editorStates.set(prevProps.path, this._editor.saveViewState());
this._openFile(path, value);
} else if (value !== this._editor.getModel().getValue()) {
const model = this._editor.getModel();
if (value !== model.getValue()) {
model.pushEditOperations(
[],
[
{
range: model.getFullModelRange(),
text: value,
},
]
);
}
}
}
componentWillUnmount() {
this._linterWorker && this._linterWorker.terminate();
this._typingsWorker && this._typingsWorker.termnate();
this._subscription && this._subscription.dispose();
this._editor && this._editor.dispose();
this._phantom &&
this._phantom.contentWindow.removeEventListener(
'resize',
this._handleResize
);
}
clearSelection() {
const selection = this._editor.getSelection();
this._editor.setSelection(
new monaco.Selection(
selection.startLineNumber,
selection.startColumn,
selection.startLineNumber,
selection.startColumn
)
);
}
_initializeFile = (path: string, value: string) => {
let model = monaco.editor
.getModels()
.find(model => model.uri.path === path);
if (model) {
// If a model exists, we need to update it's value
// This is needed because the content for the file might have been modified externally
// Use `pushEditOperations` instead of `setValue` or `applyEdits` to preserve undo stack
model.pushEditOperations(
[],
[
{
range: model.getFullModelRange(),
text: value,
},
]
);
} else {
model = monaco.editor.createModel(
value,
'javascript',
new monaco.Uri().with({ path })
);
model.updateOptions({
tabSize: 2,
insertSpaces: true,
});
}
};
_openFile = (path: string, value: string) => {
this._initializeFile(path, value);
const model = monaco.editor
.getModels()
.find(model => model.uri.path === path);
this._editor.setModel(model);
// Restore the editor state for the file
const editorState = editorStates.get(path);
if (editorState) {
this._editor.restoreViewState(editorState);
}
this._editor.focus();
// Subscribe to change in value so we can notify the parent
this._subscription && this._subscription.dispose();
this._subscription = this._editor.getModel().onDidChangeContent(() => {
const value = this._editor.getModel().getValue();
this.props.onValueChange(value);
this._lintCode(value);
});
};
_lintCode = code => {
const model = this._editor.getModel();
monaco.editor.setModelMarkers(model, 'eslint', []);
this._linterWorker.postMessage({
code,
version: model.getVersionId(),
});
};
_addTypings = ({ typings }) => {
Object.keys(typings).forEach(path => {
let extraLib = extraLibs.get(path);
extraLib && extraLib.dispose();
extraLib = monaco.languages.typescript.javascriptDefaults.addExtraLib(
typings[path],
path
);
extraLibs.set(path, extraLib);
});
};
_updateMarkers = ({ markers, version }: any) => {
requestAnimationFrame(() => {
const model = this._editor.getModel();
if (model && model.getVersionId() === version) {
monaco.editor.setModelMarkers(model, 'eslint', markers);
}
});
};
_handleResize = debounce(() => this._editor.layout(), 100, {
leading: true,
trailing: true,
});
_linterWorker: Worker;
_typingsWorker: Worker;
_subscription: any;
_editor: any;
_phantom: any;
_node: any;
render() {
return (