1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

initial source

This commit is contained in:
nkzawa 2016-10-06 08:52:50 +09:00
parent 404bee1215
commit 9b06a22f31
16 changed files with 1036 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.log
node_modules
dist

31
bin/next Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env node
import { resolve } from 'path'
import parseArgs from 'minimist'
import { spawn } from 'cross-spawn';
const defaultCommand = 'dev'
const commands = new Set([
defaultCommand,
'build',
'start'
])
let cmd = process.argv[2]
let args
if (commands.has(cmd)) {
args = process.argv.slice(3)
} else {
cmd = defaultCommand
args = process.argv.slice(2)
}
const bin = resolve(__dirname, 'next-' + cmd)
const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] })
proc.on('close', (code) => process.exit(code))
proc.on('error', (err) => {
console.log(err)
process.exit(1)
})

44
bin/next-build Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env node
import { resolve, dirname } from 'path'
import parseArgs from 'minimist'
import fs from 'mz/fs'
import mkdirp from 'mkdirp-then';
import glob from 'glob-promise'
import { transpile, bundle } from '../server/build'
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
},
boolean: ['h']
})
const dir = resolve(argv._[0] || '.')
Promise.resolve()
.then(async () => {
const paths = await glob('**/*.js', { cwd: dir, ignore: 'node_modules/**' })
await Promise.all(paths.map(async (p) => {
const code = await transpile(resolve(dir, p))
const outpath = resolve(dir, '.next', p)
await writeFile(outpath, code)
}))
const pagePaths = await glob('.next/pages/**/*.js', { cwd: dir })
await Promise.all(pagePaths.map(async (p) => {
const code = await bundle(resolve(dir, p))
const outpath = resolve(dir, '.next', p)
await writeFile(outpath, code)
}))
})
.catch((err) => {
console.error(err)
exit(1)
})
async function writeFile (path, data) {
await mkdirp(dirname(path))
await fs.writeFile(path, data, { encoding: 'utf8', flag: 'w+' })
}

27
bin/next-start Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env node
import parseArgs from 'minimist'
import Server from '../server'
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
p: 'port'
},
boolean: ['h'],
default: {
p: 3000
}
})
const dir = argv._[0] || '.'
const srv = new Server(dir)
srv.start(argv.port)
.then(() => {
console.log('> Ready on http://localhost:%d', argv.port);
})
.catch((err) => {
console.error(err)
exit(1)
})

31
client/eval-script.js Normal file
View file

@ -0,0 +1,31 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from '../lib/app'
const modules = new Map([
['react', React],
['react-dom', ReactDOM],
['next/app', App]
])
/**
* IMPORTANT: This module is compiled *without* `use strict`
* so that when we `eval` a dependency below, we don't enforce
* `use strict` implicitly.
*
* Otherwise, modules like `d3` get `eval`d and forced into
* `use strict` where they don't work (at least in current versions)
*
* To see the compilation details, look at `gulpfile.js` and the
* usage of `babel-plugin-transform-remove-strict-mode`.
*/
export default function evalScript (script) {
const module = { exports: {} }
const require = function (path) { // eslint-disable-line no-unused-vars
return modules.get(path)
}
// don't use Function() here since it changes source locations
eval(script) // eslint-disable-line no-eval
return module.exports
}

18
client/next.js Normal file
View file

@ -0,0 +1,18 @@
import { createElement } from 'react'
import { render } from 'react-dom'
import evalScript from './eval-script'
import Router from './router'
import DefaultApp from '../lib/app'
const {
__NEXT_DATA__: { app, component, props }
} = window
const App = app ? evalScript(app).default : DefaultApp
const Component = evalScript(component).default
const router = new Router({ Component, props })
const container = document.getElementById('__next')
const appProps = { Component, props, router: {} }
render(createElement(App, { ...appProps }), container)

259
client/router.js Normal file
View file

