mirror of
https://github.com/c8r/x0.git
synced 2024-10-04 00:58:09 +03:00
Rewrite for v5
This commit is contained in:
parent
c9f97e2b6f
commit
dad4e808b7
206
README.md
206
README.md
@ -22,9 +22,8 @@ npm install -g @compositor/x0
|
||||
- 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
|
||||
- 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,18 +31,20 @@ 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
|
||||
```
|
||||
|
||||
|
||||
@ -52,36 +53,36 @@ x0 src/App.js -op 8080
|
||||
Render static HTML and client-side bundle
|
||||
|
||||
```sh
|
||||
x0 build src/App.js --out-dir site
|
||||
x0 build components
|
||||
```
|
||||
|
||||
Render 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 => (
|
||||
<h1>Hello {props.data}</h1>
|
||||
)
|
||||
|
||||
App.getInitialProps = async ({
|
||||
Component,
|
||||
pathname
|
||||
}) => {
|
||||
App.getInitialProps = async () => {
|
||||
const fetch = require('isomorphic-fetch')
|
||||
const res = await fetch('http://example.com/data')
|
||||
const data = await res.json()
|
||||
@ -92,145 +93,92 @@ App.getInitialProps = async ({
|
||||
|
||||
## CSS-in-JS
|
||||
|
||||
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
|
||||
x0 supports server-side rendering for [styled-components][sc] with zero configuration.
|
||||
To enable CSS rendering for static output, ensure that `styled-components` is installed as a dependency in your `package.json`
|
||||
|
||||
```sh
|
||||
x0 build src/App.js --cssLibrary="styled-components"
|
||||
```
|
||||
|
||||
Available options:
|
||||
|
||||
- [`styled-components`][sc]
|
||||
- [`glamorous`][glamorous]
|
||||
- [`glamor`][glamor]
|
||||
- [`fela`][fela]
|
||||
|
||||
## Head content
|
||||
|
||||
Head elements such as `<title>`, `<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.
|
||||
|
||||
```jsx
|
||||
const App = props => (
|
||||
<React.Fragment>
|
||||
<title>Hello x0</title>
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: 'body{font-family:-apple-system,BlinkMacSystemFont,sans-serif}'
|
||||
}} />
|
||||
<h1>Hello x0</h1>
|
||||
</React.Fragment>
|
||||
)
|
||||
```json
|
||||
"dependencies": {
|
||||
"styled-components": "^3.2.6"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Default props can be passed to x0 in a `package.json` field named `x0`.
|
||||
Default options can be set in the `x0` field in `package.json`.
|
||||
|
||||
```json
|
||||
"x0": {
|
||||
"static": true,
|
||||
"outDir": "site",
|
||||
"title": "Hello",
|
||||
"count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Routing
|
||||
## Head content
|
||||
|
||||
To render multiple pages and use routing, add a `routes` array to the `package.json` configuration object.
|
||||
Head elements such as `<title>`, `<meta>`, and `<style>` can be configured with the `x0` field in `package.json`.
|
||||
|
||||
```json
|
||||
"x0": {
|
||||
"routes": [
|
||||
"/",
|
||||
"/about"
|
||||
"title": "My Site",
|
||||
"meta": [
|
||||
{ "name": "twitter:card", "content": "summary" }
|
||||
{ "name": "twitter:image", "content": "kitten.png" }
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"rel": "stylesheet",
|
||||
"href": "https://fonts.googleapis.com/css?family=Roboto"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
x0 build src/App.js --static --out-dir site
|
||||
```
|
||||
## Custom HTML Template
|
||||
|
||||
The current route will be passed to the component as `props.pathname`.
|
||||
This can be used with [react-router][react-router]'s StaticRouter and BrowserRouter components.
|
||||
|
||||
```jsx
|
||||
// Example with react-router
|
||||
import React from 'react'
|
||||
import {
|
||||
StaticRouter,
|
||||
BrowserRouter,
|
||||
Route,
|
||||
Link
|
||||
} from 'react-router-dom'
|
||||
import Home from './Home'
|
||||
import About from './About'
|
||||
|
||||
// universal router component
|
||||
const Router = typeof document !== 'undefined'
|
||||
? BrowserRouter
|
||||
: StaticRouter
|
||||
|
||||
const App = props => (
|
||||
<Router
|
||||
basename={props.basename}
|
||||
location={props.pathname}>
|
||||
<nav>
|
||||
<Link to='/'>Home</Link>
|
||||
<Link to='/about'>About</Link>
|
||||
</nav>
|
||||
<Route
|
||||
exact
|
||||
path='/'
|
||||
render={() => <Home {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path='/about'
|
||||
render={() => <About {...props} />}
|
||||
/>
|
||||
</Router>
|
||||
)
|
||||
```
|
||||
|
||||
### Code Splitting
|
||||
|
||||
To split client side bundles when rendering a static site,
|
||||
x0 supports [React Loadable][react-loadable] with no additional setup needed.
|
||||
|
||||
```jsx
|
||||
// example of using React Loadable
|
||||
import React from 'react'
|
||||
import Loadable from 'react-loadable'
|
||||
|
||||
const About = Loadable({
|
||||
loading: () => <div>loading...</div>,
|
||||
loader: () => import('./About')
|
||||
})
|
||||
|
||||
const App = props => (
|
||||
<div>
|
||||
<h1>Hello</h1>
|
||||
<About />
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
### Proxy
|
||||
|
||||
If you want to proxy requests you can configure it using the `x0` key in your `package.json`.
|
||||
This can be useful when you're running a local api server during development.
|
||||
|
||||
The following example proxies all `/api` requests to `http://localhost:3000`.
|
||||
A custom HTML template can be passed as the `template` option.
|
||||
|
||||
```json
|
||||
"x0": {
|
||||
"/api": "http://localhost:3000"
|
||||
"template": "./html.js"
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// example template
|
||||
module.exports = ({
|
||||
html,
|
||||
css,
|
||||
scripts,
|
||||
title,
|
||||
meta = [],
|
||||
links = [],
|
||||
static: isStatic
|
||||
}) => `<!DOCTYPE html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
${css}
|
||||
</head>
|
||||
<div id=root>${html}</div>
|
||||
${scripts}
|
||||
`
|
||||
```
|
||||
|
||||
### Routing
|
||||
|
||||
x0 creates routes based on the file system, using [react-router][react-router].
|
||||
To set the base URL for static builds, use the `basename` option.
|
||||
|
||||
```json
|
||||
"x0": {
|
||||
"basename": "/my-site"
|
||||
}
|
||||
```
|
||||
|
||||
### webpack
|
||||
|
||||
Custom webpack loaders can be used by creating a partial `webpack.config.js` file and passing it to the `--config` option.
|
||||
Webpack configuration files named `webpack.config.js` will automatically be merged with the built-in configuration, using [webpack-merge][webpack-merge].
|
||||
To use a custom filename, pass the file path to the `--webpack` flag.
|
||||
|
||||
```js
|
||||
// webpack.config.js example
|
||||
@ -243,14 +191,8 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
x0 build App.js --config webpack.config.js
|
||||
```
|
||||
|
||||
See the [example](examples/webpack-config).
|
||||
|
||||
x0 uses [webpack-merge][webpack-merge], which means that other webpack features, such as plugins, should also work.
|
||||
|
||||
#### Related
|
||||
|
||||
- [Create React App](https://github.com/facebookincubator/create-react-app)
|
||||
|
99
bin/cli.js
99
bin/cli.js
@ -1,99 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const path = require('path')
|
||||
const meow = require('meow')
|
||||
const { pkg } = require('read-pkg-up').sync()
|
||||
const openBrowser = require('react-dev-utils/openBrowser')
|
||||
const ora = require('ora')
|
||||
const chalk = require('chalk')
|
||||
|
||||
const x0Pkg = require('../package.json')
|
||||
|
||||
require('update-notifier')({ pkg: x0Pkg }).notify()
|
||||
|
||||
const cli = meow(`
|
||||
Usage:
|
||||
|
||||
$ x0 dev src/App.js
|
||||
|
||||
$ x0 build src/App.js
|
||||
|
||||
Options:
|
||||
|
||||
-d --out-dir Output directory for static build
|
||||
|
||||
-s --static Render static HTML without client-side JS
|
||||
|
||||
-p --port Port for dev server
|
||||
|
||||
-o --open Open dev server in default browser
|
||||
|
||||
-c --config Pass a custom weback config to merge with the default config
|
||||
|
||||
--proxy Proxy requests to another server (only for dev)
|
||||
|
||||
--proxy-path Path to proxy, default: /api
|
||||
|
||||
`, {
|
||||
alias: {
|
||||
d: 'outDir',
|
||||
s: 'static',
|
||||
p: 'port',
|
||||
o: 'open',
|
||||
c: 'config',
|
||||
h: 'help',
|
||||
v: 'version',
|
||||
}
|
||||
})
|
||||
|
||||
const [ cmd, file ] = cli.input
|
||||
const options = Object.assign({}, pkg ? pkg.x0 : {}, cli.flags)
|
||||
|
||||
const absolute = f => f
|
||||
? path.isAbsolute(f) ? f : path.join(process.cwd(), f)
|
||||
: null
|
||||
|
||||
const filename = absolute(file || cmd)
|
||||
|
||||
if (options.config) {
|
||||
options.config = absolute(options.config)
|
||||
}
|
||||
|
||||
console.log(chalk.black.bgCyan(' x0 '), chalk.cyan('@compositor/x0'), '\n')
|
||||
const spinner = ora().start()
|
||||
|
||||
switch (cmd) {
|
||||
case 'build':
|
||||
spinner.start('building static site')
|
||||
const build = require('../lib/static')
|
||||
build(filename, options)
|
||||
.then(async html => {
|
||||
if (!options.outDir) {
|
||||
return console.log(html)
|
||||
}
|
||||
|
||||
spinner.succeed(`static site saved to ${options.outDir}`)
|
||||
})
|
||||
.catch(err => {
|
||||
spinner.fail('Error')
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
})
|
||||
break
|
||||
case 'dev':
|
||||
default:
|
||||
spinner.start('starting dev server')
|
||||
const dev = require('../lib/dev')
|
||||
dev(filename, options)
|
||||
.then(server => {
|
||||
const { port } = server.listeningApp.address()
|
||||
spinner.succeed(`dev server listening at http://localhost:${port}`)
|
||||
if (options.open) {
|
||||
openBrowser(`http://localhost:${port}`)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
spinner.fail(err)
|
||||
process.exit(1)
|
||||
})
|
||||
break
|
||||
}
|
159
cli.js
Executable file
159
cli.js
Executable file
@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const meow = require('meow')
|
||||
const findup = require('find-up')
|
||||
const readPkg = require('read-pkg-up').sync
|
||||
const openBrowser = require('react-dev-utils/openBrowser')
|
||||
const chalk = require('chalk')
|
||||
const clipboard = require('clipboardy')
|
||||
|
||||
const config = require('pkg-conf').sync('x0')
|
||||
const pkg = readPkg().pkg
|
||||
|
||||
const log = (...args) => {
|
||||
console.log(
|
||||
chalk.black.bgCyan(' x0 '),
|
||||
...args
|
||||
)
|
||||
}
|
||||
log.error = (...args) => {
|
||||
console.log(
|
||||
chalk.black.bgRed(' err '),
|
||||
chalk.red(...args)
|
||||
)
|
||||
}
|
||||
|
||||
const cli = meow(`
|
||||
Usage
|
||||
|
||||
Dev Server
|
||||
|
||||
x0 pages
|
||||
|
||||
Build
|
||||
|
||||
x0 build pages
|
||||
|
||||
Options
|
||||
|
||||
--webpack Path to webpack config file
|
||||
|
||||
Dev Server
|
||||
|
||||
-o --open Open dev server in default browser
|
||||
-p --port Port for dev server
|
||||
|
||||
Build
|
||||
|
||||
-d --out-dir Output directory (default dist)
|
||||
-s --static Output static HTML without JS bundle
|
||||
-t --template Path to custom HTML template
|
||||
|
||||
`, {
|
||||
flags: {
|
||||
// dev
|
||||
open: {
|
||||
type: 'boolean',
|
||||
alias: 'o'
|
||||
},
|
||||
port: {
|
||||
type: 'string',
|
||||
alias: 'p'
|
||||
},
|
||||
// build
|
||||
outDir: {
|
||||
type: 'string',
|
||||
alias: 'd'
|
||||
},
|
||||
static: {
|
||||
type: 'boolean',
|
||||
},
|
||||
template: {
|
||||
type: 'string',
|
||||
alias: 't'
|
||||
},
|
||||
// shared
|
||||
config: {
|
||||
type: 'string',
|
||||
alias: 'c'
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
},
|
||||
webpack: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const [ cmd, file ] = cli.input
|
||||
const input = path.resolve(file || cmd)
|
||||
const stats = fs.statSync(input)
|
||||
const dirname = stats.isDirectory() ? input : path.dirname(input)
|
||||
const filename = stats.isDirectory() ? null : input
|
||||
|
||||
const opts = Object.assign({
|
||||
input,
|
||||
dirname,
|
||||
filename,
|
||||
stats,
|
||||
outDir: 'dist',
|
||||
basename: '/',
|
||||
scope: {},
|
||||
pkg,
|
||||
}, config, cli.flags)
|
||||
|
||||
opts.outDir = path.resolve(opts.outDir)
|
||||
if (opts.config) opts.config = path.resolve(opts.config)
|
||||
if (opts.webpack) {
|
||||
opts.webpack = require(path.resolve(opts.webpack))
|
||||
} else {
|
||||
const webpackConfig = findup.sync('webpack.config.js', { cwd: dirname })
|
||||
if (webpackConfig) opts.webpack = require(webpackConfig)
|
||||
}
|
||||
|
||||
if (opts.template) {
|
||||
opts.template = require(path.resolve(opts.template))
|
||||
}
|
||||
|
||||
const handleError = err => {
|
||||
log.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case 'build':
|
||||
log('building static site')
|
||||
const { build } = require('.')
|
||||
build(opts)
|
||||
.then(res => {
|
||||
log('site saved to ' + opts.outDir)
|
||||
})
|
||||
.catch(handleError)
|
||||
break
|
||||
case 'dev':
|
||||
default:
|
||||
log('starting dev server')
|
||||
const { dev } = require('.')
|
||||
dev(opts)
|
||||
.then(res => {
|
||||
const { port } = res.options
|
||||
const url = `http://localhost:${port}`
|
||||
log(
|
||||
'dev server listening on',
|
||||
chalk.green(url),
|
||||
chalk.gray('(copied to clipboard)')
|
||||
)
|
||||
clipboard.write(url)
|
||||
if (opts.open) {
|
||||
openBrowser(url)
|
||||
}
|
||||
})
|
||||
.catch(handleError)
|
||||
break
|
||||
}
|
||||
|
||||
require('update-notifier')({
|
||||
pkg: require('./package.json')
|
||||
}).notify()
|
9
docs/index.js
Normal file
9
docs/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<h1>Hello x0</h1>
|
||||
)
|
||||
}
|
||||
}
|
3
index.js
3
index.js
@ -1 +1,2 @@
|
||||
module.exports = require('./lib')
|
||||
module.exports.dev = require('./lib/dev')
|
||||
module.exports.build = require('./lib/build')
|
||||
|
189
lib/build.js
Normal file
189
lib/build.js
Normal file
@ -0,0 +1,189 @@
|
||||
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 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 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'
|
||||
}
|
||||
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 }
|
||||
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 baseRoutes = await App.getRoutes()
|
||||
|
||||
// todo clean up
|
||||
const dynamicRoutes = []
|
||||
baseRoutes.forEach(route => {
|
||||
if (route.props.routes) {
|
||||
route.props.routes.forEach(subroute => dynamicRoutes.push(
|
||||
Object.assign({}, route, subroute)
|
||||
))
|
||||
}
|
||||
})
|
||||
const routes = [
|
||||
...baseRoutes.filter(route => !route.props.routes),
|
||||
...dynamicRoutes
|
||||
]
|
||||
return routes
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
)
|
||||
|
||||
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 = await getRoutes(App)
|
||||
const template = createTemplate(opts)
|
||||
|
||||
const pages = routes.map(route => renderHTML(
|
||||
Object.assign({}, route, {
|
||||
opts,
|
||||
App,
|
||||
routes,
|
||||
})
|
||||
))
|
||||
|
||||
const config = merge(baseConfig, opts.webpack)
|
||||
|
||||
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)
|
||||
resolve(stats)
|
||||
})
|
||||
})
|
||||
}
|
52
lib/config.js
Normal file
52
lib/config.js
Normal file
@ -0,0 +1,52 @@
|
||||
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/,
|
||||
use: {
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: path.resolve(__dirname, '../node_modules'),
|
||||
include: path.resolve(__dirname),
|
||||
use: {
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// common config
|
||||
module.exports = {
|
||||
stats: 'none',
|
||||
resolve: {
|
||||
modules: [
|
||||
__dirname,
|
||||
path.join(__dirname, '../node_modules'),
|
||||
'node_modules'
|
||||
]
|
||||
},
|
||||
module: {
|
||||
rules
|
||||
},
|
||||
node: {
|
||||
fs: 'empty'
|
||||
},
|
||||
plugins: []
|
||||
}
|
18
lib/createTemplate.js
Normal file
18
lib/createTemplate.js
Normal file
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
71
lib/dev.js
Normal file
71
lib/dev.js
Normal file
@ -0,0 +1,71 @@
|
||||
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.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),
|
||||
})
|
||||
)
|
||||
|
||||
config.plugins.push(
|
||||
new MiniHTMLWebpackPlugin({
|
||||
context: opts,
|
||||
template
|
||||
})
|
||||
)
|
||||
|
||||
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' }, () => {
|
||||
resolve(server)
|
||||
})
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
@ -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
|
@ -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
|
@ -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()
|
||||
]
|
||||
}
|
@ -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
|
||||
})
|
||||
})
|
||||
}
|
109
lib/dev/index.js
109
lib/dev/index.js
@ -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)
|
||||
}
|
101
lib/entry.js
Normal file
101
lib/entry.js
Normal file
@ -0,0 +1,101 @@
|
||||
import path from 'path'
|
||||
import React from 'react'
|
||||
import { render, hydrate } from 'react-dom'
|
||||
import {
|
||||
StaticRouter,
|
||||
BrowserRouter,
|
||||
Route
|
||||
} from 'react-router-dom'
|
||||
|
||||
const IS_CLIENT = typeof document !== 'undefined'
|
||||
const req = require.context(DIRNAME, false, /\.(js|mdx|jsx)$/)
|
||||
|
||||
const { filename } = 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)),
|
||||
Component: req(key).default || req(key)
|
||||
}))
|
||||
.filter(component => !/^(\.|_)/.test(component.name))
|
||||
.filter(component => typeof component.Component === 'function')
|
||||
|
||||
const initialComponents = getComponents(req)
|
||||
|
||||
const Router = IS_CLIENT ? BrowserRouter : StaticRouter
|
||||
|
||||
export const getRoutes = async (components = initialComponents) => {
|
||||
const routes = await components.map(async ({ key, name, Component }) => {
|
||||
const exact = name === index
|
||||
let pathname = exact ? '/' : '/' + name
|
||||
const props = Component.getInitialProps
|
||||
? await Component.getInitialProps({ path: pathname })
|
||||
: {}
|
||||
if (IS_CLIENT) pathname = props.path || pathname
|
||||
return {
|
||||
key: name,
|
||||
name,
|
||||
path: pathname,
|
||||
exact,
|
||||
Component,
|
||||
props
|
||||
}
|
||||
})
|
||||
return Promise.all(routes)
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
state = this.props
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes,
|
||||
basename = '',
|
||||
path = '/'
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<Router
|
||||
context={{}}
|
||||
basename={basename}
|
||||
location={path}>
|
||||
<React.Fragment>
|
||||
{routes.map(({ Component, ...route }) => (
|
||||
<Route
|
||||
{...route}
|
||||
render={props => (
|
||||
<Component
|
||||
{...props}
|
||||
{...route.props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</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(<App routes={routes} />, div)
|
||||
})
|
||||
}
|
||||
|
||||
if (IS_CLIENT && module.hot) {
|
||||
module.hot.accept(req.id, async () => {
|
||||
const next = require.context(DIRNAME, false, /\.(js|mdx|jsx)$/)
|
||||
const components = getComponents(next)
|
||||
const routes = await getRoutes(components)
|
||||
app.setState({ routes })
|
||||
})
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
module.exports.dev = require('./dev')
|
||||
module.exports.static = require('./static')
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
@ -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
|
||||
)
|
||||
})
|
@ -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 = `<style>${css}</style>`
|
||||
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
|
||||
}
|
@ -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 = []
|
||||
}) => ([
|
||||
'<!DOCTYPE html><meta charset="utf-8">',
|
||||
css,
|
||||
body,
|
||||
propsScript(initialProps),
|
||||
scripts.map(src => `<script src="${src}"></script>`).join('')
|
||||
].join(''))
|
||||
|
||||
const propsScript = initialProps =>
|
||||
initialProps ? `<script id="__initial-props__" type="application/json">${JSON.stringify(initialProps)}</script>` : ''
|
||||
|
||||
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
|
@ -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
|
25
lib/template.js
Normal file
25
lib/template.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = ({
|
||||
html = '',
|
||||
css = '',
|
||||
scripts,
|
||||
js,
|
||||
publicPath,
|
||||
title = 'x0',
|
||||
meta = [],
|
||||
links = [],
|
||||
static: staticBuild
|
||||
}) =>
|
||||
`<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<meta name='generator' content='Compositor x0'>
|
||||
<title>${title}</title>
|
||||
${meta.map(({ name, content }) => `<meta name='${name}' content='${content}'>`).join('\n')}
|
||||
${links.map(({ rel, href }) => `<link rel='${rel}' href='${href}' />`).join('\n')}
|
||||
<style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style>
|
||||
${css}
|
||||
</head>
|
||||
<div id=root>${html}</div>
|
||||
${staticBuild ? '' : scripts}
|
||||
`
|
71
package.json
71
package.json
@ -4,12 +4,12 @@
|
||||
"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",
|
||||
"static": "./cli.js build docs --static -d docs",
|
||||
"build": "./cli.js build docs -d docs -c docs/webpack.config.js",
|
||||
"test": "nyc ava",
|
||||
"cover": "nyc report --reporter=html --reporter=lcov"
|
||||
},
|
||||
@ -17,50 +17,49 @@
|
||||
"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",
|
||||
"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",
|
||||
"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"
|
||||
"chalk": "^2.4.1",
|
||||
"clipboardy": "^1.2.3",
|
||||
"connect-history-api-fallback": "^1.5.0",
|
||||
"emotion-server": "^9.1.3",
|
||||
"find-up": "^2.1.0",
|
||||
"fs-extra": "^6.0.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.3.2",
|
||||
"react-dev-utils": "^5.0.1",
|
||||
"react-dom": "^16.3.2",
|
||||
"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",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"nano-style": "^1.0.0-20",
|
||||
"nano-style": "^1.0.0",
|
||||
"nyc": "^11.2.1",
|
||||
"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"
|
||||
"refunk": "^2.2.4",
|
||||
"styled-components": "^3.2.6",
|
||||
"styled-system": "^1.1.7"
|
||||
},
|
||||
"x0": {
|
||||
"title": "Compositor x0",
|
||||
"cssLibrary": "styled-components",
|
||||
"_basename": "/x0"
|
||||
"basename": "/x0"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import test from 'ava'
|
||||
import x0Static from '../lib/static'
|
||||
import build from '../lib/build'
|
||||
|
||||
const hello = path.join(__dirname, './components/Hello.js')
|
||||
const withprops = path.join(__dirname, './components/Props.js')
|
||||
@ -22,7 +22,7 @@ test.before(clean)
|
||||
test.after(clean)
|
||||
|
||||
test.cb('static renders', t => {
|
||||
x0Static(hello, {})
|
||||
build(hello, {})
|
||||
.then(result => {
|
||||
t.is(typeof result, 'object')
|
||||
t.snapshot(result.html)
|
||||
@ -31,7 +31,7 @@ test.cb('static renders', t => {
|
||||
})
|
||||
|
||||
test.cb('static writes', t => {
|
||||
x0Static(hello, {
|
||||
build(hello, {
|
||||
outDir: 'test/output'
|
||||
})
|
||||
.then(result => {
|
||||
@ -42,7 +42,7 @@ test.cb('static writes', t => {
|
||||
})
|
||||
|
||||
test.cb('static uses getInitialProps method', t => {
|
||||
x0Static(withprops, {})
|
||||
build(withprops, {})
|
||||
.then(result => {
|
||||
t.is(typeof result, 'object')
|
||||
t.snapshot(result.html)
|
||||
@ -51,7 +51,7 @@ test.cb('static uses getInitialProps method', t => {
|
||||
})
|
||||
|
||||
test.cb('static picks up routes config', t => {
|
||||
x0Static(hello, {
|
||||
build(hello, {
|
||||
routes: [
|
||||
'/'
|
||||
]
|
||||
@ -68,7 +68,7 @@ test.cb('static makes a directory', t => {
|
||||
fs.rmdirSync(output)
|
||||
}
|
||||
|
||||
x0Static(hello, {
|
||||
build(hello, {
|
||||
outDir: 'test/output'
|
||||
})
|
||||
.then(result => {
|
||||
|
Loading…
Reference in New Issue
Block a user