mirror of
https://github.com/c8r/x0.git
synced 2024-08-16 17:00:24 +03:00
commit
3f3ab2e5fb
10
.babelrc
Normal file
10
.babelrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"presets": [
|
||||
"env",
|
||||
"stage-0",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
]
|
||||
}
|
@ -8,4 +8,5 @@ examples
|
||||
demo
|
||||
dist
|
||||
.travis.yml
|
||||
.babelrc
|
||||
create-x0
|
||||
|
25
README.md
25
README.md
@ -1,7 +1,7 @@
|
||||
|
||||
# x0
|
||||
|
||||
Zero-config React development environment and static site generator
|
||||
Document & develop React components without breaking a sweat
|
||||
|
||||
[![Build Status][build-badge]][build]
|
||||
|
||||
@ -17,19 +17,16 @@ npm install -g @compositor/x0
|
||||
## Features
|
||||
|
||||
- Zero-config
|
||||
- Hot-loading development environment
|
||||
- Works with virtually any React component\*
|
||||
- No confusing APIs
|
||||
- No plugins
|
||||
- Components over configuration
|
||||
- Use markdown, MDX, or React components
|
||||
- Automatic file system based routing
|
||||
- Exports static HTML
|
||||
- Exports JS bundles
|
||||
- Works with CSS-in-JS libraries like [styled-components][sc] and [emotion][emotion]
|
||||
- Support for async data fetching
|
||||
- Completely customizable
|
||||
- Export static sites
|
||||
- Works as an isolated development environment
|
||||
|
||||
Read more about x0 in our [blog post](https://compositor.io/blog/x0-making-react-component-development-stupid-simple/).
|
||||
|
||||
\* Custom [webpack configuration](#webpack) is required for components that rely on webpack-based features
|
||||
|
||||
## Getting Started
|
||||
|
||||
x0 renders a directory of React components, automatically handling routing based on filename.
|
||||
@ -315,16 +312,17 @@ export default () => (
|
||||
|
||||
### JSX Format
|
||||
|
||||
x0 includes support for the [Compositor JSX][jsx-loader] file format.
|
||||
x0 includes support for the Compositor JSX file format.
|
||||
|
||||
```jsx
|
||||
---
|
||||
title: Hello
|
||||
scope: import * as scope from 'rebass'
|
||||
---
|
||||
import { Box, Heading } from 'rebass'
|
||||
|
||||
<Box px={2} py={4}>
|
||||
<Heading>
|
||||
{props.title}
|
||||
{frontMatter.title}
|
||||
</Heading>
|
||||
</Box>
|
||||
```
|
||||
@ -395,3 +393,4 @@ See the [example](https://github.com/c8r/x0/tree/master/examples/webpack-config)
|
||||
[react-loadable]: https://github.com/thejameskyle/react-loadable
|
||||
[webpack-merge]: https://github.com/survivejs/webpack-merge
|
||||
[webpack]: https://webpack.js.org
|
||||
|
||||
|
22
cli.js
22
cli.js
@ -28,11 +28,13 @@ const cli = meow(`
|
||||
${chalk.gray('Options')}
|
||||
|
||||
--webpack Path to webpack config file
|
||||
--match String to match routes against using minimatch
|
||||
|
||||
${chalk.gray('Dev Server')}
|
||||
|
||||
-o --open Open dev server in default browser
|
||||
-p --port Port for dev server
|
||||
--analyze Runs with webpack-bundle-analyzer plugin
|
||||
|
||||
${chalk.gray('Build')}
|
||||
|
||||
@ -51,6 +53,7 @@ const cli = meow(`
|
||||
type: 'string',
|
||||
alias: 'p'
|
||||
},
|
||||
analyze: {},
|
||||
// build
|
||||
outDir: {
|
||||
type: 'string',
|
||||
@ -68,6 +71,9 @@ const cli = meow(`
|
||||
type: 'string',
|
||||
alias: 'c'
|
||||
},
|
||||
match: {
|
||||
type: 'string'
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
},
|
||||
@ -81,6 +87,11 @@ const cli = meow(`
|
||||
})
|
||||
|
||||
const [ cmd, file ] = cli.input
|
||||
|
||||
if (!cmd) {
|
||||
cli.showHelp(0)
|
||||
}
|
||||
|
||||
const input = path.resolve(file || cmd)
|
||||
const stats = fs.statSync(input)
|
||||
const dirname = stats.isDirectory() ? input : path.dirname(input)
|
||||
@ -106,13 +117,6 @@ if (opts.webpack) {
|
||||
if (webpackConfig) opts.webpack = require(webpackConfig)
|
||||
}
|
||||
|
||||
if (opts.app) {
|
||||
opts.app = path.resolve(opts.app)
|
||||
} else {
|
||||
const app = findup.sync('_app.js', { cwd: dirname })
|
||||
if (app) opts.app = app
|
||||
}
|
||||
|
||||
if (opts.template) {
|
||||
opts.template = require(path.resolve(opts.template))
|
||||
}
|
||||
@ -127,7 +131,7 @@ log(chalk.cyan('@compositor/x0'))
|
||||
switch (cmd) {
|
||||
case 'build':
|
||||
log.start('building static site')
|
||||
const { build } = require('.')
|
||||
const build = require('./lib/build')
|
||||
build(opts)
|
||||
.then(res => {
|
||||
log.stop('site saved to ' + opts.outDir)
|
||||
@ -137,7 +141,7 @@ switch (cmd) {
|
||||
case 'dev':
|
||||
default:
|
||||
log.start('starting dev server')
|
||||
const { dev } = require('.')
|
||||
const dev = require('./lib/dev')
|
||||
dev(opts)
|
||||
.then(({ server }) => {
|
||||
const { port } = server.options
|
||||
|
1
components.js
Normal file
1
components.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./src')
|
2
docs/404.md
Normal file
2
docs/404.md
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
# Page not found
|
84
docs/_app.js
84
docs/_app.js
@ -1,58 +1,40 @@
|
||||
import React from 'react'
|
||||
import * as scope from 'rebass'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ScopeProvider, SidebarLayout } from '../components'
|
||||
import {
|
||||
Provider,
|
||||
Provider as RebassProvider,
|
||||
Flex,
|
||||
Box,
|
||||
Container,
|
||||
Text,
|
||||
Caps,
|
||||
BlockLink,
|
||||
} from 'rebass'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Logo } from '@compositor/logo'
|
||||
import theme from './theme'
|
||||
|
||||
export default ({ render }) =>
|
||||
<Provider theme={theme}>
|
||||
<Flex alignItems='center'>
|
||||
<BlockLink
|
||||
href='https://compositor.io'>
|
||||
<Flex px={1} py={2} alignItems='center'>
|
||||
<Logo size={32} />
|
||||
<Caps fontWeight='bold'>
|
||||
Compositor
|
||||
</Caps>
|
||||
</Flex>
|
||||
</BlockLink>
|
||||
<Box mx='auto' />
|
||||
<BlockLink px={3} is={Link} to='/'>
|
||||
<Caps fontWeight='bold'>
|
||||
x0
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
<BlockLink px={3} href='https://github.com/c8r/x0'>
|
||||
<Caps fontWeight='bold'>
|
||||
GitHub
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
</Flex>
|
||||
{render()}
|
||||
<Container>
|
||||
<Flex py={4} mt={5} flexWrap='wrap'>
|
||||
<BlockLink my={2} mr={3} href='https://github.com/c8r/x0'>
|
||||
<Caps fontWeight='bold'>
|
||||
GitHub
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
<BlockLink my={2} mr={3} href='https://compositor.io'>
|
||||
<Caps fontWeight='bold'>
|
||||
Compositor
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
<Box mx='auto' />
|
||||
<Text my={2} fontSize={0}>
|
||||
© 2018 Compositor, Inc. All rights reserved
|
||||
</Text>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Provider>
|
||||
import LandingLayout from './_layout'
|
||||
import theme from './_theme'
|
||||
|
||||
export default class App extends React.Component {
|
||||
static defaultProps = {
|
||||
title: 'x0'
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes,
|
||||
route,
|
||||
children,
|
||||
} = this.props
|
||||
const { layout } = (route && route.props) || {}
|
||||
|
||||
const Layout = layout === 'landing'
|
||||
? LandingLayout
|
||||
: SidebarLayout
|
||||
|
||||
return (
|
||||
<ScopeProvider scope={scope}>
|
||||
<RebassProvider theme={theme}>
|
||||
<Layout {...this.props} />
|
||||
</RebassProvider>
|
||||
</ScopeProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
56
docs/_layout.js
Normal file
56
docs/_layout.js
Normal file
@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Container,
|
||||
Text,
|
||||
Caps,
|
||||
BlockLink,
|
||||
} from 'rebass'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Logo } from '@compositor/logo'
|
||||
|
||||
export default ({ children }) =>
|
||||
<React.Fragment>
|
||||
<Flex alignItems='center'>
|
||||
<BlockLink
|
||||
href='https://compositor.io'>
|
||||
<Flex px={1} py={2} alignItems='center'>
|
||||
<Logo size={32} />
|
||||
<Caps fontWeight='bold'>
|
||||
Compositor
|
||||
</Caps>
|
||||
</Flex>
|
||||
</BlockLink>
|
||||
<Box mx='auto' />
|
||||
<BlockLink px={3} is={Link} to='/'>
|
||||
<Caps fontWeight='bold'>
|
||||
x0
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
<BlockLink px={3} href='https://github.com/c8r/x0'>
|
||||
<Caps fontWeight='bold'>
|
||||
GitHub
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
</Flex>
|
||||
{children}
|
||||
<Container>
|
||||
<Flex py={4} mt={5} flexWrap='wrap'>
|
||||
<BlockLink my={2} mr={3} href='https://github.com/c8r/x0'>
|
||||
<Caps fontWeight='bold'>
|
||||
GitHub
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
<BlockLink my={2} mr={3} href='https://compositor.io'>
|
||||
<Caps fontWeight='bold'>
|
||||
Compositor
|
||||
</Caps>
|
||||
</BlockLink>
|
||||
<Box mx='auto' />
|
||||
<Text my={2} fontSize={0}>
|
||||
© 2018 Compositor, Inc. All rights reserved
|
||||
</Text>
|
||||
</Flex>
|
||||
</Container>
|
||||
</React.Fragment>
|
@ -1,46 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import * as Rebass from 'rebass'
|
||||
import { heading, link } from '@compositor/md'
|
||||
|
||||
const Pre = styled(Rebass.Pre)({
|
||||
borderRadius: '8px'
|
||||
})
|
||||
|
||||
export default {
|
||||
...Rebass,
|
||||
h1: heading(props =>
|
||||
<Rebass.Heading
|
||||
{...props}
|
||||
is='h1'
|
||||
fontSize={6}
|
||||
mt={3}
|
||||
/>),
|
||||
h2: heading(props =>
|
||||
<Rebass.Heading
|
||||
{...props}
|
||||
is='h2'
|
||||
fontSize={5}
|
||||
mt={3}
|
||||
mb={2}
|
||||
/>),
|
||||
h3: heading(props =>
|
||||
<Rebass.Heading
|
||||
{...props}
|
||||
is='h3'
|
||||
fontSize={4}
|
||||
mt={2}
|
||||
/>),
|
||||
a: link(props =>
|
||||
<Rebass.Link
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
pre: props =>
|
||||
<Pre
|
||||
{...props}
|
||||
p={3}
|
||||
mb={3}
|
||||
bg='#f6f6fc'
|
||||
/>
|
||||
}
|
27
docs/docs.js
27
docs/docs.js
@ -1,27 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Container
|
||||
} from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import Readme from '../README.md'
|
||||
import scope from './_scope'
|
||||
|
||||
const Prose = styled.div([], {
|
||||
'& img': {
|
||||
display: 'block',
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
'& .demo-image, & a[href="LICENSE/"]': {
|
||||
display: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
export default props =>
|
||||
<Container py={5}>
|
||||
<Prose>
|
||||
<Readme
|
||||
scope={scope}
|
||||
/>
|
||||
</Prose>
|
||||
</Container>
|
@ -1,37 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const routes = [
|
||||
{ path: '/dynamic' },
|
||||
{ path: '/dynamic/hello' },
|
||||
{ path: '/dynamic/hi' },
|
||||
]
|
||||
|
||||
export default class extends React.Component {
|
||||
static getInitialProps = async ({ path }) => {
|
||||
let title = 'dynamic'
|
||||
switch (path) {
|
||||
case '/dynamic/hello':
|
||||
title = 'hello'
|
||||
break
|
||||
case '/dynamic/hi':
|
||||
title = 'hi'
|
||||
break
|
||||
}
|
||||
return {
|
||||
routes,
|
||||
path: '/dynamic/:id*',
|
||||
title,
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div>
|
||||
<pre>dynamic routing</pre>
|
||||
<Link to='/'>Home</Link>
|
||||
<Link to='/dynamic'>Dynamic Routes</Link>
|
||||
<Link to='/dynamic/hello'>Hello</Link>
|
||||
<Link to='/dynamic/hi'>Hi</Link>
|
||||
</div>
|
||||
}
|
||||
}
|
@ -21,6 +21,10 @@ const Video = styled.video([], {
|
||||
})
|
||||
|
||||
export default class extends React.Component {
|
||||
static defaultProps = {
|
||||
layout: 'landing'
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<React.Fragment>
|
||||
@ -54,6 +58,7 @@ export default class extends React.Component {
|
||||
GitHub
|
||||
</Button>
|
||||
<Box mx={1} />
|
||||
{/*
|
||||
<Button
|
||||
is={Link}
|
||||
px={4}
|
||||
@ -62,6 +67,7 @@ export default class extends React.Component {
|
||||
to='/docs'>
|
||||
Documentation
|
||||
</Button>
|
||||
*/}
|
||||
</Flex>
|
||||
</Container>
|
||||
</React.Fragment>
|
||||
|
@ -1,23 +0,0 @@
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.md$/,
|
||||
use: [
|
||||
// 'raw-loader'
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
'env',
|
||||
'stage-0',
|
||||
'react'
|
||||
]
|
||||
}
|
||||
},
|
||||
'@compositor/md-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Hello JSX
|
||||
scope: import scope from './_scope'
|
||||
---
|
||||
import { Box, Heading, Text, Link } from 'rebass'
|
||||
|
||||
<Box px={3} py={4}>
|
||||
<Heading is='h1' mb={2}>
|
||||
{props.title}
|
||||
|
57
lib/build.js
57
lib/build.js
@ -1,4 +1,4 @@
|
||||
const fs = require('fs-extra')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const MiniHTMLWebpackPlugin = require('mini-html-webpack-plugin')
|
||||
@ -8,7 +8,7 @@ const React = require('react')
|
||||
const { renderToString, renderToStaticMarkup } = require('react-dom/server')
|
||||
const { StaticRouter } = require('react-router-dom')
|
||||
const semver = require('semver')
|
||||
|
||||
const rimraf = require('rimraf')
|
||||
const util = require('util')
|
||||
|
||||
const baseConfig = require('./config')
|
||||
@ -18,7 +18,7 @@ const getApp = opts => {
|
||||
const config = merge(baseConfig, opts.webpack)
|
||||
|
||||
config.mode = 'development'
|
||||
config.entry = opts.entry || path.join(__dirname, './entry.js')
|
||||
config.entry = opts.entry || path.join(__dirname, '../src/entry.js')
|
||||
config.output= {
|
||||
path: opts.tempdir,
|
||||
filename: 'App.js',
|
||||
@ -44,7 +44,6 @@ const getApp = opts => {
|
||||
|
||||
const STYLED_COMPONENTS_VERSION = '>=3.0'
|
||||
const EMOTION_VERSION = '>=9.0'
|
||||
const GLAMOR_VERSION = '>=2.0'
|
||||
|
||||
const getCSSLibrary = opts => {
|
||||
if (opts.cssLibrary) return opts.cssLibrary
|
||||
@ -63,11 +62,6 @@ const getCSSLibrary = opts => {
|
||||
if (!semver.satisfies(emotionVersion, EMOTION_VERSION)) return null
|
||||
return 'emotion'
|
||||
}
|
||||
if (deps.glamor) { // || deps.glamorous) {
|
||||
const glamorVersion = semver.coerce(deps.glamor)
|
||||
if (!semver.satisfies(glamorVersion, GLAMOR_VERSION)) return null
|
||||
return 'glamor'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@ -88,35 +82,25 @@ const renderHTML = ({
|
||||
const { ServerStyleSheet } = require('styled-components')
|
||||
const sheet = new ServerStyleSheet()
|
||||
html = render(
|
||||
sheet.collectStyles(
|
||||
React.createElement(App.default, { routes, path })
|
||||
)
|
||||
sheet.collectStyles(app)
|
||||
)
|
||||
css = sheet.getStyleTags()
|
||||
return { path, html, css, props }
|
||||
break
|
||||
case 'emotion':
|
||||
const { renderStylesToString } = require('emotion-server')
|
||||
html = renderStylesToString(
|
||||
render(app)
|
||||
)
|
||||
return { path, html, props }
|
||||
case 'glamor':
|
||||
// doesn't seem to be working...
|
||||
const glamor = require('glamor/server')
|
||||
const res = glamor.renderStatic(() => (
|
||||
render(app)
|
||||
))
|
||||
html = res.html
|
||||
css = `<style>${res.css}</style>`
|
||||
return { path, html, css, props }
|
||||
break
|
||||
default:
|
||||
html = render(app)
|
||||
return { path, html, props }
|
||||
break
|
||||
}
|
||||
return { html, css, path, props }
|
||||
}
|
||||
|
||||
const remove = filename => {
|
||||
fs.remove(filename, err => {
|
||||
rimraf(filename, err => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
}
|
||||
@ -165,7 +149,7 @@ module.exports = async (opts) => {
|
||||
DEV: JSON.stringify(false),
|
||||
OPTIONS: JSON.stringify(opts),
|
||||
DIRNAME: JSON.stringify(opts.dirname),
|
||||
APP: JSON.stringify(opts.app)
|
||||
MATCH: JSON.stringify(opts.match)
|
||||
})
|
||||
)
|
||||
|
||||
@ -191,7 +175,7 @@ module.exports = async (opts) => {
|
||||
if (opts.debug) {
|
||||
config.stats = 'verbose'
|
||||
}
|
||||
config.entry = path.join(__dirname, './entry')
|
||||
config.entry = path.join(__dirname, '../src/entry')
|
||||
config.output = {
|
||||
path: opts.outDir,
|
||||
filename: 'bundle.js',
|
||||
@ -208,15 +192,32 @@ module.exports = async (opts) => {
|
||||
})
|
||||
)
|
||||
})
|
||||
// 404
|
||||
config.plugins.push(
|
||||
new MiniHTMLWebpackPlugin({
|
||||
filename: '404.html',
|
||||
template,
|
||||
context: Object.assign({}, opts,
|
||||
renderHTML({
|
||||
opts,
|
||||
routes,
|
||||
App,
|
||||
props: {},
|
||||
path: '/404'
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
const compiler = webpack(config)
|
||||
|
||||
remove(opts.tempdir)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
remove(opts.tempdir)
|
||||
if (opts.static) {
|
||||
const bundle = path.join(opts.outDir, 'bundle.js')
|
||||
remove(bundle)
|
||||
|
@ -1,5 +1,12 @@
|
||||
const path = require('path')
|
||||
|
||||
const remark = {
|
||||
images: require('remark-images'),
|
||||
emoji: require('remark-emoji'),
|
||||
slug: require('remark-slug'),
|
||||
autolinkHeadings: require('remark-autolink-headings'),
|
||||
}
|
||||
|
||||
const babel = {
|
||||
presets: [
|
||||
'babel-preset-env',
|
||||
@ -22,25 +29,32 @@ const rules = [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: path.resolve(__dirname, '../node_modules'),
|
||||
include: path.resolve(__dirname),
|
||||
include: [
|
||||
path.resolve(__dirname, '..'),
|
||||
path.resolve(__dirname, '../src')
|
||||
],
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
},
|
||||
{
|
||||
test: /\.jsx$/,
|
||||
loader: require.resolve('@compositor/jsx-loader'),
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
test: /\.mdx$/,
|
||||
test: /\.(md|mdx|jsx)$/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
},
|
||||
{
|
||||
loader: require.resolve('@mdx-js/loader')
|
||||
}
|
||||
loader: require.resolve('@mdx-js/loader'),
|
||||
options: {
|
||||
mdPlugins: [
|
||||
remark.slug,
|
||||
remark.autolinkHeadings,
|
||||
remark.images,
|
||||
remark.emoji,
|
||||
]
|
||||
}
|
||||
},
|
||||
path.join(__dirname, './mdx-fm-loader'),
|
||||
]
|
||||
}
|
||||
]
|
||||
|
16
lib/dev.js
16
lib/dev.js
@ -24,7 +24,7 @@ module.exports = async (opts) => {
|
||||
|
||||
config.mode = 'development'
|
||||
config.context = opts.dirname
|
||||
config.entry = opts.entry || path.join(__dirname, './entry')
|
||||
config.entry = opts.entry || path.join(__dirname, '../src/entry')
|
||||
config.output = {
|
||||
path: path.join(process.cwd(), 'dev'),
|
||||
filename: 'dev.js',
|
||||
@ -49,7 +49,7 @@ module.exports = async (opts) => {
|
||||
DEV: JSON.stringify(true),
|
||||
OPTIONS: JSON.stringify(opts),
|
||||
DIRNAME: JSON.stringify(opts.dirname),
|
||||
APP: JSON.stringify(opts.app),
|
||||
MATCH: JSON.stringify(opts.match)
|
||||
})
|
||||
)
|
||||
|
||||
@ -60,6 +60,18 @@ module.exports = async (opts) => {
|
||||
})
|
||||
)
|
||||
|
||||
if (opts.analyze) {
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
|
||||
const analyzerPort = typeof opts.analyze === 'string'
|
||||
? opts.analyze
|
||||
: 8888
|
||||
config.plugins.push(
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerPort
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (opts.debug) {
|
||||
config.stats = 'verbose'
|
||||
// todo: enable other logging
|
||||
|
194
lib/entry.js
194
lib/entry.js
@ -1,194 +0,0 @@
|
||||
import path from 'path'
|
||||
import React from 'react'
|
||||
import { render, hydrate } from 'react-dom'
|
||||
import {
|
||||
StaticRouter,
|
||||
BrowserRouter,
|
||||
Switch,
|
||||
Route,
|
||||
Link,
|
||||
withRouter
|
||||
} from 'react-router-dom'
|
||||
|
||||
const IS_CLIENT = typeof document !== 'undefined'
|
||||
const req = require.context(DIRNAME, false, /\.(js|mdx|jsx)$/)
|
||||
|
||||
const { filename, basename = '', disableScroll } = OPTIONS
|
||||
const index = filename ? path.basename(filename, path.extname(filename)) : 'index'
|
||||
|
||||
const getComponents = req => req.keys().map(key => ({
|
||||
key,
|
||||
name: path.basename(key, path.extname(key)),
|
||||
module: req(key),
|
||||
Component: req(key).default || req(key)
|
||||
}))
|
||||
.filter(component => !/^(\.|_)/.test(component.name))
|
||||
.filter(component => typeof component.Component === 'function')
|
||||
|
||||
const initialComponents = getComponents(req)
|
||||
|
||||
const Index = ({ routes = [] }) => (
|
||||
<React.Fragment>
|
||||
<pre>{DIRNAME}</pre>
|
||||
<ul>
|
||||
{routes.map(route => (
|
||||
<li key={route.key}>
|
||||
<Link to={route.path}>
|
||||
{route.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
const DefaultApp = ({ render, routes }) => (
|
||||
<Switch>
|
||||
{render()}
|
||||
<Route render={props => (
|
||||
<Index
|
||||
{...props}
|
||||
routes={routes}
|
||||
/>
|
||||
)} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
class Catch extends React.Component {
|
||||
static getDerivedStateFromProps (props, state) {
|
||||
if (!state.err) return null
|
||||
return { err: null }
|
||||
}
|
||||
|
||||
state = {
|
||||
err: null
|
||||
}
|
||||
|
||||
componentDidCatch (err) {
|
||||
this.setState({ err })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { err } = this.state
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<pre
|
||||
children={err.toString()}
|
||||
style={{
|
||||
color: 'white',
|
||||
backgroundColor: 'red',
|
||||
fontFamily: 'Menlo, monospace',
|
||||
fontSize: '14px',
|
||||
margin: 0,
|
||||
padding: '16px',
|
||||
minHeight: '128px',
|
||||
whiteSpace: 'prewrap'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
const ScrollTop = withRouter(class extends React.Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location.pathname !== prevProps.location.pathname) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const Router = IS_CLIENT ? BrowserRouter : StaticRouter
|
||||
const App = withRouter(APP ? (require(APP).default || require(APP)) : DefaultApp)
|
||||
|
||||
export const getRoutes = async (components = initialComponents) => {
|
||||
const routes = await components.map(async ({ key, name, module, Component }) => {
|
||||
const exact = name === index
|
||||
let pathname = exact ? '/' : '/' + name
|
||||
const props = Component.getInitialProps
|
||||
? await Component.getInitialProps({ path: pathname })
|
||||
: {}
|
||||
pathname = props.path || pathname
|
||||
return {
|
||||
key: name,
|
||||
name,
|
||||
path: pathname,
|
||||
exact,
|
||||
module,
|
||||
Component,
|
||||
props
|
||||
}
|
||||
})
|
||||
return Promise.all(routes)
|
||||
}
|
||||
|
||||
export default class Root extends React.Component {
|
||||
static defaultProps = {
|
||||
path: '/',
|
||||
basename
|
||||
}
|
||||
state = this.props
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes,
|
||||
basename,
|
||||
path = '/'
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<Router
|
||||
context={{}}
|
||||
basename={basename}
|
||||
location={path}>
|
||||
<React.Fragment>
|
||||
<Catch>
|
||||
<App
|
||||
routes={routes}
|
||||
render={appProps => (
|
||||
routes.map(({ Component, ...route }) => (
|
||||
<Route
|
||||
{...route}
|
||||
render={props => (
|
||||
<Catch>
|
||||
<Component
|
||||
{...props}
|
||||
{...appProps}
|
||||
{...route.props}
|
||||
/>
|
||||
</Catch>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
/>
|
||||
</Catch>
|
||||
{!disableScroll && <ScrollTop />}
|
||||
</React.Fragment>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let app
|
||||
if (IS_CLIENT) {
|
||||
const mount = DEV ? render : hydrate
|
||||
const div = window.root || document.body.appendChild(
|
||||
document.createElement('div')
|
||||
)
|
||||
getRoutes()
|
||||
.then(routes => {
|
||||
app = mount(<Root routes={routes} />, div)
|
||||
})
|
||||
}
|
||||
|
||||
if (IS_CLIENT && module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
||||
|
13
lib/mdx-fm-loader.js
Normal file
13
lib/mdx-fm-loader.js
Normal file
@ -0,0 +1,13 @@
|
||||
// front-matter loader for mdx
|
||||
const matter = require('gray-matter')
|
||||
const stringifyObject = require('stringify-object')
|
||||
|
||||
module.exports = async function (src) {
|
||||
const callback = this.async()
|
||||
const { content, data } = matter(src)
|
||||
|
||||
const code = `export const frontMatter = ${stringifyObject(data)}
|
||||
${content}
|
||||
`
|
||||
return callback(null, code)
|
||||
}
|
79
package.json
79
package.json
@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "@compositor/x0",
|
||||
"version": "5.0.8",
|
||||
"description": "Zero-config React development environment and static site generator",
|
||||
"version": "6.0.0-4",
|
||||
"description": "Document & develop React components without breaking a sweat",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"x0": "cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./cli.js docs -op 8888",
|
||||
"start": "./cli.js docs -p 8989",
|
||||
"build": "./cli.js build docs",
|
||||
"test": "ava -T 20s",
|
||||
"test": "nyc ava --timeout=60s",
|
||||
"test:components": "nyc ava test/components.js",
|
||||
"cover": "nyc report --reporter=html --reporter=lcov"
|
||||
},
|
||||
"keywords": [
|
||||
@ -27,64 +28,77 @@
|
||||
"author": "Brent Jackson",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@compositor/jsx-loader": "^1.0.0-4",
|
||||
"@compositor/log": "^1.0.0-0",
|
||||
"@mdx-js/loader": "^0.9.0",
|
||||
"@mdx-js/mdx": "^0.9.0",
|
||||
"@mdx-js/loader": "^0.11.0",
|
||||
"@mdx-js/mdx": "^0.10.1",
|
||||
"@mdx-js/tag": "^0.11.0",
|
||||
"@rebass/markdown": "^1.0.0-1",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-macros": "^2.2.1",
|
||||
"babel-plugin-macros": "^2.2.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"browser-env": "^3.2.5",
|
||||
"chalk": "^2.4.1",
|
||||
"clipboardy": "^1.2.3",
|
||||
"connect-history-api-fallback": "^1.5.0",
|
||||
"emotion": "^9.1.3",
|
||||
"emotion-server": "^9.1.3",
|
||||
"emotion": "^9.2.3",
|
||||
"emotion-server": "^9.2.3",
|
||||
"find-up": "^2.1.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"glamor": "^2.20.40",
|
||||
"gray-matter": "^4.0.1",
|
||||
"html-minifier": "^3.5.16",
|
||||
"koa-connect": "^2.0.1",
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"meow": "^5.0.0",
|
||||
"mini-html-webpack-plugin": "^0.2.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"pkg-conf": "^2.1.0",
|
||||
"react": "^16.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.4.1",
|
||||
"react-dev-utils": "^5.0.1",
|
||||
"react-dom": "^16.4.0",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-dom": "^16.4.1",
|
||||
"react-live": "^1.10.1",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scope-provider": "^1.0.0-1",
|
||||
"read-pkg-up": "^3.0.0",
|
||||
"rebass": "^2.0.0-6",
|
||||
"remark-autolink-headings": "^5.0.0",
|
||||
"remark-emoji": "^2.0.1",
|
||||
"remark-images": "^0.8.1",
|
||||
"remark-slug": "^5.0.0",
|
||||
"semver": "^5.5.0",
|
||||
"stringify-object": "^3.2.2",
|
||||
"styled-components": ">=3.0.0",
|
||||
"styled-system": "^2.3.1",
|
||||
"update-notifier": "^2.5.0",
|
||||
"webpack": "^4.10.2",
|
||||
"webpack-merge": "^4.1.2",
|
||||
"webpack-serve": "^1.0.2"
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"webpack-merge": "^4.1.3",
|
||||
"webpack-serve": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@compositor/logo": "^1.3.5",
|
||||
"@compositor/md-loader": "^1.0.34",
|
||||
"@compositor/logo": "^1.4.0",
|
||||
"ava": "^0.25.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"nano-style": "^1.0.0",
|
||||
"nyc": "^12.0.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"rebass": "^2.0.0-2",
|
||||
"react-test-renderer": "^16.4.1",
|
||||
"refunk": "^3.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"styled-components": "^3.3.0",
|
||||
"styled-system": "^2.2.5"
|
||||
"sinon": "^6.0.0"
|
||||
},
|
||||
"x0": {
|
||||
"title": "Compositor x0",
|
||||
"title": "x0",
|
||||
"basename": "/x0",
|
||||
"meta": [
|
||||
{
|
||||
"name": "description",
|
||||
"content": "Zero-config React development environment and static site generator"
|
||||
"content": "Document & develop React components without breaking a sweat"
|
||||
},
|
||||
{
|
||||
"name": "twitter:card",
|
||||
@ -111,22 +125,13 @@
|
||||
"ava": {
|
||||
"files": [
|
||||
"test/*",
|
||||
"!test/components",
|
||||
"!test/output"
|
||||
"!**/test/components",
|
||||
"!**/test/output"
|
||||
],
|
||||
"require": [
|
||||
"babel-register"
|
||||
],
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env",
|
||||
"stage-0",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
]
|
||||
}
|
||||
"babel": "inherit"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
|
38
src/Catch.js
Normal file
38
src/Catch.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Catch extends React.Component {
|
||||
state = {
|
||||
err: null
|
||||
}
|
||||
|
||||
componentDidCatch (err) {
|
||||
this.setState({ err })
|
||||
}
|
||||
|
||||
componentWillReceiveProps (next) {
|
||||
if (!this.state.err) return
|
||||
this.setState({ err: null })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { err } = this.state
|
||||
|
||||
if (!err) return this.props.children
|
||||
|
||||
return (
|
||||
<pre
|
||||
children={err.toString()}
|
||||
style={{
|
||||
color: 'white',
|
||||
backgroundColor: 'red',
|
||||
fontFamily: 'Menlo, monospace',
|
||||
fontSize: '14px',
|
||||
margin: 0,
|
||||
padding: '16px',
|
||||
minHeight: '128px',
|
||||
whiteSpace: 'prewrap'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
12
src/CenteredLayout.js
Normal file
12
src/CenteredLayout.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Container
|
||||
} from 'rebass'
|
||||
|
||||
export default props => props.active
|
||||
? <Container
|
||||
px={3}
|
||||
py={5}
|
||||
{...props}
|
||||
/>
|
||||
: props.children
|
17
src/FileList.js
Normal file
17
src/FileList.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default ({ routes = [] }) => (
|
||||
<React.Fragment>
|
||||
<pre>{DIRNAME}</pre>
|
||||
<ul>
|
||||
{routes.map(route => (
|
||||
<li key={route.key}>
|
||||
<Link to={route.path}>
|
||||
{route.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
)
|
80
src/Library.js
Normal file
80
src/Library.js
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
style,
|
||||
gridGap,
|
||||
gridAutoRows,
|
||||
borderColor
|
||||
} from 'styled-system'
|
||||
|
||||
const gridWidth = style({
|
||||
prop: 'width',
|
||||
cssProperty: 'gridTemplateColumns',
|
||||
getter: n => `repeat(auto-fit, minmax(${n}px, 1fr))`
|
||||
})
|
||||
|
||||
const Grid = styled.div([], {
|
||||
display: 'grid'
|
||||
},
|
||||
gridWidth,
|
||||
gridGap,
|
||||
gridAutoRows
|
||||
)
|
||||
|
||||
Grid.defaultProps = {
|
||||
width: 256,
|
||||
gridAutoRows: 192
|
||||
}
|
||||
|
||||
const Card = styled(Link)([], {
|
||||
display: 'block',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid'
|
||||
}, borderColor)
|
||||
|
||||
Card.defaultProps = {
|
||||
borderColor: 'gray'
|
||||
}
|
||||
|
||||
export default class extends React.Component {
|
||||
static defaultProps = {
|
||||
fullWidth: true,
|
||||
hidePagination: true
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes = [],
|
||||
route,
|
||||
location
|
||||
} = this.props
|
||||
|
||||
const examples = routes
|
||||
.filter(r => r.dirname === route.dirname)
|
||||
.filter(r => r !== route)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid>
|
||||
{examples.map(({
|
||||
key,
|
||||
path,
|
||||
name,
|
||||
Component
|
||||
}) => (
|
||||
<Card
|
||||
key={key}
|
||||
to={path}
|
||||
>
|
||||
<Component />
|
||||
<pre>{name}</pre>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
73
src/LiveEditor.js
Normal file
73
src/LiveEditor.js
Normal file
@ -0,0 +1,73 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
LiveProvider,
|
||||
LivePreview,
|
||||
LiveEditor,
|
||||
LiveError
|
||||
} from 'react-live'
|
||||
import { ScopeConsumer } from 'react-scope-provider'
|
||||
import { Box } from 'rebass'
|
||||
import { color, borderColor } from 'styled-system'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const transformCode = src => `<React.Fragment>${src}</React.Fragment>`
|
||||
|
||||
const Preview = styled(LivePreview)([], {
|
||||
padding: '16px',
|
||||
border: '1px solid',
|
||||
borderRadius: '2px 2px 0 0',
|
||||
}, borderColor)
|
||||
Preview.defaultProps = {
|
||||
borderColor: 'gray'
|
||||
}
|
||||
|
||||
const Editor = styled(LiveEditor)([], {
|
||||
fontFamily: 'Menlo, monospace',
|
||||
fontSize: '13px',
|
||||
margin: 0,
|
||||
padding: '16px',
|
||||
borderRadius: '0 0 2px 2px',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
boxShadow: 'inset 0 0 0 1px #6cf',
|
||||
}
|
||||
}, color)
|
||||
Editor.defaultProps = {
|
||||
bg: 'gray'
|
||||
}
|
||||
|
||||
const Err = styled(LiveError)([], {
|
||||
fontFamily: 'Menlo, monospace',
|
||||
fontSize: '13px',
|
||||
padding: '8px',
|
||||
color: 'white',
|
||||
backgroundColor: 'red'
|
||||
})
|
||||
|
||||
export default ({
|
||||
code,
|
||||
scope,
|
||||
render
|
||||
}) => (
|
||||
<Box mb={4}>
|
||||
<ScopeConsumer defaultScope={scope}>
|
||||
{scope => (
|
||||
<LiveProvider
|
||||
code={code}
|
||||
scope={scope}
|
||||
mountStylesheet={false}
|
||||
transformCode={transformCode}>
|
||||
{typeof render === 'function' ? (
|
||||
render({ code, scope })
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Preview />
|
||||
<Editor />
|
||||
<Err />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</LiveProvider>
|
||||
)}
|
||||
</ScopeConsumer>
|
||||
</Box>
|
||||
)
|
30
src/LivePreview.js
Normal file
30
src/LivePreview.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
LiveProvider,
|
||||
LivePreview,
|
||||
LiveError
|
||||
} from 'react-live'
|
||||
import { ScopeConsumer } from 'react-scope-provider'
|
||||
import { Box } from 'rebass'
|
||||
|
||||
const transformCode = str => `<React.Fragment>${str}</React.Fragment>`
|
||||
|
||||
export default ({
|
||||
code,
|
||||
scope
|
||||
}) => (
|
||||
<Box mb={4}>
|
||||
<ScopeConsumer defaultScope={scope}>
|
||||
{scope => (
|
||||
<LiveProvider
|
||||
code={code}
|
||||
scope={scope}
|
||||
mountStylesheet={false}
|
||||
transformCode={transformCode}>
|
||||
<LivePreview />
|
||||
<LiveError />
|
||||
</LiveProvider>
|
||||
)}
|
||||
</ScopeConsumer>
|
||||
</Box>
|
||||
)
|
20
src/ScopeProvider.js
Normal file
20
src/ScopeProvider.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { MDXProvider } from '@mdx-js/tag'
|
||||
import { ScopeProvider } from 'react-scope-provider'
|
||||
import defaultScope from './scope'
|
||||
|
||||
export default props => {
|
||||
const scope = {
|
||||
...defaultScope,
|
||||
...props.scope
|
||||
}
|
||||
return (
|
||||
<ScopeProvider scope={scope}>
|
||||
<MDXProvider components={scope}>
|
||||
<React.Fragment>
|
||||
{props.children}
|
||||
</React.Fragment>
|
||||
</MDXProvider>
|
||||
</ScopeProvider>
|
||||
)
|
||||
}
|
22
src/ScrollTop.js
Normal file
22
src/ScrollTop.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
export default withRouter(class extends React.Component {
|
||||
componentDidUpdate (prev) {
|
||||
const { pathname, hash } = this.props.location
|
||||
if (prev.location.pathname !== pathname) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
// check performance of this
|
||||
if (hash) {
|
||||
const el = document.getElementById(hash.slice(1))
|
||||
if (!el) return
|
||||
el.scrollIntoView()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return false
|
||||
}
|
||||
})
|
308
src/SidebarLayout.js
Normal file
308
src/SidebarLayout.js
Normal file
@ -0,0 +1,308 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Link as RouterLink,
|
||||
NavLink as RouterNavLink
|
||||
} from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
Provider as RebassProvider,
|
||||
Flex,
|
||||
Box,
|
||||
Fixed,
|
||||
Container,
|
||||
Text,
|
||||
Close,
|
||||
Toolbar,
|
||||
Divider,
|
||||
Heading,
|
||||
NavLink,
|
||||
BlockLink,
|
||||
Button,
|
||||
ButtonTransparent,
|
||||
} from 'rebass'
|
||||
import { borderColor, themeGet } from 'styled-system'
|
||||
|
||||
const breakpoint = `@media screen and (min-width: 48em)`
|
||||
|
||||
export const Root = styled(Flex)([], {
|
||||
minHeight: '100vh'
|
||||
})
|
||||
|
||||
export const Sidebar = styled('div')([], {
|
||||
width: '256px',
|
||||
height: '100vh',
|
||||
flex: 'none',
|
||||
overflowY: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
transition: 'transform .2s ease-out',
|
||||
backgroundColor: '#fff',
|
||||
borderRight: '1px solid',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}, props => ({
|
||||
transform: props.open ? 'translateX(0)' : 'translateX(-100%)',
|
||||
[breakpoint]: {
|
||||
transform: 'none'
|
||||
}
|
||||
}), borderColor)
|
||||
Sidebar.defaultProps = {
|
||||
borderColor: 'gray'
|
||||
}
|
||||
|
||||
export const Overlay = styled('div')([], {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
})
|
||||
|
||||
export const MobileOnly = styled.div([], {
|
||||
[breakpoint]: {
|
||||
display: 'none'
|
||||
},
|
||||
})
|
||||
|
||||
export const MenuIcon = ({ size = 24, ...props }) =>
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 24 24'
|
||||
width={size}
|
||||
height={size}
|
||||
fill='currentcolor'
|
||||
>
|
||||
<path d='M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z' />
|
||||
</svg>
|
||||
|
||||
export const Main = props =>
|
||||
<Box
|
||||
{...props}
|
||||
is='main'
|
||||
flex='1 1 auto'
|
||||
w={1}
|
||||
pl={[ null, null, 256 ]}
|
||||
/>
|
||||
|
||||
export const MaxWidth = props =>
|
||||
<Container
|
||||
{...props}
|
||||
maxWidth={768}
|
||||
px={4}
|
||||
pt={4}
|
||||
pb={6}
|
||||
/>
|
||||
|
||||
export const Content = styled(Box)([], {
|
||||
minHeight: 'calc(100vh - 208px)'
|
||||
})
|
||||
|
||||
export const UL = styled('ul')([], {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
paddingLeft: 0,
|
||||
paddingBottom: '48px',
|
||||
})
|
||||
|
||||
export const LI = styled('li')([], {
|
||||
})
|
||||
|
||||
const depthPad = ({ to = '' }) =>
|
||||
(1 + to.split('/')
|
||||
.filter(s => s.length)
|
||||
.slice(1).length) * 16
|
||||
|
||||
const Link = styled(props => (
|
||||
<NavLink
|
||||
{...props}
|
||||
is={RouterNavLink}
|
||||
w={1}
|
||||
pl={(depthPad(props) - 4) + 'px'}
|
||||
/>
|
||||
))([], props => ({
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'transparent',
|
||||
'&.active, &:focus': {
|
||||
color: themeGet('colors.blue', '#07c')(props),
|
||||
outline: 'none',
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: 'inherit',
|
||||
}
|
||||
}))
|
||||
|
||||
Link.defaultProps = {
|
||||
to: ''
|
||||
}
|
||||
|
||||
const unhyphenate = str => str.replace(/(\w)(-)(\w)/g, '$1 $3')
|
||||
const upperFirst = str => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
const format = str => upperFirst(unhyphenate(str))
|
||||
|
||||
const NavBar = ({
|
||||
title,
|
||||
logo,
|
||||
focus,
|
||||
update,
|
||||
}) =>
|
||||
<Toolbar
|
||||
color='inherit'
|
||||
bg='transparent'>
|
||||
{logo}
|
||||
<Heading
|
||||
px={2}
|
||||
fontSize={1}>
|
||||
{title}
|
||||
</Heading>
|
||||
<Box mx='auto' />
|
||||
</Toolbar>
|
||||
|
||||
export const Nav = ({
|
||||
routes = [],
|
||||
...props
|
||||
}) =>
|
||||
<React.Fragment>
|
||||
<NavBar {...props} />
|
||||
<Divider my={0} />
|
||||
<UL>
|
||||
{routes.map(route => (
|
||||
<LI key={route.key}>
|
||||
{/^https?:\/\//.test(route.path) ? (
|
||||
<NavLink pl={3} href={route.path}>
|
||||
{route.name}
|
||||
</NavLink>
|
||||
) : (
|
||||
<Link to={route.path} exact>
|
||||
{format(route.name)}
|
||||
</Link>
|
||||
)}
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
</React.Fragment>
|
||||
|
||||
export const Pagination = ({ previous, next }) =>
|
||||
<Flex py={4} flexWrap='wrap'>
|
||||
{previous && (
|
||||
<BlockLink
|
||||
py={2}
|
||||
is={RouterLink}
|
||||
to={previous.path}>
|
||||
<Text mb={1}>Previous:</Text>
|
||||
<Text
|
||||
fontSize={3}
|
||||
fontWeight='bold'>
|
||||
{format(previous.name)}
|
||||
</Text>
|
||||
</BlockLink>
|
||||
)}
|
||||
<Box mx='auto' />
|
||||
{next && (
|
||||
<BlockLink
|
||||
py={2}
|
||||
is={RouterLink}
|
||||
to={next.path}>
|
||||
<Text mb={1}>Next:</Text>
|
||||
<Text
|
||||
fontSize={3}
|
||||
fontWeight='bold'>
|
||||
{format(next.name)}
|
||||
</Text>
|
||||
</BlockLink>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
const MobileNav = ({
|
||||
title,
|
||||
logo,
|
||||
update
|
||||
}) =>
|
||||
<MobileOnly>
|
||||
<Toolbar px={0} color='inherit' bg='transparent'>
|
||||
<ButtonTransparent
|
||||
px={2}
|
||||
borderRadius={0}
|
||||
m={0}
|
||||
mr='auto'
|
||||
title='Toggle Menu'
|
||||
onClick={e => update(toggle('menu'))}>
|
||||
{logo || <MenuIcon />}
|
||||
</ButtonTransparent>
|
||||
<Heading fontSize={1}>
|
||||
{title}
|
||||
</Heading>
|
||||
<Box width={48} ml='auto' />
|
||||
</Toolbar>
|
||||
<Divider my={0} />
|
||||
</MobileOnly>
|
||||
|
||||
const toggle = key => state => ({ [key]: !state[key] })
|
||||
const close = state => ({ menu: false })
|
||||
|
||||
export default class Layout extends React.Component {
|
||||
static propTypes = {
|
||||
routes: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
menu: false,
|
||||
update: fn => this.setState(fn)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes = [],
|
||||
children,
|
||||
route,
|
||||
title = 'x0',
|
||||
logo,
|
||||
} = this.props
|
||||
const { menu, update } = this.state
|
||||
|
||||
const opts = route ? route.props : {}
|
||||
if (opts.layout === false) return children
|
||||
const Wrapper = opts.fullWidth
|
||||
? React.Fragment
|
||||
: MaxWidth
|
||||
|
||||
const index = routes.findIndex(r => r === route)
|
||||
const pagination = {
|
||||
previous: routes[index - 1],
|
||||
next: routes[index + 1]
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MobileNav
|
||||
title={title}
|
||||
logo={logo}
|
||||
update={update}
|
||||
/>
|
||||
|
||||
<Root>
|
||||
{menu && <Overlay onClick={e => update(close)} />}
|
||||
<Sidebar
|
||||
open={menu}
|
||||
onClick={e => update(close)}>
|
||||
<Nav
|
||||
title={title}
|
||||
logo={logo}
|
||||
routes={routes}
|
||||
update={update}
|
||||
/>
|
||||
</Sidebar>
|
||||
<Main tabIndex={menu ? -1 : undefined}>
|
||||
<Wrapper>
|
||||
<Content>
|
||||
{children}
|
||||
</Content>
|
||||
{!opts.hidePagination && <Pagination {...pagination} />}
|
||||
</Wrapper>
|
||||
</Main>
|
||||
</Root>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
196
src/entry.js
Normal file
196
src/entry.js
Normal file
@ -0,0 +1,196 @@
|
||||
// Main application
|
||||
import path from 'path'
|
||||
import React from 'react'
|
||||
import { render, hydrate } from 'react-dom'
|
||||
import {
|
||||
StaticRouter,
|
||||
BrowserRouter,
|
||||
Switch,
|
||||
Route,
|
||||
Link,
|
||||
withRouter
|
||||
} from 'react-router-dom'
|
||||
import { Provider as RebassProvider } from 'rebass'
|
||||
import minimatch from 'minimatch'
|
||||
import sortBy from 'lodash.sortby'
|
||||
|
||||
import ScopeProvider from './ScopeProvider'
|
||||
import Catch from './Catch'
|
||||
import FileList from './FileList'
|
||||
import ScrollTop from './ScrollTop'
|
||||
import CenteredLayout from './CenteredLayout'
|
||||
|
||||
const IS_CLIENT = typeof document !== 'undefined'
|
||||
const req = require.context(DIRNAME, true, /\.(js|md|mdx|jsx)$/)
|
||||
|
||||
const { filename, basename = '', disableScroll } = OPTIONS
|
||||
|
||||
const getComponents = req => req.keys()
|
||||
.filter(minimatch.filter('!node_modules'))
|
||||
.filter(key => !MATCH || minimatch(key.replace(/^\.\//, ''), MATCH))
|
||||
.filter(key => !/^_/.test(path.basename(key)))
|
||||
.map(key => ({
|
||||
key,
|
||||
name: path.basename(key, path.extname(key)),
|
||||
module: req(key),
|
||||
Component: req(key).default || req(key),
|
||||
}))
|
||||
.filter(component => typeof component.Component === 'function')
|
||||
|
||||
const initialComponents = getComponents(req)
|
||||
|
||||
const DefaultApp = props => props.children
|
||||
|
||||
const Router = IS_CLIENT ? BrowserRouter : StaticRouter
|
||||
const appPath = req.keys().find(key => key === './_app.js')
|
||||
const App = appPath ? (req(appPath).default || req(appPath)) : DefaultApp
|
||||
|
||||
export const getRoutes = async (components = initialComponents) => {
|
||||
const promises = await components.map(async ({
|
||||
key,
|
||||
name,
|
||||
module,
|
||||
Component
|
||||
}) => {
|
||||
const exact = name === 'index'
|
||||
const dirname = path.dirname(key).replace(/^\./, '')
|
||||
const extname = path.extname(key)
|
||||
let pathname = dirname + (exact ? '/' : '/' + name)
|
||||
const href = pathname
|
||||
const initialProps = Component.getInitialProps
|
||||
? await Component.getInitialProps({ path: pathname })
|
||||
: {}
|
||||
const defaultProps = Component.defaultProps
|
||||
const meta = module.frontMatter || {}
|
||||
const props = { ...meta, ...initialProps, ...defaultProps }
|
||||
|
||||
// for dynamic routing
|
||||
pathname = props.path || pathname
|
||||
|
||||
if (dirname && name === 'index') {
|
||||
name = path.basename(dirname)
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
extname,
|
||||
href,
|
||||
path: pathname,
|
||||
dirname,
|
||||
exact,
|
||||
module,
|
||||
Component,
|
||||
props
|
||||
}
|
||||
})
|
||||
const routes = await Promise.all(promises)
|
||||
const filtered = routes
|
||||
.filter(r => !r.props.ignore)
|
||||
.filter(component => !/404/.test(component.name))
|
||||
let sorted = [...filtered]
|
||||
sorted = sortBy([...sorted], a => a.name)
|
||||
sorted = sortBy([...sorted], a => !a.exact)
|
||||
sorted = sortBy([...sorted], a => a.dirname)
|
||||
sorted.notfound = routes.find(component => /404/.test(component.name))
|
||||
return sorted
|
||||
}
|
||||
|
||||
const RouterState = withRouter(({ render, ...props }) => {
|
||||
const { pathname } = props.location
|
||||
const route = props.routes.find(r => r.path === pathname || r.href === pathname)
|
||||
return render({ ...props, route })
|
||||
})
|
||||
|
||||
export default class Root extends React.Component {
|
||||
static defaultProps = {
|
||||
path: '/',
|
||||
basename
|
||||
}
|
||||
state = {
|
||||
...this.props,
|
||||
...App.defaultProps
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes,
|
||||
basename,
|
||||
path = '/'
|
||||
} = this.props
|
||||
|
||||
const NotFound = routes.notfound
|
||||
? routes.notfound.Component
|
||||
: FileList
|
||||
|
||||
const render = appProps => (
|
||||
<Switch>
|
||||
{routes.map(({ Component, ...route }) => (
|
||||
<Route
|
||||
{...route}
|
||||
render={props => (
|
||||
<Catch>
|
||||
<CenteredLayout
|
||||
active={!appPath && /md/.test(route.extname)}>
|
||||
<Component
|
||||
{...props}
|
||||
{...appProps}
|
||||
{...route.props}
|
||||
/>
|
||||
</CenteredLayout>
|
||||
</Catch>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
render={props => <NotFound {...props} routes={routes} />}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
|
||||
return (
|
||||
<Router
|
||||
context={{}}
|
||||
basename={basename}
|
||||
location={path}>
|
||||
<React.Fragment>
|
||||
<RebassProvider>
|
||||
<ScopeProvider>
|
||||
<Catch>
|
||||
<RouterState
|
||||
routes={routes}
|
||||
render={(router) => (
|
||||
<App
|
||||
{...router}
|
||||
routes={routes}
|
||||
render={render}
|
||||
Component={render}
|
||||
children={render(router)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Catch>
|
||||
</ScopeProvider>
|
||||
</RebassProvider>
|
||||
{!disableScroll && <ScrollTop />}
|
||||
</React.Fragment>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_CLIENT) {
|
||||
const mount = DEV ? render : hydrate
|
||||
const div = window.root || document.body.appendChild(
|
||||
document.createElement('div')
|
||||
)
|
||||
getRoutes()
|
||||
.then(routes => {
|
||||
mount(<Root routes={routes} />, div)
|
||||
})
|
||||
}
|
||||
|
||||
if (IS_CLIENT && module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
||||
|
15
src/index.js
Normal file
15
src/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
export { Link, NavLink } from 'react-router-dom'
|
||||
export { default as Catch } from './Catch'
|
||||
export { default as CenteredLayout } from './CenteredLayout'
|
||||
export { default as FileList } from './FileList'
|
||||
export { default as LiveEditor } from './LiveEditor'
|
||||
export { default as LivePreview } from './LivePreview'
|
||||
export { default as ScopeProvider } from './ScopeProvider'
|
||||
export { default as ScrollTop } from './ScrollTop'
|
||||
export { default as scope } from './scope'
|
||||
|
||||
// kit
|
||||
export { default as Library } from './Library'
|
||||
|
||||
// layouts
|
||||
export { default as SidebarLayout } from './SidebarLayout'
|
82
src/scope.js
Normal file
82
src/scope.js
Normal file
@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import { withRouter, Link } from 'react-router-dom'
|
||||
import rebassMarkdown from '@rebass/markdown'
|
||||
import { Pre } from 'rebass'
|
||||
|
||||
import LiveEditor from './LiveEditor'
|
||||
import LivePreview from './LivePreview'
|
||||
|
||||
const cleanHREF = href => href
|
||||
.replace(/\.mdx?$/, '')
|
||||
.replace(/\.jsx?$/, '')
|
||||
|
||||
export const link = withRouter(({
|
||||
href = '',
|
||||
match,
|
||||
location,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
if (/^https?:\/\//.test(href) || /^#/.test(href)) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={className}
|
||||
children={children}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const to = cleanHREF(href, location.pathname)
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={className}
|
||||
children={children}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export const code = ({
|
||||
children,
|
||||
className,
|
||||
scope,
|
||||
...props
|
||||
}) => {
|
||||
const lang = className.replace(/^language\-/, '')
|
||||
const type = lang.charAt(0)
|
||||
const code = React.Children.toArray(children).join('\n')
|
||||
|
||||
switch (type) {
|
||||
case '.':
|
||||
return <LiveEditor code={code} scope={scope} />
|
||||
case '!':
|
||||
return <LivePreview code={code} scope={scope} />
|
||||
default:
|
||||
return (
|
||||
<Pre
|
||||
p={3}
|
||||
mt={4}
|
||||
mb={4}
|
||||
bg='gray'
|
||||
children={children}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pre = props => props.children
|
||||
|
||||
const scope = rebassMarkdown({
|
||||
a: {
|
||||
is: link
|
||||
},
|
||||
code: {
|
||||
is: code
|
||||
},
|
||||
pre: {
|
||||
is: pre,
|
||||
}
|
||||
})
|
||||
|
||||
export default scope
|
@ -20,7 +20,6 @@ const clean = () => {
|
||||
if (fs.existsSync(output)) {
|
||||
rimraf.sync(output)
|
||||
}
|
||||
// fs.remove(output)
|
||||
}
|
||||
|
||||
test.before(clean)
|
||||
@ -31,15 +30,3 @@ test('static renders', async t => {
|
||||
const html = fs.readFileSync(htmlFile, 'utf8')
|
||||
t.snapshot(html)
|
||||
})
|
||||
|
||||
test('static uses getInitialProps method', async t => {
|
||||
const res = await build(options)
|
||||
const html = fs.readFileSync(propsFile, 'utf8')
|
||||
t.snapshot(html)
|
||||
})
|
||||
|
||||
test.skip('static makes a directory', async t => {
|
||||
clean()
|
||||
const res = await build(options)
|
||||
t.pass()
|
||||
})
|
||||
|
258
test/components.js
Normal file
258
test/components.js
Normal file
@ -0,0 +1,258 @@
|
||||
import test from 'ava'
|
||||
import React from 'react'
|
||||
import { create as render } from 'react-test-renderer'
|
||||
import { StaticRouter } from 'react-router-dom'
|
||||
import sinon from 'sinon'
|
||||
import browserEnv from 'browser-env'
|
||||
|
||||
import {
|
||||
Catch,
|
||||
CenteredLayout,
|
||||
FileList,
|
||||
Library,
|
||||
LiveEditor,
|
||||
LivePreview,
|
||||
ScopeProvider,
|
||||
ScrollTop,
|
||||
SidebarLayout,
|
||||
scope
|
||||
} from '../src'
|
||||
|
||||
browserEnv()
|
||||
|
||||
global.DIRNAME = 'beep'
|
||||
|
||||
const renderJSON = el => render(el).toJSON()
|
||||
|
||||
test('CenteredLayout renders with active prop', t => {
|
||||
const json = renderJSON(
|
||||
<CenteredLayout active>
|
||||
Hello
|
||||
</CenteredLayout>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('CenteredLayout does not render without active prop', t => {
|
||||
const json = renderJSON(
|
||||
<CenteredLayout>
|
||||
Hello
|
||||
</CenteredLayout>
|
||||
)
|
||||
t.is(json, 'Hello')
|
||||
})
|
||||
|
||||
test('Catch renders', t => {
|
||||
const json = renderJSON(
|
||||
<Catch>Catch</Catch>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('Catch renders error', t => {
|
||||
const Throws = props => {
|
||||
throw new Error('nope')
|
||||
return false
|
||||
}
|
||||
const json = renderJSON(
|
||||
<Catch>
|
||||
<Throws />
|
||||
</Catch>
|
||||
)
|
||||
t.is(json.type, 'pre')
|
||||
t.is(json.children[0], 'Error: nope')
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('FileList renders', t => {
|
||||
const json = renderJSON(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<FileList
|
||||
routes={[
|
||||
{
|
||||
path: '/',
|
||||
key: '/',
|
||||
name: 'Home'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</StaticRouter>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
// doesn't seem to render correctly with refs
|
||||
test.skip('LiveEditor renders', t => {
|
||||
const json = renderJSON(
|
||||
<LiveEditor
|
||||
code='<h1>Hello</h1>'
|
||||
/>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('LivePreview renders', t => {
|
||||
const json = renderJSON(
|
||||
<LivePreview code='<h1>Hello</h1>' />
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('ScopeProvider renders', t => {
|
||||
const json = renderJSON(
|
||||
<ScopeProvider>
|
||||
Hello
|
||||
</ScopeProvider>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('ScrollTop renders', t => {
|
||||
const json = renderJSON(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
t.is(json, null)
|
||||
})
|
||||
|
||||
test('ScrollTop scrolls window on location change', t => {
|
||||
sinon.stub(window, 'scrollTo')
|
||||
const instance = render(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
instance.update(
|
||||
<StaticRouter location='/hello' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
t.true(window.scrollTo.calledOnce)
|
||||
window.scrollTo.restore()
|
||||
})
|
||||
|
||||
test('ScrollTop scrolls to hash', t => {
|
||||
const el = document.body.appendChild(
|
||||
document.createElement('div')
|
||||
)
|
||||
el.id = 'hello'
|
||||
el.scrollIntoView = sinon.spy()
|
||||
|
||||
const instance = render(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
instance.update(
|
||||
<StaticRouter location='/#hello' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
|
||||
t.true(el.scrollIntoView.calledOnce)
|
||||
})
|
||||
|
||||
test('ScrollTop does not scroll to hash when there is no element', t => {
|
||||
const el = document.body.appendChild(
|
||||
document.createElement('div')
|
||||
)
|
||||
el.scrollIntoView = sinon.spy()
|
||||
|
||||
const instance = render(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
instance.update(
|
||||
<StaticRouter location='/#hello' context={{}}>
|
||||
<ScrollTop />
|
||||
</StaticRouter>
|
||||
)
|
||||
|
||||
t.false(el.scrollIntoView.calledOnce)
|
||||
})
|
||||
|
||||
test('Library renders', t => {
|
||||
const json = renderJSON(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<Library
|
||||
route={{ dirname: '/examples', path: '/examples' }}
|
||||
routes={[
|
||||
{
|
||||
dirname: '/examples',
|
||||
key: 'hello',
|
||||
path: '/examples/hello',
|
||||
name: 'hello',
|
||||
Component: () => <pre>Hello</pre>
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StaticRouter>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
test('SidebarLayout renders', t => {
|
||||
const home = {
|
||||
key: '/',
|
||||
path: '/',
|
||||
name: 'index',
|
||||
props: {},
|
||||
Component: () => <h1>Home</h1>
|
||||
}
|
||||
const json = renderJSON(
|
||||
<StaticRouter location='/' context={{}}>
|
||||
<SidebarLayout
|
||||
children='Content'
|
||||
route={home}
|
||||
routes={[
|
||||
home,
|
||||
{
|
||||
key: '/about',
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
Component: () => <h1>About</h1>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</StaticRouter>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
||||
const blacklist = {
|
||||
pre: true
|
||||
}
|
||||
Object.keys(scope)
|
||||
.filter(key => !blacklist[key])
|
||||
.forEach(key => {
|
||||
test(`scope.${key} renders`, t => {
|
||||
const Component = scope[key]
|
||||
const json = renderJSON(
|
||||
<StaticRouter context={{}}>
|
||||
<Component />
|
||||
</StaticRouter>
|
||||
)
|
||||
t.snapshot(json)
|
||||
})
|
||||
})
|
||||
|
||||
test('scope.pre renders children only', t => {
|
||||
const json = renderJSON(
|
||||
React.createElement(scope.pre, null, 'Hello')
|
||||
)
|
||||
t.is(json, 'Hello')
|
||||
})
|
||||
|
||||
test('scope.a renders a plain link for absolute URLs', t => {
|
||||
const Link = scope.a
|
||||
const json = renderJSON(
|
||||
<StaticRouter context={{}}>
|
||||
<Link href='http://example.com'>Hello</Link>
|
||||
</StaticRouter>
|
||||
)
|
||||
t.is(json.props.href, 'http://example.com')
|
||||
t.snapshot(json)
|
||||
})
|
||||
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
className: beep
|
||||
---
|
||||
|
||||
<h1 className={`${props.className} boop`}>Hello</h1>
|
@ -8,10 +8,4 @@ Generated by [AVA](https://ava.li).
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
'<!DOCTYPE html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="generator" content="Compositor x0"><title>x0</title><style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style></head><div id="root"><h1>Hello</h1></div><script src="/bundle.js"></script>'
|
||||
|
||||
## static uses getInitialProps method
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
'<!DOCTYPE html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="generator" content="Compositor x0"><title>x0</title><style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style></head><div id="root"><h1>Hello</h1></div><script src="/bundle.js"></script>'
|
||||
'<!DOCTYPE html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="generator" content="Compositor x0"><title>x0</title><style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style></head><div id="root"><div class="sc-bxivhb dUiVDW"><h1>Hello</h1></div></div><script src="/bundle.js"></script>'
|
||||
|
Binary file not shown.
386
test/snapshots/components.js.md
Normal file
386
test/snapshots/components.js.md
Normal file
@ -0,0 +1,386 @@
|
||||
# Snapshot report for `test/components.js`
|
||||
|
||||
The actual snapshot is saved in `components.js.snap`.
|
||||
|
||||
Generated by [AVA](https://ava.li).
|
||||
|
||||
## Catch renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
'Catch'
|
||||
|
||||
## Catch renders error
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<pre
|
||||
style={
|
||||
{
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
fontFamily: 'Menlo, monospace',
|
||||
fontSize: '14px',
|
||||
margin: 0,
|
||||
minHeight: '128px',
|
||||
padding: '16px',
|
||||
whiteSpace: 'prewrap',
|
||||
}
|
||||
}
|
||||
>
|
||||
Error: nope
|
||||
</pre>
|
||||
|
||||
## CenteredLayout renders with active prop
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<div
|
||||
className="sc-jAaTju esAkYv sc-bdVaJa iHZvIS"
|
||||
>
|
||||
Hello
|
||||
</div>
|
||||
|
||||
## FileList renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
[
|
||||
<pre>
|
||||
beep
|
||||
</pre>,
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
onClick={Function {}}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
</ul>,
|
||||
]
|
||||
|
||||
## Library renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<div
|
||||
className="sc-bbmXgH kzGbIY"
|
||||
width={256}
|
||||
>
|
||||
<a
|
||||
borderColor="gray"
|
||||
className="sc-gGBfsJ eBksKh"
|
||||
href="/examples/hello"
|
||||
onClick={Function {}}
|
||||
>
|
||||
<pre>
|
||||
Hello
|
||||
</pre>
|
||||
<pre>
|
||||
hello
|
||||
</pre>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## LivePreview renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<div
|
||||
className="sc-bdVaJa kdjLZs"
|
||||
>
|
||||
<div
|
||||
className="react-live"
|
||||
>
|
||||
<div
|
||||
className="react-live-preview"
|
||||
>
|
||||
<h1>
|
||||
Hello
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## ScopeProvider renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
'Hello'
|
||||
|
||||
## SidebarLayout renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
[
|
||||
<div
|
||||
className="sc-hEsumM dahmKO"
|
||||
>
|
||||
<div
|
||||
className="sc-fMiknA jgVkvN sc-bwzfXH hKiLMS sc-bdVaJa iHZvIS"
|
||||
>
|
||||
<button
|
||||
className="sc-gzVnrw ghjHJc sc-ifAKCX fFPXoq"
|
||||
onClick={Function onClick {}}
|
||||
title="Toggle Menu"
|
||||
>
|
||||
<svg
|
||||
fill="currentcolor"
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
>
|
||||
<path
|
||||
d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2
|
||||
className="sc-VigVT dTivBW"
|
||||
>
|
||||
x0
|
||||
</h2>
|
||||
<div
|
||||
className="sc-bdVaJa hAzohM"
|
||||
/>
|
||||
</div>
|
||||
<hr
|
||||
className="sc-kgoBCf eSqAOl"
|
||||
/>
|
||||
</div>,
|
||||
<div
|
||||
className="sc-jnlKLf fQRDBU sc-bwzfXH hKiLMS sc-bdVaJa iHZvIS"
|
||||
>
|
||||
<div
|
||||
className="sc-fYxtnH VTytv"
|
||||
onClick={Function onClick {}}
|
||||
open={false}
|
||||
>
|
||||
<div
|
||||
className="sc-fMiknA fVCXKt sc-bwzfXH hKiLMS sc-bdVaJa iHZvIS"
|
||||
>
|
||||
<h2
|
||||
className="sc-VigVT gPiNJC"
|
||||
>
|
||||
x0
|
||||
</h2>
|
||||
<div
|
||||
className="sc-bdVaJa LbkC"
|
||||
/>
|
||||
</div>
|
||||
<hr
|
||||
className="sc-kgoBCf eSqAOl"
|
||||
/>
|
||||
<ul
|
||||
className="sc-cIShpX hSGVzQ"
|
||||
>
|
||||
<li
|
||||
className="sc-kafWEX dNChzQ"
|
||||
>
|
||||
<a
|
||||
"aria-current"="page"
|
||||
className="sc-feJyhm jUiESA sc-dnqmqq iUcnAA active"
|
||||
href="/"
|
||||
onClick={Function {}}
|
||||
style={{}}
|
||||
>
|
||||
Index
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="sc-kafWEX dNChzQ"
|
||||
>
|
||||
<a
|
||||
'aria-current'={null}
|
||||
className="sc-feJyhm jUiESA sc-dnqmqq iUcnAA"
|
||||
href="/about"
|
||||
onClick={Function {}}
|
||||
style={undefined}
|
||||
>
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<main
|
||||
className="sc-bdVaJa ioTXyJ"
|
||||
tabIndex={undefined}
|
||||
>
|
||||
<div
|
||||
className="sc-jAaTju jHRdEk sc-bdVaJa iHZvIS"
|
||||
>
|
||||
<div
|
||||
className="sc-ktHwxA dvWgCn sc-bdVaJa iHZvIS"
|
||||
>
|
||||
Content
|
||||
</div>
|
||||
<div
|
||||
className="sc-bwzfXH liBCIH sc-bdVaJa gkOtLU"
|
||||
>
|
||||
<div
|
||||
className="sc-bdVaJa LbkC"
|
||||
/>
|
||||
<a
|
||||
className="sc-iwsKbI leRHZB"
|
||||
href="/about"
|
||||
onClick={Function {}}
|
||||
>
|
||||
<div
|
||||
className="sc-gqjmRU emCKSy"
|
||||
>
|
||||
Next:
|
||||
</div>
|
||||
<div
|
||||
className="sc-gqjmRU entfAd"
|
||||
>
|
||||
About
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>,
|
||||
]
|
||||
|
||||
## scope.a renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<a
|
||||
className="sc-htoDjs gLCdRU"
|
||||
href="/"
|
||||
onClick={Function {}}
|
||||
/>
|
||||
|
||||
## scope.a renders a plain link for absolute URLs
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<a
|
||||
className="sc-htoDjs gLCdRU"
|
||||
href="http://example.com"
|
||||
>
|
||||
Hello
|
||||
</a>
|
||||
|
||||
## scope.blockquote renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<blockquote
|
||||
className="sc-chPdSV gReYnD sc-gqjmRU cbBDfA"
|
||||
/>
|
||||
|
||||
## scope.code renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<pre
|
||||
className="sc-kGXeez hIJWeT"
|
||||
/>
|
||||
|
||||
## scope.h1 renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<h1
|
||||
className="sc-VigVT giyuVg"
|
||||
/>
|
||||
|
||||
## scope.h2 renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<h2
|
||||
className="sc-VigVT hvHavN"
|
||||
/>
|
||||
|
||||
## scope.h3 renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<h3
|
||||
className="sc-VigVT cdnBzZ"
|
||||
/>
|
||||
|
||||
## scope.h4 renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<h4
|
||||
className="sc-VigVT bFQvfC"
|
||||
/>
|
||||
|
||||
## scope.h5 renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<h5
|
||||
className="sc-VigVT cmwWQF"
|
||||
/>
|
||||
|
||||
## scope.h6 renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<h6
|
||||
className="sc-VigVT hUIvmu"
|
||||
/>
|
||||
|
||||
## scope.hr renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<hr
|
||||
className="sc-kgoBCf hVsSJm"
|
||||
/>
|
||||
|
||||
## scope.img renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<img
|
||||
className="sc-cvbbAY exTuBk"
|
||||
/>
|
||||
|
||||
## scope.inlineCode renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<code
|
||||
className="sc-kpOJdX eABHTa"
|
||||
/>
|
||||
|
||||
## scope.li renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<li
|
||||
className="sc-gqjmRU bxHISe"
|
||||
/>
|
||||
|
||||
## scope.p renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<p
|
||||
className="sc-gqjmRU jWPlsU"
|
||||
/>
|
||||
|
||||
## scope.table renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<table
|
||||
className="sc-htpNat jBAZTS"
|
||||
/>
|
||||
|
||||
## scope.ul renders
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
<ul
|
||||
className="sc-htpNat houaHu"
|
||||
/>
|
BIN
test/snapshots/components.js.snap
Normal file
BIN
test/snapshots/components.js.snap
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user