@ -0,0 +1,259 @@
import { parse } from 'url'
import evalScript from './eval-script'
import shallowEquals from './shallow-equals'
export default class Router {
constructor (initialData) {
this.subscriptions = []
const { Component } = initialData
const { pathname } = location
const route = toRoute(pathname)
this.currentRoute = route
this.currentComponent = Component.displayName
this.currentComponentData = initialData
// set up the component cache (by route keys)
this.components = { [route]: initialData }
// in order for `e.state` to work on the `onpopstate` event
// we have to register the initial route upon initialization
const url = pathname + (location.search || '') + (location.hash || '')
this.replace(Component, url)
this.onPopState = this.onPopState.bind(this)
window.addEventListener('unload', () => {})
window.addEventListener('popstate', this.onPopState)
}
onPopState (e) {
this.abortComponentLoad()
const cur = this.currentComponent
const pathname = location.pathname
const url = pathname + (location.search || '') + (location.hash || '')
const { fromComponent, route } = e.state || {}
if (fromComponent && cur && fromComponent === cur) {
// if the component has not changed due
// to the url change, it means we only
// need to notify the subscriber about
// the URL change
this.set(url)
} else {
this.fetchComponent(route || url, (err, data) => {
if (err) {
// the only way we can appropriately handle
// this failure is deferring to the browser
// since the URL has already changed
location.reload()
} else {
this.currentRoute = route || toRoute(pathname)
this.currentComponent = data.Component.displayName
this.currentComponentData = data
this.set(url)
}
})
}
}
update (route, data) {
data.Component = evalScript(data.component).default
delete data.component
this.components[route] = data
if (route === this.currentRoute) {
let cancelled = false
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel
getInitialProps(data, (err, dataWithProps) => {
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false
}
if (cancelled) return
if (err) throw err
this.currentComponentData = dataWithProps
this.notify()
})
}
}
goTo (url, fn) {
this.change('pushState', null, url, fn)
}
back () {
history.back()
}
push (fromComponent, url, fn) {
this.change('pushState', fromComponent, url, fn)
}
replace (fromComponent, url, fn) {
this.change('replaceState', fromComponent, url, fn)
}
change (method, component, url, fn) {
this.abortComponentLoad()
const set = (name) => {
this.currentComponent = name
const state = name
? { fromComponent: name, route: this.currentRoute }
: {}
history[method](state, null, url)
this.set(url)
if (fn) fn(null)
}
const componentName = component && component.displayName
if (component && !componentName) {
throw new Error('Initial component must have a unique `displayName`')
}
if (this.currentComponent &&
componentName !== this.currentComponent) {
this.fetchComponent(url, (err, data) => {
if (!err) {
this.currentRoute = toRoute(url)
this.currentComponentData = data
set(data.Component.displayName)
}
if (fn) fn(err, data)
})
} else {
set(componentName)
}
}
set (url) {
const parsed = parse(url, true)
if (this.urlIsNew(parsed)) {
this.pathname = parsed.pathname
this.query = parsed.query
this.notify()
}
}
urlIsNew ({ pathname, query }) {
return this.pathname !== pathname || !shallowEquals(query, this.query)
}
fetchComponent (url, fn) {
const pathname = parse(url, true)
const route = toRoute(pathname)
let cancelled = false
let componentXHR = null
const cancel = () => {
cancelled = true
if (componentXHR && componentXHR.abort) {
componentXHR.abort()
}
}
if (this.components[route]) {
const data = this.components[route]
getInitialProps(data, (err, dataWithProps) => {
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false
}
if (cancelled) return
fn(err, dataWithProps)
})
this.componentLoadCancel = cancel
return
}
const componentUrl = toJSONUrl(route)
componentXHR = loadComponent(componentUrl, (err, data) => {
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false
}
if (err) {
if (!cancelled) fn(err)
} else {
// we update the cache even if cancelled
if (!this.components[route]) {
this.components[route] = data
}
if (!cancelled) fn(null, data)
}
})
this.componentLoadCancel = cancel
}
abortComponentLoad () {
if (this.componentLoadCancel) {
this.componentLoadCancel()
this.componentLoadCancel = null
}
}
notify () {
this.subscriptions.forEach(fn => fn())
}
subscribe (fn) {
this.subscriptions.push(fn)
return () => {
const i = this.subscriptions.indexOf(fn)
if (~i) this.subscriptions.splice(i, 1)
}
}
}
// every route finishing in `/test/` becomes `/test`
export function toRoute (path) {
return path.replace(/\/$/, '') || '/'
}
export function toJSONUrl (route) {
return ('/' === route ? '/index' : route) + '.json'
}
export function loadComponent (url, fn) {
return loadJSON(url, (err, data) => {
if (err && fn) fn(err)
const { component, props } = data
const Component = evalScript(component).default
getInitialProps({ Component, props }, fn)
})
}
function loadJSON (url, fn) {
const xhr = new XMLHttpRequest()
xhr.onload = () => {
let data
try {
data = JSON.parse(xhr.responseText)
} catch (err) {
fn(new Error('Failed to load JSON for ' + url))
return
}
fn(null, data)
}
xhr.onerror = () => {
if (fn) fn(new Error('XHR failed. Status: ' + xhr.status))
}
xhr.open('GET', url)
xhr.send()
return xhr
}
function getInitialProps (data, fn) {
const { Component: { getInitialProps } } = data
if (getInitialProps) {
Promise.resolve(getInitialProps({}))
.then((props) => fn(null, { ...data, props }))
.catch(fn)
} else {
fn(null, data)
}
}

