├── .babelrc ├── .gitignore ├── README.md ├── imgs ├── 1.jpeg ├── 2.png ├── 3.png ├── 4.png └── 5.jpeg ├── package.json ├── server-user.js ├── server.js ├── src ├── main │ ├── index.js │ └── services │ │ ├── application.js │ │ ├── file.js │ │ ├── menu.js │ │ ├── project.js │ │ └── window.js ├── renderer │ ├── components │ │ ├── .gitkeep │ │ ├── Editor │ │ │ ├── EditorPane.js │ │ │ ├── EditorSelectPane.css │ │ │ └── EditorSelectPane.js │ │ ├── FileTree │ │ │ └── FileTree.js │ │ ├── Layout │ │ │ ├── Footer.js │ │ │ ├── Layout.css │ │ │ ├── Pane.js │ │ │ ├── PaneGroup.js │ │ │ ├── TabGroup.js │ │ │ ├── TabItem.js │ │ │ └── index.js │ │ └── ProjFunc │ │ │ ├── Console.css │ │ │ ├── Console.js │ │ │ ├── ProjMenu.css │ │ │ └── ProjMenu.js │ ├── constants.js │ ├── index.js │ ├── models │ │ ├── .gitkeep │ │ ├── cns.js │ │ ├── editor.js │ │ ├── files.js │ │ ├── proj.js │ │ └── win.js │ ├── router.js │ ├── routes │ │ ├── IndexPage.css │ │ └── IndexPage.js │ ├── services │ │ ├── .gitkeep │ │ ├── cns.js │ │ ├── file.js │ │ ├── filter.js │ │ └── proj.js │ └── utils.js └── vendor.js ├── static ├── assets │ └── README.md ├── favicon.ico ├── index.html └── template │ └── test │ ├── config.json │ └── src │ ├── components │ ├── App.css │ └── App.js │ ├── index.js │ └── vendor.js └── webpack ├── base.config.js ├── base.config.prod.js ├── common.config.js ├── dev.config.js └── prod.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | 4 | ["es2015", {"modules": false}], 5 | //"es2015-webpack", 6 | 7 | // webpack understands the native import syntax, and uses it for tree shaking 8 | 9 | "stage-2", 10 | // Specifies what level of language features to activate. 11 | // Stage 2 is "draft", 4 is finished, 0 is strawman. 12 | // See https://tc39.github.io/process-document/ 13 | 14 | "react" 15 | // Transpile React components to JavaScript 16 | ], 17 | "plugins": [ 18 | "react-hot-loader/babel" 19 | 20 | // Enables React code to work with HMR. 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | user.webpack.config.js 3 | user.webpack.config.prod.js 4 | stop.bat 5 | old 6 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react editor 2 | 这是一个简单的 react 编辑器, 基于electron开发。 3 | 4 | 5 | ## Getting started 6 | 7 | ```bash 8 | git clone git@github.com:Jirapo/React-editor.git 9 | cd React-editor 10 | npm install 11 | npm start 12 | npm run server 13 | ``` 14 | 15 | ## Features 16 | 17 | ### repl 18 | 19 | 单页repl, 含有小型console。用户在编辑器里输入代码, 可以编译运行es6和一小段react代码, 显示运行结果。 20 | 左侧框显示常规运行结果,右侧框显示react代码结果。 21 | 22 | 23 | 24 | 25 | 26 | #### 步骤 27 | 28 | 1. 点击 'Show Console' 按钮 29 | 2. 点击 'BABEL' 控制台显示babel编译后的代码 30 | 3. 点击 'EXEC' 执行babel编译后的代码 31 | 4. 点击 'CLEAR' 清空console 32 | 33 | ### react IDE 34 | 35 | 导入react项目,不需要用户配置复杂的webpack.config.js, 这个编辑器会使用默认的webpack.config配置。 36 | 同时编辑器可以运行和打包react项目。 37 | 38 | 39 | 40 | #### 步骤 41 | 42 | 1. 请用户把 `./static/template/test` 复制到其它文件夹中 43 | 2. 在编辑器菜单中选择 `File - Add React Project`, 导入刚才的复制出去的文件夹test 44 | 3. 点击 'start' 按钮, 在浏览器中输入 `http://localhost:3001/` (端口号为 `config.json` 中的 `port` 字段值) 45 | 4. 点击 'Stop' 按钮,编辑器停止该端口的监听,并停止该项目 46 | 5. 点击 'Build' 按钮,编辑器将会打包该项目,打包目录为 `config.json` 中的 `output` 字段值 47 | 48 | #### config.json 49 | 50 | ```javascript 51 | 52 | { 53 | "port": 3002, // listening port 54 | "entry": "src/index.js", // entry file 55 | "vendor": "src/vendor.js", // vendor file 56 | "output":"dist" // output folder 57 | } 58 | 59 | ``` 60 | 61 | #### 打包目录文件 62 | 63 | 64 | 65 | 66 | ## Bugs 67 | 68 | - 文件夹没有监听。 69 | 70 | - 对于node_modules文件夹需要特殊处理。 71 | 72 | - 点击 'start', 'stop', 'build' 按钮,页面没有任何反馈。 73 | 74 | - 很多错误判断还没有写。 75 | 76 | - 样式使用postcss, 不支持less和sass。 77 | 78 | - 目前的ide流程逻辑很糟糕,需要更改。 79 | 80 | - ... 81 | 82 | ## End 83 | 84 | 请忽视文件中一大段一大段的残留代码注释…… 85 | 86 | -------------------------------------------------------------------------------- /imgs/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/imgs/1.jpeg -------------------------------------------------------------------------------- /imgs/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/imgs/2.png -------------------------------------------------------------------------------- /imgs/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/imgs/3.png -------------------------------------------------------------------------------- /imgs/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/imgs/4.png -------------------------------------------------------------------------------- /imgs/5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/imgs/5.jpeg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ele-demo", 3 | "version": "1.0.0", 4 | "description": "an simple react IDE, made by electron", 5 | "main": "./src/main/index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development electron ./src/main/index.js", 8 | "start-build": "cross-env NODE_ENV=production electron ./src/main/index.js", 9 | "server": "node server.js --port 3000 ", 10 | "server-user": "node server.js --port 3001", 11 | "build": "webpack --config webpack/prod.config.js --display-error-details --color", 12 | "build-user": "webpack --config user.webpack.config.prod.js --display-error-details --color", 13 | "watch": "webpack-dev-server --config webpack/dev.config.js --watch", 14 | "watch-user": "webpack-dev-server --config user.webpack.config.js --watch", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "author": "jirapo", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "autoprefixer": "^6.5.4", 21 | "babel": "^6.5.2", 22 | "babel-core": "^6.20.0", 23 | "babel-loader": "^6.2.9", 24 | "babel-plugin-add-module-exports": "^0.2.1", 25 | "babel-polyfill": "^6.20.0", 26 | "babel-preset-es2015-webpack": "^6.4.3", 27 | "babel-preset-latest": "^6.16.0", 28 | "babel-preset-react": "^6.16.0", 29 | "babel-preset-stage-2": "^6.16.0", 30 | "babel-register": "^6.18.0", 31 | "babel-standalone": "^6.21.1-0", 32 | "brace": "^0.9.0", 33 | "chokidar": "^1.6.1", 34 | "cross-env": "^3.1.3", 35 | "css-loader": "^0.26.1", 36 | "del": "^2.2.2", 37 | "dva": "^1.1.0", 38 | "electron": "^1.4.12", 39 | "electron-default-menu": "^1.0.0", 40 | "extract-text-webpack-plugin": "^2.0.0-beta.4", 41 | "file-loader": "^0.9.0", 42 | "html-webpack-inline-chunk-plugin": "^1.0.0", 43 | "html-webpack-plugin": "^2.24.1", 44 | "node-dir": "^0.1.16", 45 | "node-watch": "^0.4.1", 46 | "path": "^0.12.7", 47 | "postcss-loader": "^1.2.1", 48 | "react": "^15.4.1", 49 | "react-ace": "^4.1.0", 50 | "react-dom": "^15.4.1", 51 | "react-hot-loader": "^3.0.0-beta.6", 52 | "react-redux": "^4.4.6", 53 | "react-router": "^3.0.0", 54 | "react-treebeard": "^1.1.4", 55 | "redux": "^3.6.0", 56 | "style-loader": "^0.13.1", 57 | "webpack": "^2.2.0-rc.2", 58 | "webpack-dev-server": "^2.2.0-rc.0", 59 | "webpack-hot-middleware": "^2.14.0", 60 | "webpack-merge": "^1.1.2" 61 | }, 62 | "dependencies": { 63 | "chokidar": "^1.6.1", 64 | "electron": "^1.4.13", 65 | "kill-by-port": "^0.0.1", 66 | "kp": "^1.1.0", 67 | "node-watch": "^0.4.1", 68 | "webpack": "^2.2.0-rc.2" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/Jirapo/React-editor.git" 73 | }, 74 | "bugs": { 75 | "url": "https://github.com/Jirapo/React-editor/issues" 76 | }, 77 | "homepage": "https://github.com/Jirapo/React-editor#readme" 78 | } 79 | -------------------------------------------------------------------------------- /server-user.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var webpackDevMiddleware = require("webpack-dev-middleware"); 3 | var webpack = require("webpack"); 4 | var path = require("path") 5 | var http = require('http'); 6 | 7 | var app = express(); 8 | var port = process.argv[3] 9 | // var configDir = path.resolve('./', process.argv[5]) 10 | // console.log(configDir) 11 | // var webpackConfig = require(configDir) 12 | var webpackConfig = require('./user.webpack.config'); 13 | var compiler = webpack(webpackConfig); 14 | 15 | // var httpServer 16 | var httpServer = http.createServer(app) 17 | var webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, { 18 | // lazy: true, 19 | hot: true, 20 | stats: { colors: true, chunks: false }, 21 | publicPath: "/" 22 | }) 23 | 24 | app.use(webpackDevMiddlewareInstance); 25 | 26 | app.use(require('webpack-hot-middleware')(compiler)); 27 | 28 | app.use(function (req, res, next) { 29 | // console.log('Time: %d', Date.now()); 30 | // console.log(req.hostname) 31 | console.log(req.originalUrl) 32 | // console.log(req.params) 33 | console.log(req.method) 34 | 35 | if(req.originalUrl == '/stopProj'){ 36 | console.log(222) 37 | // httpServer.close(function(){ 38 | httpServer.close() 39 | process.exit(); 40 | // }); 41 | }else{ 42 | next(); 43 | } 44 | }); 45 | 46 | httpServer.listen(port); 47 | // httpServer = http.createServer(app).listen(port); 48 | // var server = app.listen(port, function () { 49 | // console.log(`Listening on port ${port}!`); 50 | // }); 51 | // const webpack = require('webpack'); 52 | // const WebpackDevServer = require('webpack-dev-server'); 53 | // const webpackConfig = require('./webpack.config') 54 | // 55 | // var bundler = new WebpackDevServer(webpack(webpackConfig), { 56 | // publicPath: '/dist/', 57 | // inline: true, 58 | // hot: true, 59 | // quiet: false, 60 | // noInfo: true, 61 | // stats: {colors: true} 62 | // }); 63 | // 64 | // bundler.listen(3001, 'localhost'); 65 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var webpackDevMiddleware = require("webpack-dev-middleware"); 3 | var webpack = require("webpack"); 4 | var path = require("path") 5 | 6 | var app = express(); 7 | var port = process.argv[3] 8 | // var configDir = path.resolve('./', process.argv[5]) 9 | // console.log(configDir) 10 | // var webpackConfig = require(configDir) 11 | var webpackConfig = require('./webpack/dev.config'); 12 | 13 | var compiler = webpack(webpackConfig); 14 | 15 | var webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, { 16 | // lazy: true, 17 | hot: true, 18 | stats: { colors: true, chunks: false }, 19 | publicPath: "/" 20 | }) 21 | 22 | app.use(webpackDevMiddlewareInstance); 23 | 24 | app.use(require('webpack-hot-middleware')(compiler)); 25 | app.listen(port, function () { 26 | console.log(`Listening on port ${port}!`); 27 | }); 28 | // var httpServer = http.createServer(app) 29 | // httpServer.listen(port); 30 | /* 31 | app.use(function (req, res, next) { 32 | // console.log('Time: %d', Date.now()); 33 | // console.log(req.hostname) 34 | console.log(req.originalUrl) 35 | // console.log(req.params) 36 | console.log(req.method) 37 | 38 | if(req.originalUrl == '/stopProj'){ 39 | console.log(222) 40 | httpServer.close(function(){ 41 | console.log(111) 42 | process.exit(); 43 | }); 44 | }else{ 45 | next(); 46 | } 47 | });*/ 48 | 49 | 50 | // httpServer = http.createServer(app).listen(port); 51 | // var server = app.listen(port, function () { 52 | // console.log(`Listening on port ${port}!`); 53 | // }); 54 | // const webpack = require('webpack'); 55 | // const WebpackDevServer = require('webpack-dev-server'); 56 | // const webpackConfig = require('./webpack.config') 57 | // 58 | // var bundler = new WebpackDevServer(webpack(webpackConfig), { 59 | // publicPath: '/dist/', 60 | // inline: true, 61 | // hot: true, 62 | // quiet: false, 63 | // noInfo: true, 64 | // stats: {colors: true} 65 | // }); 66 | // 67 | // bundler.listen(3001, 'localhost'); 68 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | // import { app, BrowserWindow } from 'electron'; 2 | // import { join } from 'path'; 3 | // import * as application from './services/application'; 4 | // import * as window from './services/window'; 5 | // import * as menu from './services/menu'; 6 | // import * as file from './services/file'; 7 | 8 | const { app, BrowserWindow } = require('electron') 9 | const { join } = require('path') 10 | const application = require('./services/application') 11 | const window = require('./services/window') 12 | const menu = require('./services/menu') 13 | const file = require('./services/file') 14 | // const project = require('./services/project') 15 | 16 | console.log('(main/index) >>>>>>>>>>>>>>>>>>'); 17 | console.log('(main/index) app start'); 18 | 19 | // if (is.dev()) { 20 | // require('electron-debug')(); 21 | // } 22 | 23 | app.on('ready', () => { 24 | console.log('(main/index) app ready'); 25 | application.init(); 26 | menu.init(); 27 | 28 | }); 29 | 30 | app.on('window-all-closed', () => { 31 | if (process.platform !== 'darwin') { 32 | app.quit(); 33 | } 34 | }); 35 | 36 | app.on('activate', () => { 37 | if (window.getCount() === 0) { 38 | application.init(); 39 | } 40 | }); 41 | 42 | app.on('quit', () => { 43 | console.log('(main/index) app quit'); 44 | console.log('(main/index) <<<<<<<<<<<<<<<<<<<'); 45 | 46 | if(process.platform == 'darwin'){ 47 | const exec = require('child_process').exec; 48 | // exec() 49 | // spawn('npm run kill', ['--port', '3001']) 50 | } 51 | 52 | // if(process.platform == 'win32'){ 53 | // const exec = require('child_process').exec; 54 | // exec('start stop.bat',{ 55 | // // cwd: files.pathDir 56 | // }, (error, stdout, stderr) => { 57 | // if (error) { 58 | // console.error(`exec error: ${error}`); 59 | // return; 60 | // } 61 | // console.log(stdout); 62 | // console.log(stderr); 63 | // }); 64 | // } 65 | 66 | }); 67 | 68 | // Register to global, so renderer can access these with remote.getGlobal 69 | global.services = { 70 | application, 71 | window, 72 | file, 73 | // project 74 | }; 75 | -------------------------------------------------------------------------------- /src/main/services/application.js: -------------------------------------------------------------------------------- 1 | /* global $dirname */ 2 | const { join } = require('path') 3 | const { create } = require('./window') 4 | 5 | 6 | 7 | var isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | init: function () { 11 | const win = create({ width: 800, height: 600 }); 12 | if(isProd){ 13 | win.loadURL(`file://${join(__dirname, '../../../dist')}/index.html`); 14 | }else{ 15 | win.loadURL('http://localhost:3000/index.html') 16 | } 17 | }, 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/services/file.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const fs = require('fs') 4 | const dir = require('node-dir') 5 | const chokidar = require('chokidar') 6 | const {dialog, ipcMain} = require('electron') 7 | 8 | // Opens the file. 9 | const openFile = (focusWindow) => { 10 | dialog.showOpenDialog({ properties: ['openFile'] }, (fileNames) => { 11 | if (!fileNames) { 12 | return false; 13 | } else { 14 | const file = path.parse(fileNames[0]) 15 | console.log(fileNames) 16 | fs.readFile(fileNames[0], 'utf-8', 17 | function (err, data) { 18 | if(err){ 19 | return false; 20 | } else { 21 | return focusWindow 22 | .webContents 23 | .send( 24 | 'winOpenFile' , { 25 | // basename: file.base, 26 | // sep: path.sep, 27 | contents: data, 28 | file: file, 29 | path: fileNames[0] 30 | // dir: file.dir, 31 | // extension: file.ext 32 | }); 33 | } 34 | }); 35 | } 36 | }); 37 | }; 38 | 39 | // Opens a folder. 40 | const openFolder = (focusWindow) => { 41 | dialog.showOpenDialog({ properties: ['openDirectory'] }, (fileNames) => { 42 | if (!fileNames) { 43 | return false; 44 | } else { 45 | console.log('preload', fileNames) 46 | 47 | 48 | 49 | // var watcher = chokidar.watch(fileNames[0], { 50 | // // ignored: /[\/\\]\./, 51 | // persistent: true 52 | // }); 53 | 54 | // watcher 55 | // .on('add', path => { 56 | // // console.log(path) 57 | // reading(fileNames[0]) 58 | // }) 59 | // .on('unlink', path => { 60 | // // console.log(path) 61 | // reading(fileNames[0]) 62 | // }) 63 | 64 | 65 | dir.paths(fileNames[0], 66 | function(err, paths) { 67 | if (err) throw err; 68 | console.log('files:\n',paths.files); 69 | console.log('subdirs:\n', paths.dirs); 70 | const files = paths.files.filter(p => path.basename(p) != '.DS_Store') 71 | 72 | return focusWindow 73 | .webContents 74 | .send( 75 | 'winOpenFolder' , { 76 | sep: path.sep, 77 | list: files, 78 | dirs: paths.dirs, 79 | folder: fileNames[0], 80 | }); 81 | }); 82 | 83 | } 84 | }); 85 | }; 86 | 87 | 88 | 89 | module.exports = { 90 | openFile, 91 | openFolder, 92 | } 93 | 94 | // Promise.all([System.import('chokidar')]) 95 | // .then(([chokidar]) => { 96 | // var watcher = chokidar.watch(data.folder, { 97 | // // ignored: /[\/\\]\./, 98 | // persistent: true 99 | // }); 100 | 101 | // watcher 102 | // .on('addDir', path => { 103 | // // console.log(path) 104 | // reading() 105 | // }) 106 | // .on('unlinkDir', path => { 107 | // // console.log(path) 108 | // reading() 109 | // }) 110 | // .on('add', path => { 111 | // // console.log(path) 112 | // reading() 113 | // }) 114 | // .on('unlink', path => { 115 | // // console.log(path) 116 | // reading() 117 | // }) 118 | 119 | // function reading(){ 120 | // dir.paths(data.folder, (err, paths) => { 121 | // const d = { 122 | // sep: data.sep, 123 | // list: paths.files, 124 | // dirs: paths.dirs, 125 | // folder: data.folder, 126 | // } 127 | // // const newFd = {...formatFile(d), originTree}; 128 | // const newFd = formatFile(d); 129 | // const newTree = addFileID(newFd, 0) 130 | // originTree = newTree; 131 | 132 | // // console.log(newTree) 133 | 134 | // dispatch({ 135 | // type: 'changeStatus', 136 | // payload: { 137 | // // pathDir: data.pathDir, 138 | // // folder: data.folder, 139 | // treeList: newTree 140 | // } 141 | // }); 142 | // }) 143 | // } 144 | 145 | // }); 146 | 147 | /*dir.readFiles(fileNames[0], 148 | { 149 | excludeDir: ['node_modules', 'bin'], 150 | exclude:['.DS_Store'] 151 | }, 152 | function(err, content, filename, next) { 153 | if (err) throw err; 154 | // console.log(filename, path.basename(filename), path.parse(filename)) 155 | console.log('cure',filename) 156 | // console.log('content:', content); 157 | next(); 158 | }, 159 | function(err, files){ 160 | if (err) throw err; 161 | console.log('finished reading forlder:', files); 162 | return focusWindow 163 | .webContents 164 | .send( 165 | 'folderOpened' , { 166 | // basename: file.base, 167 | sep: path.sep, 168 | list: files, 169 | path: fileNames[0] 170 | // dir: file.dir, 171 | // extension: file.ext 172 | }); 173 | // return () => {} 174 | });*/ 175 | 176 | // const _p = paths.dirs.map( p => path.basename(p)) 177 | 178 | /*dir.subdirs(fileNames[0], 179 | function(err, subdirs) { 180 | if (err) throw err; 181 | console.log(subdirs); 182 | return () => {} 183 | });*/ -------------------------------------------------------------------------------- /src/main/services/menu.js: -------------------------------------------------------------------------------- 1 | // import defaultMenu from 'electron-default-menu'; 2 | // import { Menu, app, shell, BrowserWindow, dialog } from 'electron'; 3 | // import { openFile, openFolder} from './file'; 4 | const defaultMenu = require('electron-default-menu') 5 | const { Menu, app, shell, BrowserWindow, dialog } = require('electron') 6 | const { openFile, openFolder} = require('./file') 7 | 8 | module.exports = { 9 | init () { 10 | const menu = defaultMenu(app, shell) 11 | 12 | menu.splice(1, 0, { 13 | label: 'File', 14 | submenu: [ 15 | { 16 | label: 'New File', 17 | accelerator: 'CmdorCtrl+N', 18 | click: (item, focusedWindow) => { 19 | focusedWindow.webContents.send('winNewFile') 20 | // dialog.showOpenDialog({ properties: ['openFile'] }, (files) => { 21 | // console.log('file:', files) 22 | // focusedWindow.webContents.send('open-file', files[0]) 23 | // }) 24 | } 25 | }, 26 | { 27 | label: 'Open File', 28 | accelerator: 'CmdorCtrl+O', 29 | click: (item, focusedWindow) => { 30 | return openFile(focusedWindow); 31 | // dialog.showOpenDialog({ properties: ['openFile'] }, (files) => { 32 | // console.log('file:', files) 33 | // focusedWindow.webContents.send('open-file', files[0]) 34 | // }) 35 | } 36 | }, 37 | { 38 | label: 'Add React Project', 39 | // accelerator: 'CmdorCtrl+O', 40 | click: (item, focusedWindow) => { 41 | return openFolder(focusedWindow); 42 | // dialog.showOpenDialog({ properties: ['openDirectory'] }, (files) => { 43 | // console.log('forlder ' ,files) 44 | // focusedWindow.webContents.send('open-file', files[0]) 45 | // }) 46 | } 47 | }, 48 | { 49 | label: 'Save File', 50 | accelerator: 'CmdorCtrl+S', 51 | click: (item, focusedWindow) => { 52 | focusedWindow.webContents.send('winSaveFile') 53 | } 54 | }, 55 | { 56 | label: 'Close File', 57 | accelerator: 'CmdorCtrl+W', 58 | click: (item, focusedWindow) => { 59 | focusedWindow.webContents.send('winCloseFile') 60 | } 61 | } 62 | ] 63 | }) 64 | 65 | Menu.setApplicationMenu(Menu.buildFromTemplate(menu)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/services/project.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const exec = require('child_process').exec; 3 | const http = require('http') 4 | 5 | module.exports = { 6 | copy: function () { 7 | 8 | }, 9 | stopProj:function (port){ 10 | var options = { 11 | host: '127.0.0.1', 12 | port: port, 13 | path: '/stopProj' 14 | }; 15 | // host: 'http://localhost:' + port, 16 | 17 | http.request(options, function(res) { 18 | res.on('data', (chunk) => { 19 | console.log(`BODY: ${chunk}`); 20 | }); 21 | res.on('end', () => { 22 | console.log('No more data in response.'); 23 | }); 24 | }).end(); 25 | // http.request(options); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/services/window.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron') 2 | 3 | let count = 0; 4 | 5 | module.exports = { 6 | getCount() { 7 | return count; 8 | }, 9 | create(opts) { 10 | count += 1; 11 | let win = new BrowserWindow(opts); 12 | win.on('close', () => { 13 | count -= 1; 14 | win = null; 15 | }); 16 | return win; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/src/renderer/components/.gitkeep -------------------------------------------------------------------------------- /src/renderer/components/Editor/EditorPane.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { render } from 'react-dom'; 3 | import brace from 'brace'; 4 | import AceEditor from 'react-ace'; 5 | import { connect } from 'dva'; 6 | 7 | 8 | 9 | // class EditorPane extends Component { 10 | 11 | // onChange(contents) { 12 | // // console.log('change',newValue); 13 | // const {dispatch} = this.props 14 | // dispatch({ 15 | // type: 'editor/changeFile', 16 | // payload: { contents} 17 | // }) 18 | // } 19 | 20 | // render(){ 21 | // const {editor: { mode, theme, contents}, dispatch} = this.props 22 | 23 | // const paneProps = { 24 | // width: "100%", 25 | // height: "100%", 26 | // mode, 27 | // theme, 28 | // onChange: this.onChange.bind(this), 29 | // name: "UNIQUE_ID_OF_DIV", 30 | // editorProps: {$blockScrolling: true}, 31 | // value: contents, 32 | // enableBasicAutocompletion: true 33 | // } 34 | 35 | // const AceEditorGen = () => 36 | 37 | // return 38 | // } 39 | // } 40 | 41 | const EditorPane = (editor) => { 42 | 43 | const paneProps = { 44 | ...editor, 45 | width: "100%", 46 | height: "100%", 47 | name: "UNIQUE_ID_OF_DIV", 48 | editorProps: {$blockScrolling: true}, 49 | enableBasicAutocompletion: true, 50 | enableLiveAutocompletion: true 51 | } 52 | 53 | return 54 | } 55 | 56 | EditorPane.propTypes = {}; 57 | 58 | export default EditorPane; 59 | 60 | // export default connect(({ editor }) => ({ editor }))(EditorPane); 61 | -------------------------------------------------------------------------------- /src/renderer/components/Editor/EditorSelectPane.css: -------------------------------------------------------------------------------- 1 | .select-grp{ 2 | 3 | float: right; 4 | color: #fff; 5 | font-size: 12px; 6 | line-height: 22px; 7 | 8 | } 9 | 10 | .select-grp label{ 11 | margin-left: 20px; 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/components/Editor/EditorSelectPane.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'dva' 3 | import { EDITOR_THEMES, EDITOR_MODE } from '../../constants' 4 | import styles from './EditorSelectPane.css'; 5 | 6 | const EditorSelectPane = ({ editor:{ mode, theme}, dispatch }) => { 7 | 8 | function changeThemeHandler(theme) { 9 | dispatch({ 10 | type: 'editor/changeStatus', 11 | payload: { theme }, 12 | }); 13 | } 14 | 15 | function changeModeHandler(mode) { 16 | dispatch({ 17 | type: 'editor/changeStatus', 18 | payload: { mode }, 19 | }); 20 | } 21 | 22 | 23 | return ( 24 |
25 | 26 | 27 | 30 | {' '} 31 | 32 | 35 |
36 | ); 37 | }; 38 | 39 | EditorSelectPane.propTypes = {}; 40 | 41 | export default connect(({ editor }) => ({ editor }))(EditorSelectPane); 42 | 43 | -------------------------------------------------------------------------------- /src/renderer/components/FileTree/FileTree.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { connect } from 'dva'; 3 | import {Treebeard} from 'react-treebeard'; 4 | import { remote } from 'electron' 5 | import { mapToMode } from '../../utils' 6 | 7 | class FileTree extends Component { 8 | 9 | constructor(props){ 10 | super(props); 11 | this.state = { 12 | cursor: props.list 13 | }; 14 | this.onToggle = this.onToggle.bind(this); 15 | } 16 | 17 | onToggle(node, toggled){ 18 | const { dispatch } = this.props; 19 | 20 | if(this.state.cursor){ 21 | this.state.cursor.active = false; 22 | } 23 | 24 | node.active = true; 25 | 26 | if(node.children){ 27 | node.toggled = toggled; 28 | dispatch({ 29 | type: 'files/toggledTree', 30 | payload: { 31 | node, 32 | } 33 | }); 34 | }else{ 35 | 36 | dispatch({ 37 | type: 'files/openFile', 38 | payload: { 39 | path: node.path, 40 | } 41 | }); 42 | 43 | } 44 | 45 | this.setState({ cursor: node }); 46 | } 47 | 48 | render(){ 49 | const {files: { treeList }, dispatch} = this.props 50 | 51 | return 52 | } 53 | } 54 | 55 | FileTree.propTypes = {}; 56 | 57 | export default connect(({ files }) => ({ files }))(FileTree); 58 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import styles from './Layout.css' 4 | 5 | const Footer = (props) =>
{props.children}
6 | 7 | Footer.propTypes = {}; 8 | 9 | export default Footer; 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/Layout.css: -------------------------------------------------------------------------------- 1 | html { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body{ 7 | height: 100%; 8 | padding: 0; 9 | margin: 0; 10 | font-family: system,-apple-system,".SFNSDisplay-Regular","Helvetica Neue",Helvetica,"Segoe UI",sans-serif; 11 | font-size: 13px; 12 | line-height: 1.6; 13 | color: #333; 14 | background-color: transparent; 15 | } 16 | 17 | .window-content{ 18 | position: relative; 19 | overflow-y: auto; 20 | display: flex; 21 | flex: 1; 22 | } 23 | 24 | .footer{ 25 | padding: 0 10px; 26 | min-height: 22px; 27 | box-shadow: inset 0 1px 0 #d4d4d4; 28 | background-color: #e8e6e8; 29 | background-image: linear-gradient(to bottom,#868484 0,#616161 100%); 30 | } 31 | 32 | .panegroup{ 33 | position: absolute; 34 | display: flex; 35 | bottom: 0; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | } 40 | 41 | .pane{ 42 | overflow-y: auto; 43 | flex: 1; 44 | border-left: 1px solid #4b4c48; 45 | position: relative; 46 | 47 | &:first-child{ 48 | border-left: none; 49 | } 50 | } 51 | 52 | .pane.hide{ 53 | display: none; 54 | } 55 | 56 | .sidebar{ 57 | background-color: #21252b; 58 | min-width: 150px; 59 | max-width: 200px; 60 | 61 | } 62 | 63 | .tabgrp{ 64 | margin-top: -1px; 65 | display: flex; 66 | /*border-top: 1px solid #989698;*/ 67 | /*border-bottom: 1px solid #989698;*/ 68 | width: 100%; 69 | height: 26px; 70 | background-color: #e8e6e6; 71 | } 72 | 73 | .tab-item{ 74 | position: relative; 75 | flex: 1; 76 | padding: 3px; 77 | font-size: 12px; 78 | text-align: center; 79 | border-left: 1px solid #989698; 80 | background-color: #d4d2d4; 81 | height: 20px; 82 | cursor: pointer; 83 | max-width: 200px; 84 | } 85 | 86 | /*.tab-item:first-child { 87 | border-left: 0; 88 | } 89 | */ 90 | .tab-item.active { 91 | 92 | /*background-color: red;*/ 93 | background-color: #eee; 94 | /*background-image: linear-gradient(to bottom,#d4d2d4 0,#cccacc 100%);*/ 95 | } 96 | 97 | .tab-item-fixed { 98 | flex: none; 99 | padding: 3px 10px; 100 | } 101 | 102 | .tab-item .ic-close-tab { 103 | position: absolute; 104 | top: 0px; 105 | left: 5px; 106 | width: 15px; 107 | height: 15px; 108 | font-size: 15px; 109 | color: #21252b; 110 | /*transition: opacity .1s linear,background-color .1s linear;*/ 111 | /*border-radius: 3px;*/ 112 | /*transform: translateY(-50%);*/ 113 | z-index: 10; 114 | cursor: pointer; 115 | } 116 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/Pane.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from 'dva' 3 | 4 | import styles from './Layout.css' 5 | 6 | const Pane = (props) => { 7 | const newClz = styles.pane + ' ' + styles[props.className] 8 | 9 | return( 10 |
{props.children}
11 | ) 12 | } 13 | 14 | Pane.propTypes = {}; 15 | 16 | export default connect()(Pane); 17 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/PaneGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './Layout.css' 4 | 5 | const Sidebar = (props) =>
{props.children}
6 | 7 | Sidebar.propTypes = {}; 8 | 9 | export default Sidebar 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/TabGroup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Pane from './Pane'; 3 | import PaneGroup from './PaneGroup'; 4 | import TabItem from './TabItem'; 5 | import styles from './Layout.css'; 6 | 7 | class TabGroup extends Component { 8 | constructor(props){ 9 | super(props); 10 | } 11 | 12 | render(){ 13 | const { tabComponents, activeIndex, onSelectItem, onCloseItem } = this.props; 14 | const tabItems = tabComponents.map((it, i) => 15 | 23 | ) 24 | 25 | const tabPanes = tabComponents.map((it, i) => 26 | {it.comp} 29 | ) 30 | 31 | return( 32 |
33 |
34 | {tabItems} 35 |
36 | 37 | {tabPanes} 38 | 39 |
40 | ) 41 | } 42 | } 43 | 44 | TabGroup.propTypes = {}; 45 | 46 | export default TabGroup 47 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/TabItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './Layout.css' 4 | const TabItem = ({ index, className, title, onSelectItem, onCloseItem }) => { 5 | 6 | const newClz = styles['tab-item'] + (className ? ' ' + styles[className] : '') 7 | return( 8 |
{ 10 | e.stopPropagation(); 11 | onSelectItem(index) 12 | }} 13 | > 14 | {e.stopPropagation(); onCloseItem(index)}}>x 17 | { title } 18 |
19 | ) 20 | } 21 | 22 | TabItem.propTypes = {}; 23 | 24 | export default TabItem 25 | -------------------------------------------------------------------------------- /src/renderer/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import Footer from './Footer'; 2 | import Pane from './Pane'; 3 | import PaneGroup from './PaneGroup'; 4 | import TabGroup from './TabGroup'; 5 | 6 | 7 | export default { 8 | Footer, 9 | Pane, 10 | PaneGroup, 11 | TabGroup, 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/components/ProjFunc/Console.css: -------------------------------------------------------------------------------- 1 | .console{ 2 | display: flex; 3 | background-color: #fff; 4 | height: 100px; 5 | box-shadow: inset 0 1px 0 #d4d4d4; 6 | padding: 5px 10px; 7 | overflow-y: auto; 8 | } 9 | 10 | .btn-grp{ 11 | flex: 1; 12 | max-width: 60px; 13 | padding: 0 5px 0 0; 14 | } 15 | 16 | .btn{ 17 | display: block; 18 | width: 60px; 19 | height: 20px; 20 | text-align: center; 21 | font-size: 12px; 22 | line-height: 12px; 23 | margin: 10px 0; 24 | /*line-height: 40px;*/ 25 | } 26 | 27 | .result{ 28 | flex: 1; 29 | word-break: break-all; 30 | line-height: 16px; 31 | border-left: 1px solid #ddd; 32 | padding: 10px; 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/components/ProjFunc/Console.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDom, {findDOMNode, unmountComponentAtNode} from 'react-dom' 3 | import { connect } from 'dva' 4 | 5 | import styles from './Console.css' 6 | 7 | class Console extends Component { 8 | 9 | constructor(props){ 10 | super(props); 11 | this.transCode = this.transCode.bind(this) 12 | this.clearConsole = this.clearConsole.bind(this) 13 | this.execCode = this.execCode.bind(this) 14 | } 15 | 16 | transCode() { 17 | const { files: { current, tabs }, dispatch } = this.props; 18 | const tab = tabs[current]; 19 | 20 | dispatch({ 21 | type: 'cns/transCode', 22 | payload: { 23 | contents: tab.contents 24 | }, 25 | }); 26 | } 27 | 28 | execCode(){ 29 | const { files: { current, tabs }, dispatch } = this.props; 30 | const tab = tabs[current]; 31 | 32 | dispatch({ 33 | type: 'cns/execCode', 34 | payload: { 35 | mount: findDOMNode(this.refs.result), 36 | contents: tab.contents 37 | }, 38 | }); 39 | } 40 | 41 | clearConsole() { 42 | unmountComponentAtNode(this.refs.result) 43 | this.props.dispatch({ 44 | type: 'cns/changeStatus', 45 | payload: { content: '', err: '' }, 46 | }); 47 | } 48 | 49 | 50 | render(){ 51 | const { cns: {content, err, isExec}, proj } = this.props; 52 | let cnt = err || proj.err ? { err || proj.err } 53 | : { content }; 54 | 55 | const showResult = isExec ? 'block' : 'none'; 56 | 57 | return( 58 |
59 |
60 | 61 | 62 | 63 |
64 |
{ cnt }
65 |
66 |
67 | ) 68 | } 69 | } 70 | 71 | Console.propTypes = {}; 72 | 73 | export default connect(({ cns, files, proj }) => ({ cns, files, proj }))(Console); 74 | 75 | -------------------------------------------------------------------------------- /src/renderer/components/ProjFunc/ProjMenu.css: -------------------------------------------------------------------------------- 1 | .proj-footer-grp{ 2 | float: right; 3 | } 4 | .proj-footer-grp button{ 5 | margin-left: 10px; 6 | height: 18px; 7 | margin-top: 3px; 8 | line-height: 12px; 9 | border-radius: 3px; 10 | border: 1px solid #717070; 11 | background-color: #fff; 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/components/ProjFunc/ProjMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'dva' 3 | import styles from './ProjMenu.css'; 4 | 5 | const ProjMenu = ({ cns, files, proj, dispatch }) => { 6 | 7 | function showConsole(show){ 8 | 9 | dispatch({ 10 | type: 'cns/changeStatus', 11 | payload: { show }, 12 | }); 13 | 14 | } 15 | 16 | function startProj(){ 17 | dispatch({ 18 | type: 'proj/startProj', 19 | }); 20 | } 21 | 22 | function stopProj(){ 23 | dispatch({ 24 | type: 'proj/stopProj', 25 | }); 26 | } 27 | 28 | function buildProj(){ 29 | dispatch({ 30 | type: 'proj/buildProj', 31 | }); 32 | } 33 | 34 | return ( 35 |
36 | { files.folder && } 37 | { files.folder && } 38 | { files.folder && } 39 | 40 |
41 | ); 42 | }; 43 | 44 | ProjMenu.propTypes = {}; 45 | 46 | export default connect(({ cns, files, proj }) => ({ cns, files, proj }))(ProjMenu); 47 | 48 | -------------------------------------------------------------------------------- /src/renderer/constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const IPC_CHANNEL = 'ipc'; 3 | 4 | export const EDITOR_MODE= ['jsx', 'javascript', 'java', 'python', 'xml', 'html', 'css', 'markdown']; 5 | 6 | export const EDITOR_THEMES = ['monokai', 'github', 'twilight', 'xcode', 'terminal']; 7 | 8 | -------------------------------------------------------------------------------- /src/renderer/index.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import RouterConfig from './router'; 3 | import files from './models/files' 4 | import editor from './models/editor' 5 | import cns from './models/cns' 6 | import proj from './models/proj' 7 | 8 | // 1. Initialize 9 | const app = dva(); 10 | 11 | // 2. Plugins 12 | 13 | // 3. Model 14 | 15 | app.model(files); 16 | app.model(editor); 17 | app.model(cns); 18 | app.model(proj); 19 | 20 | // 4. Router 21 | // app.router(require('./router')); 22 | app.router(RouterConfig); 23 | 24 | // 5. Start 25 | app.start('#root'); 26 | -------------------------------------------------------------------------------- /src/renderer/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/src/renderer/models/.gitkeep -------------------------------------------------------------------------------- /src/renderer/models/cns.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM, {findDOMNode} from 'react-dom' 3 | import _ from 'lodash' 4 | import CaptureConsole from '../services/cns' 5 | import { transformCode } from '../utils' 6 | 7 | 8 | export default { 9 | 10 | namespace: 'cns', 11 | 12 | state: { 13 | show: false, 14 | content: '', 15 | err: '', 16 | isExec: false 17 | }, 18 | 19 | effects: { 20 | *transCode({ payload: { contents }}, {put, call, select }){ 21 | 22 | const {code, err} = transformCode(contents); 23 | 24 | yield put({ 25 | type: 'changeStatus', 26 | payload: { 27 | content: code, 28 | err 29 | } 30 | }); 31 | }, 32 | *execCode({ payload: { mount, contents }}, {put, call, select }){ 33 | 34 | yield put({ 35 | type: 'transCode', 36 | payload: { 37 | contents, 38 | } 39 | }); 40 | 41 | const { content } = yield select(state => state.cns); 42 | 43 | let capturingConsole = CaptureConsole(); 44 | var str = 'ReactDOM, React, mountNode, console' 45 | var strp = [ReactDOM, React, mount, capturingConsole] 46 | 47 | try{ 48 | let f = (new Function(str, content)) 49 | f.apply(null, strp) 50 | let buffer = capturingConsole.getBuffer(); 51 | 52 | yield put({ 53 | type: 'changeStatus', 54 | payload: { 55 | isExec: true, 56 | content: buffer 57 | } 58 | }); 59 | 60 | }catch (err){ 61 | yield put({ 62 | type: 'changeStatus', 63 | payload: { 64 | err: err.toString(), 65 | isExec: false 66 | } 67 | }); 68 | } 69 | } 70 | }, 71 | 72 | reducers: { 73 | changeStatus(state, action){ 74 | return { ...state, ...action.payload}; 75 | }, 76 | 77 | 78 | }, 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /src/renderer/models/editor.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | 4 | namespace: 'editor', 5 | 6 | state: { 7 | theme: 'github', 8 | mode: 'jsx', 9 | }, 10 | 11 | effects: { 12 | 13 | }, 14 | 15 | reducers: { 16 | changeStatus(state, action){ 17 | return { ...state, ...action.payload}; 18 | }, 19 | 20 | 21 | }, 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /src/renderer/models/files.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import fs from 'fs' 3 | import { remote, ipcRenderer } from 'electron' 4 | import { mapToMode, getFileName, formatFile, resolvePath, addFileID, coverTree } from '../utils' 5 | import { saveNewFile, readFile} from '../services/file' 6 | import { filterTree, expandFilteredNodes} from '../services/filter' 7 | // const fs = remote.require('fs') 8 | 9 | const baseTab = () => ({ 10 | title: 'undefined', 11 | path: __dirname, 12 | mode: 'jsx', 13 | contents: '', 14 | }) 15 | 16 | let originTree = [] 17 | 18 | export default { 19 | 20 | namespace: 'files', 21 | 22 | state: { 23 | current: 0, 24 | treeList: [], 25 | folder: '', 26 | pathDir: '', 27 | tabs: [baseTab()] 28 | }, 29 | 30 | subscriptions: { 31 | setup({ dispatch }) { 32 | ipcRenderer 33 | .on('winNewFile', (event) => { 34 | 35 | const tab = baseTab(); 36 | 37 | dispatch({ 38 | type: 'addFile', 39 | payload: {...tab} 40 | }); 41 | }) 42 | .on('winOpenFile', (event, data) => { 43 | const {ext, base} = data.file; 44 | const extension = ext.slice(1); 45 | const mode = mapToMode(extension) 46 | 47 | 48 | dispatch({ 49 | type: 'addFile', 50 | payload: { 51 | path: data.path, 52 | contents: data.contents, 53 | mode, 54 | title: base, 55 | } 56 | }); 57 | }) 58 | .on('winOpenFolder', (event, data) => { 59 | const formatData = formatFile(data); 60 | const treeList = addFileID(formatData, 0); 61 | originTree = treeList; 62 | 63 | dispatch({ 64 | type: 'changeStatus', 65 | payload: { 66 | pathDir: data.pathDir, 67 | folder: data.folder, 68 | treeList 69 | } 70 | }); 71 | 72 | }) 73 | .on('winSaveFile', function () { 74 | dispatch({ 75 | type: 'saveFile', 76 | }); 77 | }) 78 | .on('winCloseFile', function () { 79 | 80 | dispatch({ 81 | type: 'closeFile', 82 | payload: {} 83 | }); 84 | }) 85 | 86 | }, 87 | }, 88 | 89 | effects: { 90 | *addFile({ payload: { path, contents, mode, title} }, {put, call, select }){ 91 | 92 | const { tabs, treeList } = yield select(state => state.files); 93 | let current = 0; 94 | 95 | const tab = { 96 | title, 97 | path, 98 | mode, 99 | contents, 100 | }; 101 | 102 | const item = tabs.filter(i => i.path == path); 103 | tabs.filter((it, i) => { 104 | if(it.path == path){ 105 | current = i; 106 | return true; 107 | } 108 | return false; 109 | }); 110 | 111 | if(!item.length || path == __dirname){ 112 | tabs.push(tab); 113 | current = tabs.length - 1; 114 | } 115 | 116 | yield put({ 117 | type: 'changeStatus', 118 | payload: { tabs, current} 119 | }); 120 | 121 | }, 122 | 123 | *saveFile({}, {put, call, select }){ 124 | 125 | const {current, tabs} = yield select(state => state.files); 126 | let tab = tabs[current]; 127 | 128 | if(tab.path != '/'){ 129 | 130 | // remote.require('fs').writeFile(tab.path, tab.contents, 'utf-8'); 131 | fs.writeFile(tab.path, tab.contents, 'utf-8'); 132 | 133 | }else{ 134 | 135 | const filepath = yield saveNewFile(tab.contents); 136 | const title = getFileName(filepath) 137 | 138 | yield put({ 139 | type: 'changeFile', 140 | payload: { current, file: { path: filepath, title} } 141 | }); 142 | 143 | } 144 | }, 145 | 146 | *openFile({ payload: { path } }, {put, call, select }){ 147 | 148 | const contents = yield call(readFile, path); 149 | const spl = path.lastIndexOf('.') 150 | const mode = mapToMode(path.slice(spl + 1)) 151 | const title = getFileName(path) 152 | 153 | yield put({ 154 | type: 'addFile', 155 | payload: { 156 | path, 157 | contents, 158 | mode, 159 | title 160 | } 161 | }); 162 | }, 163 | 164 | *showResult({}, {put, call, select}){ 165 | const { contents, mount } = yield select(state => state.editor); 166 | 167 | const str = 'ReactDOM.render(

