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'