11
client/shallow-equals.js Normal file
View file

@ -0,0 +1,11 @@
export default function shallowEquals (a, b) {
for (const i in a) {
if (b[i] !== a[i]) return false;
}
for (const i in b) {
if (b[i] !== a[i]) return false;
}
return true;
}

156
gulpfile.js Normal file
View file

@ -0,0 +1,156 @@
const gulp = require('gulp')
const babel = require('gulp-babel')
const cache = require('gulp-cached')
const notify_ = require('gulp-notify')
const uglify = require('gulp-uglify')
const webpack = require('webpack-stream')
const del = require('del')
const babelOptions = {
presets: ['es2015', 'react'],
plugins: [
'transform-async-to-generator',
'transform-object-rest-spread',
'transform-class-properties',
'transform-runtime'
]
}
gulp.task('compile', [
'compile-bin',
'compile-lib',
'compile-server',
'compile-client'
])
gulp.task('compile-bin', () => {
return gulp.src('bin/*')
.pipe(cache('bin'))
.pipe(babel(babelOptions))
.pipe(gulp.dest('dist/bin'))
.pipe(notify('Compiled binaries'))
})
gulp.task('compile-lib', () => {
return gulp.src('lib/**/*.js')
.pipe(cache('lib'))
.pipe(babel(babelOptions))
.pipe(gulp.dest('dist/lib'))
.pipe(notify('Compiled lib files'))
})
gulp.task('compile-server', () => {
return gulp.src('server/**/*.js')
.pipe(cache('server'))
.pipe(babel(babelOptions))
.pipe(gulp.dest('dist/server'))
.pipe(notify('Compiled server files'))
})
gulp.task('compile-client', () => {
return gulp.src('client/**/*.js')
.pipe(cache('client'))
.pipe(babel(babelOptions))
.pipe(gulp.dest('dist/client'))
.pipe(notify('Compiled client files'))
})
gulp.task('build', [
'build-dev-client',
])
gulp.task('build-release', [
'build-release-client'
])
gulp.task('build-dev-client', ['compile-lib', 'compile-client'], () => {
return gulp
.src('dist/client/next-dev.js')
.pipe(webpack({
quiet: true,
output: { filename: 'next-dev.bundle.js' }
}))
.pipe(gulp.dest('dist/client'))
.pipe(notify('Built dev client'))
})
gulp.task('build-release-client', ['compile-lib', 'compile-client'], () => {
return gulp
.src('dist/client/next.js')
.pipe(webpack({
quiet: true,
output: { filename: 'next.bundle.js' },
plugins: [
new webpack.webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})
]
}))
.pipe(uglify({ preserveComments: 'license' }))
.pipe(gulp.dest('dist/client'))
.pipe(notify('Built release client'))
})
gulp.task('watch', [
'watch-bin',
'watch-lib',
'watch-server',
'watch-client'
])
gulp.task('watch-bin', function () {
return gulp.watch('bin/*', [
'compile-bin'
])
})
gulp.task('watch-lib', function () {
return gulp.watch('lib/**/*.js', [
'compile-lib',
'build-dev-client'
])
})
gulp.task('watch-server', function () {
return gulp.watch('server/**/*.js', [
'compile-server'
])
})
gulp.task('watch-client', function () {
return gulp.watch('client/**/*.js', [
'compile-client',
'build-dev-client'
])
})
gulp.task('clean', () => {
return del(['dist'])
})
gulp.task('default', [
'compile',
'build',
'watch'
])
gulp.task('release', [
'compile',
'build-release'
])
// avoid logging to the console
// that we created a notification
notify_.logLevel(0)
// notification helper
function notify (msg) {
return notify_({
title: '▲ Next',
message: msg,
icon: false,
onLast: true
})
}

