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

Merge branch 'v3-beta' of github.com:zeit/next.js into v3-beta

This commit is contained in:
Arunoda Susiripala 2017-07-20 23:48:58 +05:30
commit d4eb9455b8
15 changed files with 320 additions and 115 deletions

View file

@ -8,7 +8,6 @@ addons:
- google-chrome-stable
language: node_js
node_js:
- "4"
- "6"
cache:
directories:

View file

@ -1,6 +1,5 @@
environment:
matrix:
- nodejs_version: "4"
- nodejs_version: "6"
# Install scripts. (runs after repo cloning)

View file

@ -54,7 +54,8 @@ export default function dynamicComponent (p, o) {
}
loadComponent () {
promise.then((AsyncComponent) => {
promise.then((m) => {
const AsyncComponent = m.default || m
// Set a readable displayName for the wrapper component
const asyncCompName = getDisplayName(AsyncComponent)
if (asyncCompName) {
@ -65,7 +66,7 @@ export default function dynamicComponent (p, o) {
this.setState({ AsyncComponent })
} else {
if (this.isServer) {
registerChunk(AsyncComponent.__webpackChunkName)
registerChunk(m.__webpackChunkName)
}
this.state.AsyncComponent = AsyncComponent
}
@ -100,9 +101,10 @@ export default function dynamicComponent (p, o) {
const loadModule = (name) => {
const promise = modulePromiseMap[name]
promise.then((Component) => {
promise.then((m) => {
const Component = m.default || m
if (this.isServer) {
registerChunk(Component.__webpackChunkName)
registerChunk(m.__webpackChunkName)
}
moduleMap[name] = Component
remainingPromises--

View file

@ -15,9 +15,12 @@ export default class Link extends Component {
}
static propTypes = exact({
href: PropTypes.string,
as: PropTypes.string,
href: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
as: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
prefetch: PropTypes.bool,
replace: PropTypes.bool,
shallow: PropTypes.bool,
passHref: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.element,
(props, propName) => {
@ -29,9 +32,7 @@ export default class Link extends Component {
return null
}
]).isRequired,
shallow: PropTypes.bool,
passHref: PropTypes.bool
]).isRequired
})
componentWillReceiveProps (nextProps) {

View file

@ -118,12 +118,14 @@
"coveralls": "2.13.1",
"cross-env": "5.0.1",
"express": "4.15.2",
"fkill": "^5.0.0",
"husky": "0.14.3",
"jest-cli": "20.0.4",
"lint-staged": "^4.0.0",
"node-fetch": "1.7.1",
"node-notifier": "5.1.2",
"nyc": "11.0.3",
"portfinder": "^1.0.13",
"react": "15.5.4",
"react-dom": "15.5.4",
"standard": "9.0.2",

View file

@ -13,7 +13,6 @@ const buildImport = (args) => (template(`
eval('require.ensure = function (deps, callback) { callback(require) }')
require.ensure([], (require) => {
let m = require(SOURCE)
m = m.default || m
m.__webpackChunkName = '${args.name}.js'
resolve(m);
}, 'chunks/${args.name}.js');
@ -23,13 +22,12 @@ const buildImport = (args) => (template(`
const weakId = require.resolveWeak(SOURCE)
try {
const weakModule = __webpack_require__(weakId)
return resolve(weakModule.default || weakModule)
return resolve(weakModule)
} catch (err) {}
require.ensure([], (require) => {
try {
let m = require(SOURCE)
m = m.default || m
resolve(m)
} catch(error) {
reject(error)

View file

@ -0,0 +1,19 @@
import React from 'react'
export default class Counter extends React.Component {
state = { count: 0 }
incr () {
const { count } = this.state
this.setState({ count: count + 1 })
}
render () {
return (
<div>
<p>COUNT: {this.state.count}</p>
<button onClick={() => this.incr()}>Increment</button>
</div>
)
}
}

View file

@ -4,6 +4,16 @@ import { readFileSync, writeFileSync, renameSync } from 'fs'
import { join } from 'path'
import { waitFor } from 'next-test-utils'
async function check (contentFn, regex) {
while (true) {
try {
const newContent = await contentFn()
if (regex.test(newContent)) break
await waitFor(1000)
} catch (ex) {}
}
}
export default (context, render) => {
describe('Hot Module Reloading', () => {
describe('syntax error', () => {
@ -21,18 +31,45 @@ export default (context, render) => {
// change the content
writeFileSync(aboutPagePath, erroredContent, 'utf8')
const errorMessage = await browser
.waitForElementByCss('pre')
.elementByCss('pre').text()
expect(errorMessage.includes('Unterminated JSX contents')).toBeTruthy()
await check(
() => browser.elementByCss('body').text(),
/Unterminated JSX contents/
)
// add the original content
writeFileSync(aboutPagePath, originalContent, 'utf8')
const newContent = await browser
.waitForElementByCss('.hmr-about-page')
.elementByCss('p').text()
expect(newContent).toBe('This is the about page.')
await check(
() => browser.elementByCss('body').text(),
/This is the about page/
)
browser.close()
})
it('should show the error on all pages', async () => {
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
const originalContent = readFileSync(aboutPagePath, 'utf8')
const erroredContent = originalContent.replace('</div>', 'div')
// change the content
writeFileSync(aboutPagePath, erroredContent, 'utf8')
const browser = await webdriver(context.appPort, '/hmr/contact')
await check(
() => browser.elementByCss('body').text(),
/Unterminated JSX contents/
)
// add the original content
writeFileSync(aboutPagePath, originalContent, 'utf8')
await check(
() => browser.elementByCss('body').text(),
/This is the contact page/
)
browser.close()
})
@ -52,27 +89,81 @@ export default (context, render) => {
renameSync(contactPagePath, newContactPagePath)
// wait until the 404 page comes
while (true) {
try {
const pageContent = await browser.elementByCss('body').text()
if (/This page could not be found/.test(pageContent)) break
} catch (ex) {}
await waitFor(1000)
}
await check(
() => browser.elementByCss('body').text(),
/This page could not be found/
)
// Rename the file back to the original filename
renameSync(newContactPagePath, contactPagePath)
// wait until the page comes back
while (true) {
try {
const pageContent = await browser.elementByCss('body').text()
if (/This is the contact page/.test(pageContent)) break
} catch (ex) {}
await check(
() => browser.elementByCss('body').text(),
/This is the contact page/
)
await waitFor(1000)
}
browser.close()
})
})
describe('editing a page', () => {
it('should detect the changes and display it', async () => {
const browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser
.elementByCss('p').text()
expect(text).toBe('This is the about page.')
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
const originalContent = readFileSync(aboutPagePath, 'utf8')
const editedContent = originalContent.replace('This is the about page', 'COOL page')
// change the content
writeFileSync(aboutPagePath, editedContent, 'utf8')
await check(
() => browser.elementByCss('body').text(),
/COOL page/
)
// add the original content
writeFileSync(aboutPagePath, originalContent, 'utf8')
await check(
() => browser.elementByCss('body').text(),
/This is the about page/
)
browser.close()
})
it('should not reload unrelated pages', async () => {
const browser = await webdriver(context.appPort, '/hmr/counter')
const text = await browser
.elementByCss('button').click()
.elementByCss('button').click()
.elementByCss('p').text()
expect(text).toBe('COUNT: 2')
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
const originalContent = readFileSync(aboutPagePath, 'utf8')
const editedContent = originalContent.replace('This is the about page', 'COOL page')
// Change the about.js page
writeFileSync(aboutPagePath, editedContent, 'utf8')
// wait for 5 seconds
await waitFor(5000)
// Check whether the this page has reloaded or not.
const newText = await browser
.elementByCss('p').text()
expect(newText).toBe('COUNT: 2')
// restore the about page content.
writeFileSync(aboutPagePath, originalContent, 'utf8')
browser.close()
})

View file

@ -2,34 +2,25 @@
import { join } from 'path'
import {
nextServer,
renderViaAPI,
renderViaHTTP,
startApp,
stopApp
findPort,
launchApp,
killApp
} from 'next-test-utils'
// test suits
import xPoweredBy from './xpowered-by'
import rendering from './rendering'
import misc from './misc'
import clientNavigation from './client-navigation'
import hmr from './hmr'
import dynamic from './dynamic'
const context = {}
context.app = nextServer({
dir: join(__dirname, '../'),
dev: true,
quiet: true
})
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
describe('Basic Features', () => {
beforeAll(async () => {
context.server = await startApp(context.app)
context.appPort = context.server.address().port
context.appPort = await findPort()
context.server = await launchApp(join(__dirname, '../'), context.appPort, true)
// pre-build all pages at the start
await Promise.all([
@ -55,15 +46,16 @@ describe('Basic Features', () => {
renderViaHTTP(context.appPort, '/nav/as-path'),
renderViaHTTP(context.appPort, '/nav/as-path-using-router'),
renderViaHTTP(context.appPort, '/nested-cdm/index')
renderViaHTTP(context.appPort, '/nested-cdm/index'),
renderViaHTTP(context.appPort, '/hmr/about'),
renderViaHTTP(context.appPort, '/hmr/contact'),
renderViaHTTP(context.appPort, '/hmr/counter')
])
})
afterAll(() => stopApp(context.server))
afterAll(() => killApp(context.server))
rendering(context, 'Rendering via API', (p, q) => renderViaAPI(context.app, p, q))
rendering(context, 'Rendering via HTTP', (p, q) => renderViaHTTP(context.appPort, p, q))
xPoweredBy(context)
misc(context)
clientNavigation(context, (p, q) => renderViaHTTP(context.appPort, p, q))
dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q))
hmr(context, (p, q) => renderViaHTTP(context.appPort, p, q))

View file

@ -1,16 +0,0 @@
/* global describe, test, expect */
export default function (context) {
describe('Misc', () => {
test('finishes response', async () => {
const res = {
finished: false,
end () {
this.finished = true
}
}
const html = await context.app.renderToHTML({}, res, '/finish-response', {})
expect(html).toBeFalsy()
})
})
}

View file

@ -1,37 +0,0 @@
/* global describe, test, expect */
import { pkg } from 'next-test-utils'
export default function ({ app }) {
describe('X-Powered-By header', () => {
test('set it by default', async () => {
const req = { url: '/stateless', headers: {} }
const headers = {}
const res = {
setHeader (key, value) {
headers[key] = value
},
end () {}
}
await app.render(req, res, req.url)
expect(headers['X-Powered-By']).toEqual(`Next.js ${pkg.version}`)
})
test('do not set it when poweredByHeader==false', async () => {
const req = { url: '/stateless', headers: {} }
const originalConfigValue = app.config.poweredByHeader
app.config.poweredByHeader = false
const res = {
setHeader (key, value) {
if (key === 'X-Powered-By') {
throw new Error('Should not set the X-Powered-By header')
}
},
end () {}
}
await app.render(req, res, req.url)
app.config.poweredByHeader = originalConfigValue
})
})
}

View file

@ -2,6 +2,7 @@
import { join } from 'path'
import {
pkg,
nextServer,
nextBuild,
startApp,
@ -89,5 +90,51 @@ describe('Production Usage', () => {
})
})
describe('Misc', () => {
it('should handle already finished responses', async () => {
const res = {
finished: false,
end () {
this.finished = true
}
}
const html = await app.renderToHTML({}, res, '/finish-response', {})
expect(html).toBeFalsy()
})
})
describe('X-Powered-By header', () => {
it('should set it by default', async () => {
const req = { url: '/stateless', headers: {} }
const headers = {}
const res = {
setHeader (key, value) {
headers[key] = value
},
end () {}
}
await app.render(req, res, req.url)
expect(headers['X-Powered-By']).toEqual(`Next.js ${pkg.version}`)
})
it('should not set it when poweredByHeader==false', async () => {
const req = { url: '/stateless', headers: {} }
const originalConfigValue = app.config.poweredByHeader
app.config.poweredByHeader = false
const res = {
setHeader (key, value) {
if (key === 'X-Powered-By') {
throw new Error('Should not set the X-Powered-By header')
}
},
end () {}
}
await app.render(req, res, req.url)
app.config.poweredByHeader = originalConfigValue
})
})
dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))
})

View file

@ -2,6 +2,10 @@ import fetch from 'node-fetch'
import qs from 'querystring'
import http from 'http'
import express from 'express'
import path from 'path'
import portfinder from 'portfinder'
import { spawn } from 'child_process'
import fkill from 'fkill'
import server from '../../dist/server/next'
import build from '../../dist/server/build'
@ -23,6 +27,48 @@ export function renderViaHTTP (appPort, pathname, query = {}) {
return fetch(url).then((res) => res.text())
}
export function findPort () {
portfinder.basePort = 20000 + Math.ceil(Math.random() * 10000)
return portfinder.getPortPromise()
}
// Launch the app in dev mode.
export function launchApp (dir, port) {
const cwd = path.resolve(__dirname, '../../')
return new Promise((resolve, reject) => {
const instance = spawn('node', ['dist/bin/next', dir, '-p', port], { cwd })
function handleStdout (data) {
const message = data.toString()
if (/> Ready on/.test(message)) {
resolve(instance)
}
process.stdout.write(message)
}
function handleStderr (data) {
process.stderr.write(data.toString())
}
instance.stdout.on('data', handleStdout)
instance.stderr.on('data', handleStderr)
instance.on('close', () => {
instance.stdout.removeListener('data', handleStdout)
instance.stderr.removeListener('data', handleStderr)
})
instance.on('error', (err) => {
reject(err)
})
})
}
// Kill a launched app
export async function killApp (instance) {
await fkill(instance.pid)
}
export async function startApp (app) {
await app.prepare()
const handler = app.getRequestHandler()

View file

@ -74,6 +74,13 @@ acorn@^5.0.0, acorn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.0.tgz#e468bf609b0672700e02f878ae2f1b360fe24b4f"
aggregate-error@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-1.0.0.tgz#888344dad0220a72e3af50906117f48771925fac"
dependencies:
clean-stack "^1.0.0"
indent-string "^3.0.0"
ajv-keywords@^1.0.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
@ -290,7 +297,7 @@ async@2.0.1:
dependencies:
lodash "^4.8.0"
async@^1.4.0:
async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@ -1298,6 +1305,10 @@ circular-json@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d"
clean-stack@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31"
cli-cursor@^1.0.1, cli-cursor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
@ -1514,6 +1525,13 @@ cross-env@5.0.1:
cross-spawn "^5.1.0"
is-windows "^1.0.0"
cross-spawn-async@^2.1.1:
version "2.2.5"
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc"
dependencies:
lru-cache "^4.0.0"
which "^1.2.8"
cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@ -2096,6 +2114,14 @@ exec-sh@^0.2.0:
dependencies:
merge "^1.1.3"
execa@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.1.1.tgz#b09c2a9309bc0ef0501479472db3180f8d4c3edd"
dependencies:
cross-spawn-async "^2.1.1"
object-assign "^4.0.1"
strip-eof "^1.0.0"
execa@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36"
@ -2108,6 +2134,18 @@ execa@^0.5.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe"
dependencies:
cross-spawn "^5.0.1"
get-stream "^3.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
@ -2321,6 +2359,15 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
fkill@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/fkill/-/fkill-5.0.0.tgz#642f746aa631c965afc76c0d56dcc492a1ee504f"
dependencies:
aggregate-error "^1.0.0"
arrify "^1.0.0"
execa "^0.6.3"
taskkill "^2.0.0"
flat-cache@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
@ -3599,7 +3646,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
dependencies:
js-tokens "^3.0.0"
lru-cache@^4.0.1:
lru-cache@^4.0.0, lru-cache@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
@ -3764,7 +3811,7 @@ mkdirp@0.5.0:
dependencies:
minimist "0.0.8"
"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
@ -4291,6 +4338,14 @@ pluralize@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
portfinder@^1.0.13:
version "1.0.13"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
dependencies:
async "^1.5.2"
debug "^2.2.0"
mkdirp "0.5.x"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -4371,11 +4426,11 @@ public-encrypt@^4.0.0:
parse-asn1 "^5.0.0"
randombytes "^2.0.1"
punycode@1.3.2:
punycode@1.3.2, punycode@^1.2.4:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
punycode@^1.2.4, punycode@^1.4.1:
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
@ -5181,6 +5236,13 @@ tar@^2.2.1:
fstream "^1.0.2"
inherits "2"
taskkill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/taskkill/-/taskkill-2.0.0.tgz#a354305702a964357033027aa949eaed5331b784"
dependencies:
arrify "^1.0.0"
execa "^0.1.1"
taskr@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/taskr/-/taskr-1.0.6.tgz#6ba5b671f51703f780fb6335d3dc792cfcf9c192"
@ -5539,7 +5601,7 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
which@^1.2.10, which@^1.2.12, which@^1.2.4, which@^1.2.9:
which@^1.2.10, which@^1.2.12, which@^1.2.4, which@^1.2.8, which@^1.2.9:
version "1.2.14"
resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
dependencies: