├── .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 |
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 |
--------------------------------------------------------------------------------