71
lib/app.js Normal file
View file

@ -0,0 +1,71 @@
import React, { Component, PropTypes } from 'react'
export default class App extends Component {
static childContextTypes = {
router: PropTypes.object,
headManager: PropTypes.object
}
constructor (props) {
super(props)
this.state = propsToState(props)
this.close = null
}
componentWillReceiveProps (nextProps) {
const state = propsToState(nextProps)
try {
this.setState(state)
} catch (err) {
console.error(err)
}
}
componentDidMount () {
const { router } = this.props
this.close = router.subscribe(() => {
const state = propsToState({
router,
data: router.currentComponentData
})
try {
this.setState(state)
} catch (err) {
console.error(err)
}
})
}
componentWillUnmount () {
if (this.close) this.close()
}
getChildContext () {
const { router, headManager } = this.props
return { router, headManager }
}
render () {
const { Component, props } = this.state
return React.createElement(Component, { ...props })
}
}
function propsToState (props) {
const { Component, router } = props
const url = {
query: router.query,
pathname: router.pathname,
back: () => router.back(),
goTo: (url, fn) => router.goTo(url, fn),
push: (url, fn) => router.push(Component, url, fn),
replace: (url, fn) => router.replace(Component, url, fn)
}
return {
Component,
props: { ...props.props, url }
}
}

59
lib/document.js Normal file
View file

@ -0,0 +1,59 @@
import React, { Component, PropTypes } from 'react'
import htmlescape from 'htmlescape'
export default class Document extends Component {
static childContextTypes = {
_documentProps: PropTypes.any
}
getChildContext () {
return {
_documentProps: this.props
}
}
render () {
return <html>
<Head/>
<body>
<Main/>
<DevTools/>
<NextScript/>
</body>
</html>
}
}
export function Head (props, context) {
const { head } = context._documentProps
const h = (head || [])
.map((h, i) => React.cloneElement(h, { key: '_next' + i }))
return <head>{h}</head>
}
Head.contextTypes = { _documentProps: PropTypes.any }
export function Main (props, context) {
const { html, data } = context._documentProps;
return <div>
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
<script dangerouslySetInnerHTML={{ __html: '__NEXT_DATA__ = ' + htmlescape(data) }}></script>
</div>
}
Main.contextTypes = { _documentProps: PropTypes.any }
export function DevTools (props, context) {
const { hotReload } = context._documentProps
return hotReload ? <div id='__next-hot-code-reloading-indicator'/> : null
}
DevTools.contextTypes = { _documentProps: PropTypes.any }
export function NextScript (props, context) {
const { hotReload } = context._documentProps;
const src = !hotReload ? '/_next/next.bundle.js' : '/_next/next-dev.bundle.js'
return <script type='text/javascript' src={src}/>
}
NextScript.contextTypes = { _documentProps: PropTypes.any }

