1
1
mirror of https://github.com/jxnblk/mdx-deck.git synced 2024-11-26 00:35:02 +03:00

Add Head component and move webpack into repo

This commit is contained in:
Brent Jackson 2018-08-14 18:33:45 -04:00
parent fbafcb21b9
commit 8be9abe530
9 changed files with 401 additions and 5 deletions

12
cli.js
View File

@ -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)

View File

@ -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'
<Head>
<title>My Presentation</title>
</Head>
```
## Image
Use the `<Image />` component to render a fullscreen image (using the CSS `background-image` property).

23
lib/build.js Normal file
View File

@ -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

129
lib/config.js Normal file
View File

@ -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
}) => `<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<style>*{box-sizing:border-box}body{font-family:system-ui,sans-serif;margin:0}</style>
<title>${title}</title>
</head>
<body>
<div id=root></div>
${HTMLPlugin.generateJSReferences(js, publicPath)}
</body>
</html>
`
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

45
lib/dev.js Normal file
View File

@ -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

81
lib/overlay.js Normal file
View File

@ -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 = `<span>${title}</span>
<br />
<br />${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' })
})

View File

@ -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": {},

94
src/Head.js Normal file
View File

@ -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 (
<Context.Provider value={context}>
{this.props.children}
</Context.Provider>
)
}
}
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 (
<Context.Consumer
children={({ push }) => {
push(children)
return false
}}
/>
)
}
return createPortal(
children,
document.head
)
}
}

View File

@ -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'