Hello, world!!

, mountNode)'; 168 | const code = transformCode(str) 169 | (new Function('ReactDOM', 'React', 'mountNode', code))(ReactDOM, React, mount); 170 | 171 | yield put({ 172 | type: 'changeStatus', 173 | payload: { 174 | showResult: true, 175 | results: code 176 | } 177 | }); 178 | }, 179 | 180 | *closeFile({ payload: { current } }, {put, call, select }){ 181 | const files = yield select(state => state.files); 182 | const cur = current != null ? current : files.current; 183 | 184 | const tabs = files.tabs.filter((it, i) => i != cur); 185 | const newCur = cur == 0 ? 0 : cur - 1; 186 | yield put({ 187 | type: 'changeStatus', 188 | payload: { 189 | tabs, 190 | current: newCur 191 | } 192 | }) 193 | }, 194 | 195 | *toggledTree({ payload: { node } }, {put, call, select }){ 196 | coverTree(node, originTree, (node, newNode) => node.id == newNode.id ) 197 | } 198 | 199 | }, 200 | 201 | reducers: { 202 | changeStatus(state, action){ 203 | return { ...state, ...action.payload}; 204 | }, 205 | changeFile(state, action){ 206 | const { current, file} = action.payload 207 | let tabs = Object.assign([], state.tabs); 208 | const tab = {...tabs[current], ...file}; 209 | tabs[current] = tab; 210 | 211 | return {...state, tabs}; 212 | 213 | }, 214 | }, 215 | 216 | }; 217 | -------------------------------------------------------------------------------- /src/renderer/models/proj.js: -------------------------------------------------------------------------------- 1 | import { remote, ipcRenderer } from 'electron' 2 | import del from 'del' 3 | import fs from 'fs' 4 | import { exec } from 'child_process' 5 | 6 | import { mapToMode, getFileName, formatFile, resolvePath } from '../utils' 7 | import { execCode, editWbpkFile } from '../services/proj' 8 | import { saveNewFile, readFile} from '../services/file' 9 | 10 | // const fs = remote.require('fs') 11 | const APP_PATH = process.cwd() 12 | // const exec = remote.require('child_process').exec; 13 | 14 | export default { 15 | 16 | namespace: 'proj', 17 | 18 | state: { 19 | start: false, 20 | build: false, 21 | err: '', 22 | config:{ 23 | "port": 3001, 24 | "entry": "src/index.js", 25 | "vendor": "src/vendor.js", 26 | "output":"dist" 27 | }, 28 | user_path_entry: '', 29 | user_path_vendor: '', 30 | user_path_output: '', 31 | }, 32 | 33 | subscriptions: { 34 | setup({ dispatch }) { 35 | ipcRenderer 36 | .on('winOpenFolder', (event, data) => { 37 | 38 | }) 39 | 40 | 41 | }, 42 | }, 43 | 44 | effects: { 45 | 46 | *startProj({ }, {put, call, select }){ 47 | const { folder} = yield select(state => state.files); 48 | 49 | let wbpk = yield call(readFile, resolvePath(APP_PATH, 'webpack/base.config.js')); 50 | let config = yield call(readFile, resolvePath(folder, 'config.json')); 51 | 52 | let baseConfig = JSON.parse(config) 53 | let newFoler = folder.replace(/\\/g, '/') 54 | 55 | let user_path_entry = resolvePath(newFoler, baseConfig.entry).replace(/\\/g, '/') 56 | let user_path_vendor = resolvePath(newFoler, baseConfig.vendor).replace(/\\/g, '/') 57 | let user_path_output = resolvePath(newFoler, baseConfig.output).replace(/\\/g, '/') 58 | 59 | wbpk = wbpk.replace(/user_path_entry/g, `'${user_path_entry}'`); 60 | wbpk = wbpk.replace(/user_path_vendor/g, `'${user_path_vendor}'`); 61 | wbpk = wbpk.replace(/user_path_output/g, `'${user_path_output}'`); 62 | 63 | let newWpPath = resolvePath(APP_PATH, 'user.webpack.config.js'); 64 | // remote.require('fs').writeFile(newWpPath, wbpk, 'utf-8'); 65 | fs.writeFile(newWpPath, wbpk, 'utf-8'); 66 | 67 | yield put({ 68 | type: 'changeStatus', 69 | payload: { 70 | start: true, 71 | config: baseConfig, 72 | user_path_entry, 73 | user_path_vendor, 74 | user_path_output 75 | } 76 | }); 77 | 78 | try{ 79 | 80 | // start server to listen 81 | // yield call(execCode, `node server.js --port ${baseConfig.port} --config ./user.webpack.config.js`); 82 | yield call(execCode, {env: 'development' }, `node server-user.js --port ${baseConfig.port}`); 83 | 84 | }catch (err){ 85 | console.log(err) 86 | 87 | yield put({ 88 | type: 'changeStatus', 89 | payload: { 90 | start: false, 91 | err: err.toString() 92 | } 93 | }); 94 | } 95 | }, 96 | 97 | *stopProj({ }, {put, call, select }){ 98 | const { config } = yield select(state => state.proj) 99 | 100 | fetch(`http://localhost:${config.port}/stopProj`) 101 | 102 | yield put({ 103 | type: 'changeStatus', 104 | payload: { 105 | start: false, 106 | } 107 | }); 108 | 109 | }, 110 | 111 | *buildProj({ }, {put, call, select }){ 112 | const { start } = yield select(state => state.proj) 113 | 114 | let wbpk = yield call(readFile, resolvePath(APP_PATH, 'webpack/base.config.prod.js')); 115 | let a, b, c ; 116 | 117 | if(start){ 118 | yield put({ 119 | type: 'stopProj', 120 | payload: {} 121 | }); 122 | 123 | const { config, user_path_entry, user_path_vendor, user_path_output } = yield select(state => state.proj) 124 | a = user_path_entry 125 | b = user_path_vendor 126 | c = user_path_output 127 | 128 | }else{ 129 | const { folder} = yield select(state => state.files); 130 | let config = yield call(readFile, resolvePath(folder, 'config.json')); 131 | 132 | let baseConfig = JSON.parse(config) 133 | let newFoler = folder.replace(/\\/g, '/') 134 | 135 | a = resolvePath(newFoler, baseConfig.entry).replace(/\\/g, '/') 136 | b = resolvePath(newFoler, baseConfig.vendor).replace(/\\/g, '/') 137 | c = resolvePath(newFoler, baseConfig.output).replace(/\\/g, '/') 138 | 139 | } 140 | 141 | wbpk = wbpk.replace(/user_path_entry/g, `'${a}'`); 142 | wbpk = wbpk.replace(/user_path_vendor/g, `'${b}'`); 143 | wbpk = wbpk.replace(/user_path_output/g, `'${c}'`); 144 | 145 | let newWpPath = resolvePath(APP_PATH, 'user.webpack.config.prod.js'); 146 | fs.writeFile(newWpPath, wbpk, 'utf-8'); 147 | 148 | yield del.sync([c + '/**'], { 149 | force: true 150 | }) 151 | 152 | yield call(execCode, {}, `npm run build-user`); 153 | 154 | 155 | } 156 | }, 157 | 158 | reducers: { 159 | changeStatus(state, action){ 160 | return { ...state, ...action.payload}; 161 | }, 162 | }, 163 | 164 | }; 165 | 166 | -------------------------------------------------------------------------------- /src/renderer/models/win.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | 3 | export default { 4 | 5 | namespace: 'win', 6 | 7 | state: { 8 | instance: null, 9 | url: '', 10 | }, 11 | 12 | effects: { 13 | *newWindow({ }, {put, call, select }){ 14 | let win = remote.getGlobal('services').window.create({ width: 800, height: 600 }) 15 | yield put({ 16 | type: 'changeStatus', 17 | payload: { 18 | instance: win, 19 | } 20 | }); 21 | }, 22 | *closeWindow({ }, {put, call, select }){ 23 | const { instance } = yield select(state => state.win) 24 | instance.close() 25 | yield put({ 26 | type: 'changeStatus', 27 | payload: { 28 | instance: null, 29 | } 30 | }); 31 | // remote.getGlobal('services').window.create({ width: 800, height: 600 }) 32 | 33 | } 34 | }, 35 | 36 | reducers: { 37 | changeStatus(state, action){ 38 | return { ...state, ...action.payload}; 39 | }, 40 | 41 | }, 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /src/renderer/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route } from 'dva/router'; 3 | import IndexPage from './routes/IndexPage'; 4 | 5 | function RouterConfig({ history }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default RouterConfig; 14 | -------------------------------------------------------------------------------- /src/renderer/routes/IndexPage.css: -------------------------------------------------------------------------------- 1 | .window{ 2 | position: absolute; 3 | bottom: 0; 4 | display: flex; 5 | flex-direction: column; 6 | background-color: #fff; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | } 11 | 12 | .window-content{ 13 | position: relative; 14 | overflow-y: auto; 15 | display: flex; 16 | flex: 1; 17 | } 18 | 19 | /* 20 | .normal { 21 | font-family: Georgia, sans-serif; 22 | margin-top: 3em; 23 | text-align: center; 24 | } 25 | 26 | .title { 27 | font-size: 2.5rem; 28 | font-weight: normal; 29 | letter-spacing: -1px; 30 | } 31 | 32 | .welcome { 33 | height: 328px; 34 | background: url(../assets/yay.jpg) no-repeat center 0; 35 | background-size: 388px 328px; 36 | } 37 | 38 | .list { 39 | font-size: 1.2em; 40 | margin-top: 1.8em; 41 | list-style: none; 42 | line-height: 1.5em; 43 | } 44 | 45 | .list code { 46 | background: #f7f7f7; 47 | } 48 | */ 49 | -------------------------------------------------------------------------------- /src/renderer/routes/IndexPage.js: -------------------------------------------------------------------------------- 1 | 2 | import React, {Component} from 'react'; 3 | import ReactDOM from 'react-dom' 4 | import { connect } from 'dva'; 5 | import { remote } from 'electron'; 6 | 7 | 8 | import styles from './IndexPage.css'; 9 | 10 | import EditorPane from '../components/Editor/EditorPane'; 11 | import EditorSelectPane from '../components/Editor/EditorSelectPane'; 12 | import FileTree from '../components/FileTree/FileTree'; 13 | import Console from '../components/ProjFunc/Console'; 14 | import ProjMenu from '../components/ProjFunc/ProjMenu'; 15 | import Layout from '../components/Layout' 16 | const { Footer, PaneGroup, Pane, TabGroup } = Layout 17 | 18 | class IndexPage extends Component { 19 | 20 | constructor(props){ 21 | super(props); 22 | } 23 | 24 | onChange( contents ){ 25 | const {files: { current, tabs}, dispatch } = this.props; 26 | dispatch({ 27 | type: 'files/changeFile', 28 | payload: { current, file: { contents }} 29 | }); 30 | } 31 | 32 | componentWillReceiveProps(next){ 33 | const {files: { current, tabs}, dispatch} = next; 34 | 35 | if(tabs.length){ 36 | const file = tabs[current]; 37 | if(file.mode != this.props.editor.mode){ 38 | dispatch({ 39 | type: 'editor/changeStatus', 40 | payload: { mode: file.mode }, 41 | }); 42 | } 43 | } 44 | 45 | } 46 | 47 | render(){ 48 | const { editor:{ theme }, files, cns, dispatch } = this.props 49 | 50 | const tabComponents = files.tabs.map( (it, i) => { 51 | const {title, mode, contents} = it; 52 | const editorPaneProps = { 53 | theme, 54 | mode, 55 | value: contents, 56 | onChange: this.onChange.bind(this) 57 | } 58 | const comp = 59 | return { 60 | title, 61 | comp, 62 | } 63 | }) 64 | 65 | const tabProps = { 66 | tabComponents, 67 | activeIndex: files.current, 68 | onSelectItem: (current) => { 69 | dispatch({ 70 | type: 'files/changeStatus', 71 | payload: { current } 72 | }) 73 | }, 74 | onCloseItem: (current) => { 75 | dispatch({ 76 | type: 'files/closeFile', 77 | payload: { current } 78 | }) 79 | } 80 | } 81 | 82 | const cnsComp = cns.show ? : null; 83 | 84 | return ( 85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 | { cnsComp } 93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | export default connect(({ editor, files, cns }) => ({ editor, files, cns }))(IndexPage); 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/renderer/services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/src/renderer/services/.gitkeep -------------------------------------------------------------------------------- /src/renderer/services/cns.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export default (capture) => { 5 | let capturingConsole = Object.create(console); 6 | let buffer = []; 7 | 8 | function capture() { 9 | if (this !== capturingConsole) { return; } 10 | 11 | let logs = _.map(arguments, log => JSON.stringify(log)); 12 | 13 | buffer.push(logs.join(' ')); 14 | } 15 | 16 | 17 | capturingConsole.clear = function() { 18 | buffer = []; 19 | console.clear(); 20 | }; 21 | 22 | ['error', 'log', 'info', 'debug'].forEach(function(key) { 23 | capturingConsole[key] = function() { 24 | Function.prototype.apply.call(console[key], console, arguments); 25 | capture.apply(this, arguments); 26 | }; 27 | }); 28 | 29 | capturingConsole.getBuffer = () => buffer.join('\n') 30 | 31 | return capturingConsole; 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/services/file.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | import fs from 'fs' 3 | // const fs = remote.require('fs') 4 | 5 | 6 | export const saveNewFile = (contents) => { 7 | return new Promise((resolve, reject) => { 8 | remote.dialog.showSaveDialog(function (filepath) { 9 | if(filepath){ 10 | fs.writeFile(filepath, contents, 'utf-8'); 11 | resolve(filepath) 12 | }else{ 13 | reject() 14 | } 15 | }); 16 | }) 17 | } 18 | 19 | export const readFile = (path) => { 20 | return new Promise((resolve, reject) => { 21 | fs.readFile(path, 'utf-8', function (err, data) { 22 | if (err) reject() 23 | resolve(data) 24 | 25 | }); 26 | }) 27 | } -------------------------------------------------------------------------------- /src/renderer/services/filter.js: -------------------------------------------------------------------------------- 1 | export const defaultMatcher = (filterText, node) => { 2 | return node.name.toLowerCase().indexOf(filterText.toLowerCase()) !== -1; 3 | }; 4 | 5 | export const findNode = (node, filter, matcher) => { 6 | return matcher(filter, node) || // i match 7 | (node.children && // or i have decendents and one of them match 8 | node.children.length && 9 | !!node.children.find(child => findNode(child, filter, matcher))); 10 | }; 11 | 12 | export const filterTree = (node, filter, matcher = defaultMatcher) => { 13 | // If im an exact match then all my children get to stay 14 | if(matcher(filter, node) || !node.children){ return node; } 15 | // If not then only keep the ones that match or have matching descendants 16 | const filtered = node.children 17 | .filter(child => findNode(child, filter, matcher)) 18 | .map(child => filterTree(child, filter, matcher)); 19 | return Object.assign({}, node, { children: filtered }); 20 | }; 21 | 22 | export const expandFilteredNodes = (node, filter, matcher = defaultMatcher) => { 23 | let children = node.children; 24 | if(!children || children.length === 0){ 25 | return Object.assign({}, node, { toggled: false }); 26 | } 27 | const childrenWithMatches = node.children.filter(child => findNode(child, filter, matcher)); 28 | const shouldExpand = childrenWithMatches.length > 0; 29 | // If im going to expand, go through all the matches and see if thier children need to expand 30 | if(shouldExpand){ 31 | children = childrenWithMatches.map(child => { 32 | return expandFilteredNodes(child, filter, matcher); 33 | }); 34 | } 35 | return Object.assign({}, node, { 36 | children: children, 37 | toggled: shouldExpand 38 | }); 39 | }; -------------------------------------------------------------------------------- /src/renderer/services/proj.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | // const exec = remote.require('child_process').exec; 3 | // const spawn = remote.require('child_process').spawn; 4 | import { saveNewFile, readFile} from './file' 5 | import { resolvePath } from '../utils' 6 | import {exec, spawn} from 'child_process' 7 | 8 | export const execCode = (opt, cmd) => { 9 | const options = { 10 | killSignal: 'SIGTERM', 11 | // env: 'development', // faild in statestr 12 | ...opt 13 | } 14 | // console.log(options) 15 | return new Promise((resolve, reject) => { 16 | const ex = exec(cmd, options, (error, stdout, stderr) => { 17 | if (error) { 18 | reject(error) 19 | } 20 | if(stderr){ 21 | reject(stderr) 22 | } 23 | // console.log(stderr); 24 | resolve({ 25 | stdout 26 | }) 27 | }); 28 | }) 29 | } 30 | 31 | 32 | export const spwanCodeByPort = ( port ) => { 33 | 34 | return new Promise((resolve, reject) => { 35 | 36 | const ls = spawn('lsof',['-nP', `-i:${port}`]) 37 | const grep = spawn('grep', ['LISTEN']) 38 | 39 | ls.stdout.on('data', (data) => { 40 | grep.stdin.write(data); 41 | }); 42 | 43 | ls.stderr.on('data', (data) => { 44 | reject(data.toString()) 45 | console.log(`ps stderr: ${data}`); 46 | }); 47 | 48 | ls.on('close', (code) => { 49 | console.log('close ls') 50 | if (code !== 0) { 51 | console.log(`ps process exited with code ${code}`); 52 | } 53 | grep.stdin.end(); 54 | }); 55 | 56 | grep.stdout.on('data', (data) => { 57 | resolve(data.toString()) 58 | }); 59 | 60 | grep.stderr.on('data', (data) => { 61 | reject(data.toString()) 62 | console.log(`grep stderr: ${data}`); 63 | }); 64 | 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/utils.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | import { transform } from 'babel-standalone' 3 | const path = remote.require('path') 4 | 5 | 6 | export const mapToMode = (ext) => { 7 | 8 | const str = ext.toLowerCase(); 9 | 10 | if(['jsx'].indexOf(str) != -1) return 'jsx'; 11 | if(['js'].indexOf(str) != -1) return 'javascript'; 12 | if(['java'].indexOf(str) != -1) return 'java'; 13 | if(['xml'].indexOf(str) != -1) return 'xml'; 14 | if(['html', 'ejs', 'htm'].indexOf(str) != -1) return 'html'; 15 | if(['py'].indexOf(str) != -1) return 'python'; 16 | if(['css', 'less', 'sass', 'scss', 'styl'].indexOf(str) != -1) return 'css'; 17 | 18 | return 'markdown'; 19 | 20 | } 21 | 22 | export const getFileName = (dir) => { 23 | return path.parse(dir).base 24 | } 25 | 26 | 27 | export const formatFile = ({list, dirs, sep, folder}) => { 28 | const sepLen = sep.length 29 | const a = list.map(i => i.slice(folder.length + sepLen)) 30 | const b = dirs.map(i => i.slice(folder.length + sepLen)) 31 | 32 | let d = [], e = Array.from({length: b.length}, i => []); 33 | 34 | let files = { 35 | active: true, 36 | children: [], 37 | toggled: true, 38 | name: getFileName(folder, sep) 39 | } 40 | 41 | function inter(p, obj, sp, file){ 42 | const arr = p.split(sep) 43 | const name = arr[0] 44 | 45 | if(arr.length > 1){ 46 | const k = obj.filter(o => o.name == name) 47 | inter(p.slice(name.length + sepLen), k[0].children, sp, file) 48 | }else{ 49 | const k = obj.filter(o => o.name == name) 50 | 51 | if(k.length == 0 ){ 52 | obj.push({ 53 | name: name, 54 | active: false, 55 | children: file 56 | }) 57 | obj.sort((a, b) => a.name.localeCompare(b.name)) 58 | } 59 | 60 | return obj 61 | } 62 | 63 | // if(arr.length > 1){ 64 | // inter(p.slice(name.length + sepLen), o[name], sp, file) 65 | // }else{ 66 | // o[name] = {path: sp, children: file} 67 | // return o 68 | // } 69 | } 70 | 71 | 72 | list.forEach((l, i) => { 73 | 74 | const index = a[i].lastIndexOf(sep) 75 | 76 | if(index == -1){ 77 | files.children.push({ 78 | name: a[i], 79 | active: false, 80 | path: l 81 | }) 82 | 83 | }else{ 84 | dirs.forEach((d, j) => { 85 | 86 | const str = l.slice(0, l.lastIndexOf(sep)) 87 | 88 | if(str === d){ 89 | e[j].push({ 90 | name: getFileName(l, sep), 91 | path: l, 92 | active: false 93 | }) 94 | } 95 | }) 96 | } 97 | 98 | }) 99 | 100 | b.forEach((i, j) => inter(i, d, i, e[j])) 101 | 102 | 103 | files.children = [...files.children, ...d].sort((a, b) => a.name.localeCompare(b.name)) 104 | 105 | return files 106 | 107 | } 108 | 109 | export const addFileID = (node, i) => { 110 | let children = node.children 111 | if(!children || children.length === 0){ 112 | return Object.assign({}, node, { id: i}); 113 | } 114 | let child = children.map((n, j) => addFileID(n, i + '-' + j)) 115 | return Object.assign({}, node, { children: child, id: i }) 116 | } 117 | 118 | export const coverTree = (filter, node, matcher) => { 119 | if(matcher(filter, node)){ 120 | return Object.assign({}, node, filter); 121 | } 122 | 123 | if(!node.children) return node; 124 | 125 | const filtered = node.children.map(child => coverTree(filter, child, matcher)) 126 | 127 | return Object.assign({}, node, { children: filtered }); 128 | } 129 | 130 | export const delTree = (filter, node, matcher) => { 131 | if(matcher(filter, node)){ 132 | return false 133 | } 134 | 135 | if(!node.children) return true; 136 | 137 | const filtered = node.children.filter(child => delTree(filter, child, matcher)) 138 | 139 | return Object.assign({}, node, { children: filtered }); 140 | } 141 | 142 | 143 | export const transformCode = (contents) => { 144 | 145 | try{ 146 | const { code } = transform(contents.trim(), { 147 | presets: ['es2015', 'react', 'stage-0'], 148 | compact: true, 149 | }); 150 | 151 | return { code } 152 | 153 | }catch(err){ 154 | 155 | return { err: err.toString() } 156 | } 157 | 158 | } 159 | 160 | export const resolvePath = (root, dir) => path.resolve(root, dir) 161 | -------------------------------------------------------------------------------- /src/vendor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | // polyfills and vendors 4 | 5 | require('babel-polyfill'); 6 | require('babel-standalone'); 7 | require('react'); 8 | require('react-dom'); 9 | require('react-redux'); 10 | require('react-router'); 11 | require('dva'); 12 | require('react-ace'); 13 | require('brace'); 14 | require('react-treebeard'); 15 | require('electron'); 16 | require('lodash'); 17 | require('path'); 18 | // require('node-watch') 19 | 20 | 21 | require('brace/mode/jsx'); 22 | require('brace/mode/javascript'); 23 | require('brace/mode/java'); 24 | require('brace/mode/python'); 25 | require('brace/mode/xml'); 26 | require('brace/mode/ruby'); 27 | require('brace/mode/sass'); 28 | require('brace/mode/markdown'); 29 | require('brace/mode/json'); 30 | require('brace/mode/html'); 31 | require('brace/mode/handlebars'); 32 | require('brace/mode/coffee'); 33 | require('brace/mode/css'); 34 | require('brace/theme/monokai'); 35 | require('brace/theme/github'); 36 | require('brace/theme/twilight'); 37 | require('brace/theme/xcode'); 38 | require('brace/theme/terminal'); 39 | -------------------------------------------------------------------------------- /static/assets/README.md: -------------------------------------------------------------------------------- 1 | ### put your assets here 2 | 3 | 1. images 4 | 2. other standalone libraries(no npm support) 5 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jirapo/React-editor/490c7c59887910f09af21983f551de070861e4ff/static/favicon.ico -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /static/template/test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3001, 3 | "entry": "src/index.js", 4 | "vendor": "src/vendor.js", 5 | "output":"dist" 6 | } -------------------------------------------------------------------------------- /static/template/test/src/components/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | text-size-adjust: none; 3 | font-family: helvetica, arial, sans-serif; 4 | line-height: 200%; 5 | padding: 6px 20px 30px; 6 | } -------------------------------------------------------------------------------- /static/template/test/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './App.css'; 3 | 4 | const App = () => ( 5 |
6 |

Hello,world

7 |
8 | ); 9 | 10 | export default App; -------------------------------------------------------------------------------- /static/template/test/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { AppContainer } from 'react-hot-loader'; 5 | // AppContainer is a necessary wrapper component for HMR 6 | 7 | import App from './components/App'; 8 | 9 | const render = () => { 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | }; 17 | 18 | render(); 19 | 20 | // Hot Module Replacement API 21 | if (module.hot) { 22 | module.hot.accept('./components/App', render); 23 | } 24 | -------------------------------------------------------------------------------- /static/template/test/src/vendor.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | require('react'); 3 | require('react-dom'); 4 | require('lodash') 5 | -------------------------------------------------------------------------------- /webpack/base.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | 8 | module.exports = { 9 | 10 | context: __dirname, 11 | 12 | devtool: 'inline-source-map', 13 | 14 | resolve: { 15 | 16 | extensions: ['.js', '.jsx', '.scss', '.css'], 17 | 18 | modules: [ path.resolve(__dirname), path.resolve(__dirname, 'node_modules')] 19 | 20 | }, 21 | 22 | resolveLoader:{ 23 | 24 | // modules: [ path.resolve(__dirname), 'node_modules'] 25 | modules: [ path.resolve(__dirname), path.resolve(__dirname, 'node_modules')] 26 | }, 27 | 28 | entry: { 29 | 'app': ['react-hot-loader/patch', 30 | user_path_entry 31 | // path.resolve(user_proj_dir, 'src/index.js') 32 | // path.resolve(user_proj_dir, user_proj_entry) 33 | ], 34 | vendor: user_path_vendor 35 | // 'vendor': path.resolve(user_proj_dir, 'src/vendor.js') 36 | // 'vendor': path.resolve(user_proj_dir, 'src/vendor.js') 37 | }, 38 | 39 | output: { 40 | 41 | // path: path.resolve(user_proj_dir, 'dist'), 42 | path: user_path_output, 43 | 44 | filename: '[name].js', 45 | 46 | publicPath: '/' 47 | 48 | }, 49 | 50 | module: { 51 | 52 | rules: [ 53 | { 54 | test: /\.js$/, 55 | exclude: /node_modules/, 56 | loader: 'babel-loader', 57 | query: { 58 | cacheDirectory: true, 59 | presets: ['babel-preset-es2015', 'babel-preset-stage-2', 'babel-preset-react'].map(require.resolve), 60 | } 61 | }, 62 | { 63 | test: /\.css$/, 64 | use: [ 65 | { 66 | loader: 'style-loader' 67 | }, 68 | { 69 | loader: 'css-loader', 70 | options: { 71 | importLoaders: 2, 72 | modules: true, 73 | localIdentName: '[name]__[local]' 74 | } 75 | }, 76 | { 77 | loader: 'postcss-loader' 78 | }, 79 | ] 80 | }, 81 | { 82 | test: /\.(mp4|webm)$/, 83 | loader: 'url-loader?limit=10000' 84 | }, 85 | { test: /\.woff2{0,1}$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' }, 86 | { test: /\.(otf|eot|svg|ttf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' }, 87 | { test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192'} 88 | 89 | ] 90 | 91 | }, 92 | 93 | plugins: [ 94 | 95 | new CommonsChunkPlugin({ 96 | name: ['app', 'vendor', 'manifest'], 97 | minChunks: Infinity 98 | }), 99 | new HtmlWebpackPlugin({ 100 | inject: true, 101 | template: path.resolve(__dirname, 'static/index.html'), 102 | }), 103 | new webpack.HotModuleReplacementPlugin() 104 | ], 105 | // devServer: { 106 | // host: 'localhost', 107 | // port: 3001, 108 | // open: true, 109 | // hot: true, 110 | // stats: { chunks: false }, 111 | // historyApiFallback: true, 112 | // watchOptions: { 113 | // aggregateTimeout: 300, 114 | // poll: 1000 115 | // }, 116 | // } 117 | 118 | }; 119 | -------------------------------------------------------------------------------- /webpack/base.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const autoprefixer = require('autoprefixer'); 4 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const InlineChunkWebpackPlugin = require('html-webpack-inline-chunk-plugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); 9 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 10 | 11 | 12 | module.exports = { 13 | 14 | context: __dirname, 15 | 16 | devtool: 'source-map', 17 | 18 | resolve: { 19 | 20 | extensions: ['.js', '.jsx', '.scss', '.css'], 21 | 22 | modules: [ path.resolve(__dirname), path.resolve(__dirname, 'node_modules')] 23 | 24 | }, 25 | 26 | resolveLoader:{ 27 | 28 | // modules: [ path.resolve(__dirname), 'node_modules'] 29 | modules: [ path.resolve(__dirname), path.resolve(__dirname, 'node_modules')] 30 | }, 31 | 32 | entry: { 33 | 'app': user_path_entry, 34 | 'vendor': user_path_vendor 35 | }, 36 | 37 | output: { 38 | 39 | path: user_path_output, 40 | 41 | filename: '[name]-[chunkhash].min.js', 42 | 43 | chunkFilename: '[chunkhash].js' 44 | 45 | }, 46 | 47 | module: { 48 | 49 | rules: [ 50 | { 51 | test: /\.js$/, 52 | exclude: /node_modules/, 53 | loader: 'babel-loader', 54 | query: { 55 | cacheDirectory: true, 56 | presets: [ 57 | 'babel-preset-es2015', 58 | // 'babel-preset-es2015-webpack', 59 | // 'babel-plugin-transform-es2015-template-literals', 60 | // 'transform-es2015-literals', 61 | // 'transform-es2015-function-name', 62 | // 'transform-es2015-arrow-functions', 63 | // 'transform-es2015-block-scoped-functions', 64 | // 'transform-es2015-classes', 65 | // 'transform-es2015-object-super', 66 | // 'transform-es2015-shorthand-properties', 67 | // 'transform-es2015-computed-properties', 68 | // 'transform-es2015-for-of', 69 | // 'transform-es2015-sticky-regex', 70 | // 'transform-es2015-unicode-regex', 71 | // 'check-es2015-constants', 72 | // 'transform-es2015-spread', 73 | // 'transform-es2015-parameters', 74 | // 'transform-es2015-destructuring', 75 | // 'transform-es2015-block-scoping', 76 | // 'transform-es2015-typeof-symbol', 77 | // ['transform-regenerator', { async: false, asyncGenerators: false }], 78 | 'babel-preset-stage-2', 79 | 'babel-preset-react'].map(require.resolve), 80 | } 81 | }, 82 | { 83 | test: /\.css$/, 84 | loader: ExtractTextPlugin.extract({ 85 | fallbackLoader: 'style-loader', 86 | loader: [ 87 | 'css-loader?modules&localIdentName=[name]__[local]&minimize&sourceMap&importLoaders=2', 88 | 'postcss-loader', 89 | ] 90 | }) 91 | }, 92 | { 93 | test: /\.(mp4|webm)$/, 94 | loader: 'url-loader?limit=10000' 95 | }, 96 | { test: /\.woff2{0,1}$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' }, 97 | { test: /\.(otf|eot|svg|ttf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' }, 98 | { test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192'} 99 | 100 | ] 101 | 102 | }, 103 | 104 | plugins: [ 105 | 106 | new CommonsChunkPlugin({ 107 | name: 'manifest', 108 | minChunks: Infinity, 109 | }), 110 | 111 | new HtmlWebpackPlugin({ 112 | inject: true, 113 | template: path.resolve(__dirname, 'static/index.html'), 114 | // favicon: path.resolve(__dirname, '../static/favicon.ico'), 115 | minify: { 116 | removeComments: true, 117 | collapseWhitespace: true, 118 | removeRedundantAttributes: true, 119 | useShortDoctype: true, 120 | removeEmptyAttributes: true, 121 | removeStyleLinkTypeAttributes: true, 122 | keepClosingSlash: true, 123 | minifyJS: true, 124 | minifyCSS: true, 125 | minifyURLs: true 126 | } 127 | }), 128 | new InlineChunkWebpackPlugin({ 129 | inlineChunks: ['manifest'] 130 | }), 131 | new ExtractTextPlugin('[name].css'), 132 | new UglifyJsPlugin({ 133 | compressor: { 134 | screw_ie8: true, 135 | warnings: false 136 | }, 137 | mangle: { 138 | screw_ie8: true 139 | }, 140 | output: { 141 | comments: false, 142 | screw_ie8: true 143 | }, 144 | // sourceMap: true 145 | }), 146 | new LoaderOptionsPlugin({ 147 | options: { 148 | context: '/', 149 | postcss: function () { 150 | return [autoprefixer]; 151 | } 152 | } 153 | }) 154 | ] 155 | }; 156 | -------------------------------------------------------------------------------- /webpack/common.config.js: -------------------------------------------------------------------------------- 1 | // webpack plugins 2 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 3 | // const HappyPack = require('happypack'); 4 | // const happyThreadPool = HappyPack.ThreadPool({ size: 4}); 5 | 6 | 7 | module.exports = { 8 | target: "electron-renderer", 9 | 10 | resolve: { 11 | 12 | extensions: ['.js', '.jsx', '.scss', '.css'], 13 | 14 | modules: ['node_modules'] 15 | 16 | }, 17 | 18 | module: { 19 | 20 | rules: [ 21 | 22 | { 23 | test: /\.js$/, 24 | // include: './src/', 25 | exclude: /node_modules/, 26 | loader: 'babel-loader', 27 | query: { 28 | cacheDirectory: true, 29 | 30 | }, 31 | // use: ['babel-loader'] 32 | // use: ['happypack/loader'] 33 | }, 34 | // { 35 | // test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/, 36 | // loader: 'file-loader', 37 | // }, 38 | { 39 | test: /\.(mp4|webm)$/, 40 | loader: 'url-loader?limit=10000' 41 | }, 42 | { test: /\.woff2{0,1}$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' }, 43 | { test: /\.(otf|eot|svg|ttf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' }, 44 | { test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192'} 45 | 46 | ] 47 | 48 | }, 49 | 50 | plugins: [ 51 | // new HappyPack({ 52 | // // cache: true, 53 | // loaders: [ 54 | // { 55 | // path: 'babel-loader', 56 | // query: { 57 | // plugins: [ 58 | // 'react-hot-loader/babel', 59 | // ], 60 | // presets: ['es2015', 'stage-2', 'react'], 61 | // cacheDirectory: true 62 | // } 63 | // } 64 | // ], 65 | // threads: 2 66 | // }), 67 | // new HappyPack({ 68 | // id: 'js', 69 | // loaders: [ 'babel-loader' ], 70 | // threadPool: happyThreadPool, 71 | // }), 72 | new CommonsChunkPlugin({ 73 | name: ['app', 'vendor', 'manifest'], 74 | minChunks: Infinity 75 | }) 76 | ] 77 | 78 | }; 79 | 80 | // options:{ 81 | // presets:[ 82 | // 'transform-es2015-template-literals', 83 | // // 'babel-plugin-transform-es2015-template-literals', 84 | // 'transform-es2015-literals', 85 | // 'transform-es2015-function-name', 86 | // 'transform-es2015-arrow-functions', 87 | // 'transform-es2015-block-scoped-functions', 88 | // 'transform-es2015-classes', 89 | // 'transform-es2015-object-super', 90 | // 'transform-es2015-shorthand-properties', 91 | // 'transform-es2015-computed-properties', 92 | // 'transform-es2015-for-of', 93 | // 'transform-es2015-sticky-regex', 94 | // 'transform-es2015-unicode-regex', 95 | // 'check-es2015-constants', 96 | // 'transform-es2015-spread', 97 | // 'transform-es2015-parameters', 98 | // 'transform-es2015-destructuring', 99 | // 'transform-es2015-block-scoping', 100 | // 'transform-es2015-typeof-symbol', 101 | // ['transform-regenerator', { async: false, asyncGenerators: false }], 102 | // ] 103 | // } 104 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const webpackMerge = require('webpack-merge'); 4 | const webpackCommon = require('./common.config'); 5 | 6 | 7 | // webpack plugins 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | module.exports = webpackMerge(webpackCommon, { 12 | 13 | devtool: 'inline-source-map', 14 | 15 | entry: { 16 | 'app': ['react-hot-loader/patch', 17 | './src/renderer/index.js' 18 | ], 19 | 'vendor': './src/vendor.js' 20 | }, 21 | 22 | output: { 23 | 24 | path: path.resolve(__dirname, '../static/dist'), 25 | 26 | filename: '[name].js', 27 | 28 | // sourceMapFilename: '[name].map', 29 | 30 | // chunkFilename: '[id]-chunk.js', 31 | 32 | publicPath: '/' 33 | 34 | }, 35 | 36 | module: { 37 | 38 | rules: [ 39 | { 40 | test: /\.css$/, 41 | loader: ExtractTextPlugin.extract({ 42 | fallbackLoader: 'style-loader', 43 | loader: [ 44 | 'css-loader?modules&localIdentName=[name]__[local]&minimize&sourceMap&importLoaders=2', 45 | 'postcss-loader', 46 | // 'sass-loader?outputStyle=expanded&sourceMap&sourceMapContents' 47 | ] 48 | }) 49 | } 50 | // { 51 | // test: /\.js$/, 52 | // exclude: /node_modules/, 53 | // use: ['babel-loader'] 54 | // }, 55 | // { 56 | // test: /\.css$/, 57 | // use: [ 58 | // { 59 | // loader: 'style-loader' 60 | // }, 61 | // { 62 | // loader: 'css-loader', 63 | // options: { 64 | // importLoaders: 2, 65 | // modules: true, 66 | // localIdentName: '[name]__[local]' 67 | // } 68 | // }, 69 | // { 70 | // loader: 'postcss-loader' 71 | // }, 72 | // ] 73 | // } 74 | ] 75 | 76 | }, 77 | 78 | plugins: [ 79 | 80 | new HtmlWebpackPlugin({ 81 | inject: true, 82 | template: path.resolve(__dirname, '../static/index.html'), 83 | // favicon: path.resolve(__dirname, '../static/favicon.ico') 84 | }), 85 | new ExtractTextPlugin('[name].css'), 86 | new webpack.HotModuleReplacementPlugin() 87 | ], 88 | 89 | devServer: { 90 | host: 'localhost', 91 | port: 3000, 92 | // open: true, 93 | hot: true, 94 | stats: { chunks: false }, 95 | historyApiFallback: true, 96 | watchOptions: { 97 | aggregateTimeout: 300, 98 | poll: 1000 99 | }, 100 | }, 101 | performance: { 102 | hints: false 103 | } 104 | 105 | }); 106 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpackMerge = require('webpack-merge'); 3 | const autoprefixer = require('autoprefixer'); 4 | const webpackCommon = require('./common.config'); 5 | 6 | // webpack plugins 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | const DefinePlugin = require('webpack/lib/DefinePlugin'); 9 | const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); 10 | // const CleanWebpackPlugin = require('clean-webpack-plugin'); 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 12 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 13 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 14 | const InlineChunkWebpackPlugin = require('html-webpack-inline-chunk-plugin'); 15 | 16 | module.exports = webpackMerge(webpackCommon, { 17 | 18 | bail: true, 19 | 20 | devtool: 'source-map', 21 | 22 | entry: { 23 | 'app': [ 24 | './src/renderer/index.js' 25 | ], 26 | 'vendor': './src/vendor.js' 27 | }, 28 | 29 | output: { 30 | 31 | path: path.resolve(__dirname, '../dist'), 32 | 33 | filename: '[name]-[chunkhash].min.js', 34 | // filename: '[name].js', 35 | // sourceMapFilename: '[name]-[hash].map', 36 | 37 | // chunkFilename: '[id]-[chunkhash].js' 38 | chunkFilename: '[chunkhash].js' 39 | 40 | }, 41 | 42 | module: { 43 | 44 | // TODO: use webpack old syntax to compatible with ExtractTextPlugin 45 | // https://github.com/webpack/extract-text-webpack-plugin/issues/275 46 | rules: [ 47 | { 48 | test: /\.css$/, 49 | loader: ExtractTextPlugin.extract({ 50 | fallbackLoader: 'style-loader', 51 | loader: [ 52 | 'css-loader?modules&localIdentName=[name]__[local]&minimize&sourceMap&importLoaders=2', 53 | 'postcss-loader', 54 | // 'sass-loader?outputStyle=expanded&sourceMap&sourceMapContents' 55 | ] 56 | }) 57 | } 58 | ] 59 | 60 | }, 61 | 62 | plugins: [ 63 | new CommonsChunkPlugin({ 64 | name: 'manifest', 65 | // filename: 'manifest.js', 66 | minChunks: Infinity, 67 | }), 68 | 69 | new HtmlWebpackPlugin({ 70 | inject: true, 71 | template: path.resolve(__dirname, '../static/index.html'), 72 | // favicon: path.resolve(__dirname, '../static/favicon.ico'), 73 | // excludeChunks: ['vendors'], 74 | minify: { 75 | removeComments: true, 76 | collapseWhitespace: true, 77 | removeRedundantAttributes: true, 78 | useShortDoctype: true, 79 | removeEmptyAttributes: true, 80 | removeStyleLinkTypeAttributes: true, 81 | keepClosingSlash: true, 82 | minifyJS: true, 83 | minifyCSS: true, 84 | minifyURLs: true 85 | } 86 | }), 87 | new InlineChunkWebpackPlugin({ 88 | inlineChunks: ['manifest'] 89 | }), 90 | // new DedupePlugin(), 91 | // new CleanWebpackPlugin(['dist'], { 92 | // root: path.resolve(__dirname, '..'), 93 | // exclude: '.gitignore' 94 | // }), 95 | // new DefinePlugin({ 96 | // 'process.env': { 97 | // NODE_ENV: '"production"' 98 | // } 99 | // }), 100 | new ExtractTextPlugin('[name].css'), 101 | new UglifyJsPlugin({ 102 | compressor: { 103 | screw_ie8: true, 104 | warnings: false 105 | }, 106 | mangle: { 107 | screw_ie8: true 108 | }, 109 | output: { 110 | comments: false, 111 | screw_ie8: true 112 | }, 113 | // sourceMap: true 114 | }), 115 | new LoaderOptionsPlugin({ 116 | options: { 117 | context: '/', 118 | postcss: function () { 119 | return [autoprefixer]; 120 | } 121 | } 122 | }) 123 | ] 124 | 125 | }); 126 | --------------------------------------------------------------------------------