52
package.json Normal file
View file

@ -0,0 +1,52 @@
{
"name": "@zeit/next",
"version": "0.0.0",
"description": "",
"main": "./dist/lib/index.js",
"files": [
"dist"
],
"bin": {
"next": "./dist/bin/next"
},
"scripts": {
"build": "gulp",
"test": "ava"
},
"dependencies": {
"babel-core": "6.17.0",
"babel-generator": "6.17.0",
"babel-loader": "6.2.5",
"babel-plugin-module-alias": "1.6.0",
"babel-plugin-transform-async-to-generator": "6.16.0",
"babel-plugin-transform-class-properties": "6.16.0",
"babel-plugin-transform-object-rest-spread": "6.16.0",
"babel-plugin-transform-runtime": "6.15.0",
"babel-preset-es2015": "6.16.0",
"babel-preset-react": "6.16.0",
"babel-runtime": "6.11.6",
"cross-spawn": "4.0.2",
"glob-promise": "1.0.6",
"htmlescape": "1.1.1",
"memory-fs": "0.3.0",
"minimist": "1.2.0",
"mkdirp-then": "1.2.0",
"mz": "2.4.0",
"path-match": "1.2.4",
"react": "15.3.2",
"react-dom": "15.3.2",
"send": "0.14.1",
"url": "0.11.0",
"webpack": "1.13.2"
},
"devDependencies": {
"ava": "0.16.0",
"gulp": "3.9.1",
"gulp-babel": "6.1.2",
"gulp-cached": "1.1.0",
"gulp-notify": "2.2.0",
"gulp-uglify": "2.0.0",
"webpack-stream": "3.2.0"
},
"license": "MIT"
}

93
server/build.js Normal file
View file

@ -0,0 +1,93 @@
import { resolve, dirname, basename } from 'path'
import webpack from 'webpack'
import { transformFile } from 'babel-core'
import MemoryFS from 'memory-fs'
import preset2015 from 'babel-preset-es2015'
import presetReact from 'babel-preset-react'
import transformAsyncToGenerator from 'babel-plugin-transform-async-to-generator'
import transformClassProperties from 'babel-plugin-transform-class-properties'
import transformObjectRestSpread from 'babel-plugin-transform-object-rest-spread'
import transformRuntime from 'babel-plugin-transform-runtime'
import moduleAlias from 'babel-plugin-module-alias'
const babelRuntimePath = require.resolve('babel-runtime/package')
.replace(/[\\\/]package\.json$/, '');
export function transpile (path, { root = process.cwd() } = {}) {
return new Promise((resolve, reject) => {
transformFile(path, {
presets: [preset2015, presetReact],
plugins: [
transformAsyncToGenerator,
transformClassProperties,
transformObjectRestSpread,
transformRuntime,
[
moduleAlias,
[
{ src: `npm:${babelRuntimePath}`, expose: 'babel-runtime' },
{ src: `npm:${require.resolve('react')}`, expose: 'react' }
]
]
],
ast: false
}, (err, result) => {
if (err) return reject(err)
resolve(result.code)
})
})
}
export function bundle (path, { root = process.cwd() } = {}) {
const fs = new MemoryFS()
const compiler = webpack({
entry: path,
output: {
path: dirname(path),
filename: basename(path),
libraryTarget: 'commonjs2'
},
externals: [
'react',
'react-dom',
'next',
'next/head',
'next/link',
'next/component',
'next/app'
],
resolveLoader: {
root: resolve(__dirname, '..', '..', 'node_modules')
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
sourceMap: false
})
],
module: {
preLoaders: [
{ test: /\.json$/, loader: 'json-loader' }
]
}
})
compiler.outputFileSystem = fs
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) return reject(err)
const jsonStats = stats.toJson()
if (jsonStats.errors.length > 0) {
const error = new Error(jsonStats.errors[0])
error.errors = jsonStats.errors
error.warnings = jsonStats.warnings
return reject(error)
}
resolve(fs.readFileSync(path))
})
})
}

