diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..8932ea1
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,10 @@
+{
+ "presets": [
+ "env",
+ "stage-0",
+ "react"
+ ],
+ "plugins": [
+ "transform-runtime"
+ ]
+}
diff --git a/.npmignore b/.npmignore
index 90ffae2..17f04c7 100644
--- a/.npmignore
+++ b/.npmignore
@@ -8,4 +8,5 @@ examples
demo
dist
.travis.yml
+.babelrc
create-x0
diff --git a/README.md b/README.md
index 49a235e..8e964e3 100644
--- a/README.md
+++ b/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'
+
- {props.title}
+ {frontMatter.title}
```
@@ -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
+
diff --git a/cli.js b/cli.js
index ed53b71..1e7ef10 100755
--- a/cli.js
+++ b/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
diff --git a/components.js b/components.js
new file mode 100644
index 0000000..a2958b7
--- /dev/null
+++ b/components.js
@@ -0,0 +1 @@
+module.exports = require('./src')
diff --git a/docs/404.md b/docs/404.md
new file mode 100644
index 0000000..598dbb8
--- /dev/null
+++ b/docs/404.md
@@ -0,0 +1,2 @@
+
+# Page not found
diff --git a/docs/_app.js b/docs/_app.js
index e6aca89..478845d 100644
--- a/docs/_app.js
+++ b/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 }) =>
-
-
-
-
-
-
- Compositor
-
-
-
-
-
-
- x0
-
-
-
-
- GitHub
-
-
-
- {render()}
-
-
-
-
- GitHub
-
-
-
-
- Compositor
-
-
-
-
- © 2018 Compositor, Inc. All rights reserved
-
-
-
-
+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 (
+
+
+
+
+
+ )
+ }
+}
diff --git a/docs/_layout.js b/docs/_layout.js
new file mode 100644
index 0000000..9301e5e
--- /dev/null
+++ b/docs/_layout.js
@@ -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 }) =>
+
+
+
+
+
+
+ Compositor
+
+
+
+
+
+
+ x0
+
+
+
+
+ GitHub
+
+
+
+ {children}
+
+
+
+
+ GitHub
+
+
+
+
+ Compositor
+
+
+
+
+ © 2018 Compositor, Inc. All rights reserved
+
+
+
+
diff --git a/docs/_scope.js b/docs/_scope.js
deleted file mode 100644
index dbf7edf..0000000
--- a/docs/_scope.js
+++ /dev/null
@@ -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 =>
- ),
- h2: heading(props =>
- ),
- h3: heading(props =>
- ),
- a: link(props =>
-
- ),
- pre: props =>
-
-}
diff --git a/docs/theme.js b/docs/_theme.js
similarity index 100%
rename from docs/theme.js
rename to docs/_theme.js
diff --git a/docs/docs.js b/docs/docs.js
deleted file mode 100644
index 4be4317..0000000
--- a/docs/docs.js
+++ /dev/null
@@ -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 =>
-
-
-
-
-
diff --git a/docs/dynamic.js b/docs/dynamic.js
deleted file mode 100644
index 3e76460..0000000
--- a/docs/dynamic.js
+++ /dev/null
@@ -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
-
dynamic routing
-
Home
-
Dynamic Routes
-
Hello
-
Hi
-
- }
-}
diff --git a/docs/index.js b/docs/index.js
index 52d8dc5..0290f41 100644
--- a/docs/index.js
+++ b/docs/index.js
@@ -21,6 +21,10 @@ const Video = styled.video([], {
})
export default class extends React.Component {
+ static defaultProps = {
+ layout: 'landing'
+ }
+
render () {
return (
@@ -54,6 +58,7 @@ export default class extends React.Component {
GitHub
+ {/*
+ */}
diff --git a/docs/webpack.config.js b/docs/webpack.config.js
deleted file mode 100644
index 9718488..0000000
--- a/docs/webpack.config.js
+++ /dev/null
@@ -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'
- ]
- }
- ]
- }
-}
diff --git a/examples/jsx/pages/index.jsx b/examples/jsx/pages/index.jsx
index 562ff3b..529f90e 100644
--- a/examples/jsx/pages/index.jsx
+++ b/examples/jsx/pages/index.jsx
@@ -1,7 +1,8 @@
---
title: Hello JSX
-scope: import scope from './_scope'
---
+import { Box, Heading, Text, Link } from 'rebass'
+
{props.title}
diff --git a/lib/build.js b/lib/build.js
index e0e8665..fc1854f 100644
--- a/lib/build.js
+++ b/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 = ``
- 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)
diff --git a/lib/config.js b/lib/config.js
index 04dafb0..df88d86 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -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'),
]
}
]
diff --git a/lib/dev.js b/lib/dev.js
index c81c5ff..7e2b964 100644
--- a/lib/dev.js
+++ b/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
diff --git a/lib/entry.js b/lib/entry.js
deleted file mode 100644
index 41eb723..0000000
--- a/lib/entry.js
+++ /dev/null
@@ -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 = [] }) => (
-
- {DIRNAME}
-
- {routes.map(route => (
- -
-
- {route.name}
-
-
- ))}
-
-
-)
-
-const DefaultApp = ({ render, routes }) => (
-
- {render()}
- (
-
- )} />
-
-)
-
-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 (
-
- )
- }
-
- 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 (
-
-
-
- (
- routes.map(({ Component, ...route }) => (
- (
-
-
-
- )}
- />
- ))
- )}
- />
-
- {!disableScroll && }
-
-
- )
- }
-}
-
-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(, div)
- })
-}
-
-if (IS_CLIENT && module.hot) {
- module.hot.accept()
-}
-
diff --git a/lib/mdx-fm-loader.js b/lib/mdx-fm-loader.js
new file mode 100644
index 0000000..28a04c6
--- /dev/null
+++ b/lib/mdx-fm-loader.js
@@ -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)
+}
diff --git a/package.json b/package.json
index e37f89e..df4460e 100644
--- a/package.json
+++ b/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"
diff --git a/src/Catch.js b/src/Catch.js
new file mode 100644
index 0000000..ae57c52
--- /dev/null
+++ b/src/Catch.js
@@ -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 (
+
+ )
+ }
+}
diff --git a/src/CenteredLayout.js b/src/CenteredLayout.js
new file mode 100644
index 0000000..353c4bf
--- /dev/null
+++ b/src/CenteredLayout.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import {
+ Container
+} from 'rebass'
+
+export default props => props.active
+ ?
+ : props.children
diff --git a/src/FileList.js b/src/FileList.js
new file mode 100644
index 0000000..5a58375
--- /dev/null
+++ b/src/FileList.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+export default ({ routes = [] }) => (
+
+ {DIRNAME}
+
+ {routes.map(route => (
+ -
+
+ {route.name}
+
+
+ ))}
+
+
+)
diff --git a/src/Library.js b/src/Library.js
new file mode 100644
index 0000000..4d9ad57
--- /dev/null
+++ b/src/Library.js
@@ -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 (
+
+
+ {examples.map(({
+ key,
+ path,
+ name,
+ Component
+ }) => (
+
+
+ {name}
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/src/LiveEditor.js b/src/LiveEditor.js
new file mode 100644
index 0000000..6ac3ac6
--- /dev/null
+++ b/src/LiveEditor.js
@@ -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 => `${src}`
+
+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
+}) => (
+
+
+ {scope => (
+
+ {typeof render === 'function' ? (
+ render({ code, scope })
+ ) : (
+
+
+
+
+
+ )}
+
+ )}
+
+
+)
diff --git a/src/LivePreview.js b/src/LivePreview.js
new file mode 100644
index 0000000..989b18f
--- /dev/null
+++ b/src/LivePreview.js
@@ -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 => `${str}`
+
+export default ({
+ code,
+ scope
+}) => (
+
+
+ {scope => (
+
+
+
+
+ )}
+
+
+)
diff --git a/src/ScopeProvider.js b/src/ScopeProvider.js
new file mode 100644
index 0000000..d302900
--- /dev/null
+++ b/src/ScopeProvider.js
@@ -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 (
+
+
+
+ {props.children}
+
+
+
+ )
+}
diff --git a/src/ScrollTop.js b/src/ScrollTop.js
new file mode 100644
index 0000000..72da1ff
--- /dev/null
+++ b/src/ScrollTop.js
@@ -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
+ }
+})
diff --git a/src/SidebarLayout.js b/src/SidebarLayout.js
new file mode 100644
index 0000000..3559aea
--- /dev/null
+++ b/src/SidebarLayout.js
@@ -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 }) =>
+
+
+export const Main = props =>
+
+
+export const MaxWidth = props =>
+
+
+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 => (
+
+))([], 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,
+}) =>
+
+ {logo}
+
+ {title}
+
+
+
+
+export const Nav = ({
+ routes = [],
+ ...props
+}) =>
+
+
+
+
+ {routes.map(route => (
+ -
+ {/^https?:\/\//.test(route.path) ? (
+
+ {route.name}
+
+ ) : (
+
+ {format(route.name)}
+
+ )}
+
+ ))}
+
+
+
+export const Pagination = ({ previous, next }) =>
+
+ {previous && (
+
+ Previous:
+
+ {format(previous.name)}
+
+
+ )}
+
+ {next && (
+
+ Next:
+
+ {format(next.name)}
+
+
+ )}
+
+
+const MobileNav = ({
+ title,
+ logo,
+ update
+}) =>
+
+
+ update(toggle('menu'))}>
+ {logo || }
+
+
+ {title}
+
+
+
+
+
+
+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 (
+
+
+
+
+ {menu && update(close)} />}
+ update(close)}>
+
+
+
+
+
+ {children}
+
+ {!opts.hidePagination && }
+
+
+
+
+ )
+ }
+}
diff --git a/src/entry.js b/src/entry.js
new file mode 100644
index 0000000..dc07be8
--- /dev/null
+++ b/src/entry.js
@@ -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 => (
+
+ {routes.map(({ Component, ...route }) => (
+ (
+
+
+
+
+
+ )}
+ />
+ ))}
+ }
+ />
+
+ )
+
+ return (
+
+
+
+
+
+ (
+
+ )}
+ />
+
+
+
+ {!disableScroll && }
+
+
+ )
+ }
+}
+
+if (IS_CLIENT) {
+ const mount = DEV ? render : hydrate
+ const div = window.root || document.body.appendChild(
+ document.createElement('div')
+ )
+ getRoutes()
+ .then(routes => {
+ mount(, div)
+ })
+}
+
+if (IS_CLIENT && module.hot) {
+ module.hot.accept()
+}
+
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..78ed264
--- /dev/null
+++ b/src/index.js
@@ -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'
diff --git a/src/scope.js b/src/scope.js
new file mode 100644
index 0000000..de44527
--- /dev/null
+++ b/src/scope.js
@@ -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 (
+
+ )
+ }
+ const to = cleanHREF(href, location.pathname)
+ return (
+
+ )
+})
+
+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
+ case '!':
+ return
+ default:
+ return (
+
+ )
+ }
+}
+
+const pre = props => props.children
+
+const scope = rebassMarkdown({
+ a: {
+ is: link
+ },
+ code: {
+ is: code
+ },
+ pre: {
+ is: pre,
+ }
+})
+
+export default scope
diff --git a/test/build.js b/test/build.js
index 1568e2e..7bdbb49 100644
--- a/test/build.js
+++ b/test/build.js
@@ -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()
-})
diff --git a/test/components.js b/test/components.js
new file mode 100644
index 0000000..adffd77
--- /dev/null
+++ b/test/components.js
@@ -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(
+
+ Hello
+
+ )
+ t.snapshot(json)
+})
+
+test('CenteredLayout does not render without active prop', t => {
+ const json = renderJSON(
+
+ Hello
+
+ )
+ t.is(json, 'Hello')
+})
+
+test('Catch renders', t => {
+ const json = renderJSON(
+ Catch
+ )
+ t.snapshot(json)
+})
+
+test('Catch renders error', t => {
+ const Throws = props => {
+ throw new Error('nope')
+ return false
+ }
+ const json = renderJSON(
+
+
+
+ )
+ t.is(json.type, 'pre')
+ t.is(json.children[0], 'Error: nope')
+ t.snapshot(json)
+})
+
+test('FileList renders', t => {
+ const json = renderJSON(
+
+
+
+ )
+ t.snapshot(json)
+})
+
+// doesn't seem to render correctly with refs
+test.skip('LiveEditor renders', t => {
+ const json = renderJSON(
+
+ )
+ t.snapshot(json)
+})
+
+test('LivePreview renders', t => {
+ const json = renderJSON(
+
+ )
+ t.snapshot(json)
+})
+
+test('ScopeProvider renders', t => {
+ const json = renderJSON(
+
+ Hello
+
+ )
+ t.snapshot(json)
+})
+
+test('ScrollTop renders', t => {
+ const json = renderJSON(
+
+
+
+ )
+ t.is(json, null)
+})
+
+test('ScrollTop scrolls window on location change', t => {
+ sinon.stub(window, 'scrollTo')
+ const instance = render(
+
+
+
+ )
+ instance.update(
+
+
+
+ )
+ 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(
+
+
+
+ )
+ instance.update(
+
+
+
+ )
+
+ 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(
+
+
+
+ )
+ instance.update(
+
+
+
+ )
+
+ t.false(el.scrollIntoView.calledOnce)
+})
+
+test('Library renders', t => {
+ const json = renderJSON(
+
+ Hello
+ },
+ ]}
+ />
+
+ )
+ t.snapshot(json)
+})
+
+test('SidebarLayout renders', t => {
+ const home = {
+ key: '/',
+ path: '/',
+ name: 'index',
+ props: {},
+ Component: () => Home
+ }
+ const json = renderJSON(
+
+ About
+ }
+ ]}
+ />
+
+ )
+ 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(
+
+
+
+ )
+ 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(
+
+ Hello
+
+ )
+ t.is(json.props.href, 'http://example.com')
+ t.snapshot(json)
+})
+
diff --git a/test/components/compositor.jsx b/test/components/compositor.jsx
deleted file mode 100644
index 1d6afa0..0000000
--- a/test/components/compositor.jsx
+++ /dev/null
@@ -1,5 +0,0 @@
----
-className: beep
----
-
-Hello
diff --git a/test/snapshots/build.js.md b/test/snapshots/build.js.md
index 5bc7e2b..cab9150 100644
--- a/test/snapshots/build.js.md
+++ b/test/snapshots/build.js.md
@@ -8,10 +8,4 @@ Generated by [AVA](https://ava.li).
> Snapshot 1
- 'x0Hello
'
-
-## static uses getInitialProps method
-
-> Snapshot 1
-
- 'x0Hello
'
+ 'x0'
diff --git a/test/snapshots/build.js.snap b/test/snapshots/build.js.snap
index ff845dd..5034be3 100644
Binary files a/test/snapshots/build.js.snap and b/test/snapshots/build.js.snap differ
diff --git a/test/snapshots/components.js.md b/test/snapshots/components.js.md
new file mode 100644
index 0000000..421cdfa
--- /dev/null
+++ b/test/snapshots/components.js.md
@@ -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
+
+
+ Error: nope
+
+
+## CenteredLayout renders with active prop
+
+> Snapshot 1
+
+
+ Hello
+
+
+## FileList renders
+
+> Snapshot 1
+
+ [
+
+ beep
+
,
+ ,
+ ]
+
+## Library renders
+
+> Snapshot 1
+
+
+
+## LivePreview renders
+
+> Snapshot 1
+
+
+
+## ScopeProvider renders
+
+> Snapshot 1
+
+ 'Hello'
+
+## SidebarLayout renders
+
+> Snapshot 1
+
+ [
+ ,
+ ,
+ ]
+
+## scope.a renders
+
+> Snapshot 1
+
+
+
+## scope.a renders a plain link for absolute URLs
+
+> Snapshot 1
+
+
+ Hello
+
+
+## scope.blockquote renders
+
+> Snapshot 1
+
+
+
+## scope.code renders
+
+> Snapshot 1
+
+
+
+## scope.h1 renders
+
+> Snapshot 1
+
+
+
+## scope.h2 renders
+
+> Snapshot 1
+
+
+
+## scope.h3 renders
+
+> Snapshot 1
+
+
+
+## scope.h4 renders
+
+> Snapshot 1
+
+
+
+## scope.h5 renders
+
+> Snapshot 1
+
+
+
+## scope.h6 renders
+
+> Snapshot 1
+
+
+
+## scope.hr renders
+
+> Snapshot 1
+
+
+
+## scope.img renders
+
+> Snapshot 1
+
+
+
+## scope.inlineCode renders
+
+> Snapshot 1
+
+
+
+## scope.li renders
+
+> Snapshot 1
+
+
+
+## scope.p renders
+
+> Snapshot 1
+
+
+
+## scope.table renders
+
+> Snapshot 1
+
+
+
+## scope.ul renders
+
+> Snapshot 1
+
+
diff --git a/test/snapshots/components.js.snap b/test/snapshots/components.js.snap
new file mode 100644
index 0000000..99c7e2b
Binary files /dev/null and b/test/snapshots/components.js.snap differ