diff --git a/cli.js b/cli.js
index acaadb9..4efbcde 100755
--- a/cli.js
+++ b/cli.js
@@ -3,7 +3,6 @@ const path = require('path')
const meow = require('meow')
const open = require('react-dev-utils/openBrowser')
const chalk = require('chalk')
-const ok = require('ok-cli')
const remark = {
emoji: require('remark-emoji'),
unwrapImages: require('remark-unwrap-images')
@@ -132,10 +131,13 @@ const opts = Object.assign({
opts.outDir = path.resolve(opts.outDir)
+let dev
+
switch (cmd) {
case 'build':
log('building')
- ok.build(opts)
+ const build = require('./lib/build')
+ build(opts)
.then(res => {
log('done')
})
@@ -147,7 +149,8 @@ switch (cmd) {
case 'pdf':
log('exporting to PDF')
const pdf = require('./lib/pdf')
- ok(opts)
+ dev = require('./lib/dev')
+ dev(opts)
.then(({ server }) => {
log('rendering PDF')
pdf(opts)
@@ -169,7 +172,8 @@ switch (cmd) {
case 'dev':
default:
log('starting dev server')
- ok(opts)
+ dev = require('./lib/dev')
+ dev(opts)
.then(res => {
const url = 'http://localhost:' + res.port
if (opts.open) open(url)
diff --git a/docs/components.md b/docs/components.md
index 47f82fb..72f0d0a 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -3,6 +3,18 @@
mdx-deck includes a few built-in components to help with creating presentations.
+## Head
+
+TK
+
+```mdx
+import { Head } from 'mdx-deck'
+
+
+ My Presentation
+
+```
+
## Image
Use the `` component to render a fullscreen image (using the CSS `background-image` property).
diff --git a/lib/build.js b/lib/build.js
new file mode 100644
index 0000000..9f7cd81
--- /dev/null
+++ b/lib/build.js
@@ -0,0 +1,23 @@
+const webpack = require('webpack')
+const createConfig = require('./config')
+
+const build = async (opts = {}) => {
+ const config = createConfig(opts)
+ config.mode = 'production'
+ config.output = {
+ path: opts.outDir
+ }
+ const compiler = webpack(config)
+
+ return new Promise((resolve, reject) => {
+ compiler.run((err, stats) => {
+ if (err) {
+ reject(err)
+ return
+ }
+ resolve(stats)
+ })
+ })
+}
+
+module.exports = build
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 0000000..9ed413d
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,129 @@
+const path = require('path')
+const webpack = require('webpack')
+const HTMLPlugin = require('mini-html-webpack-plugin')
+const ProgressBarPlugin = require('progress-bar-webpack-plugin')
+const chalk = require('chalk')
+
+const babel = {
+ presets: [
+ 'babel-preset-env',
+ 'babel-preset-stage-0',
+ 'babel-preset-react',
+ ].map(require.resolve)
+}
+
+const rules = [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: require.resolve('babel-loader'),
+ options: babel
+ },
+ {
+ test: /\.js$/,
+ exclude: path.resolve(__dirname, '../node_modules'),
+ include: [
+ path.resolve(__dirname, '..'),
+ ],
+ loader: require.resolve('babel-loader'),
+ options: babel
+ },
+ {
+ test: /\.mdx?$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: require.resolve('babel-loader'),
+ options: babel
+ },
+ require.resolve('@mdx-js/loader'),
+ ]
+ }
+]
+
+const template = ({
+ title = 'ok',
+ js,
+ publicPath
+}) => `
+
+
+
+
+
+${title}
+
+
+
+${HTMLPlugin.generateJSReferences(js, publicPath)}
+
+
+`
+
+const baseConfig = {
+ stats: 'errors-only',
+ mode: 'development',
+ module: {
+ rules
+ },
+ resolve: {
+ modules: [
+ path.relative(process.cwd(), path.join(__dirname, '../node_modules')),
+ 'node_modules'
+ ]
+ },
+ plugins: [
+ new ProgressBarPlugin({
+ width: '24',
+ complete: '█',
+ incomplete: chalk.gray('░'),
+ format: [
+ chalk.magenta('[mdx-deck] :bar'),
+ chalk.magenta(':percent'),
+ chalk.gray(':elapseds :msg'),
+ ].join(' '),
+ summary: false,
+ customSummary: () => {},
+ })
+ ]
+}
+
+const createConfig = (opts = {}) => {
+ baseConfig.context = opts.dirname
+
+ baseConfig.resolve.modules.push(
+ opts.dirname,
+ path.join(opts.dirname, 'node_modules')
+ )
+
+ baseConfig.entry = [
+ path.join(__dirname, '../src/entry.js')
+ ]
+
+ const defs = Object.assign({}, opts.globals, {
+ OPTIONS: JSON.stringify(opts),
+ APP_FILENAME: JSON.stringify(opts.entry),
+ HOT_PORT: JSON.stringify(opts.hotPort)
+ })
+
+ baseConfig.plugins.push(
+ new webpack.DefinePlugin(defs),
+ new HTMLPlugin({ template, context: opts })
+ )
+
+ const config = typeof opts.config === 'function'
+ ? opts.config(baseConfig)
+ : baseConfig
+
+ if (config.resolve.alias) {
+ const hotAlias = config.resolve.alias['webpack-hot-client/client']
+ if (!fs.existsSync(hotAlias)) {
+ const hotPath = path.dirname(require.resolve('webpack-hot-client/client'))
+ config.resolve.alias['webpack-hot-client/client'] = hotPath
+ }
+ }
+
+ return config
+}
+
+module.exports = createConfig
diff --git a/lib/dev.js b/lib/dev.js
new file mode 100644
index 0000000..e503ae8
--- /dev/null
+++ b/lib/dev.js
@@ -0,0 +1,45 @@
+const path = require('path')
+const Koa = require('koa')
+const getPort = require('get-port')
+const koaWebpack = require('koa-webpack')
+const koaStatic = require('koa-static')
+const createConfig = require('./config')
+
+const devMiddleware = {
+ publicPath: '/',
+ clientLogLevel: 'error',
+ stats: 'errors-only',
+ logLevel: 'error',
+}
+
+const start = async (opts = {}) => {
+ const app = new Koa()
+ opts.hotPort = await getPort()
+ const hotClient = {
+ port: opts.hotPort,
+ logLevel: 'error'
+ }
+ opts.dirname = opts.dirname || path.dirname(opts.entry)
+ const config = createConfig(opts)
+ config.entry.push(
+ path.join(__dirname, './overlay.js'),
+ )
+
+ const middleware = await koaWebpack({
+ config,
+ devMiddleware,
+ hotClient
+ })
+ const port = opts.port || await getPort()
+ app.use(middleware)
+ app.use(koaStatic(opts.dirname))
+
+ const server = app.listen(port)
+ return new Promise((resolve) => {
+ middleware.devMiddleware.waitUntilValid(() => {
+ resolve({ server, app, middleware, port })
+ })
+ })
+}
+
+module.exports = start
diff --git a/lib/overlay.js b/lib/overlay.js
new file mode 100644
index 0000000..436ae9a
--- /dev/null
+++ b/lib/overlay.js
@@ -0,0 +1,81 @@
+const ansiHTML = require('ansi-html')
+const Entities = require('html-entities').AllHtmlEntities
+const entities = new Entities()
+
+const colors = {
+ reset: ['transparent', 'transparent'],
+ black: '000000',
+ red: 'FF0000',
+ green: '00FF00',
+ yellow: 'FFFF00',
+ blue: '0000FF',
+ magenta: 'FF00FF',
+ cyan: '00FFFF',
+ lightgrey: 'EEEEEE',
+ darkgrey: '666666'
+};
+ansiHTML.setColors(colors)
+
+let overlay
+
+const style = (el, styles) => {
+ for (const key in styles) {
+ el.style[key] = styles[key]
+ }
+ return el
+}
+
+const show = ({
+ title = '',
+ text = ''
+}) => {
+ overlay = document.body.appendChild(
+ document.createElement('pre')
+ )
+ style(overlay, {
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ boxSizing: 'border-box',
+ fontFamily: 'Menlo, monospace',
+ fontSize: '12px',
+ overflow: 'auto',
+ lineHeight: 1.5,
+ padding: '8px',
+ margin: 0,
+ color: 'magenta',
+ backgroundColor: 'black'
+ })
+ const code = ansiHTML(entities.encode(text))
+ overlay.innerHTML = `${title}
+
+
${code}
+ `
+}
+
+const destroy = () => {
+ if (!overlay) return
+ document.body.removeChild(overlay)
+}
+
+const ws = new WebSocket('ws://localhost:' + HOT_PORT)
+
+ws.addEventListener('message', msg => {
+ const data = JSON.parse(msg.data)
+ switch (data.type) {
+ case 'errors':
+ const [ text ] = data.data.errors
+ console.error(data.data.errors)
+ show({ title: 'failed to compile', text })
+ break
+ case 'ok':
+ destroy()
+ break
+ }
+})
+
+ws.addEventListener('close', () => {
+ show({ title: 'disconnected' })
+})
diff --git a/package.json b/package.json
index b9d7a3e..87b1e7c 100644
--- a/package.json
+++ b/package.json
@@ -23,15 +23,21 @@
"@compositor/webfont": "^1.0.39",
"@mdx-js/mdx": "^0.15.0-1",
"@mdx-js/tag": "^0.14.1",
+ "ansi-html": "0.0.7",
"chalk": "^2.4.1",
"clipboardy": "^1.2.3",
+ "get-port": "^4.0.0",
"gray-matter": "^4.0.1",
"hhmmss": "^1.0.0",
+ "html-entities": "^1.2.1",
+ "koa": "^2.5.2",
+ "koa-static": "^5.0.0",
+ "koa-webpack": "^5.1.0",
"loader-utils": "^1.1.0",
"lodash.debounce": "^4.0.8",
"meow": "^5.0.0",
+ "mini-html-webpack-plugin": "^0.2.3",
"normalize-newline": "^3.0.0",
- "ok-cli": "^3.1.1",
"pkg-conf": "^2.1.0",
"prop-types": "^15.6.2",
"puppeteer": "^1.6.1",
@@ -46,6 +52,7 @@
"styled-components": ">=3.0.0",
"styled-system": "^3.0.2",
"superbox": "^2.1.0",
+ "webpack": "^4.16.5",
"webpack-hot-client": "^4.1.1"
},
"peerDependencies": {},
diff --git a/src/Head.js b/src/Head.js
new file mode 100644
index 0000000..e775374
--- /dev/null
+++ b/src/Head.js
@@ -0,0 +1,94 @@
+import React from 'react'
+import { createPortal } from 'react-dom'
+
+const noop = () => {
+ console.warn('Missing HeadProvider')
+}
+
+export const Context = React.createContext({
+ tags: [],
+ push: noop
+})
+
+export class HeadProvider extends React.Component {
+ static defaultProps = {
+ tags: []
+ }
+
+ push = (elements) => {
+ this.props.tags.push(...elements)
+ }
+
+ render () {
+ const context = {
+ ...this.state,
+ push: this.push
+ }
+
+ return (
+
+ {this.props.children}
+
+ )
+ }
+}
+
+export class Head extends React.Component {
+ state = {
+ didMount: false
+ }
+
+ rehydrate = () => {
+ const children = React.Children.toArray(this.props.children)
+ const nodes = [
+ ...document.head.querySelectorAll('[data-head]')
+ ]
+
+ nodes.forEach(node => {
+ node.remove()
+ })
+ children.forEach(child => {
+ if (child.type === 'title') {
+ const title = document.head.querySelector('title')
+ if (title) title.remove()
+ }
+ if (child.type === 'meta') {
+ const meta = document.head.querySelector(`meta[name=${child.props.name}]`)
+ if (meta) meta.remove()
+ }
+ })
+
+ this.setState({
+ didMount: true
+ })
+ }
+
+ componentDidMount () {
+ this.rehydrate()
+ }
+
+ render () {
+ const children = React.Children.toArray(this.props.children)
+ .map(child => React.cloneElement(child, {
+ 'data-head': true
+ }))
+
+ const { didMount } = this.state
+
+ if (!didMount) {
+ return (
+ {
+ push(children)
+ return false
+ }}
+ />
+ )
+ }
+
+ return createPortal(
+ children,
+ document.head
+ )
+ }
+}
diff --git a/src/index.js b/src/index.js
index 67e1e23..5159887 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,6 +19,7 @@ import GoogleFonts from './GoogleFonts'
import defaultTheme from './themes'
import defaultComponents from './components'
+export { default as Head } from './Head'
export { default as Image } from './Image'
export { default as Notes } from './Notes'
export { default as Appear } from './Appear'