├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── cli.js ├── docs ├── beep.mdx ├── hello.mdx ├── index.mdx └── ok-mdx.gif ├── lib ├── App.js ├── Logo.js ├── ace-theme.js ├── config.js ├── dev.js ├── entry.js ├── progress-plugin.js └── template.js ├── package.json └── packages └── ok-cli ├── .gitignore ├── .npmignore ├── README.md ├── cli.js ├── docs ├── Layout.js ├── components.js └── hello.mdx ├── index.js ├── lib ├── build.js ├── entry.js ├── index.js └── overlay.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | site 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | site 2 | docs 3 | packages 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | before_deploy: 5 | - npm run docs 6 | deploy: 7 | provider: pages 8 | skip_cleanup: true 9 | github_token: $GH_TOKEN 10 | local_dir: site 11 | on: 12 | branch: master 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ok-mdx 3 | 4 | 5 | 6 | Browser-based [MDX][] editor 7 | 8 | ```sh 9 | npm i -g ok-mdx 10 | ``` 11 | 12 | ```sh 13 | mkdir docs 14 | touch docs/hello.mdx 15 | mdx docs --open 16 | ``` 17 | 18 | - Quickly prototype with React components 19 | - Zero configuration 20 | - Mix markdown with JSX 21 | - Live edit and autosave 22 | 23 | ## What is this for? 24 | 25 | MDX is great for documentation, building demos, or quickly prototyping with React components, 26 | without the need to set up a full-blown React application. 27 | Similar to [Compositor x0][x0], ok-mdx is meant to be installed as a global command line utility 28 | that you can use alongside your application setup or in isolated sandbox environments. 29 | ok-mdx works well as a local alternative to tools like [CodeSandbox][] when working with React components. 30 | 31 | ## Getting Started 32 | 33 | ok-mdx needs a directory of `.mdx` or `.md` files to work. 34 | 35 | After installing ok-mdx, create a folder and an empty `.mdx` file with the following command: 36 | 37 | ```sh 38 | mkdir docs && touch docs/hello.mdx 39 | ``` 40 | 41 | Start the ok-mdx app: 42 | 43 | ```sh 44 | mdx docs --open 45 | ``` 46 | 47 | This will open the application in your default browser, showing a list of the MDX files. 48 | Click on a filename to open the editor view. 49 | In the right panel, add some text to see the preview on the left. 50 | 51 | ### MDX Format 52 | 53 | MDX is a superset of [markdown][], which can also render [JSX][] instead of HTML. 54 | 55 | ```mdx 56 | # Markdown Heading 57 | 58 | 59 | ``` 60 | 61 | ### Importing Components 62 | 63 | In order to import components, be sure they're installed locally. 64 | This requires a `package.json` file in your current directory. 65 | 66 | To create a `package.json` file, run `npm init -y`. 67 | 68 | To install a component, use `npm install`. The following will install [grid-styled][] and [styled-components][] as a local dependency. 69 | 70 | ```sh 71 | npm i grid-styled styled-components 72 | ``` 73 | 74 | To use components, import them at the top of your MDX file: 75 | 76 | ```mdx 77 | import { Flex, Box } from 'grid-styled' 78 | 79 | # Hello 80 | 81 | 82 | 83 | Flex 84 | 85 | 86 | Box 87 | 88 | 89 | ``` 90 | 91 | ## Options 92 | 93 | ``` 94 | -o --open Opens development server in default browser 95 | -p --port Port for development server 96 | --vim Enable editor Vim mode 97 | ``` 98 | 99 | ### Exporting 100 | 101 | ok-mdx is only meant to be used for development. To export your MDX files, consider one of the following tools: 102 | 103 | - [Compositor x0][x0]: great for creating documentation, blogs, static sites, or other small demos 104 | - [Next.js][next.js]: great for creating production-ready, server-side rendered React applications 105 | 106 | ## Related 107 | 108 | - [mdx-go][] 109 | - [mdx-deck][] 110 | - [Compositor x0][x0] 111 | - [Compositor Iso][iso] 112 | - [MDX][] 113 | - [CodeSandbox][] 114 | 115 | [mdx-go]: https://github.com/jxnblk/mdx-go 116 | [mdx-deck]: https://github.com/jxnblk/mdx-deck 117 | [x0]: https://github.com/c8r/x0 118 | [iso]: https://compositor.io/iso 119 | [MDX]: https://github.com/mdx-js/mdx 120 | [CodeSandbox]: https://codesandbox.io 121 | [markdown]: https://daringfireball.net/projects/markdown/syntax 122 | [JSX]: https://facebook.github.io/jsx/ 123 | [grid-styled]: https://github.com/jxnblk/grid-styled 124 | [styled-components]: https://github.com/styled-components/styled-components 125 | [next.js]: https://github.com/zeit/next.js 126 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const meow = require('meow') 5 | const open = require('react-dev-utils/openBrowser') 6 | const chalk = require('chalk') 7 | // const log = require('./lib/log') 8 | 9 | const log = (...args) => { 10 | console.log( 11 | chalk.cyan('[mdx]'), 12 | ...args 13 | ) 14 | } 15 | log.error = (...args) => { 16 | console.log( 17 | chalk.red('[err]'), 18 | ...args 19 | ) 20 | } 21 | 22 | const config = require('pkg-conf').sync('ok-mdx') 23 | 24 | const cli = meow(` 25 | Usage: 26 | 27 | $ mdx docs 28 | 29 | Options: 30 | 31 | -o --open Opens development server in default browser 32 | -p --port Port for development server 33 | --vim Enable editor Vim mode 34 | 35 | `, { 36 | flags: { 37 | open: { 38 | type: 'boolean', 39 | alias: 'o' 40 | }, 41 | port: { 42 | type: 'string', 43 | alias: 'p' 44 | }, 45 | vim: { 46 | type: 'boolean', 47 | }, 48 | } 49 | }) 50 | 51 | const [ cmd, dir ] = cli.input 52 | if (!cmd) { 53 | cli.showHelp(0) 54 | } 55 | const dirname = path.resolve(dir || cmd) 56 | 57 | const opts = Object.assign({ 58 | dirname, 59 | port: 8080, 60 | title: 'ok-mdx', 61 | }, config, cli.flags) 62 | 63 | switch (cmd) { 64 | case 'dev': 65 | default: 66 | const dev = require('./lib/dev') 67 | log(`starting dev server (${dir || cmd})`) 68 | dev(opts) 69 | .then(({ server }) => { 70 | const { port } = server.options 71 | const url = `http://localhost:${port}` 72 | log('dev server listening on', chalk.cyan(url)) 73 | if (opts.open) open(url) 74 | }) 75 | .catch(err => { 76 | log.error(err) 77 | process.exit(1) 78 | }) 79 | } 80 | 81 | require('update-notifier')({ 82 | pkg: cli.pkg 83 | }).notify() 84 | -------------------------------------------------------------------------------- /docs/beep.mdx: -------------------------------------------------------------------------------- 1 | import Box from 'superbox' 2 | 3 | # Hello MDX 4 | 5 | 14 | superbox 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/hello.mdx: -------------------------------------------------------------------------------- 1 | import Box from 'superbox' 2 | 3 | # Hello MDX 4 | 5 | 19 | wow 20 | 21 | 22 | 23 | This should live update in the browser when you edit the code on the right. 24 | 25 | -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | import Box from 'superbox/isocss' 2 | 3 | # ok-mdx 4 | 5 | 18 | 19 | 20 | Browser-based [MDX][] editor 21 | 22 | 35 | GitHub 36 | 37 | 38 | [ok-mdx]: https://github.com/jxnblk/ok-mdx 39 | [MDX]: https://github.com/mdx-js/mdx 40 | -------------------------------------------------------------------------------- /docs/ok-mdx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/ok-mdx/b4afb2cf15fa6bb0f13bef682076d33cad15ef09/docs/ok-mdx.gif -------------------------------------------------------------------------------- /lib/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | BrowserRouter, 4 | Route, 5 | Switch, 6 | withRouter, 7 | Link 8 | } from 'react-router-dom' 9 | import AceEditor from 'react-ace' 10 | import Box from 'superbox/isocss' 11 | import 'brace/mode/jsx' 12 | import 'brace/theme/monokai' 13 | import './ace-theme' 14 | import 'brace/keybinding/vim' 15 | import { Burger } from 'reline' 16 | import Folder from 'rmdi/lib/Folder' 17 | import File from 'rmdi/lib/InsertDriveFile' 18 | import Logo from './Logo' 19 | 20 | class Catch extends React.Component { 21 | state = { 22 | err: null 23 | } 24 | 25 | componentDidUpdate (prev) { 26 | if (prev.children !== this.props.children) { 27 | this.setState({ err: null }) 28 | } 29 | } 30 | 31 | componentDidCatch (err) { 32 | console.error(err) 33 | this.setState({ err }) 34 | } 35 | 36 | render () { 37 | const { err } = this.state 38 | if (err) { 39 | return ( 40 | 47 | ) 48 | } 49 | try { 50 | return ( 51 | 52 | {this.props.children} 53 | 54 | ) 55 | } catch (e) { 56 | console.error(e) 57 | return false 58 | } 59 | } 60 | } 61 | 62 | const Flex = props => 63 | 70 | 71 | const Main = props => 72 | 81 | 82 | const SideBar = props => 83 | 86 | 97 | 98 | SideBar.defaultProps = { 99 | width: [ 192, 320, 512 ] 100 | } 101 | 102 | const Pre = props => 103 | 114 | 115 | const Container = props => 116 | 125 | 126 | const Progress = ({ value }) => 127 | 128 | 135 | 136 | 137 | class Editor extends React.Component { 138 | state = { 139 | code: this.props.code 140 | } 141 | 142 | handleChange = code => { 143 | this.setState({ code }) 144 | this.props.onChange(code) 145 | } 146 | 147 | shouldComponentUpdate (props, state) { 148 | if (!props.focused) return true 149 | if (state.code !== this.state.code) return true 150 | if (props.path !== this.props.path) return true 151 | if (props.vim !== this.props.vim) return true 152 | return false 153 | } 154 | 155 | render () { 156 | const { vim } = this.props 157 | const { code } = this.state 158 | return ( 159 | 164 | 177 | 178 | ) 179 | } 180 | } 181 | 182 | const StatusBar = ({ 183 | percent, 184 | status, 185 | focused 186 | }) => 187 | 194 | 195 | 204 | {status ? status : 'ready'} 205 | 206 | {false && } 207 | 208 | 218 | 219 | 220 | 221 | 222 | const Toolbar = props => 223 | 230 |
231 |       {props.file}
232 |     
233 | 243 | 244 | 245 |
246 | 247 | const Directory = ({ 248 | dirname, 249 | routes = [] 250 | }) => 251 | 258 | 259 | 260 | 261 |
262 |           {dirname}
263 |         
264 |
265 | 271 | {routes.map(route => ( 272 |
  • 273 | 283 | 284 | {route.key} 285 | 286 |
  • 287 | ))} 288 |
    289 |
    290 | 291 | 292 | 293 |
    294 | 295 | const Disconnected = () => 296 | 302 | 309 | 316 |
    317 |         Disconnected from server
    318 |       
    319 |
    320 |
    321 | 322 | const Err = ({ error }) => { 323 | return ( 324 | 336 |
    337 |         {error.text}
    338 |       
    339 |
    345 |     
    346 |   )
    347 | }
    348 | 
    349 | const App = withRouter(class extends React.Component {
    350 |   static defaultProps = {
    351 |     ...OPTIONS,
    352 |     dirname: DIRNAME
    353 |   }
    354 | 
    355 |   state = {
    356 |     focused: true,
    357 |     routes: [],
    358 |     percent: 0,
    359 |     status: '',
    360 |     error: null
    361 |   }
    362 | 
    363 |   writeFile = (filename) => (content) => {
    364 |     if (!this.socket) return
    365 |     const json = JSON.stringify({ filename, content })
    366 |     this.socket.send(json)
    367 |   }
    368 | 
    369 |   handleFocus = e => {
    370 |     this.setState({ focused: true })
    371 |   }
    372 | 
    373 |   handleBlur = e => {
    374 |     this.setState({ focused: false })
    375 |   }
    376 | 
    377 |   componentDidMount () {
    378 |     this.socket = new WebSocket('ws://localhost:' + SOCKET_PORT)
    379 |     this.socket.onopen = () => {
    380 |       this.socket.send(JSON.stringify('boop'))
    381 |     }
    382 |     this.socket.onmessage = (msg) => {
    383 |       const data = JSON.parse(msg.data)
    384 |       if (data.error) {
    385 |         console.error(data.error)
    386 |         this.setState({ error: data.error })
    387 |       } else {
    388 |         console.log(data)
    389 |         this.setState({ error: null })
    390 |       }
    391 |     }
    392 |     this.socket.onclose = () => {
    393 |       this.props.history.push('/_disconnected')
    394 |     }
    395 | 
    396 |     /* debounce or figure out performant way of handling
    397 |     this.progress = new WebSocket('ws://localhost:' + PROGRESS_PORT)
    398 |     this.progress.onmessage = msg => {
    399 |       const { percent, status } = JSON.parse(msg.data)
    400 |       this.setState({ percent, status })
    401 |     }
    402 |     */
    403 | 
    404 |     window.addEventListener('focus', this.handleFocus)
    405 |     window.addEventListener('blur', this.handleBlur)
    406 |     if (this.props.location.pathname === '/_disconnected') {
    407 |       this.props.history.push('/')
    408 |     }
    409 |   }
    410 | 
    411 |   render () {
    412 |     const { percent, status } = this.state
    413 |     const { Provider, routes, vim } = this.props
    414 | 
    415 |     const Wrap = Provider || Container
    416 | 
    417 |     return (
    418 |       
    419 |         
    420 |           {routes.map(({ Component, ...route }) => (
    421 |              (
    424 |                 
    425 |                   
    426 | 427 | 428 | 429 | {this.state.error && } 430 | 431 | 432 |
    433 | 434 | 435 | 442 | 443 |
    444 | )} 445 | /> 446 | ))} 447 | ( 450 | 451 | )} 452 | /> 453 | ( 455 | 456 | )} 457 | /> 458 |
    459 |
    460 | ) 461 | } 462 | }) 463 | 464 | export default class Root extends React.Component { 465 | render () { 466 | return ( 467 | 468 | 469 | 470 | ) 471 | } 472 | } 473 | if (module.hot) { module.hot.accept() } 474 | -------------------------------------------------------------------------------- /lib/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ 4 | size = 96 5 | }) => 6 | 12 | 16 | 23 | 30 | ok 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/ace-theme.js: -------------------------------------------------------------------------------- 1 | ace.define('ace/theme/zero', 2 | ['require','exports','module','ace/lib/dom'], 3 | function(acequire, exports, module) { 4 | 5 | exports.isDark = true // false 6 | exports.cssClass = 'ace-zero' 7 | 8 | exports.cssText = `.ace-zero { 9 | color: #fff; 10 | background-color: #000; 11 | } 12 | .ace-zero .ace_gutter { 13 | color: #666; 14 | background-color: rgba(255, 255, 255, .0625); 15 | } 16 | .ace-zero .ace_gutter-active-line { 17 | background-color: rgba(255, 255, 255, .125); 18 | } 19 | .ace-zero .ace_selection { 20 | background-color: rgba(255, 255, 255, .25); 21 | } 22 | .ace-zero .ace_cursor { 23 | border-color: #f0f; 24 | background-color: transparent; 25 | } 26 | .normal-mode .ace_cursor { 27 | background-color: #f0f !important; 28 | } 29 | ` 30 | 31 | var dom = acequire('../lib/dom') 32 | dom.importCssString(exports.cssText, exports.cssClass) 33 | }) 34 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 3 | const HTMLPlugin = require('mini-html-webpack-plugin') 4 | const chalk = require('chalk') 5 | const template = require('./template') 6 | 7 | const babel = { 8 | presets: [ 9 | 'babel-preset-env', 10 | 'babel-preset-stage-0', 11 | 'babel-preset-react', 12 | ].map(require.resolve) 13 | } 14 | 15 | const rules = [ 16 | { 17 | test: /\.js$/, 18 | exclude: /node_modules/, 19 | loader: require.resolve('babel-loader'), 20 | options: babel 21 | }, 22 | { 23 | test: /\.js$/, 24 | exclude: path.resolve(__dirname, '../node_modules'), 25 | include: [ 26 | path.resolve(__dirname, '..'), 27 | path.resolve(__dirname, '../src'), 28 | ], 29 | loader: require.resolve('babel-loader'), 30 | options: babel 31 | }, 32 | { 33 | test: /\.(md|mdx|jsx)$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: require.resolve('babel-loader'), 38 | options: babel 39 | }, 40 | require.resolve('@mdx-js/loader') 41 | ] 42 | } 43 | ] 44 | 45 | module.exports = { 46 | stats: 'none', 47 | resolve: { 48 | modules: [ 49 | __dirname, 50 | path.join(__dirname, '../node_modules'), 51 | path.relative(process.cwd(), path.join(__dirname, '../node_modules')), 52 | 'node_modules' 53 | ], 54 | }, 55 | module: { 56 | rules 57 | }, 58 | plugins: [ 59 | new ProgressBarPlugin({ 60 | width: '24', 61 | complete: '█', 62 | incomplete: chalk.gray('░'), 63 | format: [ 64 | chalk.cyan('[mdx] :bar'), 65 | chalk.cyan(':percent'), 66 | chalk.gray(':elapseds :msg'), 67 | ].join(' '), 68 | summaryContent: [ 69 | chalk.cyan('[mdx]'), 70 | chalk.gray('done '), 71 | ].join(' '), 72 | summary: false, 73 | }), 74 | new HTMLPlugin({ 75 | template 76 | }) 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /lib/dev.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const serve = require('webpack-serve') 5 | const history = require('connect-history-api-fallback') 6 | const convert = require('koa-connect') 7 | const config = require('./config') 8 | const WebSocket = require('ws') 9 | const getPort = require('get-port') 10 | const mdx = require('@mdx-js/mdx') 11 | const babel = require('babel-core') 12 | // const ProgressPlugin = require('./progress-plugin') 13 | const ansiHTML = require('ansi-html') 14 | const { AllHtmlEntities } = require('html-entities') 15 | 16 | const entities = new AllHtmlEntities() 17 | ansiHTML.setColors({ 18 | reset: [,'transparent'], 19 | black: '000', 20 | red: 'f00', 21 | green: '0f0', 22 | yellow: 'ff0', 23 | blue: '00f', 24 | magenta: 'f0f', 25 | cyan: '0ff', 26 | lightgrey: 'eee', 27 | darkgrey: '444' 28 | }) 29 | 30 | const devMiddleware = { 31 | hot: true, 32 | logLevel: 'error', 33 | clientLogLevel: 'none', 34 | stats: 'errors-only' 35 | } 36 | 37 | const noop = () => {} 38 | const updateFile = opts => ({ filename, content }, cb = noop) => { 39 | const filepath = path.join(opts.dirname, filename) 40 | if (!fs.existsSync(filepath)) return 41 | try { 42 | const jsx = mdx.sync(content) 43 | babel.transform(jsx, { 44 | presets: [ 45 | 'babel-preset-env', 46 | 'babel-preset-stage-0', 47 | 'babel-preset-react', 48 | ].map(require.resolve) 49 | }) 50 | fs.writeFile(filepath, content, cb) 51 | } catch (e) { 52 | const err = { 53 | text: e.toString(), 54 | code: ansiHTML(entities.encode(e.codeFrame)) 55 | } 56 | cb(err) 57 | } 58 | } 59 | 60 | module.exports = async (opts) => { 61 | if (opts.basename) delete opts.basename 62 | 63 | config.mode = 'development' 64 | config.context = opts.dirname 65 | config.entry = path.join(__dirname, './entry') 66 | config.output = { 67 | path: path.join(process.cwd(), 'dev'), 68 | filename: 'dev.js', 69 | publicPath: '/' 70 | } 71 | 72 | config.resolve.modules.push( 73 | path.join(opts.dirname, 'node_modules'), 74 | opts.dirname 75 | ) 76 | 77 | if (config.resolve.alias) { 78 | const whcAlias = config.resolve.alias['webpack-hot-client/client'] 79 | if (!fs.existsSync(whcAlias)) { 80 | const whcPath = path.dirname(require.resolve('webpack-hot-client/client')) 81 | config.resolve.alias['webpack-hot-client/client'] = whcPath 82 | } 83 | } 84 | 85 | const SOCKET_PORT = await getPort() 86 | const socketServer = new WebSocket.Server({ port: SOCKET_PORT }) 87 | const update = updateFile(opts) 88 | const PROGRESS_PORT = await getPort() 89 | const HOT_PORT = await getPort() 90 | process.env.WEBPACK_SERVE_OVERLAY_WS_URL = 'ws://localhost:' + HOT_PORT 91 | 92 | socketServer.on('connection', (socket) => { 93 | socket.on('message', msg => { 94 | const data = JSON.parse(msg) 95 | if (data.filename) { 96 | update(data, (err) => { 97 | if (err) { 98 | const json = JSON.stringify({ 99 | error: err 100 | }) 101 | socket.send(json) 102 | return 103 | } 104 | socket.send(JSON.stringify({ message: 'saved' })) 105 | }) 106 | } 107 | }) 108 | socket.send(JSON.stringify({ message: 'beep' })) 109 | }) 110 | 111 | config.plugins.push( 112 | // new ProgressPlugin({ port: PROGRESS_PORT }), 113 | new webpack.DefinePlugin({ 114 | DEV: JSON.stringify(true), 115 | OPTIONS: JSON.stringify(opts), 116 | DIRNAME: JSON.stringify(opts.dirname), 117 | SOCKET_PORT: JSON.stringify(SOCKET_PORT), 118 | PROGRESS_PORT: JSON.stringify(PROGRESS_PORT), 119 | 'process.env': { WEBPACK_SERVE_OVERLAY_WS_URL: JSON.stringify('ws://localhost:' + HOT_PORT) } 120 | }) 121 | ) 122 | 123 | const serveOpts = { 124 | config, 125 | devMiddleware, 126 | logLevel: 'error', 127 | content: opts.dirname, 128 | port: opts.port, 129 | hotClient: { 130 | reload: false, 131 | logLevel: 'error', 132 | port: HOT_PORT 133 | }, 134 | add: (app, middleware, options) => { 135 | app.use(convert(history({}))) 136 | } 137 | } 138 | 139 | if (opts.verbose) { 140 | serveOpts.logLevel = 'debug' 141 | serveOpts.devMiddleware.logLevel = 'debug' 142 | serveOpts.devMiddleware.stats = 'verbose' 143 | serveOpts.hotClient.logLevel = 'debug' 144 | serveOpts.config.stats = 'verbose' 145 | } 146 | 147 | return new Promise((resolve, reject) => { 148 | serve({}, serveOpts) 149 | .then(server => { 150 | server.on('build-finished', () => { 151 | resolve({ server }) 152 | }) 153 | }) 154 | .catch(reject) 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /lib/entry.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import App from './App' 5 | 6 | require('webpack-serve-overlay') 7 | 8 | const req = require.context(DIRNAME, true, /\.(md|mdx|jsx)$/) 9 | const codeContext = require.context('!!raw-loader!' + DIRNAME, true, /\.(md|mdx|jsx)$/) 10 | const routes = req.keys() 11 | .filter(key => !/node_modules/.test(key)) 12 | .map(key => { 13 | const extname = path.extname(key) 14 | const name = path.basename(key, extname) 15 | const exact = name === 'index' 16 | const dirname = path.dirname(key).replace(/^\./, '') 17 | const pathname = dirname + '/' + (exact ? '' : name) 18 | let mod, Component 19 | try { 20 | mod = req(key) 21 | Component = mod.default 22 | } catch (err) { 23 | console.error(err) 24 | } 25 | const code = codeContext(key) 26 | if (typeof Component !== 'function') return null 27 | return { 28 | key, 29 | name, 30 | extname, 31 | dirname, 32 | exact, 33 | path: pathname, 34 | Component, 35 | code 36 | } 37 | }) 38 | .filter(Boolean) 39 | 40 | const providerContext = require.context(DIRNAME, false, /\_app\.js$/) 41 | const Provider = providerContext.keys().length 42 | ? providerContext('./_app.js').default 43 | : null 44 | 45 | const app = render( 46 | , 50 | root 51 | ) 52 | 53 | if (module.hot) { 54 | module.hot.accept() 55 | } 56 | -------------------------------------------------------------------------------- /lib/progress-plugin.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const WebSocket = require('ws') 3 | 4 | const PORT = '98765' 5 | 6 | function ProgressPlugin (opts = {}) { 7 | const port = opts.port || PORT 8 | const server = new WebSocket.Server({ port }) 9 | let socket 10 | server.on('connection', s => { 11 | socket = s 12 | }) 13 | return new webpack.ProgressPlugin((percent, status) => { 14 | if (!socket || !socket.send) return 15 | const json = JSON.stringify({ percent, status }) 16 | socket.send(json) 17 | }) 18 | } 19 | 20 | module.exports = ProgressPlugin 21 | -------------------------------------------------------------------------------- /lib/template.js: -------------------------------------------------------------------------------- 1 | const { generateJSReferences } = require('mini-html-webpack-plugin') 2 | 3 | module.exports = ({ 4 | html = '', 5 | css = '', 6 | js, 7 | publicPath, 8 | }) => ` 9 | 10 | 11 | 12 | 13 | 14 | ok-mdx 15 | 16 | ${css} 17 | 18 | 19 |
    ${html}
    20 | ${generateJSReferences(js, publicPath)} 21 | 22 | 23 | ` 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ok-mdx", 3 | "version": "1.0.0-7", 4 | "description": "", 5 | "main": "lib/dev.js", 6 | "bin": { 7 | "mdx": "./cli.js" 8 | }, 9 | "scripts": { 10 | "start": "./cli.js docs -o --vim", 11 | "docs": "ok build docs -d site --static", 12 | "test": "./cli.js" 13 | }, 14 | "keywords": [], 15 | "author": "Brent Jackson", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@mdx-js/loader": "^0.14.0", 19 | "@mdx-js/mdx": "^0.14.0", 20 | "@mdx-js/tag": "^0.14.0", 21 | "ansi-html": "0.0.7", 22 | "babel-core": "^6.26.3", 23 | "babel-loader": "^7.1.5", 24 | "babel-preset-env": "^1.7.0", 25 | "babel-preset-react": "^6.24.1", 26 | "babel-preset-stage-0": "^6.24.1", 27 | "brace": "^0.11.1", 28 | "chalk": "^2.4.1", 29 | "connect-history-api-fallback": "^1.5.0", 30 | "get-port": "^3.2.0", 31 | "html-entities": "^1.2.1", 32 | "isocss": "^1.0.0-3", 33 | "koa-connect": "^2.0.1", 34 | "meow": "^5.0.0", 35 | "mini-html-webpack-plugin": "^0.2.3", 36 | "pkg-conf": "^2.1.0", 37 | "progress-bar-webpack-plugin": "^1.11.0", 38 | "raw-loader": "^0.5.1", 39 | "react": "^16.4.1", 40 | "react-ace": "^6.1.4", 41 | "react-dev-utils": "^5.0.1", 42 | "react-dom": "^16.4.1", 43 | "react-live": "^1.11.0", 44 | "react-router-dom": "^4.3.1", 45 | "reline": "^1.0.0-beta.3", 46 | "rmdi": "^1.0.1", 47 | "styled-components": "^3.3.3", 48 | "superbox": "^2.1.0", 49 | "update-notifier": "^2.5.0", 50 | "webpack": "^4.16.1", 51 | "webpack-serve": "^2.0.2", 52 | "webpack-serve-overlay": "^0.2.2", 53 | "ws": "^5.2.2" 54 | }, 55 | "devDependencies": { 56 | "ok-cli": "^2.0.8" 57 | }, 58 | "ok": { 59 | "title": "ok-mdx" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/ok-cli/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /packages/ok-cli/.npmignore: -------------------------------------------------------------------------------- 1 | test.js 2 | docs 3 | coverage 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /packages/ok-cli/README.md: -------------------------------------------------------------------------------- 1 | 2 | # ok-cli 3 | 4 | 5 | 6 | Hyperminimal dev server for React & [MDX][] 7 | 8 | - :zero: Zero config 9 | - :headphones: No plugins 10 | - 🧠 Smart defaults 11 | - :atom_symbol: Render React or MDX 12 | - :fire: Blazing 13 | 14 | ```sh 15 | npm i -g ok-cli 16 | ``` 17 | 18 | ```sh 19 | ok docs/hello.mdx 20 | ``` 21 | 22 | ## Using React 23 | 24 | ok-cli will render the default exported component of a module. 25 | 26 | ```jsx 27 | // example App.js 28 | import React from 'react' 29 | 30 | export default props => 31 |

    Hello

    32 | ``` 33 | 34 | ```sh 35 | ok docs/App.js 36 | ``` 37 | 38 | ## Using MDX 39 | 40 | MDX is a superset of [markdown][], 41 | which lets you mix [JSX][] with markdown syntax. 42 | 43 | ```mdx 44 | import Button from './Button' 45 | 46 | # Markdown Heading 47 | 48 | 49 | ``` 50 | 51 | ### Layouts 52 | 53 | MDX also supports [layouts][] with React components. 54 | The default export in an MDX file will wrap the contents of the document. 55 | 56 | ```jsx 57 | // example Layout.js 58 | import React from 'react' 59 | 60 | export default ({ children }) => 61 |
    67 | {children} 68 |
    69 | ``` 70 | 71 | ```mdx 72 | import Layout from './Layout' 73 | 74 | export default Layout 75 | 76 | # Hello 77 | ``` 78 | 79 | ### Components 80 | 81 | ok-cli has built-in support for customizing the [components][] used in MDX. 82 | Export a named `components` object from the MDX document to customize the MDX markdown components. 83 | 84 | ```jsx 85 | // example components.js 86 | import React from 'react' 87 | 88 | export default { 89 | h1: props =>

    90 | } 91 | ``` 92 | 93 | ```mdx 94 | export { default as components } from './components' 95 | 96 | # Hello 97 | ``` 98 | 99 | ## Options 100 | 101 | - `--port`, `-p` Port for the dev server 102 | - `--no-open` Prevent opening in default browser 103 | 104 | ## Node API 105 | 106 | ```js 107 | const start = require('ok-cli') 108 | 109 | const options = { 110 | entry: './src/App.js' 111 | } 112 | 113 | start(options) 114 | .then(({ app, middleware, port }) => { 115 | console.log('listening on port:', port) 116 | }) 117 | ``` 118 | 119 | MIT License 120 | 121 | [x0]: https://compositor/x0 122 | [MDX]: https://github.com/mdx-js/mdx 123 | [markdown]: https://daringfireball.net/projects/markdown/syntax 124 | [JSX]: https://facebook.github.io/jsx/ 125 | [layouts]: https://github.com/mdx-js/mdx#export-default 126 | [components]: https://github.com/mdx-js/mdx#component-customization 127 | -------------------------------------------------------------------------------- /packages/ok-cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const meow = require('meow') 4 | const open = require('react-dev-utils/openBrowser') 5 | const clipboard = require('clipboardy') 6 | const chalk = require('chalk') 7 | 8 | const start = require('./lib') 9 | const build = require('./lib/build') 10 | 11 | const log = (...args) => { 12 | console.log( 13 | chalk.magenta('[ok]'), 14 | ...args 15 | ) 16 | } 17 | 18 | log.error = (...args) => { 19 | console.log( 20 | chalk.red('[err]'), 21 | ...args 22 | ) 23 | } 24 | 25 | const cli = meow(` 26 | ${chalk.magenta('[ok]')} 27 | 28 | ${chalk.gray('Usage:')} 29 | 30 | ${chalk.magenta('$ ok pages/hello.mdx')} 31 | 32 | ${chalk.magenta('$ ok pages/App.js')} 33 | 34 | ${chalk.gray('Options:')} 35 | 36 | ${chalk.magenta('-p --port')} Development server port 37 | 38 | ${chalk.magenta('--no-open')} Prevent from opening in default browser 39 | 40 | 41 | `, { 42 | flags: { 43 | port: { 44 | type: 'string', 45 | alias: 'p' 46 | }, 47 | open: { 48 | type: 'boolean', 49 | alias: 'o', 50 | default: true 51 | }, 52 | outDir: { 53 | type: 'string', 54 | alias: 'd', 55 | default: 'dist' 56 | }, 57 | help: { 58 | type: 'boolean', 59 | alias: 'h' 60 | }, 61 | version: { 62 | type: 'boolean', 63 | alias: 'v' 64 | } 65 | } 66 | }) 67 | 68 | const [ cmd, file ] = cli.input 69 | const entry = file || cmd 70 | 71 | if (!entry) { 72 | cli.showHelp(0) 73 | } 74 | 75 | const opts = Object.assign({ 76 | entry: path.resolve(entry) 77 | }, cli.flags) 78 | 79 | opts.outDir = path.resolve(opts.outDir) 80 | 81 | 82 | switch (cmd) { 83 | case 'build': 84 | log('exporting') 85 | build(opts) 86 | .then(stats => { 87 | log('exported') 88 | }) 89 | .catch(err => { 90 | log.error(err) 91 | process.exit(1) 92 | }) 93 | break 94 | case 'dev': 95 | default: 96 | log('starting dev server') 97 | start(opts) 98 | .then(({ port, app, middleware }) => { 99 | const url = `http://localhost:${port}` 100 | clipboard.write(url) 101 | console.log() 102 | log('server listening on', chalk.magenta(url)) 103 | if (opts.open) open(url) 104 | }) 105 | .catch(err => { 106 | log.error(err) 107 | process.exit(1) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /packages/ok-cli/docs/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ children }) => 4 |
    10 | {children} 11 |
    12 | -------------------------------------------------------------------------------- /packages/ok-cli/docs/components.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default { 4 | h1: props =>

    5 | } 6 | -------------------------------------------------------------------------------- /packages/ok-cli/docs/hello.mdx: -------------------------------------------------------------------------------- 1 | import Layout from './Layout' 2 | export { default as components } from './components' 3 | 4 | export default Layout 5 | 6 | # hello ok 7 | 8 | ```sh 9 | npm i -g ok-cli 10 | ``` 11 | 12 | ```sh 13 | ok hello.mdx 14 | ``` 15 | -------------------------------------------------------------------------------- /packages/ok-cli/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index') 2 | module.exports.config = require('./lib/index').config 3 | module.exports.build = require('./lib/build') 4 | -------------------------------------------------------------------------------- /packages/ok-cli/lib/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const createConfig = require('./index').createConfig 3 | 4 | const build = async (opts = {}) => { 5 | const config = createConfig(opts) 6 | config.mode = 'production' 7 | config.output = { 8 | path: opts.outDir 9 | } 10 | const compiler = webpack(config) 11 | 12 | return new Promise((resolve, reject) => { 13 | compiler.run((err, stats) => { 14 | if (err) { 15 | reject(err) 16 | return 17 | } 18 | resolve(stats) 19 | }) 20 | }) 21 | } 22 | 23 | module.exports = build 24 | -------------------------------------------------------------------------------- /packages/ok-cli/lib/entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | 4 | const mod = require(APP_FILENAME) 5 | const App = mod.default 6 | const { components } = mod 7 | render(, window.root) 8 | 9 | if (module.hot) module.hot.accept() 10 | -------------------------------------------------------------------------------- /packages/ok-cli/lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const Koa = require('koa') 4 | const getPort = require('get-port') 5 | const koaWebpack = require('koa-webpack') 6 | const koaStatic = require('koa-static') 7 | const HTMLPlugin = require('mini-html-webpack-plugin') 8 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 9 | const chalk = require('chalk') 10 | 11 | const devMiddleware = { 12 | publicPath: '/', 13 | clientLogLevel: 'error', 14 | stats: 'errors-only', 15 | logLevel: 'error', 16 | } 17 | 18 | const babel = { 19 | presets: [ 20 | 'babel-preset-env', 21 | 'babel-preset-stage-0', 22 | 'babel-preset-react', 23 | ].map(require.resolve) 24 | } 25 | 26 | const rules = [ 27 | { 28 | test: /\.js$/, 29 | exclude: /node_modules/, 30 | loader: require.resolve('babel-loader'), 31 | options: babel 32 | }, 33 | { 34 | test: /\.js$/, 35 | exclude: path.resolve(__dirname, '../node_modules'), 36 | include: [ 37 | path.resolve(__dirname, '..'), 38 | ], 39 | loader: require.resolve('babel-loader'), 40 | options: babel 41 | }, 42 | { 43 | test: /\.mdx?$/, 44 | exclude: /node_modules/, 45 | use: [ 46 | { 47 | loader: require.resolve('babel-loader'), 48 | options: babel 49 | }, 50 | require.resolve('@mdx-js/loader'), 51 | ] 52 | } 53 | ] 54 | 55 | const template = ({ 56 | title = 'ok', 57 | js, 58 | publicPath 59 | }) => ` 60 | 61 | 62 | 63 | 64 | 65 | ${title} 66 | 67 | 68 |
    69 | ${HTMLPlugin.generateJSReferences(js, publicPath)} 70 | 71 | 72 | ` 73 | 74 | const baseConfig = { 75 | stats: 'errors-only', 76 | mode: 'development', 77 | module: { 78 | rules 79 | }, 80 | resolve: { 81 | modules: [ 82 | path.relative(process.cwd(), path.join(__dirname, '../node_modules')), 83 | 'node_modules' 84 | ] 85 | }, 86 | plugins: [ 87 | new ProgressBarPlugin({ 88 | width: '24', 89 | complete: '█', 90 | incomplete: chalk.gray('░'), 91 | format: [ 92 | chalk.magenta('[ok] :bar'), 93 | chalk.magenta(':percent'), 94 | chalk.gray(':elapseds :msg'), 95 | ].join(' '), 96 | summary: false, 97 | customSummary: () => {}, 98 | }) 99 | ] 100 | } 101 | 102 | const createConfig = (opts = {}) => { 103 | baseConfig.context = opts.dirname 104 | 105 | baseConfig.resolve.modules.push( 106 | opts.dirname, 107 | path.join(opts.dirname, 'node_modules') 108 | ) 109 | 110 | baseConfig.entry = [ 111 | path.join(__dirname, './entry.js') 112 | ] 113 | 114 | const defs = Object.assign({}, opts.globals, { 115 | OPTIONS: JSON.stringify(opts), 116 | APP_FILENAME: JSON.stringify(opts.entry), 117 | HOT_PORT: JSON.stringify(opts.hotPort) 118 | }) 119 | 120 | baseConfig.plugins.push( 121 | new webpack.DefinePlugin(defs), 122 | new HTMLPlugin({ template, context: opts }) 123 | ) 124 | 125 | const config = typeof opts.config === 'function' 126 | ? opts.config(baseConfig) 127 | : baseConfig 128 | 129 | if (config.resolve.alias) { 130 | const hotAlias = config.resolve.alias['webpack-hot-client/client'] 131 | if (!fs.existsSync(hotAlias)) { 132 | const hotPath = path.dirname(require.resolve('webpack-hot-client/client')) 133 | config.resolve.alias['webpack-hot-client/client'] = hotPath 134 | } 135 | } 136 | 137 | return config 138 | } 139 | 140 | const start = async (opts = {}) => { 141 | const app = new Koa() 142 | opts.hotPort = await getPort() 143 | const hotClient = { 144 | port: opts.hotPort, 145 | logLevel: 'error' 146 | } 147 | opts.dirname = opts.dirname || path.dirname(opts.entry) 148 | const config = createConfig(opts) 149 | config.entry.push( 150 | path.join(__dirname, './overlay.js'), 151 | ) 152 | 153 | const middleware = await koaWebpack({ 154 | config, 155 | devMiddleware, 156 | hotClient 157 | }) 158 | const port = opts.port || await getPort() 159 | app.use(middleware) 160 | app.use(koaStatic(opts.dirname)) 161 | 162 | const server = app.listen(port) 163 | return new Promise((resolve) => { 164 | middleware.devMiddleware.waitUntilValid(() => { 165 | resolve({ server, app, middleware, port }) 166 | }) 167 | }) 168 | } 169 | 170 | module.exports = start 171 | module.exports.config = baseConfig 172 | module.exports.createConfig = createConfig 173 | -------------------------------------------------------------------------------- /packages/ok-cli/lib/overlay.js: -------------------------------------------------------------------------------- 1 | const ansiHTML = require('ansi-html') 2 | const Entities = require('html-entities').AllHtmlEntities 3 | const entities = new Entities() 4 | 5 | const colors = { 6 | reset: ['transparent', 'transparent'], 7 | black: '000000', 8 | red: 'FF0000', 9 | green: '00FF00', 10 | yellow: 'FFFF00', 11 | blue: '0000FF', 12 | magenta: 'FF00FF', 13 | cyan: '00FFFF', 14 | lightgrey: 'EEEEEE', 15 | darkgrey: '666666' 16 | }; 17 | ansiHTML.setColors(colors) 18 | 19 | let overlay 20 | 21 | const style = (el, styles) => { 22 | for (const key in styles) { 23 | el.style[key] = styles[key] 24 | } 25 | return el 26 | } 27 | 28 | const show = ({ 29 | title = '', 30 | text = '' 31 | }) => { 32 | overlay = document.body.appendChild( 33 | document.createElement('pre') 34 | ) 35 | style(overlay, { 36 | position: 'fixed', 37 | top: 0, 38 | right: 0, 39 | bottom: 0, 40 | left: 0, 41 | boxSizing: 'border-box', 42 | fontFamily: 'Menlo, monospace', 43 | fontSize: '12px', 44 | overflow: 'auto', 45 | lineHeight: 1.5, 46 | padding: '8px', 47 | margin: 0, 48 | color: 'magenta', 49 | backgroundColor: 'black' 50 | }) 51 | const code = ansiHTML(entities.encode(text)) 52 | overlay.innerHTML = `${title} 53 |
    54 |
    ${code} 55 | ` 56 | } 57 | 58 | const destroy = () => { 59 | if (!overlay) return 60 | document.body.removeChild(overlay) 61 | } 62 | 63 | const ws = new WebSocket('ws://localhost:' + HOT_PORT) 64 | 65 | ws.addEventListener('message', msg => { 66 | const data = JSON.parse(msg.data) 67 | switch (data.type) { 68 | case 'errors': 69 | const [ text ] = data.data.errors 70 | console.error(data.data.errors) 71 | show({ title: 'failed to compile', text }) 72 | break 73 | case 'ok': 74 | destroy() 75 | break 76 | } 77 | }) 78 | 79 | ws.addEventListener('close', () => { 80 | show({ title: 'disconnected' }) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/ok-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ok-cli", 3 | "version": "3.1.1", 4 | "description": "Hyperminimal development server for React & MDX", 5 | "main": "index.js", 6 | "bin": { 7 | "ok": "./cli.js" 8 | }, 9 | "scripts": { 10 | "start": "./cli.js docs/hello.mdx", 11 | "build": "./cli.js build docs/hello.mdx", 12 | "help": "./cli.js", 13 | "test": "nyc ava" 14 | }, 15 | "keywords": [], 16 | "author": "Brent Jackson", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@mdx-js/loader": "^0.15.0-1", 20 | "@mdx-js/mdx": "^0.15.0-1", 21 | "ansi-html": "0.0.7", 22 | "babel-core": "^6.26.3", 23 | "babel-loader": "^7.1.5", 24 | "babel-preset-env": "^1.7.0", 25 | "babel-preset-react": "^6.24.1", 26 | "babel-preset-stage-0": "^6.24.1", 27 | "chalk": "^2.4.1", 28 | "clipboardy": "^1.2.3", 29 | "get-port": "^4.0.0", 30 | "html-entities": "^1.2.1", 31 | "koa": "^2.5.2", 32 | "koa-static": "^5.0.0", 33 | "koa-webpack": "^5.1.0", 34 | "meow": "^5.0.0", 35 | "mini-html-webpack-plugin": "^0.2.3", 36 | "progress-bar-webpack-plugin": "^1.11.0", 37 | "react": "^16.4.1", 38 | "react-dev-utils": "^5.0.1", 39 | "react-dom": "^16.4.1", 40 | "webpack": "^4.16.3" 41 | }, 42 | "devDependencies": { 43 | "ava": "^0.25.0", 44 | "nyc": "^12.0.2", 45 | "supertest": "^3.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/ok-cli/test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const test = require('ava') 3 | const request = require('supertest') 4 | const start = require('./lib') 5 | 6 | const util = require('util') 7 | 8 | let server 9 | 10 | test.serial('starts', async t => { 11 | const res = await start({ 12 | entry: path.join(__dirname, './docs/hello.mdx') 13 | }) 14 | t.is(typeof res, 'object') 15 | t.is(typeof res.app, 'object') 16 | t.is(typeof res.middleware, 'function') 17 | t.is(typeof res.port, 'number') 18 | server = res.server 19 | }) 20 | 21 | test('returns html', async t => { 22 | const res = await request(server).get('/') 23 | .expect(200) 24 | .expect('Content-Type', 'text/html; charset=UTF-8') 25 | t.is(typeof res.text, 'string') 26 | }) 27 | 28 | test('serves bundled.js', async t => { 29 | const res = await request(server).get('/main.js') 30 | .expect(200) 31 | .expect('Content-Type', 'application/javascript; charset=UTF-8') 32 | t.is(typeof res.text, 'string') 33 | }) 34 | --------------------------------------------------------------------------------