diff --git a/.gitignore b/.gitignore index 5dadb79..a4c28e4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage .nyc_output test/output TMP +dist diff --git a/.npmignore b/.npmignore index a545358..c755bc6 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,10 @@ TMP coverage .nyc_output +test test/output docs examples +demo +dist +.travis.yml diff --git a/.travis.yml b/.travis.yml index 16e60e4..1adc919 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,13 @@ language: node_js node_js: - - 8.9 + - 10.0 +before_deploy: + - npm install + - npm run build +deploy: + provider: pages + skip_cleanup: true + github_token: $GH_TOKEN + local_dir: dist + on: + branch: master diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..233d6f5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ + +# Changelog + +## 5.0.0 + +- x0 now accepts a folder of components as the entry argument +- Automatic routing based on filename +- Dev server uses [webpack-serve](https://github.com/webpack-contrib/webpack-serve) under the hood +- Uses [mini-html-webpack-plugin](https://github.com/styleguidist/mini-html-webpack-plugin) +- Default HTML head contents for UTF-8 charset and viewport meta tag +- Minimal base CSS styling +- Rendering the `` in the component is no longer supported +- Webpack is used both for the client and static rendering, enabling webpack features in `getInitialProps` +- Support for [babel-plugin-macros](https://github.com/kentcdodds/babel-plugin-macros) +- Default props can no longer be passed through the `package.json` config +- The `routes` array in `package.json` is no longer supported +- Adding [react-router](https://github.com/ReactTraining/react-router) is no longer necessary +- Removes [react-loadable](https://github.com/jamiebuilds/react-loadable) support +- Proxy option is no longer supported, but can be configured with a custom webpack config +- Automatically looks for a `webpack.config.js` file in the directory +- The `--config` flag has been renamed to `--webpack` +- Automatic support for [styled-components](https://github.com/styled-components/styled-components) +- Automatic support for [emotion](https://github.com/emotion-js/emotion) +- Custom HTML template option +- Supports custom App component to wrap all routes +- Support for [JSX](https://github.com/c8r/jsx-loader) file format +- Support for [MDX](https://github.com/mdx-js/mdx) file format + +### Migrating from v4 + +- A directory should be passed to the x0 command, instead of a single file +- React router is not necessary for routing +- The `routes` option is no longer supported +- HTML head contents should be removed from components +- Viewport and charset meta tags are included by default +- Use the custom template option or head options to populate HTML head contents +- Default props set in options should be added to the components +- Custom usage of react-loadable will require additional setup +- The `--config` flag should be renamed to `--webpack` +- The `proxy` option is no longer supported +- The `cssLibrary` option is no longer required +- Support for automatic static rendering with `glamor`, `glamorous`, and `fela` is no longer supported +- The `getInitialProps` method's `pathname` argument has be renamed to `path` +- The `getInitialProps` method *only* receives the `path` argument, all other arguments are deprecated + diff --git a/README.md b/README.md index 5966740..e03b9c3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ # x0 Zero-config React development environment and static site generator + [![Build Status][build-badge]][build] ```sh @@ -11,7 +12,7 @@ npm install -g @compositor/x0 [build-badge]: https://img.shields.io/travis/c8r/x0/master.svg?style=flat-square [build]: https://travis-ci.org/c8r/x0 -![screen-demo](docs/demo.gif) + ## Features @@ -19,12 +20,11 @@ npm install -g @compositor/x0 - Hot-loading development environment - Works with virtually any React component\* - No confusing APIs -- Renders static HTML -- Renders JS bundles -- Works with CSS-in-JS libraries like [styled-components][sc] and [glamorous][glamorous] -- Support for routing with [react-router][react-router] +- 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 -- Support for code splitting with [React Loadable][react-loadable] \* Custom [webpack configuration](#webpack) is required for components that rely on webpack-based features @@ -32,56 +32,58 @@ npm install -g @compositor/x0 ## Isolated development environment ```sh -x0 src/App.js +x0 components ``` Options: ``` - -o --open Open dev server in default browser - -p --port Set custom port for dev server +-o --open Open dev server in default browser +-p --port Custom port for dev server +-t --template Path to custom HTML template +--webpack Path to custom webpack configuration ``` ```sh -x0 src/App.js -op 8080 +x0 components -op 8080 ``` -## Static Render +## Static Build -Render static HTML and client-side bundle +Export static HTML and client-side bundle ```sh -x0 build src/App.js --out-dir site +x0 build components ``` -Render static HTML without bundle +Export static HTML without bundle ```sh -x0 build src/App.js --out-dir site --static +x0 build components --static ``` Options ``` - -d --out-dir Directory to save index.html and bundle.js to - -s --static Only render static HTML (no client-side JS) +-d --out-dir Output directory (default dist) +-s --static Output static HTML without JS bundle +-t --template Path to custom HTML template +--webpack Path to custom webpack configuration ``` + ## Fetching Data -Use the `getInitialProps` static method to fetch data for static rendering. -This method was inspired by [Next.js][nextjs] but only works for static rendering. +Use the async `getInitialProps` static method to fetch data for static rendering. +This method was inspired by [Next.js][nextjs]. ```jsx -const App = props => ( +const Index = props => (