109
server/index.js Normal file
View file

@ -0,0 +1,109 @@
import http from 'http'
import { resolve } from 'path'
import send from 'send'
import Router from './router'
import { render, renderJSON } from './render'
export default class Server {
constructor (root) {
this.root = resolve(root)
this.router = new Router()
this.http = http.createServer((req, res) => {
this.run(req, res).catch((err) => {
this.renderError(req, res, err)
})
})
}
async start (port) {
this.router.get('/_next/:path+', async (req, res, params) => {
const path = (params.path || []).join('/')
await this.serveStatic(req, res, path)
})
this.router.get('/:path+.json', async (req, res, params) => {
const path = (params.path || []).join('/')
await this.renderJSON(req, res, path)
})
this.router.get('/:path*', async (req, res, params) => {
const path = (params.path || []).join('/')
await this.render(req, res, path)
})
await new Promise((resolve, reject) => {
this.http.listen(port, (err) => {
if (err) return reject(err)
resolve()
})
})
}
async run (req, res) {
const fn = this.router.match(req, res)
if (fn) {
await fn()
} else {
await this.render404(req, res)
}
}
async render (req, res, path) {
const { root } = this
let html
try {
html = await render(path, req, res, { root })
} catch (err) {
if ('MODULE_NOT_FOUND' === err.code) {
return this.render404(req, res)
}
throw err
}
res.setHeader('Content-Type', 'text/html')
res.setHeader('Content-Length', Buffer.byteLength(html))
res.end(html)
}
async renderJSON (req, res, path) {
let json
try {
json = await renderJSON(path, { root })
} catch (err) {
if ('MODULE_NOT_FOUND' === err.code) {
return this.render404(req, res)
}
throw err
}
res.setHeader('Content-Type', 'application/json')
res.setHeader('Content-Length', Buffer.byteLength(json))
res.end(json)
}
async render404 (req, res) {
res.writeHead(404)
res.end('Not Found')
}
async renderError (req, res, err) {
console.error(err)
res.writeHead(500)
res.end('Error')
}
serveStatic (req, res, path) {
const p = resolve(__dirname, '../client', path)
return new Promise((resolve, reject) => {
send(req, p)
.on('error', (err) => {
if ('ENOENT' === err.code) {
this.render404(req, res).then(resolve, reject)
} else {
reject(err)
}
})
.pipe(res)
.on('finish', resolve)
})
}
}

37
server/render.js Normal file
View file

@ -0,0 +1,37 @@
import { relative, resolve } from 'path'
import { createElement } from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import fs from 'mz/fs'
import Document from '../lib/document'
import App from '../lib/app'
export async function render (path, req, res, { root = process.cwd() } = {}) {
const mod = require(resolve(root, '.next', 'pages', path)) || {}
const Component = mod.default
let props = {}
if (Component.getInitialProps) {
props = await Component.getInitialProps({ req, res })
}
const bundlePath = resolve(root, '.next', '.next', 'pages', path || 'index.js')
const component = await fs.readFile(bundlePath, 'utf8')
const app = createElement(App, {
Component,
props,
router: {}
})
const doc = createElement(Document, {
head: [],
html: renderToString(app),
data: { component },
hotReload: false
})
return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
}
export async function renderJSON (path) {
}

35
server/router.js Normal file
View file

@ -0,0 +1,35 @@
import { parse } from 'url'
import pathMatch from 'path-match'
const route = pathMatch()
export default class Router {
constructor () {
this.routes = new Map()
}
get (path, fn) {
this.add('GET', path, fn)
}
add (method, path, fn) {
const routes = this.routes.get(method) || new Set()
routes.add({ match: route(path), fn })
this.routes.set(method, routes)
}
match (req, res) {
const routes = this.routes.get(req.method)
if (!routes) return
const { pathname } = parse(req.url)
for (const r of routes) {
const params = r.match(pathname)
if (params) {
return async () => {
return r.fn(req, res, params)
}
}
}
}
}