1
1
mirror of https://github.com/c8r/x0.git synced 2024-07-14 16:50:34 +03:00

Merge pull request #73 from c8r/mdx-fm

Next
This commit is contained in:
Brent Jackson 2018-06-25 18:44:32 -04:00 committed by GitHub
commit 3f3ab2e5fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1795 additions and 502 deletions

10
.babelrc Normal file
View File

@ -0,0 +1,10 @@
{
"presets": [
"env",
"stage-0",
"react"
],
"plugins": [
"transform-runtime"
]
}

View File

@ -8,4 +8,5 @@ examples
demo
dist
.travis.yml
.babelrc
create-x0

View File

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

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

@ -0,0 +1 @@
module.exports = require('./src')

2
docs/404.md Normal file
View File

@ -0,0 +1,2 @@
# Page not found

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),
]
}
]

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
---
className: beep
---
<h1 className={`${props.className} boop`}>Hello</h1>

View File

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

View 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"
/>

Binary file not shown.