Hello {props.data}

) -App.getInitialProps = async ({ - Component, - pathname -}) => { +Index.getInitialProps = async () => { const fetch = require('isomorphic-fetch') const res = await fetch('http://example.com/data') const data = await res.json() @@ -90,147 +92,224 @@ App.getInitialProps = async ({ } ``` -## CSS-in-JS +## Custom App -x0 supports server-side rendering for [styled-components][sc], [glamor][glamor], [glamorous][glamorous], and [fela][fela]. -To enable CSS rendering for static output, use the `cssLibrary` option +A custom `App` component can be provided by including an `_app.js` file. +The `App` component uses the [render props][render-props] pattern to provide additional state and props to its child routes. -```sh -x0 build src/App.js --cssLibrary="styled-components" -``` +[render-props]: https://reactjs.org/docs/render-props.html -Available options: +```jsx +// example _app.js +import React from 'react' -- [`styled-components`][sc] -- [`glamorous`][glamorous] -- [`glamor`][glamor] -- [`fela`][fela] +export default class extends React.Component { + state = { + count: 0 + } -## Head content + update = fn => this.setState(fn) + + render () { + const { render, routes } = this.props -Head elements such as ``, `<meta>`, and `<style>` can be rendered at the beginning of a component. -Browsers should handle this correctly since the `<head>` and `<body>` elements are optional in HTML 5. + return render({ + ...this.state, + decrement: () => this.update(s => ({ count: s.count - 1 })), + increment: () => this.update(s => ({ count: s.count + 1 })) + }) + } +} +``` + +### Layouts + +The `App` component can also be used to provide a common layout for all routes. ```jsx -const App = props => ( - <React.Fragment> - <title>Hello x0 -

x0

Zero-config React development environment and static site generator
v4.0.0-0
npm install @compositor/x0
Get Started
Read the docs and get started on GitHub or sign up for updates.
Documentation
\ No newline at end of file diff --git a/docs/index.js b/docs/index.js new file mode 100644 index 0000000..52d8dc5 --- /dev/null +++ b/docs/index.js @@ -0,0 +1,70 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import styled from 'styled-components' +import { + Container, + Box, + Flex, + Caps, + Heading, + Text, + Button, + Pre, +} from 'rebass' +import { Logo } from '@compositor/logo' + +const Video = styled.video([], { + display: 'block', + maxWidth: '100%', + height: 'auto', + borderRadius: '16px', +}) + +export default class extends React.Component { + render () { + return ( + + + + x0: Zero-config React development environment & static site generator + + + +
npm i -g @compositor/x0
+ + + + + +
+
+ ) + } +} diff --git a/docs/theme.js b/docs/theme.js new file mode 100644 index 0000000..4ef59b4 --- /dev/null +++ b/docs/theme.js @@ -0,0 +1,8 @@ +import { theme } from 'rebass' + +export default { + ...theme, + radii: [ + 0, 4, 8, 16 + ] +} diff --git a/docs/webpack.config.js b/docs/webpack.config.js index 884b38c..9718488 100644 --- a/docs/webpack.config.js +++ b/docs/webpack.config.js @@ -3,7 +3,20 @@ module.exports = { rules: [ { test: /\.md$/, - use: 'raw-loader' + use: [ + // 'raw-loader' + { + loader: 'babel-loader', + options: { + presets: [ + 'env', + 'stage-0', + 'react' + ] + } + }, + '@compositor/md-loader' + ] } ] } diff --git a/examples/bundle/App.js b/examples/bundle/App.js index e2630e8..e218191 100644 --- a/examples/bundle/App.js +++ b/examples/bundle/App.js @@ -1,5 +1,5 @@ import React from 'react' -import connect from 'refunk' +import { connect } from 'refunk' const dec = state => ({ count: state.count - 1 }) const inc = state => ({ count: state.count + 1 }) diff --git a/examples/styled-components/App.js b/examples/styled-components/App.js index c5f571e..6f7f147 100644 --- a/examples/styled-components/App.js +++ b/examples/styled-components/App.js @@ -1,6 +1,6 @@ import React from 'react' import styled, { ServerStyleSheet } from 'styled-components' -import connect from 'refunk' +import { connect } from 'refunk' const css = `*{box-sizing:border-box} body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;margin:0}` diff --git a/index.js b/index.js index ce914ea..837fcd8 100644 --- a/index.js +++ b/index.js @@ -1 +1,2 @@ -module.exports = require('./lib') +module.exports.dev = require('./lib/dev') +module.exports.build = require('./lib/build') diff --git a/lib/build.js b/lib/build.js new file mode 100644 index 0000000..fef193d --- /dev/null +++ b/lib/build.js @@ -0,0 +1,214 @@ +const fs = require('fs-extra') +const path = require('path') +const webpack = require('webpack') +const MiniHTMLWebpackPlugin = require('mini-html-webpack-plugin') +const { generateJSReferences } = require('mini-html-webpack-plugin') +const merge = require('webpack-merge') +const React = require('react') +const { renderToString, renderToStaticMarkup } = require('react-dom/server') +const { StaticRouter } = require('react-router-dom') +const semver = require('semver') + +const util = require('util') + +const baseConfig = require('./config') +const createTemplate = require('./createTemplate') + +const getApp = opts => { + const config = merge(baseConfig, opts.webpack) + + config.mode = 'development' + config.entry = path.join(__dirname, './entry.js') + config.output= { + path: opts.tempdir, + filename: 'App.js', + libraryTarget: 'umd' + } + config.target = 'node' + + const compiler = webpack(config) + + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + reject(err) + return + } + const App = require( + path.resolve(opts.tempdir, './App.js') + ) + resolve(App) + }) + }) +} + +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 + if (!opts.pkg) return null + const deps = Object.assign({}, + opts.pkg.devDependencies, + opts.pkg.dependencies + ) + if (deps['styled-components']) { + const scVersion = semver.coerce(deps['styled-components']) + if (!semver.satisfies(scVersion, STYLED_COMPONENTS_VERSION)) return null + return 'styled-components' + } + if (deps.emotion) { + const emotionVersion = semver.coerce(deps.emotion) + 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 +} + +const renderHTML = ({ + opts, + routes, + App, + props, + path +}) => { + const render = opts.static ? renderToStaticMarkup : renderToString + const cssLibrary = getCSSLibrary(opts) + const app = React.createElement(App.default, { routes, path }) + let html + let css + switch (cssLibrary) { + case 'styled-components': + const { ServerStyleSheet } = require('styled-components') + const sheet = new ServerStyleSheet() + html = render( + sheet.collectStyles( + React.createElement(App.default, { routes, path }) + ) + ) + css = sheet.getStyleTags() + return { path, html, css, props } + 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 } + default: + html = render(app) + return { path, html, props } + } +} + +const remove = filename => { + fs.remove(filename, err => { + if (err) console.log(err) + }) +} + +const getRoutes = async (App) => { + const routes = await App.getRoutes() + + const dynamicRoutes = [] + routes.forEach(route => { + if (route.props.routes) { + route.props.routes.forEach(subroute => dynamicRoutes.push( + Object.assign({}, route, subroute) + )) + } + }) + const staticRoutes = [ + ...routes.filter(route => !route.props.routes), + ...dynamicRoutes + ] + return { routes, staticRoutes } +} + +module.exports = async (opts) => { + // mutation + baseConfig.resolve.modules.unshift( + path.join(opts.dirname, 'node_modules'), + opts.dirname + ) + + // mutation + baseConfig.plugins.push( + new webpack.DefinePlugin({ + DEV: JSON.stringify(false), + OPTIONS: JSON.stringify(opts), + DIRNAME: JSON.stringify(opts.dirname), + APP: JSON.stringify(opts.app) + }) + ) + + opts.tempdir = path.join(opts.outDir, 'TEMP') + if (!fs.existsSync(opts.outDir)) fs.mkdirSync(opts.outDir) + if (!fs.existsSync(opts.tempdir)) fs.mkdirSync(opts.tempdir) + + const App = await getApp(opts) + const { routes, staticRoutes } = await getRoutes(App) + const template = createTemplate(opts) + + const pages = staticRoutes.map(route => renderHTML( + Object.assign({}, route, { + opts, + App, + routes, + }) + )) + + const config = merge(baseConfig, opts.webpack) + + config.mode = opts.debug ? 'development' : 'production' + if (opts.debug) { + config.stats = 'verbose' + } + config.entry = path.join(__dirname, './entry') + config.output = { + path: opts.outDir, + filename: 'bundle.js', + publicPath: (opts.basename || '') + '/' + } + + // push per route/page + pages.forEach(({ path, html, css, props }) => { + config.plugins.push( + new MiniHTMLWebpackPlugin({ + filename: path + '/index.html', + context: Object.assign({}, opts, props, { html, css }), + template + }) + ) + }) + const compiler = webpack(config) + + 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) + } + resolve(stats) + }) + }) +} diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..c2039d9 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,65 @@ +const path = require('path') + +const babel = { + presets: [ + 'babel-preset-env', + 'babel-preset-stage-0', + 'babel-preset-react', + ].map(require.resolve), + plugins: [ + 'babel-plugin-macros', + 'babel-plugin-transform-runtime' + ].map(require.resolve) +} + +const rules = [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: require.resolve('babel-loader'), + options: babel + }, + { + test: /\.js$/, + exclude: path.resolve(__dirname, '../node_modules'), + include: path.resolve(__dirname), + loader: require.resolve('babel-loader'), + options: babel + }, + { + test: /\.jsx$/, + loader: require.resolve('@compositor/jsx-loader'), + options: {} + }, + { + test: /\.mdx$/, + use: [ + { + loader: require.resolve('babel-loader'), + options: babel + }, + { + loader: require.resolve('@mdx-js/loader') + } + ] + } +] + +// common config +module.exports = { + stats: 'none', + resolve: { + modules: [ + __dirname, + path.join(__dirname, '../node_modules'), + 'node_modules' + ] + }, + module: { + rules + }, + node: { + fs: 'empty' + }, + plugins: [] +} diff --git a/lib/createTemplate.js b/lib/createTemplate.js new file mode 100644 index 0000000..efa38bb --- /dev/null +++ b/lib/createTemplate.js @@ -0,0 +1,18 @@ +const { generateJSReferences } = require('mini-html-webpack-plugin') +const { minify } = require('html-minifier') +const defaultTemplate = require('./template') + +module.exports = opts => { + const template = opts.template || defaultTemplate + return context => { + const scripts = generateJSReferences(context.js, context.publicPath) + return minify( + template(Object.assign({}, context, { + scripts + })), + { + collapseWhitespace: true + } + ) + } +} diff --git a/lib/dev.js b/lib/dev.js new file mode 100644 index 0000000..0363ea0 --- /dev/null +++ b/lib/dev.js @@ -0,0 +1,78 @@ +const path = require('path') +const webpack = require('webpack') +const serve = require('webpack-serve') +const history = require('connect-history-api-fallback') +const convert = require('koa-connect') +const MiniHTMLWebpackPlugin = require('mini-html-webpack-plugin') +const merge = require('webpack-merge') + +const baseConfig = require('./config') +const createTemplate = require('./createTemplate') + +const dev = { + hot: true, + logLevel: 'error', + clientLogLevel: 'none', + stats: 'errors-only' +} + +module.exports = async (opts) => { + const config = merge(baseConfig, opts.webpack) + const template = createTemplate(opts) + + config.mode = 'development' + config.context = opts.dirname + config.entry = path.join(__dirname, './entry') + config.output= { + path: path.join(process.cwd(), 'dev'), + filename: 'dev.js', + publicPath: '/' + } + + config.resolve.modules.unshift( + opts.dirname, + path.join(opts.dirname, 'node_modules') + ) + + config.plugins.push( + new webpack.DefinePlugin({ + DEV: JSON.stringify(true), + OPTIONS: JSON.stringify(opts), + DIRNAME: JSON.stringify(opts.dirname), + APP: JSON.stringify(opts.app), + }) + ) + + config.plugins.push( + new MiniHTMLWebpackPlugin({ + context: opts, + template + }) + ) + + if (opts.debug) { + config.stats = 'verbose' + // todo: enable other logging + } + + const serveOpts = { + config, + dev, + logLevel: 'error', + port: opts.port, + hot: { logLevel: 'error' }, + add: (app, middleware, options) => { + app.use(convert(history({}))) + } + } + + return new Promise((resolve, reject) => { + serve(serveOpts) + .then(server => { + server.compiler.hooks.done.tap({ name: 'x0' }, (stats) => { + resolve({ server, stats }) + }) + }) + .catch(reject) + }) +} diff --git a/lib/dev/App.js b/lib/dev/App.js deleted file mode 100644 index 6691e50..0000000 --- a/lib/dev/App.js +++ /dev/null @@ -1,23 +0,0 @@ -const React = require('react') -const h = React.createElement -const Catch = require('./Catch') - -class App extends React.Component { - constructor (props) { - super(props) - - this.state = { - Component: props.Component, - } - } - - render () { - const { Component } = this.state - - return h(Catch, null, - h(Component, this.props) - ) - } -} - -module.exports = App diff --git a/lib/dev/Catch.js b/lib/dev/Catch.js deleted file mode 100644 index 5ab0ba6..0000000 --- a/lib/dev/Catch.js +++ /dev/null @@ -1,38 +0,0 @@ -const React = require('react') - -class Catch extends React.Component { - constructor () { - super() - - this.state = { - err: null - } - } - - componentDidCatch (err) { - console.error(err) - this.setState({ err }) - } - - componentWillReceiveProps (next) { - this.setState({ err: null }) - } - - render () { - if (this.state.err) { - return React.createElement('pre', { - style: { - fontFamily: '"Roboto Mono", Menlo, monospace', - whiteSpace: 'pre-wrap', - padding: 32, - color: 'white', - backgroundColor: 'red' - } - }, this.state.err.toString()) - } - - return React.Children.only(this.props.children) - } -} - -module.exports = Catch diff --git a/lib/dev/config.js b/lib/dev/config.js deleted file mode 100644 index e2f779d..0000000 --- a/lib/dev/config.js +++ /dev/null @@ -1,49 +0,0 @@ -const path = require('path') -const webpack = require('webpack') - -// dev webpack config -module.exports = { - mode: 'development', - devtool: 'source-map', - entry: [ - path.join(__dirname, './entry'), - ], - output: { - path: __dirname, - filename: 'dev.js', - publicPath: '/' - }, - resolve: { - modules: [ - path.join(__dirname, '../../node_modules'), - 'node_modules' - ] - }, - module: { - rules: [ - { - test: /\.js?$/, - exclude: /node_modules/, - use: { - loader: require.resolve('babel-loader'), - options: { - presets: [ - 'babel-preset-env', - 'babel-preset-stage-0', - 'babel-preset-react' - ].map(require.resolve), - plugins: [ - require.resolve('babel-plugin-transform-runtime') - ] - } - } - } - ] - }, - node: { - fs: 'empty' - }, - plugins: [ - new webpack.HotModuleReplacementPlugin() - ] -} diff --git a/lib/dev/entry.js b/lib/dev/entry.js deleted file mode 100644 index 17493fc..0000000 --- a/lib/dev/entry.js +++ /dev/null @@ -1,26 +0,0 @@ -const React = require('react') -const { render } = require('react-dom') -const App = require('./App') - -const div = typeof APP !== 'undefined' ? APP : document.body - -const id = require.resolve(COMPONENT) -const req = require(COMPONENT) -const Component = req.default || req - -const props = Object.assign({ - id, - Component -}, PROPS) - -const app = render(React.createElement(App, props), div) - -if (module.hot) { - module.hot.accept(id, function () { - const next = require(COMPONENT) - const NextComponent = next.default || next - app.setState({ - Component: NextComponent - }) - }) -} diff --git a/lib/dev/index.js b/lib/dev/index.js deleted file mode 100644 index 794681e..0000000 --- a/lib/dev/index.js +++ /dev/null @@ -1,109 +0,0 @@ -require('babel-register')({ - presets: [ - [ require.resolve('babel-preset-env'), { - targets: { - node: '8' - } - }], - require.resolve('babel-preset-stage-0'), - require.resolve('babel-preset-react') - ], - plugins: [ - 'react-loadable/babel', - 'babel-plugin-syntax-dynamic-import', - 'babel-plugin-dynamic-import-node', - ].map(require.resolve) -}) -const path = require('path') -const webpack = require('webpack') -const DevServer = require('webpack-dev-server') -const merge = require('webpack-merge') -const config = require('./config') - -const getWebpackApp = require('../static/webpackNodeApp') - -const devOptions = { - hot: true, - historyApiFallback: { - index: '/dev' - }, - overlay: true -} - -const start = async (filename, options, config) => { - const Component = await getWebpackApp(filename, options) - - const { - proxy, - port = 8000 - } = options - - const getProps = typeof Component.getInitialProps === 'function' - ? Component.getInitialProps - : async () => null - - const initialProps = await getProps(Object.assign({ - Component - }, options)) - - const props = Object.assign({}, options, initialProps) - - const defs = new webpack.DefinePlugin({ - COMPONENT: JSON.stringify(filename), - PROPS: JSON.stringify(props) - }) - - config.plugins.push(defs) - - devOptions.contentBase = path.dirname(filename) - - if (proxy) { - devOptions.proxy = proxy - } - - const compiler = webpack(config) - const server = new DevServer(compiler, devOptions) - - return new Promise((resolve, reject) => { - compiler.plugin('done', () => { - resolve(server) - }) - - server.listen(port, err => { - if (err) throw err - }) - }) -} - -module.exports = (filename, options = {}) => { - if (!filename) return - const dirname = path.dirname(filename) - - const { - port = 8000 - } = options - - config.resolve.modules.unshift( - dirname, - path.join(process.cwd(), 'node_modules'), - path.join(dirname, 'node_modules'), - ) - - config.entry.push( - `webpack-dev-server/client?http://localhost:${port}`, - 'webpack/hot/only-dev-server' - ) - - let mergedConfig = config - - if (options.basename) { - config.output.publicPath = options.basename + '/' - } - - if (options.config) { - const userConfig = require(options.config) - mergedConfig = merge(config, userConfig) - } - - return start(filename, options, mergedConfig) -} diff --git a/lib/entry.js b/lib/entry.js new file mode 100644 index 0000000..41eb723 --- /dev/null +++ b/lib/entry.js @@ -0,0 +1,194 @@ +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}
+ +
+) + +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/index.js b/lib/index.js
deleted file mode 100644
index 31373ca..0000000
--- a/lib/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-module.exports.dev = require('./dev')
-module.exports.static = require('./static')
diff --git a/lib/static/client.js b/lib/static/client.js
deleted file mode 100644
index 6f44aaa..0000000
--- a/lib/static/client.js
+++ /dev/null
@@ -1,96 +0,0 @@
-const path = require('path')
-const webpack = require('webpack')
-const MinifyPlugin = require('babel-minify-webpack-plugin')
-const { ReactLoadablePlugin } = require('react-loadable/webpack')
-const merge = require('webpack-merge')
-
-const config = {
-  mode: 'production',
-  entry: [
-    path.join(__dirname, './entry')
-  ],
-  output: {
-    filename: 'bundle.js',
-    publicPath: '/'
-  },
-  resolve: {
-    modules: [
-      path.join(__dirname, '../../node_modules'),
-      'node_modules'
-    ]
-  },
-  module: {
-    rules: [
-      {
-        test: /\.js$/,
-        exclude: /node_modules/,
-        use: {
-          loader: require.resolve('babel-loader'),
-          options: {
-            presets: [
-              'babel-preset-env',
-              'babel-preset-stage-0',
-              'babel-preset-react'
-            ].map(require.resolve),
-            plugins: [
-              require.resolve('babel-plugin-transform-runtime')
-            ]
-          }
-        }
-      }
-    ]
-  },
-  node: {
-    fs: 'empty'
-  },
-  plugins: [
-    new ReactLoadablePlugin({
-      filename: path.join(__dirname, './TMP/react-loadable.json'),
-    })
-  ]
-}
-
-module.exports = (filename, options = {}) => {
-  if (options.static || !options.outDir) return
-
-  const dirname = path.dirname(filename)
-
-  config.output.path = path.join(process.cwd(), options.outDir)
-
-  if (options.basename) {
-    config.output.publicPath = options.basename + '/'
-  }
-
-  config.resolve.modules.unshift(
-    dirname,
-    path.join(process.cwd(), 'node_modules'),
-    path.join(dirname, 'node_modules')
-  )
-
-  config.plugins.push(
-    new webpack.DefinePlugin({
-      'process.env': {
-        NODE_ENV: JSON.stringify('production')
-      },
-      COMPONENT: JSON.stringify(filename)
-    })
-  )
-
-  let mergedConfig = config
-  if (options.config) {
-    const userConfig = require(options.config)
-    mergedConfig = merge(config, userConfig)
-  }
-
-  const compiler = webpack(mergedConfig)
-
-  return new Promise((resolve, reject) => {
-    compiler.run((err, stats) => {
-      if (err) {
-        console.log(err)
-        reject(err)
-      }
-      resolve(stats)
-    })
-  })
-}
diff --git a/lib/static/entry.js b/lib/static/entry.js
deleted file mode 100644
index c88ffcf..0000000
--- a/lib/static/entry.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const React = require('react')
-const { hydrate } = require('react-dom')
-const Loadable = require('react-loadable')
-
-const App = require(COMPONENT).default || require(COMPONENT)
-const data = document.getElementById('__initial-props__').innerHTML
-const props = JSON.parse(data)
-const div = document.documentElement
-
-Loadable.preloadReady()
-  .then(() => {
-    hydrate(
-      React.createElement(App, props),
-      div
-    )
-  })
diff --git a/lib/static/getCSS.js b/lib/static/getCSS.js
deleted file mode 100644
index 9c32c25..0000000
--- a/lib/static/getCSS.js
+++ /dev/null
@@ -1,59 +0,0 @@
-const React = require('react')
-const { renderToString } = require('react-dom/server')
-
-const getSC = (Component, props) => {
-  const SC = require('styled-components')
-  const sheet = new SC.ServerStyleSheet()
-  renderToString(
-    sheet.collectStyles(
-      React.createElement(Component, props)
-    )
-  )
-  const tags = sheet.getStyleTags()
-  return tags
-}
-
-const getGlamor = (Component, props) => {
-  const glamor = require('glamor/server')
-  const { css } = glamor.renderStatic(() => (
-    renderToString(
-      React.createElement(Component, props)
-    )
-  ))
-  const tag = ``
-  return tag
-}
-
-const getFela = (Component, props) => {
-  if (!props.renderer) {
-    console.log('Warning: Fela static rendering requires a `renderer` to be passed through the `getInitialProps()` method.')
-    return ''
-  }
-  const fela = require('fela')
-  const felaDOM = require('fela-dom')
-  const renderer = props.renderer || fela.createRenderer()
-  renderToString(
-    React.createElement(Component, props)
-  )
-  const tag = felaDOM.renderToMarkup(renderer)
-  return tag
-}
-
-const libraries = {
-  'styled-components': getSC,
-  glamorous: getGlamor,
-  glamor: getGlamor,
-  fela: getFela,
-}
-
-const noop = () => ''
-
-module.exports = (Component, props = {}) => {
-  const { cssLibrary } = props
-  const getCSS = libraries[cssLibrary] || noop
-
-  // style tag strings
-  const css = getCSS(Component, props)
-
-  return css
-}
diff --git a/lib/static/index.js b/lib/static/index.js
deleted file mode 100644
index ccf0660..0000000
--- a/lib/static/index.js
+++ /dev/null
@@ -1,136 +0,0 @@
-require('babel-register')({
-  presets: [
-    [ require.resolve('babel-preset-env'), {
-      targets: {
-        node: '8'
-      }
-    }],
-    require.resolve('babel-preset-stage-0'),
-    require.resolve('babel-preset-react')
-  ],
-  plugins: [
-    'react-loadable/babel',
-    'babel-plugin-syntax-dynamic-import',
-    'babel-plugin-dynamic-import-node',
-  ].map(require.resolve)
-})
-
-const fs = require('fs')
-const path = require('path')
-const React = require('react')
-const { renderToString, renderToStaticMarkup } = require('react-dom/server')
-const Loadable = require('react-loadable')
-const { getBundles } = require('react-loadable/webpack')
-
-const getWebpackApp = require('./webpackNodeApp')
-const client = require('./client')
-const getCSS = require('./getCSS')
-
-const createHTML = ({
-  body,
-  css = '',
-  initialProps,
-  scripts = []
-}) => ([
-  '',
-  css,
-  body,
-  propsScript(initialProps),
-  scripts.map(src => ``).join('')
-].join(''))
-
-const propsScript = initialProps =>
-  initialProps ? `` : ''
-
-const render = (Component, props, isStatic, modules = []) =>
-  (isStatic ? renderToStaticMarkup : renderToString)(
-    React.createElement(Loadable.Capture, {
-      report: mod => modules.push(mod)
-    },
-      React.createElement(Component, props)
-    )
-  )
-
-const renderHTML = async (Component, options) => {
-  const modules = []
-  const isStatic = options.static || !options.outDir
-  const base = options.basename || ''
-  const script = base + '/bundle.js'
-
-  const hasInitialProps = typeof Component.getInitialProps === 'function'
-  const getProps = hasInitialProps ? Component.getInitialProps : () => ({})
-
-  const initialProps = await getProps(Object.assign({ Component }, options))
-  const props = Object.assign({}, options, initialProps)
-
-  const preloaded = await Loadable.preloadAll()
-  const body = render(Component, props, isStatic, modules)
-
-  const loadableStats = isStatic ? {} : require(path.join(__dirname, './TMP/react-loadable.json'))
-  const bundles = isStatic ? [] : getBundles(loadableStats, modules)
-    .map(bundle => bundle.file)
-    .map(src => base + '/' + src)
-
-  const scripts = isStatic ? [] : [
-    script,
-    ...bundles
-  ]
-
-  let css = ''
-  if (props.cssLibrary) {
-    css = getCSS(Component, props)
-  }
-
-  const html = createHTML({
-    body,
-    css,
-    initialProps: isStatic ? null : props,
-    scripts
-  })
-
-  return html
-}
-
-const writePage = async (Component, options) => {
-  const html = await renderHTML(Component, options)
-
-  if (options.outDir) {
-    const dir = path.join(
-      process.cwd(),
-      options.outDir,
-      options.pathname || ''
-    )
-    if (!fs.existsSync(dir)) {
-      fs.mkdirSync(dir)
-    }
-    const name = path.join(dir, 'index.html')
-    fs.writeFileSync(name, html)
-  }
-
-  return html
-}
-
-const createStatic = async (filename, baseOptions) => {
-  const Component = await getWebpackApp(filename, baseOptions)
-
-  const options = Object.assign({}, Component.defaultProps, baseOptions)
-
-  const bundle = await client(filename, options)
-
-  let html
-  if (options.routes && options.routes.length) {
-    html = await options.routes.map(async pathname => {
-      const res = await writePage(Component, Object.assign({}, options, {
-        pathname
-      }))
-      return res
-    })
-  } else {
-    html = await writePage(Component, options)
-  }
-
-
-  return { html, bundle }
-}
-
-module.exports = createStatic
diff --git a/lib/static/webpackNodeApp.js b/lib/static/webpackNodeApp.js
deleted file mode 100644
index 793c590..0000000
--- a/lib/static/webpackNodeApp.js
+++ /dev/null
@@ -1,82 +0,0 @@
-const path = require('path')
-const webpack = require('webpack')
-const merge = require('webpack-merge')
-
-// bundle App for usage in node when a custom webpack config is provided
-const config = {
-  mode: 'development',
-  output: {
-    path: path.join(__dirname, './TMP'),
-    filename: 'App.js',
-    libraryExport: 'default',
-    libraryTarget: 'umd',
-  },
-  target: 'node',
-  resolve: {
-    modules: []
-  },
-  module: {
-    rules: [
-      {
-        test: /\.js$/,
-        exclude: /node_modules/,
-        use: {
-          loader: require.resolve('babel-loader'),
-          options: {
-            presets: [
-              'babel-preset-env',
-              'babel-preset-stage-0',
-              'babel-preset-react'
-            ].map(require.resolve),
-            plugins: [
-              require.resolve('babel-plugin-transform-runtime')
-            ]
-          }
-        }
-      },
-    ]
-  },
-  plugins: [],
-}
-
-const bundleApp = (filename, options = {}) => {
-  const dirname = path.dirname(filename)
-
-  config.entry = filename
-
-  config.resolve.modules.unshift(
-    dirname,
-    path.join(process.cwd(), 'node_modules'),
-    path.join(dirname, 'node_modules'),
-  )
-
-  const userConfig = require(options.config)
-  const mergedConfig = merge(config, userConfig)
-  const compiler = webpack(mergedConfig)
-
-  return new Promise((resolve, reject) => {
-    compiler.run((err, stats) => {
-      if (err) {
-        reject(err)
-        return
-      }
-
-      try {
-        const App = require('./TMP/App')
-        resolve(App)
-      } catch (e) {
-        reject(e)
-      }
-    })
-  })
-}
-
-const getApp = async (filename, options) => {
-  if (!options.config) {
-    const req = require(filename)
-    return req.default || req
-  }
-  return await bundleApp(filename, options)
-}
-
-module.exports = getApp
diff --git a/lib/template.js b/lib/template.js
new file mode 100644
index 0000000..555f2fb
--- /dev/null
+++ b/lib/template.js
@@ -0,0 +1,25 @@
+module.exports = ({
+  html = '',
+  css = '',
+  scripts,
+  js,
+  publicPath,
+  title = 'x0',
+  meta = [],
+  links = [],
+  static: staticBuild
+}) =>
+`
+
+
+
+
+${title}
+${meta.map(({ name, content }) => ``).join('\n')}
+${links.map(({ rel, href }) => ``).join('\n')}
+
+${css}
+
+
${html}
+${staticBuild ? '' : scripts} +` diff --git a/package.json b/package.json index 1f694ec..3fcedcb 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,14 @@ { "name": "@compositor/x0", - "version": "4.0.3", + "version": "5.0.0-14", "description": "Zero-config React development environment and static site generator", "main": "index.js", "bin": { - "x0": "bin/cli.js" + "x0": "cli.js" }, "scripts": { - "start": "./bin/cli.js docs/App.js -op 8888", - "static": "./bin/cli.js build docs/App.js --static -d docs", - "build": "./bin/cli.js build docs/App.js -d docs -c docs/webpack.config.js", + "start": "./cli.js docs -op 8888", + "build": "./cli.js build docs", "test": "nyc ava", "cover": "nyc report --reporter=html --reporter=lcov" }, @@ -17,49 +16,59 @@ "author": "Brent Jackson", "license": "MIT", "dependencies": { - "babel-core": "^6.26.0", - "babel-loader": "^7.1.2", - "babel-minify-webpack-plugin": "^0.2.0", - "babel-plugin-dynamic-import-node": "^1.2.0", - "babel-plugin-syntax-dynamic-import": "^6.18.0", + "@compositor/jsx-loader": "^1.0.0-3", + "@compositor/log": "^1.0.0-0", + "@mdx-js/loader": "^0.9.0", + "@mdx-js/mdx": "^0.9.0", + "babel-core": "^6.26.3", + "babel-loader": "^7.1.4", + "babel-plugin-macros": "^2.2.1", "babel-plugin-transform-runtime": "^6.23.0", - "babel-preset-env": "^1.6.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", - "chalk": "^2.1.0", - "fela": "^6.1.0", - "fela-dom": "^7.0.0", + "chalk": "^2.4.1", + "clipboardy": "^1.2.3", + "connect-history-api-fallback": "^1.5.0", + "emotion": "^9.1.3", + "emotion-server": "^9.1.3", + "find-up": "^2.1.0", + "fs-extra": "^6.0.1", "glamor": "^2.20.40", - "glamorous": "^4.11.0", - "meow": "^3.7.0", - "ora": "^1.3.0", - "pkg-up": "^2.0.0", - "react": "^16.2.0", - "react-dev-utils": "^4.2.1", - "react-dom": "^16.2.0", - "react-fela": "^6.1.1", - "react-loadable": "^5.3.1", - "read-pkg-up": "^2.0.0", - "update-notifier": "^2.2.0", - "webpack": "^4.0.0", - "webpack-dev-server": "^3.0.0", - "webpack-merge": "^4.1.1" + "html-minifier": "^3.5.15", + "koa-connect": "^2.0.1", + "meow": "^5.0.0", + "mini-html-webpack-plugin": "^0.2.3", + "pkg-conf": "^2.1.0", + "react": "^16.4.0", + "react-dev-utils": "^5.0.1", + "react-dom": "^16.4.0", + "react-router": "^4.2.0", + "react-router-dom": "^4.2.2", + "read-pkg-up": "^3.0.0", + "semver": "^5.5.0", + "update-notifier": "^2.5.0", + "webpack": "^4.8.3", + "webpack-merge": "^4.1.2", + "webpack-serve": "^1.0.2" }, "devDependencies": { - "ava": "^0.24.0", + "@compositor/logo": "^1.3.5", + "@compositor/md-loader": "^1.0.27", + "ava": "^0.25.0", "isomorphic-fetch": "^2.2.1", - "nano-style": "^1.0.0-20", - "nyc": "^11.2.1", + "nano-style": "^1.0.0", + "nyc": "^11.8.0", "raw-loader": "^0.5.1", - "react-router-dom": "^4.2.2", - "refunk": "^2.0.0-1", - "styled-components": "^2.2.4", - "styled-system": "^1.0.7" + "rebass": "^2.0.0-2", + "refunk": "^3.0.1", + "rimraf": "^2.6.2", + "styled-components": "^3.3.0", + "styled-system": "^2.2.5" }, "x0": { "title": "Compositor x0", - "cssLibrary": "styled-components", "_basename": "/x0" }, "ava": { diff --git a/test/build.js b/test/build.js new file mode 100644 index 0000000..1568e2e --- /dev/null +++ b/test/build.js @@ -0,0 +1,45 @@ +import fs from 'fs' +import path from 'path' +import test from 'ava' +import rimraf from 'rimraf' +import build from '../lib/build' + +const input = path.resolve('test/components') +const output = path.resolve('test/output') +const htmlFile = path.resolve('test/output/index.html') +const propsFile = path.resolve('test/output/props/index.html') +const bundleFile = path.resolve('test/output/bundle.js') + +const options = { + input, + dirname: input, + outDir: output, +} + +const clean = () => { + if (fs.existsSync(output)) { + rimraf.sync(output) + } + // fs.remove(output) +} + +test.before(clean) +test.after(clean) + +test('static renders', async t => { + const res = await build(options) + 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/Hello.js b/test/components/index.js similarity index 100% rename from test/components/Hello.js rename to test/components/index.js diff --git a/test/components/Props.js b/test/components/props.js similarity index 100% rename from test/components/Props.js rename to test/components/props.js diff --git a/test/snapshots/build.js.md b/test/snapshots/build.js.md new file mode 100644 index 0000000..5bc7e2b --- /dev/null +++ b/test/snapshots/build.js.md @@ -0,0 +1,17 @@ +# Snapshot report for `test/build.js` + +The actual snapshot is saved in `build.js.snap`. + +Generated by [AVA](https://ava.li). + +## static renders + +> Snapshot 1 + + 'x0

Hello

' + +## static uses getInitialProps method + +> Snapshot 1 + + 'x0

Hello

' diff --git a/test/snapshots/build.js.snap b/test/snapshots/build.js.snap new file mode 100644 index 0000000..ff845dd Binary files /dev/null and b/test/snapshots/build.js.snap differ diff --git a/test/snapshots/static.js.md b/test/snapshots/static.js.md index 1a9b81a..3267fcd 100644 --- a/test/snapshots/static.js.md +++ b/test/snapshots/static.js.md @@ -4,28 +4,14 @@ The actual snapshot is saved in `static.js.snap`. Generated by [AVA](https://ava.li). -## static picks up routes config - -> Snapshot 1 - - [ - Promise {}, - ] - ## static renders > Snapshot 1 - '

Hello

' + 'x0

Hello

' ## static uses getInitialProps method > Snapshot 1 - '

Hello

' - -## static writes - -> Snapshot 1 - - '

Hello

' + 'x0

Hello

' diff --git a/test/snapshots/static.js.snap b/test/snapshots/static.js.snap index 2b58ca5..6679696 100644 Binary files a/test/snapshots/static.js.snap and b/test/snapshots/static.js.snap differ diff --git a/test/snapshots/template.js.md b/test/snapshots/template.js.md new file mode 100644 index 0000000..acefd59 --- /dev/null +++ b/test/snapshots/template.js.md @@ -0,0 +1,43 @@ +# Snapshot report for `test/template.js` + +The actual snapshot is saved in `template.js.snap`. + +Generated by [AVA](https://ava.li). + +## template snapshot + +> Snapshot 1 + + `␊ + ␊ + ␊ + ␊ + ␊ + Test␊ + ␊ + ␊ + ␊ + ␊ + ␊ +
hello
␊ + ␊ + ` + +## template static snapshot + +> Snapshot 1 + + `␊ + ␊ + ␊ + ␊ + ␊ + Test␊ + ␊ + ␊ + ␊ + ␊ + ␊ +
hello
␊ + ␊ + ` diff --git a/test/snapshots/template.js.snap b/test/snapshots/template.js.snap new file mode 100644 index 0000000..1071cca Binary files /dev/null and b/test/snapshots/template.js.snap differ diff --git a/test/static.js b/test/static.js deleted file mode 100644 index a424785..0000000 --- a/test/static.js +++ /dev/null @@ -1,77 +0,0 @@ -import fs from 'fs' -import path from 'path' -import test from 'ava' -import x0Static from '../lib/static' - -const hello = path.join(__dirname, './components/Hello.js') -const withprops = path.join(__dirname, './components/Props.js') -const output = path.join(__dirname, './output') -const htmlFile = path.join(__dirname, './output/index.html') -const bundleFile = path.join(__dirname, './output/bundle.js') - -const clean = () => { - if (fs.existsSync(htmlFile)) { - fs.unlinkSync(htmlFile) - } - if (fs.existsSync(bundleFile)) { - fs.unlinkSync(bundleFile) - } -} - -test.before(clean) -test.after(clean) - -test.cb('static renders', t => { - x0Static(hello, {}) - .then(result => { - t.is(typeof result, 'object') - t.snapshot(result.html) - t.end() - }) -}) - -test.cb('static writes', t => { - x0Static(hello, { - outDir: 'test/output' - }) - .then(result => { - t.is(typeof result, 'object') - t.snapshot(result.html) - t.end() - }) -}) - -test.cb('static uses getInitialProps method', t => { - x0Static(withprops, {}) - .then(result => { - t.is(typeof result, 'object') - t.snapshot(result.html) - t.end() - }) -}) - -test.cb('static picks up routes config', t => { - x0Static(hello, { - routes: [ - '/' - ] - }) - .then(result => { - t.is(typeof result, 'object') - t.snapshot(result.html) - t.end() - }) -}) - -test.cb('static makes a directory', t => { - if (fs.existsSync(output)) { - fs.rmdirSync(output) - } - - x0Static(hello, { - outDir: 'test/output' - }) - .then(result => { - t.end() - }) -}) diff --git a/test/template.js b/test/template.js new file mode 100644 index 0000000..25f5b00 --- /dev/null +++ b/test/template.js @@ -0,0 +1,35 @@ +import test from 'ava' +import template from '../lib/template' + +test('template snapshot', t => { + const html = template({ + title: 'Test', + html: 'hello', + css: '', + scripts: '', + meta: [ + { name: 'twitter:card', content: 'summary' } + ], + links: [ + { rel: 'stylesheet', content: 'hello.css' } + ] + }) + t.snapshot(html) +}) + +test('template static snapshot', t => { + const html = template({ + title: 'Test', + html: 'hello', + css: '', + static: true, + scripts: '', + meta: [ + { name: 'twitter:card', content: 'summary' } + ], + links: [ + { rel: 'stylesheet', content: 'hello.css' } + ] + }) + t.snapshot(html) +})