diff --git a/.github/issue_template.md b/.github/issue_template.md index 90eefe6e..34f82d93 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,12 +1,18 @@ + + -- [ ] I have searched the [issues](https://github.com/zeit/next.js/issues) of this repository and believe that this is not a duplicate. +- [ ] I have searched the [issues](https://github.com/zeit/next.js/issues?q=is%3Aissue) of this repository and believe that this is not a duplicate. ## Expected Behavior diff --git a/.gitignore b/.gitignore index e351dbc6..96da5cb7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,9 @@ node_modules package-lock.json test/node_modules -# logs +# logs & pids *.log +pids # coverage .nyc_output diff --git a/.travis.yml b/.travis.yml index dbb967eb..2e441f97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,8 +24,7 @@ email: "leo@zeit.co", tag: "canary", api_key: { - secure: - "br9gLncKeLSoL7iOq0PXFeD1Gp3jRI8QMCSNIrGdZg/MuLeuwYPv36kmMgf7aGIfpjFLtU2/eVrOHSB+T5g1nKgOsqmWsfZE1tQYWph0//ookd/sj+IyxIx8nVLbUV/C4ctYOhX/efotRgNn56w5Av5hWc1IQLmW9mSN8ijrQnM+GzRI8QitiofeY2EP3N1eO8vC8E2oGkOsAdcypiX6lFG908zyWt7X2SD+iOsK2eAHjxoAEUdrxE5a8gTDhcTH6qnmtBs9rCeEKbO3JZjEy5dvccxlX3Nd+2GC1rckayk6o5L/zveTilsUx6Auqqbwn1dT5ffQuYsV4RPofs8IMrhnizc8y+OfUcCCpBJ4ia4w7N8FEP56TnRNTFoFBGJL5Y6NfeT0HHAlClxUWMG9pRGWGN+sskODDQ9FEntGZoqwV396ogs+3YhkxbY0AIr84QOctflsFcPtOgr/CoBeOsjbB+o9+Rlsqwlf3u3B7qhtU9eV6KcMfK4x9qW+2cwTllK+gD8S9wILx5BChkfx99g/7u/Rg1PCym64tTsDOBtqTVC2YCqeYYvjmpw4Vl3ofLrFsoNQnbmb6Q5+JSpOcJ/bEj7P/FuZdlU0fNV28tFhElu5caKhSCJz/avUlXG7NeveW1Ee8gjhURC4V/l4ryacyjA2vcDY/4RRkWtHNr4=" + secure: "mvLXMXn1z1ri29wAy5/CCrDuO7ZC3gxckcnY5W00cbL8aFuK5buvpizKr3dz1pYX7vC55I+nZMtVa2vn4oOlTae07dd0BYGmmpiwq692XD4P+PHVs8D5MlbiT62vIJl/ezv/TcyoAbfIVPfUfLXA7H6a04Kodyzj3LffannOYpZNTPRNSdNAtkVtHPCuHksVpZoj6WhmC52MBEWkh0TCV3OdbeQAs6z3m6j81XcTduhXdraO4UmyXi/ML+eAgEBSejH70/7O/RSGludBOtIWMuxSrEey4wB4F4/xpN7k4LjPQRS+EtUBg9DSb3vB5y/hGwbDqwx7gGfN/yP/ssc9j3G8syTevv3fxH4ver9kg5tvltTJ8/gOzV7nF2sK1+Nb8legL4fLwHvgThf4sB9dAgNc7R8dYNgYwivkLUrrAy9WJsu7OsXLhg3NpEb9PcbKH7yFwrAzQjRPguVxg7bC02g41v0c2i1UXTb0D0/1KW5Mdg7HYZEw4uUXRhJCLWZbOcBCWdmiB+hwKSaq+aO49C5aX2UAyIsUPmG0OgqY91H/Y09KP3O6jN4j7ADrIIrdMwcyx15UJDZMwADCHHesnzeelDah7w3H6q+/heOA4nzbWhZaYQEDK4/aouRDINdXMmCmt9NNYjUA50BAI2dPpqetjaqq3gaPSAXsMvzNvRs=" }, skip_cleanup: true, on: { diff --git a/bin/next-dev b/bin/next-dev index edf2255b..dc1693b0 100755 --- a/bin/next-dev +++ b/bin/next-dev @@ -62,7 +62,9 @@ srv.start(argv.port, argv.hostname) .catch((err) => { if (err.code === 'EADDRINUSE') { let errorMessage = `Port ${argv.port} is already in use.` - const pkgAppPath = require('pkg-up').sync('.') + const pkgAppPath = require('find-up').sync('package.json', { + cwd: dir + }) const appPackage = JSON.parse(readFileSync(pkgAppPath, 'utf8')) const nextScript = Object.entries(appPackage.scripts).find(scriptLine => scriptLine[1] === 'next') if (nextScript) errorMessage += `\nUse \`npm run ${nextScript[0]} -- -p \`.` diff --git a/client/index.js b/client/index.js index 7ffe6605..e29a523d 100644 --- a/client/index.js +++ b/client/index.js @@ -7,6 +7,7 @@ import App from '../lib/app' import { loadGetInitialProps, getURL } from '../lib/utils' import PageLoader from '../lib/page-loader' import * as asset from '../lib/asset' +import * as envConfig from '../lib/runtime-config' // Polyfill Promise globally // This is needed because Webpack2's dynamic loading(common chunks) code @@ -26,7 +27,8 @@ const { query, buildId, chunks, - assetPrefix + assetPrefix, + runtimeConfig }, location } = window @@ -36,6 +38,11 @@ const { __webpack_public_path__ = `${assetPrefix}/_next/webpack/` //eslint-disable-line // Initialize next/asset with the assetPrefix asset.setAssetPrefix(assetPrefix) +// Initialize next/config with the environment configuration +envConfig.setConfig({ + serverRuntimeConfig: {}, + publicRuntimeConfig: runtimeConfig +}) const asPath = getURL() diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js index 24477fe5..e05b0195 100644 --- a/client/webpack-hot-middleware-client.js +++ b/client/webpack-hot-middleware-client.js @@ -1,4 +1,5 @@ -import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?autoConnect=false' +import 'event-source-polyfill' +import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?autoConnect=false&overlay=false&reload=true' import Router from '../lib/router' const { @@ -9,8 +10,6 @@ const { export default () => { webpackHotMiddlewareClient.setOptionsAndConnect({ - overlay: false, - reload: true, path: `${assetPrefix}/_next/webpack-hmr` }) diff --git a/config.js b/config.js new file mode 100644 index 00000000..c718915e --- /dev/null +++ b/config.js @@ -0,0 +1 @@ +module.exports = require('./dist/lib/runtime-config') diff --git a/constants.js b/constants.js new file mode 100644 index 00000000..707c6478 --- /dev/null +++ b/constants.js @@ -0,0 +1 @@ +module.exports = require('./dist/lib/constants') diff --git a/errors/no-on-app-updated-hook.md b/errors/no-on-app-updated-hook.md new file mode 100644 index 00000000..ec0e0524 --- /dev/null +++ b/errors/no-on-app-updated-hook.md @@ -0,0 +1,25 @@ +# Router.onAppUpdated is removed + +Due to [this bug fix](https://github.com/zeit/next.js/pull/3849), we had to remove the `Router.onAppUpdated` hook. But the default functionality of this feature is still in effect. + +We use this hook to detect a new app deployment when switching pages and act accordingly. Although there are many things you can do in this hook, it's often used to navigate the page via the server as shown below: + +```js +Router.onAppUpdated = function(nextRoute) { + location.href = nextRoute +} +``` + +In this hook, you can't wait for a network request or a promise to get resolved. And you can't block the page navigation. So, the things you can do is limited. + +One real use of this hook is to persist your application state to local-storage before the page navigation. For that, you can use the [`window.onbeforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload) hook instead. + +This is code for that: + +```js +window.onbeforeunload = function(e) { + // Get the application state (usually from a store like Redux) + const appState = {} + localStorage.setItem('app-state', JSON.stringify(appState)); +}; +``` diff --git a/examples/active-class-name/README.md b/examples/active-class-name/README.md index e314afeb..83d5ca9d 100644 --- a/examples/active-class-name/README.md +++ b/examples/active-class-name/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example active-class-name active-class-name-app +```bash +npx create-next-app --example active-class-name active-class-name-app +# or +yarn create next-app --example active-class-name active-class-name-app ``` ### Download manually diff --git a/examples/basic-css/README.md b/examples/basic-css/README.md index a3f58158..ccca8b64 100644 --- a/examples/basic-css/README.md +++ b/examples/basic-css/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example basic-css basic-css-app +```bash +npx create-next-app --example basic-css basic-css-app +# or +yarn create next-app --example basic-css basic-css-app ``` ### Download manually diff --git a/examples/custom-server-actionhero/README.md b/examples/custom-server-actionhero/README.md new file mode 100644 index 00000000..e4c0d6e5 --- /dev/null +++ b/examples/custom-server-actionhero/README.md @@ -0,0 +1,99 @@ +# Running Next.JS and React /inside/ of ActionHero + +This server will render dynamic next.js/react pages on some routes, and normal ActionHero API requests on others. +This configuration works with both Next and ActionHero hot reloading of code. + +A more detailed example showcasing how to use fetch and web sockets to interact with your API can be found here: https://github.com/actionhero/next-in-actionhero + +## To install: +(assuming you have [node](http://nodejs.org/) and NPM installed) + +`npm install` + +## To Run: +`npm start` + + +## How does this work? + +1. Create an initializer to load next.js and create a handler that can extract the normal node `req` and `res` from the connection + +```js +// initializers/next.js + +const {Initializer, api} = require('actionhero') +const next = require('next') + +module.exports = class NextInitializer extends Initializer { + constructor () { + super() + this.name = 'next' + } + + async initialize () { + api.next = { + render: async (connection) => { + if (connection.type !== 'web') { throw new Error('Connections for NEXT apps must be of type "web"') } + const req = connection.rawConnection.req + const res = connection.rawConnection.res + return api.next.handle(req, res) + } + } + + api.next.dev = (api.env === 'development') + if (api.next.dev) { api.log('Running next in development mode...') } + + api.next.app = next({dev: api.next.dev}) + api.next.handle = api.next.app.getRequestHandler() + await api.next.app.prepare() + } + + async stop () { + await api.next.app.close() + } +} +``` + +2. Create an action which will run the above `api.next.render(connection)`. Note that we will not be relying on ActionHero to respond to the client's request in this case, and leave that up to next (via: `data.toRender = false`) + +```js +// actions/next.js + +const {Action, api} = require('actionhero') + +module.exports = class CreateChatRoom extends Action { + constructor () { + super() + this.name = 'render' + this.description = 'I render the next.js react website' + } + + async run (data) { + data.toRender = false + return api.next.render(data.connection) + } +} + +``` + +3. Tell ActionHero to use the api rather than the file server as the top-level route in `api.config.servers.web.rootEndpointType = 'api'`. This will allows "/" to listen to API requests. Also update `api.config.general.paths.public = [ path.join(__dirname, '/../static') ]`. In this configuration, the next 'static' renderer will take priority over the ActionHero 'public file' api. Note that any static assets (CSS, fonts, etc) will need to be in "./static" rather than "./public". + +Note that this is where the websocket server, if you enable it, will place the `ActionheroWebsocketClient` libraray. + +4. Configure a wild-card route at the lowest priority of your GET handler to catch all web requests that aren't caught by other actions: + +```js +// config/routes.js + +exports['default'] = { + routes: (api) => { + return { + get: [ + { path: '/time', action: 'time' }, + + { path: '/', matchTrailingPathParts: true, action: 'render' } + ] + } + } +} +``` diff --git a/examples/custom-server-actionhero/actions/render.js b/examples/custom-server-actionhero/actions/render.js new file mode 100644 index 00000000..60bd2700 --- /dev/null +++ b/examples/custom-server-actionhero/actions/render.js @@ -0,0 +1,15 @@ +'use strict' +const {Action, api} = require('actionhero') + +module.exports = class CreateChatRoom extends Action { + constructor () { + super() + this.name = 'render' + this.description = 'I render the next.js react website' + } + + async run (data) { + data.toRender = false + return api.next.render(data.connection) + } +} diff --git a/examples/custom-server-actionhero/config/api.js b/examples/custom-server-actionhero/config/api.js new file mode 100644 index 00000000..3e85fc02 --- /dev/null +++ b/examples/custom-server-actionhero/config/api.js @@ -0,0 +1,97 @@ +'use strict' + +const path = require('path') + +exports['default'] = { + general: (api) => { + const packageJSON = require(api.projectRoot + path.sep + 'package.json') + + return { + apiVersion: packageJSON.version, + serverName: packageJSON.name, + // id can be set here, or it will be generated dynamically. + // Be sure that every server you run has a unique ID (which will happen when generated dynamically) + // id: 'myActionHeroServer', + // A unique token to your application that servers will use to authenticate to each other + serverToken: 'change-me', + // the redis prefix for actionhero's cache objects + cachePrefix: 'actionhero:cache:', + // the redis prefix for actionhero's cache/lock objects + lockPrefix: 'actionhero:lock:', + // how long will a lock last before it exipres (ms)? + lockDuration: 1000 * 10, // 10 seconds + // Watch for changes in actions and tasks, and reload/restart them on the fly + developmentMode: true, + // How many pending actions can a single connection be working on + simultaneousActions: 5, + // allow connections to be created without remoteIp and remotePort (they will be set to 0) + enforceConnectionProperties: true, + // disables the whitelisting of client params + disableParamScrubbing: false, + // params you would like hidden from any logs + filteredParams: [], + // values that signify missing params + missingParamChecks: [null, '', undefined], + // The default filetype to server when a user requests a directory + directoryFileType: 'index.html', + // What log-level should we use for file requests? + fileRequestLogLevel: 'info', + // The default priority level given to middleware of all types (action, connection, say, and task) + defaultMiddlewarePriority: 100, + // Which channel to use on redis pub/sub for RPC communication + channel: 'actionhero', + // How long to wait for an RPC call before considering it a failure + rpcTimeout: 5000, + // should CLI methods and help include internal ActionHero CLI methods? + cliIncludeInternal: true, + // configuration for your actionhero project structure + paths: { + 'action': [path.join(__dirname, '/../actions')], + 'task': [path.join(__dirname, '/../tasks')], + 'public': [path.join(__dirname, '/../static')], + 'pid': [path.join(__dirname, '/../pids')], + 'log': [path.join(__dirname, '/../log')], + 'server': [path.join(__dirname, '/../servers')], + 'cli': [path.join(__dirname, '/../bin')], + 'initializer': [path.join(__dirname, '/../initializers')], + 'plugin': [path.join(__dirname, '/../node_modules')], + 'locale': [path.join(__dirname, '/../locales')] + }, + // hash containing chat rooms you wish to be created at server boot + startingChatRooms: { + // format is {roomName: {authKey, authValue}} + // 'secureRoom': {authorized: true}, + } + } + } +} + +exports.test = { + general: (api) => { + return { + id: 'test-server-' + process.pid, + serverToken: 'serverToken-' + process.pid, + developmentMode: true, + startingChatRooms: { + 'defaultRoom': {}, + 'otherRoom': {} + }, + paths: { + 'locale': [ + // require('os').tmpdir() + require('path').sep + 'locales', + path.join(__dirname, '/../locales') + ] + }, + rpcTimeout: 3000 + } + } +} + +exports.production = { + general: (api) => { + return { + fileRequestLogLevel: 'debug', + developmentMode: false + } + } +} diff --git a/examples/custom-server-actionhero/config/errors.js b/examples/custom-server-actionhero/config/errors.js new file mode 100644 index 00000000..458afb0f --- /dev/null +++ b/examples/custom-server-actionhero/config/errors.js @@ -0,0 +1,148 @@ +'use strict' + +// error messages can be strings of objects +exports['default'] = { + errors: (api) => { + return { + '_toExpand': false, + + // /////////////// + // SERIALIZERS // + // /////////////// + + serializers: { + servers: { + web: (error) => { + if (error.message) { + return String(error.message) + } else { + return error + } + }, + websocket: (error) => { + if (error.message) { + return String(error.message) + } else { + return error + } + }, + socket: (error) => { + if (error.message) { + return String(error.message) + } else { + return error + } + }, + specHelper: (error) => { + if (error.message) { + return 'Error: ' + String(error.message) + } else { + return error + } + } + } + }, + + // /////////// + // ACTIONS // + // /////////// + + // When a params for an action is invalid + invalidParams: (data, validationErrors) => { + if (validationErrors.length >= 0) { return validationErrors[0] } + return data.connection.localize('actionhero.errors.invalidParams') + }, + + // When a required param for an action is not provided + missingParams: (data, missingParams) => { + return data.connection.localize(['actionhero.errors.missingParams', {param: missingParams[0]}]) + }, + + // user requested an unknown action + unknownAction: (data) => { + return data.connection.localize('actionhero.errors.unknownAction') + }, + + // action not useable by this client/server type + unsupportedServerType: (data) => { + return data.connection.localize(['actionhero.errors.unsupportedServerType', {type: data.connection.type}]) + }, + + // action failed because server is mid-shutdown + serverShuttingDown: (data) => { + return data.connection.localize('actionhero.errors.serverShuttingDown') + }, + + // action failed because this client already has too many pending acitons + // limit defined in api.config.general.simultaneousActions + tooManyPendingActions: (data) => { + return data.connection.localize('actionhero.errors.tooManyPendingActions') + }, + + dataLengthTooLarge: (maxLength, receivedLength) => { + return api.i18n.localize(['actionhero.errors.dataLengthTooLarge', {maxLength: maxLength, receivedLength: receivedLength}]) + }, + + // /////////////// + // FILE SERVER // + // /////////////// + + // The body message to accompany 404 (file not found) errors regarding flat files + // You may want to load in the contnet of 404.html or similar + fileNotFound: (connection) => { + return connection.localize(['actionhero.errors.fileNotFound']) + }, + + // user didn't request a file + fileNotProvided: (connection) => { + return connection.localize('actionhero.errors.fileNotProvided') + }, + + // something went wrong trying to read the file + fileReadError: (connection, error) => { + return connection.localize(['actionhero.errors.fileReadError', {error: String(error)}]) + }, + + // /////////////// + // CONNECTIONS // + // /////////////// + + verbNotFound: (connection, verb) => { + return connection.localize(['actionhero.errors.verbNotFound', {verb: verb}]) + }, + + verbNotAllowed: (connection, verb) => { + return connection.localize(['actionhero.errors.verbNotAllowed', {verb: verb}]) + }, + + connectionRoomAndMessage: (connection) => { + return connection.localize('actionhero.errors.connectionRoomAndMessage') + }, + + connectionNotInRoom: (connection, room) => { + return connection.localize(['actionhero.errors.connectionNotInRoom', {room: room}]) + }, + + connectionAlreadyInRoom: (connection, room) => { + return connection.localize(['actionhero.errors.connectionAlreadyInRoom', {room: room}]) + }, + + connectionRoomHasBeenDeleted: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomHasBeenDeleted') + }, + + connectionRoomNotExist: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomNotExist') + }, + + connectionRoomExists: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomExists') + }, + + connectionRoomRequired: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomRequired') + } + + } + } +} diff --git a/examples/custom-server-actionhero/config/i18n.js b/examples/custom-server-actionhero/config/i18n.js new file mode 100644 index 00000000..979beef7 --- /dev/null +++ b/examples/custom-server-actionhero/config/i18n.js @@ -0,0 +1,36 @@ +exports['default'] = { + i18n: (api) => { + return { + // visit https://github.com/mashpie/i18n-node to see all configuration options + // locale path can be configired from within ./config/api.js + locales: ['en'], + + // how would you like your lanaguages to fall back if a translation string is missing? + fallbacks: { + // 'es': 'en' + }, + + // configure i18n to allow for object-style key lookup + objectNotation: true, + + // should actionhero append any missing translations to the locale file? + updateFiles: true, + + // this will configure logging and error messages in the log(s) + defaultLocale: 'en', + + // the name of the method by which to determine the connection's locale + // by default, every request will be in the 'en' locale + // this method will be called witin the localiazation middleware on all requests + determineConnectionLocale: 'api.i18n.determineConnectionLocale' + } + } +} + +exports.test = { + i18n: (api) => { + return { + updateFiles: true + } + } +} diff --git a/examples/custom-server-actionhero/config/logger.js b/examples/custom-server-actionhero/config/logger.js new file mode 100644 index 00000000..a67e323c --- /dev/null +++ b/examples/custom-server-actionhero/config/logger.js @@ -0,0 +1,60 @@ +'use strict' + +const fs = require('fs') +const cluster = require('cluster') + +exports['default'] = { + logger: (api) => { + let logger = {transports: []} + + // console logger + if (cluster.isMaster) { + logger.transports.push(function (api, winston) { + return new (winston.transports.Console)({ + colorize: true, + level: 'info', + timestamp: function () { return api.id + ' @ ' + new Date().toISOString() } + }) + }) + } + + // file logger + logger.transports.push(function (api, winston) { + if (api.config.general.paths.log.length === 1) { + const logDirectory = api.config.general.paths.log[0] + try { + fs.mkdirSync(logDirectory) + } catch (e) { + if (e.code !== 'EEXIST') { + throw (new Error('Cannot create log directory @ ' + logDirectory)) + } + } + } + + return new (winston.transports.File)({ + filename: api.config.general.paths.log[0] + '/' + api.pids.title + '.log', + level: 'info', + timestamp: function () { return api.id + ' @ ' + new Date().toISOString() } + }) + }) + + // the maximum length of param to log (we will truncate) + logger.maxLogStringLength = 100 + + // you can optionally set custom log levels + // logger.levels = {good: 0, bad: 1}; + + // you can optionally set custom log colors + // logger.colors = {good: 'blue', bad: 'red'}; + + return logger + } +} + +exports.test = { + logger: (api) => { + return { + transports: null + } + } +} diff --git a/examples/custom-server-actionhero/config/plugins.js b/examples/custom-server-actionhero/config/plugins.js new file mode 100644 index 00000000..01b01572 --- /dev/null +++ b/examples/custom-server-actionhero/config/plugins.js @@ -0,0 +1,27 @@ +exports['default'] = { + plugins: (api) => { + /* + If you want to use plugins in your application, include them here: + + return { + 'myPlugin': { path: __dirname + '/../node_modules/myPlugin' } + } + + You can also toggle on or off sections of a plugin to include (default true for all sections): + + return { + 'myPlugin': { + path: __dirname + '/../node_modules/myPlugin', + actions: true, + tasks: true, + initializers: true, + servers: true, + public: true, + cli: true + } + } + */ + + return {} + } +} diff --git a/examples/custom-server-actionhero/config/redis.js b/examples/custom-server-actionhero/config/redis.js new file mode 100644 index 00000000..f392d929 --- /dev/null +++ b/examples/custom-server-actionhero/config/redis.js @@ -0,0 +1,49 @@ +let host = process.env.REDIS_HOST || '127.0.0.1' +let port = process.env.REDIS_PORT || 6379 +let db = process.env.REDIS_DB || 0 +let password = process.env.REDIS_PASSWORD || null +const maxBackoff = 1000 + +if (process.env.REDIS_URL) { + password = process.env.REDIS_URL.match(/redis:\/\/.*:(.*)@.*:\d*$/i)[1] + host = process.env.REDIS_URL.match(/redis:\/\/.*:.*@(.*):\d*$/i)[1] + port = parseInt(process.env.REDIS_URL.match(/redis:\/\/.*:.*@.*:(\d*)$/i)[1]) +} + +exports['default'] = { + redis: (api) => { + // konstructor: The redis client constructor method. All redis methods must be promises + // args: The arguments to pass to the constructor + // buildNew: is it `new konstructor()` or just `konstructor()`? + + function retryStrategy (times) { + if (times === 1) { + const error = 'Unable to connect to Redis - please check your Redis config!' + if (process.env.NODE_ENV === 'test') { console.error(error) } else { api.log(error, 'error') } + return 5000 + } + return Math.min(times * 50, maxBackoff) + } + + return { + enabled: true, + + '_toExpand': false, + client: { + konstructor: require('ioredis'), + args: [{ port: port, host: host, password: password, db: db, retryStrategy: retryStrategy }], + buildNew: true + }, + subscriber: { + konstructor: require('ioredis'), + args: [{ port: port, host: host, password: password, db: db, retryStrategy: retryStrategy }], + buildNew: true + }, + tasks: { + konstructor: require('ioredis'), + args: [{ port: port, host: host, password: password, db: db, retryStrategy: retryStrategy }], + buildNew: true + } + } + } +} diff --git a/examples/custom-server-actionhero/config/routes.js b/examples/custom-server-actionhero/config/routes.js new file mode 100644 index 00000000..c126f8e5 --- /dev/null +++ b/examples/custom-server-actionhero/config/routes.js @@ -0,0 +1,9 @@ +exports['default'] = { + routes: (api) => { + return { + get: [ + { path: '/', matchTrailingPathParts: true, action: 'render' } + ] + } + } +} diff --git a/examples/custom-server-actionhero/config/servers/socket.js b/examples/custom-server-actionhero/config/servers/socket.js new file mode 100644 index 00000000..f4c918d2 --- /dev/null +++ b/examples/custom-server-actionhero/config/servers/socket.js @@ -0,0 +1,37 @@ +'use strict' + +exports['default'] = { + servers: { + socket: (api) => { + return { + enabled: (process.env.ENABLE_TCP_SERVER !== undefined), + // TCP or TLS? + secure: false, + // Passed to tls.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Port or Socket + port: 5000, + // Which IP to listen on (use 0.0.0.0 for all) + bindIP: '0.0.0.0', + // Enable TCP KeepAlive pings on each connection? + setKeepAlive: false, + // Delimiter string for incoming messages + delimiter: '\n', + // Maximum incoming message string length in Bytes (use 0 for Infinite) + maxDataLength: 0 + } + } + } +} + +exports.test = { + servers: { + socket: (api) => { + return { + enabled: true, + port: 1001 + (process.pid % 64535), + secure: false + } + } + } +} diff --git a/examples/custom-server-actionhero/config/servers/web.js b/examples/custom-server-actionhero/config/servers/web.js new file mode 100644 index 00000000..6ad3dd09 --- /dev/null +++ b/examples/custom-server-actionhero/config/servers/web.js @@ -0,0 +1,121 @@ +'use strict' + +const os = require('os') + +exports['default'] = { + servers: { + web: (api) => { + return { + enabled: true, + // HTTP or HTTPS? + secure: false, + // Passed to https.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Should we redirect all traffic to the first host in this array if hte request header doesn't match? + // i.e.: [ 'https://www.site.com' ] + allowedRequestHosts: process.env.ALLOWED_HOSTS ? process.env.ALLOWED_HOSTS.split(',') : [], + // Port or Socket Path + port: process.env.PORT || 8080, + // Which IP to listen on (use '0.0.0.0' for all; '::' for all on ipv4 and ipv6) + // Set to `null` when listening to socket + bindIP: '0.0.0.0', + // Any additional headers you want actionhero to respond with + httpHeaders: { + 'X-Powered-By': api.config.general.serverName, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE', + 'Access-Control-Allow-Headers': 'Content-Type' + }, + // Route that actions will be served from; secondary route against this route will be treated as actions, + // IE: /api/?action=test == /api/test/ + urlPathForActions: 'api', + // Route that static files will be served from; + // path (relative to your project root) to serve static content from + // set to `null` to disable the file server entirely + urlPathForFiles: 'public', + // When visiting the root URL, should visitors see 'api' or 'file'? + // Visitors can always visit /api and /public as normal + rootEndpointType: 'api', + // simple routing also adds an 'all' route which matches /api/:action for all actions + simpleRouting: true, + // queryRouting allows an action to be defined via a URL param, ie: /api?action=:action + queryRouting: true, + // The cache or (if etags are enabled) next-revalidation time to be returned for all flat files served from /public; defined in seconds + flatFileCacheDuration: 60, + // Add an etag header to requested flat files which acts as fingerprint that changes when the file is updated; + // Client will revalidate the fingerprint at latest after flatFileCacheDuration and reload it if the etag (and therfore the file) changed + // or continue to use the cached file if it's still valid + enableEtag: true, + // should we save the un-parsed HTTP POST/PUT payload to connection.rawConnection.params.rawBody? + saveRawBody: false, + // How many times should we try to boot the server? + // This might happen if the port is in use by another process or the socketfile is claimed + bootAttempts: 1, + // Settings for determining the id of an http(s) request (browser-fingerprint) + fingerprintOptions: { + cookieKey: 'sessionID', + toSetCookie: true, + onlyStaticElements: false, + settings: { + path: '/', + expires: 3600000 + } + }, + // Options to be applied to incoming file uploads. + // More options and details at https://github.com/felixge/node-formidable + formOptions: { + uploadDir: os.tmpdir(), + keepExtensions: false, + maxFieldsSize: 1024 * 1024 * 100 + }, + // Should we pad JSON responses with whitespace to make them more human-readable? + // set to null to disable + padding: 2, + // Options to configure metadata in responses + metadataOptions: { + serverInformation: true, + requesterInformation: true + }, + // When true, returnErrorCodes will modify the response header for http(s) clients if connection.error is not null. + // You can also set connection.rawConnection.responseHttpCode to specify a code per request. + returnErrorCodes: true, + // should this node server attempt to gzip responses if the client can accept them? + // this will slow down the performance of actionhero, and if you need this funcionality, it is recommended that you do this upstream with nginx or your load balancer + compress: false, + // options to pass to the query parser + // learn more about the options @ https://github.com/hapijs/qs + queryParseOptions: {} + } + } + } +} + +exports.production = { + servers: { + web: (api) => { + return { + padding: null, + metadataOptions: { + serverInformation: false, + requesterInformation: false + } + } + } + } +} + +exports.test = { + servers: { + web: (api) => { + return { + secure: false, + port: process.env.PORT || 1000 + (process.pid % 64535), + matchExtensionMime: true, + metadataOptions: { + serverInformation: true, + requesterInformation: true + } + } + } + } +} diff --git a/examples/custom-server-actionhero/config/servers/websocket.js b/examples/custom-server-actionhero/config/servers/websocket.js new file mode 100644 index 00000000..f72c0975 --- /dev/null +++ b/examples/custom-server-actionhero/config/servers/websocket.js @@ -0,0 +1,62 @@ +'use strict' + +// Note that to use the websocket server, you also need the web server enabled + +exports['default'] = { + servers: { + websocket: (api) => { + return { + enabled: true, + // you can pass a FQDN (string) here or 'window.location.origin' + clientUrl: 'window.location.origin', + // Directory to render client-side JS. + // Path should start with "/" and will be built starting from api.config..general.paths.public + clientJsPath: 'javascript/', + // the name of the client-side JS file to render. Both `.js` and `.min.js` versions will be created + // do not include the file exension + // set to `undefined` to not render the client-side JS on boot + clientJsName: 'ActionheroWebsocketClient', + // should the server signal clients to not reconnect when the server is shutdown/reboot + destroyClientsOnShutdown: false, + + // websocket Server Options: + server: { + // authorization: null, + // pathname: '/primus', + // parser: 'JSON', + // transformer: 'websockets', + // plugin: {}, + // timeout: 35000, + // origins: '*', + // methods: ['GET','HEAD','PUT','POST','DELETE','OPTIONS'], + // credentials: true, + // maxAge: '30 days', + // exposed: false, + }, + + // websocket Client Options: + client: { + apiPath: '/api' // the api base endpoint on your actionhero server + // reconnect: {}, + // timeout: 10000, + // ping: 25000, + // pong: 10000, + // strategy: "online", + // manual: false, + // websockets: true, + // network: true, + // transport: {}, + // queueSize: Infinity, + } + } + } + } +} + +exports['test'] = { + servers: { + websocket: (api) => { + return { clientUrl: null } + } + } +} diff --git a/examples/custom-server-actionhero/config/tasks.js b/examples/custom-server-actionhero/config/tasks.js new file mode 100644 index 00000000..f32d4d42 --- /dev/null +++ b/examples/custom-server-actionhero/config/tasks.js @@ -0,0 +1,61 @@ +exports['default'] = { + tasks: (api) => { + return { + // Should this node run a scheduler to promote delayed tasks? + scheduler: false, + // what queues should the taskProcessors work? + queues: ['*'], + // Logging levels of task workers + workerLogging: { + failure: 'error', // task failure + success: 'info', // task success + start: 'info', + end: 'info', + cleaning_worker: 'info', + poll: 'debug', + job: 'debug', + pause: 'debug', + internalError: 'error', + multiWorkerAction: 'debug' + }, + // Logging levels of the task scheduler + schedulerLogging: { + start: 'info', + end: 'info', + poll: 'debug', + enqueue: 'debug', + reEnqueue: 'debug', + working_timestamp: 'debug', + transferred_job: 'debug' + }, + // how long to sleep between jobs / scheduler checks + timeout: 5000, + // at minimum, how many parallel taskProcessors should this node spawn? + // (have number > 0 to enable, and < 1 to disable) + minTaskProcessors: 0, + // at maximum, how many parallel taskProcessors should this node spawn? + maxTaskProcessors: 0, + // how often should we check the event loop to spawn more taskProcessors? + checkTimeout: 500, + // how many ms would constitue an event loop delay to halt taskProcessors spawning? + maxEventLoopDelay: 5, + // When we kill off a taskProcessor, should we disconnect that local redis connection? + toDisconnectProcessors: true, + // Customize Resque primitives, replace null with required replacement. + resque_overrides: { + queue: null, + multiWorker: null, + scheduler: null + } + } + } +} + +exports.test = { + tasks: (api) => { + return { + timeout: 100, + checkTimeout: 50 + } + } +} diff --git a/examples/custom-server-actionhero/initializers/next.js b/examples/custom-server-actionhero/initializers/next.js new file mode 100644 index 00000000..2297e70c --- /dev/null +++ b/examples/custom-server-actionhero/initializers/next.js @@ -0,0 +1,32 @@ +'use strict' +const {Initializer, api} = require('actionhero') +const next = require('next') + +module.exports = class NextInitializer extends Initializer { + constructor () { + super() + this.name = 'next' + } + + async initialize () { + api.next = { + render: async (connection) => { + if (connection.type !== 'web') { throw new Error('Connections for NEXT apps must be of type "web"') } + const req = connection.rawConnection.req + const res = connection.rawConnection.res + return api.next.handle(req, res) + } + } + + api.next.dev = (api.env === 'development') + if (api.next.dev) { api.log('Running next in development mode...') } + + api.next.app = next({dev: api.next.dev}) + api.next.handle = api.next.app.getRequestHandler() + await api.next.app.prepare() + } + + async stop () { + await api.next.app.close() + } +} diff --git a/examples/custom-server-actionhero/locales/en.json b/examples/custom-server-actionhero/locales/en.json new file mode 100644 index 00000000..11b004f3 --- /dev/null +++ b/examples/custom-server-actionhero/locales/en.json @@ -0,0 +1,34 @@ +{ + "actionhero": { + "welcomeMessage": "Hello! Welcome to the actionhero api", + "goodbyeMessage": "Bye!", + "cache": { + "objectNotFound": "Object not found", + "objectLocked": "Object locked", + "objectExpired": "Object expired" + }, + "errors": { + "invalidParams": "validation error", + "missingParams": "{{param}} is a required parameter for this action", + "unknownAction": "unknown action or invalid apiVersion", + "unsupportedServerType": "this action does not support the {{type}} connection type", + "serverShuttingDown": "the server is shutting down", + "tooManyPendingActions": "you have too many pending requests", + "dataLengthTooLarge": "data length is too big ({{maxLength}} received/{{receivedLength}} max)", + "fileNotFound": "That file is not found", + "fileNotProvided": "file is a required param to send a file", + "fileReadError": "error reading file: {{error}}", + "verbNotFound": "I do not know know to perform this verb ({{verb}})", + "verbNotAllowed": "verb not found or not allowed ({{verb}})", + "connectionRoomAndMessage": "both room and message are required", + "connectionNotInRoom": "connection not in this room ({{room}})", + "connectionAlreadyInRoom": "connection already in this room ({{room}})", + "connectionRoomHasBeenDeleted": "this room has been deleted", + "connectionRoomNotExist": "room does not exist", + "connectionRoomExists": "room exists", + "connectionRoomRequired": "a room is required" + } + }, + "Your random number is {{number}}": "Your random number is {{number}}", + "Node Healthy": "Node Healthy" +} diff --git a/examples/custom-server-actionhero/package.json b/examples/custom-server-actionhero/package.json new file mode 100644 index 00000000..469e9a09 --- /dev/null +++ b/examples/custom-server-actionhero/package.json @@ -0,0 +1,17 @@ +{ + "engines": { + "node": ">=8.0.0" + }, + "dependencies": { + "actionhero": "^18.1.2", + "ioredis": "^3.2.2", + "isomorphic-fetch": "^2.2.1", + "next": "^5.0.1-canary.9", + "react": "^16.1.1", + "react-dom": "^16.1.1", + "ws": "^4.1.0" + }, + "scripts": { + "start": "actionhero start" + } +} diff --git a/examples/custom-server-actionhero/pages/a.js b/examples/custom-server-actionhero/pages/a.js new file mode 100644 index 00000000..16eb9898 --- /dev/null +++ b/examples/custom-server-actionhero/pages/a.js @@ -0,0 +1 @@ +export default () =>
a
diff --git a/examples/custom-server-actionhero/pages/b.js b/examples/custom-server-actionhero/pages/b.js new file mode 100644 index 00000000..3791f48b --- /dev/null +++ b/examples/custom-server-actionhero/pages/b.js @@ -0,0 +1 @@ +export default () =>
b
diff --git a/examples/custom-server-actionhero/pages/index.js b/examples/custom-server-actionhero/pages/index.js new file mode 100644 index 00000000..d044fc1e --- /dev/null +++ b/examples/custom-server-actionhero/pages/index.js @@ -0,0 +1,9 @@ +import React from 'react' +import Link from 'next/link' + +export default () => ( + +) diff --git a/examples/custom-server-express/README.md b/examples/custom-server-express/README.md index f13371fe..f61a45dd 100644 --- a/examples/custom-server-express/README.md +++ b/examples/custom-server-express/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server-express custom-server-express-app +```bash +npx create-next-app --example custom-server-express custom-server-express-app +# or +yarn create next-app --example custom-server-express custom-server-express-app ``` ### Download manually diff --git a/examples/custom-server-fastify/README.md b/examples/custom-server-fastify/README.md index 0045a5c5..25d388f1 100644 --- a/examples/custom-server-fastify/README.md +++ b/examples/custom-server-fastify/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server-fastify custom-server-fastify-app +```bash +npx create-next-app --example custom-server-fastify custom-server-fastify-app +# or +yarn create next-app --example custom-server-fastify custom-server-fastify-app ``` ### Download manually diff --git a/examples/custom-server-hapi/README.md b/examples/custom-server-hapi/README.md index 8a0a56a2..787f1b5d 100644 --- a/examples/custom-server-hapi/README.md +++ b/examples/custom-server-hapi/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server-hapi custom-server-hapi-app +```bash +npx create-next-app --example custom-server-hapi custom-server-hapi-app +# or +yarn create next-app --example custom-server-hapi custom-server-hapi-app ``` ### Download manually diff --git a/examples/custom-server-koa/README.md b/examples/custom-server-koa/README.md index a3c4c7f9..e4c32ff9 100644 --- a/examples/custom-server-koa/README.md +++ b/examples/custom-server-koa/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server-koa custom-server-koa-app +```bash +npx create-next-app --example custom-server-koa custom-server-koa-app +# or +yarn create next-app --example custom-server-koa custom-server-koa-app ``` ### Download manually diff --git a/examples/custom-server-koa/server.js b/examples/custom-server-koa/server.js index 562f0d4a..532abeee 100644 --- a/examples/custom-server-koa/server.js +++ b/examples/custom-server-koa/server.js @@ -33,8 +33,7 @@ app.prepare() }) server.use(router.routes()) - server.listen(port, (err) => { - if (err) throw err + server.listen(port, () => { console.log(`> Ready on http://localhost:${port}`) }) }) diff --git a/examples/custom-server-micro/README.md b/examples/custom-server-micro/README.md index 18aa50cc..39979b0f 100644 --- a/examples/custom-server-micro/README.md +++ b/examples/custom-server-micro/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server-micro custom-server-micro-app +```bash +npx create-next-app --example custom-server-micro custom-server-micro-app +# or +yarn create next-app --example custom-server-micro custom-server-micro-app ``` ### Download manually diff --git a/examples/custom-server-nodemon/README.md b/examples/custom-server-nodemon/README.md index 26c8182d..0ee78d27 100644 --- a/examples/custom-server-nodemon/README.md +++ b/examples/custom-server-nodemon/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server-nodemon custom-server-nodemon-app +```bash +npx create-next-app --example custom-server-nodemon custom-server-nodemon-app +# or +yarn create next-app --example custom-server-nodemon custom-server-nodemon-app ``` ### Download manually diff --git a/examples/custom-server-typescript/README.md b/examples/custom-server-typescript/README.md new file mode 100644 index 00000000..a440e616 --- /dev/null +++ b/examples/custom-server-typescript/README.md @@ -0,0 +1,43 @@ +[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/custom-server-typescript) + +# Custom server with TypeScript + Nodemon example + +## How to use + +### Using `create-next-app` + +Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: + +```bash +npx create-next-app --example custom-server-typescript custom-server-typescript-app +# or +yarn create next-app --example custom-server-typescript custom-server-typescript-app +``` + +### Download manually + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/custom-server-typescript +cd custom-server-typescript +``` + +Install it and run: + +```bash +npm install +npm run dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) + +```bash +now +``` + +## The idea behind the example + +The example shows how you can use [TypeScript](https://typescriptlang.com) on both the server and the client while using [Nodemon](https://nodemon.io/) to live reload the server code without affecting the Next.js universal code. +Server entry point is `server/index.ts` in development and `production-server/index.js` in production. +The second directory should be added to `.gitignore`. diff --git a/examples/custom-server-typescript/next.config.js b/examples/custom-server-typescript/next.config.js new file mode 100644 index 00000000..d8b638bd --- /dev/null +++ b/examples/custom-server-typescript/next.config.js @@ -0,0 +1,2 @@ +const withTypescript = require('@zeit/next-typescript') +module.exports = withTypescript() diff --git a/examples/custom-server-typescript/nodemon.json b/examples/custom-server-typescript/nodemon.json new file mode 100644 index 00000000..24f3bee0 --- /dev/null +++ b/examples/custom-server-typescript/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["server/**/*.ts"], + "execMap": { + "ts": "ts-node --compilerOptions '{\"module\":\"commonjs\"}'" + } +} diff --git a/examples/custom-server-typescript/package.json b/examples/custom-server-typescript/package.json new file mode 100644 index 00000000..11faa2f8 --- /dev/null +++ b/examples/custom-server-typescript/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "dev": "nodemon server/index.ts", + "build": "next build && tsc --project tsconfig.server.json", + "start": "NODE_ENV=production node production-server/index.js" + }, + "dependencies": { + "next": "latest", + "react": "^16.2.0", + "react-dom": "^16.2.0" + }, + "devDependencies": { + "@types/next": "^2.4.7", + "@types/react": "^16.0.36", + "@zeit/next-typescript": "^0.0.8", + "nodemon": "^1.12.1", + "ts-node": "^4.1.0", + "typescript": "^2.7.1" + } +} diff --git a/examples/custom-server-typescript/pages/a.tsx b/examples/custom-server-typescript/pages/a.tsx new file mode 100644 index 00000000..c5359797 --- /dev/null +++ b/examples/custom-server-typescript/pages/a.tsx @@ -0,0 +1,3 @@ +import React from 'react' + +export default () =>
a
diff --git a/examples/custom-server-typescript/pages/b.tsx b/examples/custom-server-typescript/pages/b.tsx new file mode 100644 index 00000000..9bde4d9d --- /dev/null +++ b/examples/custom-server-typescript/pages/b.tsx @@ -0,0 +1,3 @@ +import React from 'react' + +export default () =>
b
diff --git a/examples/custom-server-typescript/pages/index.tsx b/examples/custom-server-typescript/pages/index.tsx new file mode 100644 index 00000000..0e22b819 --- /dev/null +++ b/examples/custom-server-typescript/pages/index.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import Link from 'next/link' + +export default () => ( + +) diff --git a/examples/custom-server-typescript/server/index.ts b/examples/custom-server-typescript/server/index.ts new file mode 100644 index 00000000..97b6e24d --- /dev/null +++ b/examples/custom-server-typescript/server/index.ts @@ -0,0 +1,28 @@ +import { createServer } from 'http' +import { parse } from 'url' +import * as next from 'next' + +const port = parseInt(process.env.PORT, 10) || 3000 +const dev = process.env.NODE_ENV !== 'production' +const app = next({ dev }) +const handle = app.getRequestHandler() + +app.prepare() +.then(() => { + createServer((req, res) => { + const parsedUrl = parse(req.url, true) + const { pathname, query } = parsedUrl + + if (pathname === '/a') { + app.render(req, res, '/a', query) + } else if (pathname === '/b') { + app.render(req, res, '/b', query) + } else { + handle(req, res, parsedUrl) + } + }) + .listen(port, (err) => { + if (err) throw err + console.log(`> Ready on http://localhost:${port}`) + }) +}) diff --git a/examples/custom-server-typescript/tsconfig.json b/examples/custom-server-typescript/tsconfig.json new file mode 100644 index 00000000..41a1f0af --- /dev/null +++ b/examples/custom-server-typescript/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "jsx": "preserve", + "allowJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true, + "skipLibCheck": true, + "baseUrl": ".", + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "dom", + "es2015", + "es2016" + ] + } +} diff --git a/examples/custom-server-typescript/tsconfig.server.json b/examples/custom-server-typescript/tsconfig.server.json new file mode 100644 index 00000000..c9e53ca0 --- /dev/null +++ b/examples/custom-server-typescript/tsconfig.server.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "production-server/" + }, + "include": ["server/**/*.ts"] +} diff --git a/examples/custom-server/README.md b/examples/custom-server/README.md index ce526ae8..79c1c393 100644 --- a/examples/custom-server/README.md +++ b/examples/custom-server/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example custom-server custom-server-app +```bash +npx create-next-app --example custom-server custom-server-app +# or +yarn create next-app --example custom-server custom-server-app ``` ### Download manually diff --git a/examples/data-fetch/README.md b/examples/data-fetch/README.md index 5235d096..c30ba332 100644 --- a/examples/data-fetch/README.md +++ b/examples/data-fetch/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example data-fetch data-fetch-app +```bash +npx create-next-app --example data-fetch data-fetch-app +# or +yarn create next-app --example data-fetch data-fetch-app ``` ### Download manually diff --git a/examples/form-handler/README.md b/examples/form-handler/README.md index bda443b5..370f5969 100644 --- a/examples/form-handler/README.md +++ b/examples/form-handler/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example form-handler form-handler-app +```bash +npx create-next-app --example form-handler form-handler-app +# or +yarn create next-app --example form-handler form-handler-app ``` ### Download manually diff --git a/examples/head-elements/README.md b/examples/head-elements/README.md index 740d70ab..8d32b78f 100644 --- a/examples/head-elements/README.md +++ b/examples/head-elements/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example head-elements head-elements-app +```bash +npx create-next-app --example head-elements head-elements-app +# or +yarn create next-app --example head-elements head-elements-app ``` ### Download manually diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md index 839be69c..62e27468 100644 --- a/examples/hello-world/README.md +++ b/examples/hello-world/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example hello-world hello-world-app +```bash +npx create-next-app --example hello-world hello-world-app +# or +yarn create next-app --example hello-world hello-world-app ``` ### Download manually diff --git a/examples/layout-component/README.md b/examples/layout-component/README.md index 488846eb..a347a77b 100644 --- a/examples/layout-component/README.md +++ b/examples/layout-component/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example layout-component layout-component-app +```bash +npx create-next-app --example layout-component layout-component-app +# or +yarn create next-app --example layout-component layout-component-app ``` ### Download manually diff --git a/examples/nested-components/README.md b/examples/nested-components/README.md index 55fccf47..fd2e1ad6 100644 --- a/examples/nested-components/README.md +++ b/examples/nested-components/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example nested-components nested-components-app +```bash +npx create-next-app --example nested-components nested-components-app +# or +yarn create next-app --example nested-components nested-components-app ``` ### Download manually diff --git a/examples/page-transitions/README.md b/examples/page-transitions/README.md index a9de94c5..5f537bbb 100644 --- a/examples/page-transitions/README.md +++ b/examples/page-transitions/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example page-transitions page-transitions-app +```bash +npx create-next-app --example page-transitions page-transitions-app +# or +yarn create next-app --example page-transitions page-transitions-app ``` ### Download manually diff --git a/examples/parameterized-routing/README.md b/examples/parameterized-routing/README.md index 5b00521d..e4df754f 100644 --- a/examples/parameterized-routing/README.md +++ b/examples/parameterized-routing/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example parameterized-routing parameterized-routing-app +```bash +npx create-next-app --example parameterized-routing parameterized-routing-app +# or +yarn create next-app --example parameterized-routing parameterized-routing-app ``` ### Download manually diff --git a/examples/progressive-render/README.md b/examples/progressive-render/README.md index a9c0e78a..b98dc15b 100644 --- a/examples/progressive-render/README.md +++ b/examples/progressive-render/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example progressive-render progressive-render-app +```bash +npx create-next-app --example progressive-render progressive-render-app +# or +yarn create next-app --example progressive-render progressive-render-app ``` ### Download manually diff --git a/examples/root-static-files/README.md b/examples/root-static-files/README.md index 2ea50f7e..53879a1e 100644 --- a/examples/root-static-files/README.md +++ b/examples/root-static-files/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example root-static-files root-static-files-app +```bash +npx create-next-app --example root-static-files root-static-files-app +# or +yarn create next-app --example root-static-files root-static-files-app ``` ### Download manually diff --git a/examples/shared-modules/README.md b/examples/shared-modules/README.md index 26567d14..95cc44dd 100644 --- a/examples/shared-modules/README.md +++ b/examples/shared-modules/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example shared-modules shared-modules-app +```bash +npx create-next-app --example shared-modules shared-modules-app +# or +yarn create next-app --example shared-modules shared-modules-app ``` ### Download manually diff --git a/examples/ssr-caching/README.md b/examples/ssr-caching/README.md index 6c219c20..ce835c18 100644 --- a/examples/ssr-caching/README.md +++ b/examples/ssr-caching/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example ssr-caching ssr-caching-app +```bash +npx create-next-app --example ssr-caching ssr-caching-app +# or +yarn create next-app --example ssr-caching ssr-caching-app ``` ### Download manually diff --git a/examples/svg-components/README.md b/examples/svg-components/README.md index 49cd75b6..bdb25628 100644 --- a/examples/svg-components/README.md +++ b/examples/svg-components/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example svg-components svg-components-app +```bash +npx create-next-app --example svg-components svg-components-app +# or +yarn create next-app --example svg-components svg-components-app ``` ### Download manually diff --git a/examples/using-inferno/README.md b/examples/using-inferno/README.md index dd65bc32..8d787acf 100644 --- a/examples/using-inferno/README.md +++ b/examples/using-inferno/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example using-inferno using-inferno-app +```bash +npx create-next-app --example using-inferno using-inferno-app +# or +yarn create next-app --example using-inferno using-inferno-app ``` ### Download manually diff --git a/examples/using-nerv/README.md b/examples/using-nerv/README.md index 0156310f..f0144133 100644 --- a/examples/using-nerv/README.md +++ b/examples/using-nerv/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example using-nerv using-nerv-app +```bash +npx create-next-app --example using-nerv using-nerv-app +# or +yarn create next-app --example using-nerv using-nerv-app ``` ### Download manually diff --git a/examples/using-preact/README.md b/examples/using-preact/README.md index d68cb891..aa8728f8 100644 --- a/examples/using-preact/README.md +++ b/examples/using-preact/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example using-preact using-preact-app +```bash +npx create-next-app --example using-preact using-preact-app +# or +yarn create next-app --example using-preact using-preact-app ``` ### Download manually diff --git a/examples/using-router/README.md b/examples/using-router/README.md index 39753ce4..f1294879 100644 --- a/examples/using-router/README.md +++ b/examples/using-router/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example using-router using-router-app +```bash +npx create-next-app --example using-router using-router-app +# or +yarn create next-app --example using-router using-router-app ``` ### Download manually diff --git a/examples/using-with-router/README.md b/examples/using-with-router/README.md index 0750f27f..851368f0 100644 --- a/examples/using-with-router/README.md +++ b/examples/using-with-router/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example using-with-router using-with-router-app +```bash +npx create-next-app --example using-with-router using-with-router-app +# or +yarn create next-app --example using-with-router using-with-router-app ``` ### Download manually diff --git a/examples/with-absolute-imports/.babelrc b/examples/with-absolute-imports/.babelrc index 0e194558..4fabde62 100644 --- a/examples/with-absolute-imports/.babelrc +++ b/examples/with-absolute-imports/.babelrc @@ -1,5 +1,5 @@ { - "presets": "next/babel", + "presets": ["next/babel"], "plugins": [ [ "module-resolver", diff --git a/examples/with-absolute-imports/README.md b/examples/with-absolute-imports/README.md index a9e0de1e..769b354d 100644 --- a/examples/with-absolute-imports/README.md +++ b/examples/with-absolute-imports/README.md @@ -6,9 +6,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-absolute-imports with-absolute-imports-app +```bash +npx create-next-app --example with-absolute-imports with-absolute-imports-app +# or +yarn create next-app --example with-absolute-imports with-absolute-imports-app ``` ### Download manually @@ -16,8 +17,8 @@ create-next-app --example with-absolute-imports with-absolute-imports-app Download the example [or clone the repo](https://github.com/zeit/next.js): ```bash -curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-absolute-import -cd with-absolute-import +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-absolute-imports +cd with-absolute-imports ``` Install it and run: diff --git a/examples/with-algolia-react-instantsearch/README.md b/examples/with-algolia-react-instantsearch/README.md index 8503ad69..eb07b499 100644 --- a/examples/with-algolia-react-instantsearch/README.md +++ b/examples/with-algolia-react-instantsearch/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-algolia-react-instantsearch with-algolia-react-instantsearch-app +```bash +npx create-next-app --example with-algolia-react-instantsearch with-algolia-react-instantsearch-app +# or +yarn create next-app --example with-algolia-react-instantsearch with-algolia-react-instantsearch-app ``` ### Download manually diff --git a/examples/with-amp/README.md b/examples/with-amp/README.md index 58d2a8db..58f185ec 100644 --- a/examples/with-amp/README.md +++ b/examples/with-amp/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-amp with-amp-app +```bash +npx create-next-app --example with-amp with-amp-app +# or +yarn create next-app --example with-amp with-amp-app ``` ### Download manually diff --git a/examples/with-analytics/README.md b/examples/with-analytics/README.md index ec305fb4..c72d8d3c 100644 --- a/examples/with-analytics/README.md +++ b/examples/with-analytics/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-analytics with-analytics-app +```bash +npx create-next-app --example with-analytics with-analytics-app +# or +yarn create next-app --example with-analytics with-analytics-app ``` ### Download manually diff --git a/examples/with-ant-design/.babelrc b/examples/with-ant-design/.babelrc index 0a0cd952..647afcc4 100644 --- a/examples/with-ant-design/.babelrc +++ b/examples/with-ant-design/.babelrc @@ -6,7 +6,7 @@ "import", { "libraryName": "antd", - "style": false + "style": "css" } ] ] diff --git a/examples/with-ant-design/README.md b/examples/with-ant-design/README.md index dda38111..f8cbfb3a 100644 --- a/examples/with-ant-design/README.md +++ b/examples/with-ant-design/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-ant-design with-ant-design-app +```bash +npx create-next-app --example with-ant-design with-ant-design-app +# or +yarn create next-app --example with-ant-design with-ant-design-app ``` ### Download manually diff --git a/examples/with-ant-design/index.js b/examples/with-ant-design/index.js index ec9e13dc..06ac9a7a 100644 --- a/examples/with-ant-design/index.js +++ b/examples/with-ant-design/index.js @@ -4,7 +4,7 @@ export default ({ children }) => - + + +) diff --git a/examples/with-apollo-and-redux-saga/components/Clock.js b/examples/with-apollo-and-redux-saga/components/Clock.js new file mode 100644 index 00000000..80f74cf1 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Clock.js @@ -0,0 +1,35 @@ +import React from 'react' + +const pad = n => (n < 10 ? `0${n}` : n) + +const format = t => { + const hours = t.getUTCHours() + const minutes = t.getUTCMinutes() + const seconds = t.getUTCSeconds() + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}` +} + +function Clock ({ lastUpdate, light }) { + return ( + +

Clock:

+
+ {format(new Date(lastUpdate || Date.now()))} + +
+
+ ) +} + +export default Clock diff --git a/examples/with-apollo-and-redux-saga/components/ErrorMessage.js b/examples/with-apollo-and-redux-saga/components/ErrorMessage.js new file mode 100644 index 00000000..c4a800c7 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/ErrorMessage.js @@ -0,0 +1,13 @@ +export default ({message}) => ( + +) diff --git a/examples/with-apollo-and-redux-saga/components/Header.js b/examples/with-apollo-and-redux-saga/components/Header.js new file mode 100644 index 00000000..bc6b696a --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Header.js @@ -0,0 +1,31 @@ +import Link from 'next/link' +import { withRouter } from 'next/router' + +const Header = ({ router: { pathname } }) => ( +
+ + Home + + + About + + + Blog + + +
+) + +export default withRouter(Header) diff --git a/examples/with-apollo-and-redux-saga/components/Page.js b/examples/with-apollo-and-redux-saga/components/Page.js new file mode 100644 index 00000000..22e8edf3 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Page.js @@ -0,0 +1,19 @@ +import React from 'react' +import { connect } from 'react-redux' + +import PageCount from './PageCount' +import Clock from './Clock' +import Placeholder from './Placeholder' + +function Page ({ clock, placeholder, linkTo, title }) { + return ( + +

{title}

+ + + +
+ ) +} + +export default connect(state => state)(Page) diff --git a/examples/with-apollo-and-redux-saga/components/PageCount.js b/examples/with-apollo-and-redux-saga/components/PageCount.js new file mode 100644 index 00000000..e9f977cd --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PageCount.js @@ -0,0 +1,34 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { countIncrease, countDecrease } from '../lib/count/actions' + +class PageCount extends Component { + increase = () => { + this.props.dispatch(countIncrease()) + } + decrease = () => { + this.props.dispatch(countDecrease()) + } + + render () { + const { count } = this.props + return ( +
+ +

+ PageCount: {count} +

+ + +
+ ) + } +} + +const mapStateToProps = ({ count }) => ({ count }) +export default connect(mapStateToProps)(PageCount) diff --git a/examples/with-apollo-and-redux-saga/components/Placeholder.js b/examples/with-apollo-and-redux-saga/components/Placeholder.js new file mode 100644 index 00000000..3820e967 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Placeholder.js @@ -0,0 +1,20 @@ +import React from 'react' + +export default ({placeholder}) => ( + +

JSON:

+ {placeholder.data && ( +
+        {JSON.stringify(placeholder.data, null, 2)}
+      
+ )} + {placeholder.error && ( +

Error: {placeholder.error.message}

+ )} + +
+) diff --git a/examples/with-apollo-and-redux-saga/components/Post.js b/examples/with-apollo-and-redux-saga/components/Post.js new file mode 100644 index 00000000..dc892ae9 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Post.js @@ -0,0 +1,62 @@ +import React from 'react' +import { withRouter } from 'next/router' +import { graphql } from 'react-apollo' +import gql from 'graphql-tag' +import ErrorMessage from './ErrorMessage' +import PostVoteUp from './PostVoteUp' +import PostVoteDown from './PostVoteDown' +import PostVoteCount from './PostVoteCount' + +function Post ({ id, data: { error, Post } }) { + if (error) return + if (Post) { + return ( +
+
+

{Post.title}

+

ID: {Post.id}
URL: {Post.url}

+ + + + + +
+ +
+ ) + } + return
Loading
+} + +const post = gql` + query post($id: ID!) { + Post(id: $id) { + id + title + votes + url + createdAt + } + } +` + +// The `graphql` wrapper executes a GraphQL query and makes the results +// available on the `data` prop of the wrapped component (PostList) +const ComponentWithMutation = graphql(post, { + options: ({ router: { query } }) => ({ + variables: { + id: query.id + } + }), + props: ({ data }) => ({ + data + }) +})(Post) + +export default withRouter(ComponentWithMutation) diff --git a/examples/with-apollo-and-redux-saga/components/PostList.js b/examples/with-apollo-and-redux-saga/components/PostList.js new file mode 100644 index 00000000..b46340a9 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostList.js @@ -0,0 +1,143 @@ +import { graphql } from 'react-apollo' +import gql from 'graphql-tag' +import { Router } from '../routes' +import ErrorMessage from './ErrorMessage' +import PostVoteUp from './PostVoteUp' +import PostVoteDown from './PostVoteDown' +import PostVoteCount from './PostVoteCount' + +const POSTS_PER_PAGE = 10 + +function handleClick (event, id) { + event.preventDefault() + // With route name and params + // Router.pushRoute('blog/entry', { id: id }) + // With route URL + Router.pushRoute(`/blog/${id}`) +} + +function PostList ({ + data: { loading, error, allPosts, _allPostsMeta }, + loadMorePosts +}) { + if (error) return + if (allPosts && allPosts.length) { + const areMorePosts = allPosts.length < _allPostsMeta.count + return ( +
+ + {areMorePosts ? ( + + ) : ( + '' + )} + +
+ ) + } + return
Loading
+} + +export const allPosts = gql` + query allPosts($first: Int!, $skip: Int!) { + allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) { + id + title + votes + url + createdAt + } + _allPostsMeta { + count + } + } +` + +export const allPostsQueryVars = { + skip: 0, + first: POSTS_PER_PAGE +} + +// The `graphql` wrapper executes a GraphQL query and makes the results +// available on the `data` prop of the wrapped component (PostList) +export default graphql(allPosts, { + options: { + variables: allPostsQueryVars + }, + props: ({ data }) => ({ + data, + loadMorePosts: () => { + return data.fetchMore({ + variables: { + skip: data.allPosts.length + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return previousResult + } + return Object.assign({}, previousResult, { + // Append the new posts results to the old one + allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts] + }) + } + }) + } + }) +})(PostList) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteButton.js b/examples/with-apollo-and-redux-saga/components/PostVoteButton.js new file mode 100644 index 00000000..1ec82b4a --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteButton.js @@ -0,0 +1,30 @@ +export default ({id, votes, onClickHandler, className}) => ( + +) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteCount.js b/examples/with-apollo-and-redux-saga/components/PostVoteCount.js new file mode 100644 index 00000000..53fb1285 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteCount.js @@ -0,0 +1,12 @@ +export default ({votes}) => ( + + {votes} + + +) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteDown.js b/examples/with-apollo-and-redux-saga/components/PostVoteDown.js new file mode 100644 index 00000000..06340f29 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteDown.js @@ -0,0 +1,42 @@ +import React from 'react' +import { graphql } from 'react-apollo' +import gql from 'graphql-tag' +import PostVoteButton from './PostVoteButton' + +function PostVoteDown ({ downvote, votes, id }) { + return ( + downvote(id, votes - 1)} + /> + ) +} + +const downvotePost = gql` + mutation updatePost($id: ID!, $votes: Int) { + updatePost(id: $id, votes: $votes) { + id + __typename + votes + } + } +` + +export default graphql(downvotePost, { + props: ({ ownProps, mutate }) => ({ + downvote: (id, votes) => + mutate({ + variables: { id, votes }, + optimisticResponse: { + __typename: 'Mutation', + updatePost: { + __typename: 'Post', + id: ownProps.id, + votes: ownProps.votes - 1 + } + } + }) + }) +})(PostVoteDown) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteUp.js b/examples/with-apollo-and-redux-saga/components/PostVoteUp.js new file mode 100644 index 00000000..58c7494c --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteUp.js @@ -0,0 +1,42 @@ +import React from 'react' +import { graphql } from 'react-apollo' +import gql from 'graphql-tag' +import PostVoteButton from './PostVoteButton' + +function PostVoteUp ({ upvote, votes, id }) { + return ( + upvote(id, votes + 1)} + /> + ) +} + +const upvotePost = gql` + mutation updatePost($id: ID!, $votes: Int) { + updatePost(id: $id, votes: $votes) { + id + __typename + votes + } + } +` + +export default graphql(upvotePost, { + props: ({ ownProps, mutate }) => ({ + upvote: (id, votes) => + mutate({ + variables: { id, votes }, + optimisticResponse: { + __typename: 'Mutation', + updatePost: { + __typename: 'Post', + id: ownProps.id, + votes: ownProps.votes + 1 + } + } + }) + }) +})(PostVoteUp) diff --git a/examples/with-apollo-and-redux-saga/components/Submit.js b/examples/with-apollo-and-redux-saga/components/Submit.js new file mode 100644 index 00000000..6bc8d092 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Submit.js @@ -0,0 +1,71 @@ +import { graphql } from 'react-apollo' +import gql from 'graphql-tag' +import { allPosts, allPostsQueryVars } from './PostList' + +function Submit ({ createPost }) { + function handleSubmit (event) { + event.preventDefault() + + const form = event.target + + const formData = new window.FormData(form) + createPost(formData.get('title'), formData.get('url')) + + form.reset() + } + + return ( +
+

Submit

+ + + + +
+ ) +} + +const createPost = gql` + mutation createPost($title: String!, $url: String!) { + createPost(title: $title, url: $url) { + id + title + votes + url + createdAt + } + } +` + +export default graphql(createPost, { + props: ({ mutate }) => ({ + createPost: (title, url) => + mutate({ + variables: { title, url }, + update: (proxy, { data: { createPost } }) => { + const data = proxy.readQuery({ + query: allPosts, + variables: allPostsQueryVars + }) + proxy.writeQuery({ + query: allPosts, + data: { + ...data, + allPosts: [createPost, ...data.allPosts] + }, + variables: allPostsQueryVars + }) + } + }) + }) +})(Submit) diff --git a/examples/with-apollo-and-redux-saga/lib/clock/actions.js b/examples/with-apollo-and-redux-saga/lib/clock/actions.js new file mode 100644 index 00000000..3ef5384e --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/clock/actions.js @@ -0,0 +1,20 @@ +export const actionTypes = { + START_CLOCK: 'START_CLOCK', + TICK_CLOCK: 'TICK_CLOCK' +} + +export function startClock(isServer=true) { + return { + type: actionTypes.START_CLOCK, + light: isServer, + lastUpdate: null + } +} + +export function tickClock(isServer) { + return { + type: actionTypes.TICK_CLOCK, + light: !isServer, + lastUpdate: Date.now() + } +} diff --git a/examples/with-apollo-and-redux-saga/lib/clock/reducers.js b/examples/with-apollo-and-redux-saga/lib/clock/reducers.js new file mode 100644 index 00000000..3edb2519 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/clock/reducers.js @@ -0,0 +1,21 @@ +import { actionTypes } from './actions' + +export const initialState = { + lastUpdate: 0, + light: false +} + +function reducer(state = initialState, action) { + switch (action.type) { + case actionTypes.TICK_CLOCK: + return { + ...state, + ...{ lastUpdate: action.lastUpdate, light: !!action.light } + } + + default: + return state + } +} + +export default reducer diff --git a/examples/with-apollo-and-redux-saga/lib/clock/sagas.js b/examples/with-apollo-and-redux-saga/lib/clock/sagas.js new file mode 100644 index 00000000..d567add2 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/clock/sagas.js @@ -0,0 +1,18 @@ +import { delay } from 'redux-saga' +import { call, put, take } from 'redux-saga/effects' +import es6promise from 'es6-promise' +import 'isomorphic-unfetch' + +import { actionTypes, tickClock } from './actions' + +es6promise.polyfill() + +function* runClockSaga() { + yield take(actionTypes.START_CLOCK) + while (true) { + yield put(tickClock(false)) + yield call(delay, 800) + } +} + +export default call(runClockSaga) diff --git a/examples/with-apollo-and-redux-saga/lib/count/actions.js b/examples/with-apollo-and-redux-saga/lib/count/actions.js new file mode 100644 index 00000000..caf6da60 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/count/actions.js @@ -0,0 +1,12 @@ +export const actionTypes = { + COUNT_INCREASE: 'COUNT_INCREASE', + COUNT_DECREASE: 'COUNT_DECREASE' +} + +export function countIncrease() { + return { type: actionTypes.COUNT_INCREASE } +} + +export function countDecrease() { + return { type: actionTypes.COUNT_DECREASE } +} diff --git a/examples/with-apollo-and-redux-saga/lib/count/reducers.js b/examples/with-apollo-and-redux-saga/lib/count/reducers.js new file mode 100644 index 00000000..3114af7f --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/count/reducers.js @@ -0,0 +1,18 @@ +import { actionTypes } from './actions' + +const initialState = 0 + +function reducer(state = initialState, action) { + switch (action.type) { + case actionTypes.COUNT_INCREASE: + return state + 1 + + case actionTypes.COUNT_DECREASE: + return state - 1 + + default: + return state + } +} + +export default reducer diff --git a/examples/with-apollo-and-redux-saga/lib/initApollo.js b/examples/with-apollo-and-redux-saga/lib/initApollo.js new file mode 100644 index 00000000..ff38d7b0 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/initApollo.js @@ -0,0 +1,38 @@ +import { ApolloClient } from 'apollo-client' +import { HttpLink } from 'apollo-link-http' +import { InMemoryCache } from 'apollo-cache-inmemory' +import fetch from 'isomorphic-unfetch' + +let apolloClient = null + +// Polyfill fetch() on the server (used by apollo-client) +if (!process.browser) { + global.fetch = fetch +} + +function create(initialState) { + return new ApolloClient({ + connectToDevTools: process.browser, + ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once) + link: new HttpLink({ + uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute) + credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers` + }), + cache: new InMemoryCache().restore(initialState || {}) + }) +} + +export default function initApollo(initialState) { + // Make sure to create a new client for every server-side request so that data + // isn't shared between connections (which would be bad) + if (!process.browser) { + return create(initialState) + } + + // Reuse client on the client-side + if (!apolloClient) { + apolloClient = create(initialState) + } + + return apolloClient +} diff --git a/examples/with-apollo-and-redux-saga/lib/placeholder/actions.js b/examples/with-apollo-and-redux-saga/lib/placeholder/actions.js new file mode 100644 index 00000000..8ab4df3c --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/placeholder/actions.js @@ -0,0 +1,23 @@ +export const actionTypes = { + LOAD_DATA: 'LOAD_DATA', + LOAD_DATA_SUCCESS: 'LOAD_DATA_SUCCESS', + LOAD_DATA_ERROR: 'LOAD_DATA_ERROR' +} + +export function loadData() { + return { type: actionTypes.LOAD_DATA } +} + +export function loadDataSuccess(data) { + return { + type: actionTypes.LOAD_DATA_SUCCESS, + data + } +} + +export function loadDataError(error) { + return { + type: actionTypes.LOAD_DATA_ERROR, + error + } +} diff --git a/examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js b/examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js new file mode 100644 index 00000000..4aba7960 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js @@ -0,0 +1,27 @@ +import { actionTypes } from './actions' + +export const initialState = { + data: null, + error: false +} + +function reducer(state = initialState, action) { + switch (action.type) { + case actionTypes.LOAD_DATA_SUCCESS: + return { + ...state, + ...{ data: action.data } + } + + case actionTypes.LOAD_DATA_ERROR: + return { + ...state, + ...{ error: action.error } + } + + default: + return state + } +} + +export default reducer diff --git a/examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js b/examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js new file mode 100644 index 00000000..adac726d --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js @@ -0,0 +1,19 @@ +import { put, takeLatest } from 'redux-saga/effects' +import es6promise from 'es6-promise' +import 'isomorphic-unfetch' + +import { actionTypes, loadDataSuccess, loadDataError } from './actions' + +es6promise.polyfill() + +function* loadDataSaga() { + try { + const res = yield fetch('https://jsonplaceholder.typicode.com/users') + const data = yield res.json() + yield put(loadDataSuccess(data)) + } catch (err) { + yield put(loadDataError(err)) + } +} + +export default takeLatest(actionTypes.LOAD_DATA, loadDataSaga) diff --git a/examples/with-apollo-and-redux-saga/lib/rootReducer.js b/examples/with-apollo-and-redux-saga/lib/rootReducer.js new file mode 100644 index 00000000..46aacc0e --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/rootReducer.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux' + +import clock from './clock/reducers' +import count from './count/reducers' +import placeholder from './placeholder/reducers' + +export default combineReducers({ + clock, + count, + placeholder +}) diff --git a/examples/with-apollo-and-redux-saga/lib/rootSaga.js b/examples/with-apollo-and-redux-saga/lib/rootSaga.js new file mode 100644 index 00000000..b42700b4 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/rootSaga.js @@ -0,0 +1,10 @@ +import { all } from 'redux-saga/effects' + +import clock from './clock/sagas' +import placeholder from './placeholder/sagas' + +function* rootSaga() { + yield all([clock, placeholder]) +} + +export default rootSaga diff --git a/examples/with-apollo-and-redux-saga/lib/withApollo.js b/examples/with-apollo-and-redux-saga/lib/withApollo.js new file mode 100644 index 00000000..e7ae9c8d --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/withApollo.js @@ -0,0 +1,92 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { ApolloProvider, getDataFromTree } from 'react-apollo' +import Head from 'next/head' +import initApollo from './initApollo' + +// Gets the display name of a JSX component for dev tools +function getComponentDisplayName(Component) { + return Component.displayName || Component.name || 'Unknown' +} + +export default ComposedComponent => { + return class WithApollo extends React.Component { + static displayName = `WithApollo(${getComponentDisplayName( + ComposedComponent + )})` + static propTypes = { + serverState: PropTypes.object.isRequired + } + + static async getInitialProps(ctx) { + // Initial serverState with apollo (empty) + let serverState = { + apollo: { + data: {} + } + } + + // Evaluate the composed component's getInitialProps() + let composedInitialProps = {} + if (ComposedComponent.getInitialProps) { + composedInitialProps = await ComposedComponent.getInitialProps(ctx) + } + + // Run all GraphQL queries in the component tree + // and extract the resulting data + if (!process.browser) { + const apollo = initApollo() + + try { + // Run all GraphQL queries + await getDataFromTree( + + + , + { + router: { + asPath: ctx.asPath, + pathname: ctx.pathname, + query: ctx.query + } + } + ) + } catch (error) { + // Prevent Apollo Client GraphQL errors from crashing SSR. + // Handle them in components via the data.error prop: + // http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error + } + // getDataFromTree does not call componentWillUnmount + // head side effect therefore need to be cleared manually + Head.rewind() + + // Extract query data from the store + const state = {} + + // Extract query data from the Apollo store + serverState = Object.assign( + state, + { apollo: { data: apollo.cache.extract() } } + ) + } + + return { + serverState, + ...composedInitialProps + } + } + + constructor(props) { + super(props) + this.apollo = initApollo(props.serverState.apollo.data) + } + + render() { + return ( + + + + ) + } + } +} diff --git a/examples/with-apollo-and-redux-saga/lib/withReduxSaga.js b/examples/with-apollo-and-redux-saga/lib/withReduxSaga.js new file mode 100644 index 00000000..f3d75033 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/withReduxSaga.js @@ -0,0 +1,25 @@ +import { composeWithDevTools } from 'redux-devtools-extension' + +import { createStore, applyMiddleware } from 'redux' +import createSagaMiddleware from 'redux-saga' +import nextReduxWrapper from 'next-redux-wrapper' +import nextReduxSaga from 'next-redux-saga' + +import rootReducer from './rootReducer' +import rootSaga from './rootSaga' + +const sagaMiddleware = createSagaMiddleware() + +export function configureStore(initialState = {}) { + const store = createStore( + rootReducer, + initialState, + composeWithDevTools(applyMiddleware(sagaMiddleware)) + ) + store.sagaTask = sagaMiddleware.run(rootSaga) + return store +} + +export default function (BaseComponent) { + return nextReduxWrapper(configureStore)(nextReduxSaga(BaseComponent)) +} diff --git a/examples/with-apollo-and-redux-saga/package.json b/examples/with-apollo-and-redux-saga/package.json new file mode 100644 index 00000000..ff124d7c --- /dev/null +++ b/examples/with-apollo-and-redux-saga/package.json @@ -0,0 +1,31 @@ +{ + "name": "with-apollo-and-redux-saga", + "version": "1.0.0", + "scripts": { + "dev": "node server.js", + "build": "next build", + "start": "NODE_ENV=production node server.js" + }, + "dependencies": { + "apollo-client-preset": "^1.0.4", + "es6-promise": "^4.1.1", + "graphql": "^0.11.7", + "graphql-anywhere": "^4.0.2", + "graphql-tag": "^2.5.0", + "isomorphic-unfetch": "^2.0.0", + "next": "latest", + "next-redux-saga": "^1.0.0", + "next-redux-wrapper": "^1.3.5", + "next-routes": "^1.2.0", + "prop-types": "^15.6.0", + "react": "^16.0.0", + "react-apollo": "^2.0.0", + "react-dom": "^16.0.0", + "react-redux": "^5.0.0", + "redux": "^3.7.0", + "redux-devtools-extension": "^2.13.2", + "redux-saga": "^0.16.0" + }, + "author": "", + "license": "ISC" +} diff --git a/examples/with-apollo-and-redux-saga/pages/about.js b/examples/with-apollo-and-redux-saga/pages/about.js new file mode 100644 index 00000000..174a07bd --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/about.js @@ -0,0 +1,23 @@ +import App from '../components/App' +import Header from '../components/Header' + +export default () => ( + +
+
+

The Idea Behind This Example

+

+ Apollo is a GraphQL client that allows you to easily query the exact data you need from a GraphQL server. In addition to fetching and mutating data, Apollo analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run, fetching more results from the server. +

+

+ In this simple example, we integrate Apollo seamlessly with Next by wrapping our pages inside a higher-order component (HOC). Using the HOC pattern we're able to pass down a central store of query result data created by Apollo into our React component hierarchy defined inside each page of our Next application. +

+

+ On initial page load, while on the server and inside getInitialProps, we invoke the Apollo method, getDataFromTree. This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized. +

+

+ This example relies on graph.cool for its GraphQL backend. +

+
+ +) diff --git a/examples/with-apollo-and-redux-saga/pages/blog/entry.js b/examples/with-apollo-and-redux-saga/pages/blog/entry.js new file mode 100644 index 00000000..eca2c5ab --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/blog/entry.js @@ -0,0 +1,12 @@ +import withApollo from '../../lib/withApollo' + +import App from '../../components/App' +import Header from '../../components/Header' +import Post from '../../components/Post' + +export default withApollo(() => ( + +
+ + +)) diff --git a/examples/with-apollo-and-redux-saga/pages/blog/index.js b/examples/with-apollo-and-redux-saga/pages/blog/index.js new file mode 100644 index 00000000..9f8d01c5 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/blog/index.js @@ -0,0 +1,31 @@ +import React from 'react' + +import withApollo from '../../lib/withApollo' +import withReduxSaga from '../../lib/withReduxSaga' + +import { startClock } from '../../lib/clock/actions' + +import App from '../../components/App' +import Header from '../../components/Header' +import Submit from '../../components/Submit' +import PostList from '../../components/PostList' + +class BlogIndex extends React.Component { + componentDidMount () { + this.props.dispatch(startClock()) + } + + render () { + return ( + +
+ + + + ) + } +} + +export default withReduxSaga( + withApollo(BlogIndex) +) diff --git a/examples/with-apollo-and-redux-saga/pages/index.js b/examples/with-apollo-and-redux-saga/pages/index.js new file mode 100644 index 00000000..c2534f32 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/index.js @@ -0,0 +1,35 @@ +import React from 'react' + +import withReduxSaga from '../lib/withReduxSaga' + +import { startClock } from '../lib/clock/actions' +import { countIncrease } from '../lib/count/actions' +import { loadData } from '../lib/placeholder/actions' + +import App from '../components/App' +import Header from '../components/Header' +import Page from '../components/Page' + +class PageIndex extends React.Component { + static async getInitialProps ({ store }) { + store.dispatch(countIncrease()) + if (!store.getState().placeholder.data) { + store.dispatch(loadData()) + } + } + + componentDidMount () { + this.props.dispatch(startClock()) + } + + render () { + return ( + +
+ + + ) + } +} + +export default withReduxSaga(PageIndex) diff --git a/examples/with-apollo-and-redux-saga/routes.js b/examples/with-apollo-and-redux-saga/routes.js new file mode 100644 index 00000000..80006cf8 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/routes.js @@ -0,0 +1,8 @@ +/** + * Parameterized Routing with next-route + * + * Benefits: Less code, and easily handles complex url structures +**/ +const routes = (module.exports = require('next-routes')()) + +routes.add('blog/entry', '/blog/:id') diff --git a/examples/with-apollo-and-redux-saga/server.js b/examples/with-apollo-and-redux-saga/server.js new file mode 100644 index 00000000..e977177b --- /dev/null +++ b/examples/with-apollo-and-redux-saga/server.js @@ -0,0 +1,31 @@ +/** + * server.js + * + * You can use the default server.js by simply running `next`, + * but a custom one is required to do paramaterized urls like + * blog/:slug. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * BENEVOLENT WEB LLC BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {createServer} = require('http') +const next = require('next') + +const port = parseInt(process.env.PORT, 10) || 3000 +const dev = process.env.NODE_ENV !== 'production' +const app = next({dev}) +const routes = require('./routes') + +const handler = routes.getRequestHandler(app) +app.prepare().then(() => { + createServer(handler) + .listen(port, (err) => { + if (err) throw err + console.log(`> Ready on http://localhost:${port}`) + }) +}) diff --git a/examples/with-apollo-and-redux/README.md b/examples/with-apollo-and-redux/README.md index 654bde05..548a4556 100644 --- a/examples/with-apollo-and-redux/README.md +++ b/examples/with-apollo-and-redux/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-apollo-and-redux with-apollo-and-redux-app +```bash +npx create-next-app --example with-apollo-and-redux with-apollo-and-redux-app +# or +yarn create next-app --example with-apollo-and-redux with-apollo-and-redux-app ``` ### Download manually diff --git a/examples/with-apollo-and-redux/pages/index.js b/examples/with-apollo-and-redux/pages/index.js index e3223a36..9c50b02e 100644 --- a/examples/with-apollo-and-redux/pages/index.js +++ b/examples/with-apollo-and-redux/pages/index.js @@ -50,6 +50,6 @@ const mapDispatchToProps = dispatch => { } } -export default withRedux(initStore, null, mapDispatchToProps)( - withApollo(Index) +export default withApollo( + withRedux(initStore, null, mapDispatchToProps)(Index) ) diff --git a/examples/with-apollo-auth/README.md b/examples/with-apollo-auth/README.md index 11794fa2..ff14ceff 100644 --- a/examples/with-apollo-auth/README.md +++ b/examples/with-apollo-auth/README.md @@ -11,9 +11,10 @@ https://next-with-apollo-auth.now.sh Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-apollo-auth with-apollo-auth-app +```bash +npx create-next-app --example with-apollo-auth with-apollo-auth-app +# or +yarn create next-app --example with-apollo-auth with-apollo-auth-app ``` ### Download manually diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md index bbf1f857..563485e8 100644 --- a/examples/with-apollo/README.md +++ b/examples/with-apollo/README.md @@ -11,9 +11,10 @@ https://next-with-apollo.now.sh Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-apollo with-apollo-app +```bash +npx create-next-app --example with-apollo with-apollo-app +# or +yarn create next-app --example with-apollo with-apollo-app ``` ### Download manually diff --git a/examples/with-apollo/components/PostUpvoter.js b/examples/with-apollo/components/PostUpvoter.js index 6617ed2b..11bf1433 100644 --- a/examples/with-apollo/components/PostUpvoter.js +++ b/examples/with-apollo/components/PostUpvoter.js @@ -1,6 +1,6 @@ import React from 'react' import { graphql } from 'react-apollo' -import gql from 'graphql-tag' +import { gql } from 'apollo-boost' function PostUpvoter ({ upvote, votes, id }) { return ( diff --git a/examples/with-apollo/lib/initApollo.js b/examples/with-apollo/lib/initApollo.js index ff38d7b0..f7271abd 100644 --- a/examples/with-apollo/lib/initApollo.js +++ b/examples/with-apollo/lib/initApollo.js @@ -1,6 +1,6 @@ -import { ApolloClient } from 'apollo-client' -import { HttpLink } from 'apollo-link-http' -import { InMemoryCache } from 'apollo-cache-inmemory' +import { ApolloClient } from 'apollo-boost' +import { HttpLink } from 'apollo-boost' +import { InMemoryCache } from 'apollo-boost' import fetch from 'isomorphic-unfetch' let apolloClient = null diff --git a/examples/with-apollo/lib/withData.js b/examples/with-apollo/lib/withData.js index 8c3bc8c0..d854618a 100644 --- a/examples/with-apollo/lib/withData.js +++ b/examples/with-apollo/lib/withData.js @@ -20,11 +20,7 @@ export default ComposedComponent => { static async getInitialProps(ctx) { // Initial serverState with apollo (empty) - let serverState = { - apollo: { - data: {} - } - } + let serverState // Evaluate the composed component's getInitialProps() let composedInitialProps = {} @@ -38,15 +34,14 @@ export default ComposedComponent => { try { // Run all GraphQL queries await getDataFromTree( - - - , + , { router: { asPath: ctx.asPath, pathname: ctx.pathname, query: ctx.query - } + }, + client: apollo } ) } catch (error) { diff --git a/examples/with-apollo/package.json b/examples/with-apollo/package.json index 86b65f1b..0bdb262a 100644 --- a/examples/with-apollo/package.json +++ b/examples/with-apollo/package.json @@ -7,15 +7,13 @@ "start": "next start" }, "dependencies": { - "apollo-client-preset": "1.0.4", - "graphql": "0.11.7", - "graphql-anywhere": "4.0.2", - "graphql-tag": "2.5.0", + "apollo-boost": "^0.1.3", + "graphql": "0.13.2", "isomorphic-unfetch": "2.0.0", "next": "latest", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react": "16.2.0", - "react-apollo": "2.0.1", + "react-apollo": "2.1.0", "react-dom": "16.2.0" }, "author": "", diff --git a/examples/with-asset-imports/README.md b/examples/with-asset-imports/README.md index 1e1d221c..c2f58c78 100644 --- a/examples/with-asset-imports/README.md +++ b/examples/with-asset-imports/README.md @@ -6,9 +6,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-asset-imports with-asset-imports-app +```bash +npx create-next-app --example with-asset-imports with-asset-imports-app +# or +yarn create next-app --example with-asset-imports with-asset-imports-app ``` ### Download manually diff --git a/examples/with-babel-macros/README.md b/examples/with-babel-macros/README.md index b40e6293..6780c923 100644 --- a/examples/with-babel-macros/README.md +++ b/examples/with-babel-macros/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-babel-macros with-babel-macros-app +```bash +npx create-next-app --example with-babel-macros with-babel-macros-app +# or +yarn create next-app --example with-babel-macros with-babel-macros-app ``` ### Download manually diff --git a/examples/with-custom-reverse-proxy/README.md b/examples/with-custom-reverse-proxy/README.md index 9966f1d8..bb5774a2 100644 --- a/examples/with-custom-reverse-proxy/README.md +++ b/examples/with-custom-reverse-proxy/README.md @@ -9,7 +9,7 @@ Sorry for the extra packages. I belong to the minority camp of writing ES6 code ## How to run it -``` +```bash npm i; npm run build; npm run dev; ``` diff --git a/examples/with-cxs/README.md b/examples/with-cxs/README.md index 4e28c2a1..4d2fbbe8 100644 --- a/examples/with-cxs/README.md +++ b/examples/with-cxs/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-cxs with-cxs-app +```bash +npx create-next-app --example with-cxs with-cxs-app +# or +yarn create next-app --example with-cxs with-cxs-app ``` ### Download manually diff --git a/examples/with-data-prefetch/README.md b/examples/with-data-prefetch/README.md index 32e0c642..25b087a2 100644 --- a/examples/with-data-prefetch/README.md +++ b/examples/with-data-prefetch/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-data-prefetch with-data-prefetch-app +```bash +npx create-next-app --example with-data-prefetch with-data-prefetch-app +# or +yarn create next-app --example with-data-prefetch with-data-prefetch-app ``` ### Download manually diff --git a/examples/with-dotenv/README.md b/examples/with-dotenv/README.md index 1c434dda..95cba840 100644 --- a/examples/with-dotenv/README.md +++ b/examples/with-dotenv/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-dotenv with-dotenv-app +```bash +npx create-next-app --example with-dotenv with-dotenv-app +# or +yarn create next-app --example with-dotenv with-dotenv-app ``` ### Download manually diff --git a/examples/with-draft-js/README.md b/examples/with-draft-js/README.md index 5a9c90ac..64c4531e 100644 --- a/examples/with-draft-js/README.md +++ b/examples/with-draft-js/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-draft-js +```bash +npx create-next-app --example with-draft-js +# or +yarn create next-app --example with-draft-js with-draft-js-app ``` ### Download manually diff --git a/examples/with-dynamic-import/README.md b/examples/with-dynamic-import/README.md index 56ef0847..39e203be 100644 --- a/examples/with-dynamic-import/README.md +++ b/examples/with-dynamic-import/README.md @@ -6,9 +6,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-dynamic-import with-dynamic-import-app +```bash +npx create-next-app --example with-dynamic-import with-dynamic-import-app +# or +yarn create next-app --example with-dynamic-import with-dynamic-import-app ``` ### Download manually diff --git a/examples/with-electron/readme.md b/examples/with-electron/README.md similarity index 89% rename from examples/with-electron/readme.md rename to examples/with-electron/README.md index 37b9641e..3a07ccda 100644 --- a/examples/with-electron/readme.md +++ b/examples/with-electron/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-electron with-electron-app +```bash +npx create-next-app --example with-electron with-electron-app +# or +yarn create next-app --example with-electron with-electron-app ``` ### Download manually diff --git a/examples/with-emotion/README.md b/examples/with-emotion/README.md index 35bd195d..7ae15dce 100644 --- a/examples/with-emotion/README.md +++ b/examples/with-emotion/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-emotion with-emotion-app +```bash +npx create-next-app --example with-emotion with-emotion-app +# or +yarn create next-app --example with-emotion with-emotion-app ``` ### Download manually diff --git a/examples/with-fela/FelaProvider.js b/examples/with-fela/FelaProvider.js new file mode 100755 index 00000000..fb82667c --- /dev/null +++ b/examples/with-fela/FelaProvider.js @@ -0,0 +1,26 @@ +import { Component } from 'react' +import PropTypes from 'prop-types' + +import { Provider } from 'react-fela' +import getFelaRenderer from './getFelaRenderer' + +const clientRenderer = getFelaRenderer() + +export default class FelaProvider extends Component { + static contextTypes = { + renderer: PropTypes.object + }; + + render () { + if (this.context.renderer) { + return this.props.children + } + + const renderer = this.props.renderer || clientRenderer + return ( + + {this.props.children} + + ) + } +} diff --git a/examples/with-fela/README.md b/examples/with-fela/README.md old mode 100644 new mode 100755 index 26c46d4e..9a69f53b --- a/examples/with-fela/README.md +++ b/examples/with-fela/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-fela with-fela-app +```bash +npx create-next-app --example with-fela with-fela-app +# or +yarn create next-app --example with-fela with-fela-app ``` ### Download manually diff --git a/examples/with-fela/fela-renderer.js b/examples/with-fela/fela-renderer.js deleted file mode 100644 index 2b287c65..00000000 --- a/examples/with-fela/fela-renderer.js +++ /dev/null @@ -1,8 +0,0 @@ -import { createRenderer } from 'fela' -import webPreset from 'fela-preset-web' - -const felaRenderer = createRenderer({ - plugins: [...webPreset] -}) - -export default felaRenderer diff --git a/examples/with-fela/getFelaRenderer.js b/examples/with-fela/getFelaRenderer.js new file mode 100644 index 00000000..c28255c9 --- /dev/null +++ b/examples/with-fela/getFelaRenderer.js @@ -0,0 +1,10 @@ +import { createRenderer } from 'fela' +import webPreset from 'fela-preset-web' + +export default function getRenderer () { + return createRenderer({ + plugins: [ + ...webPreset + ] + }) +} diff --git a/examples/with-fela/layout.js b/examples/with-fela/layout.js deleted file mode 100644 index a7abd8d3..00000000 --- a/examples/with-fela/layout.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Provider } from 'react-fela' -import felaRenderer from './fela-renderer' - -export default ({ children }) => - - {children} - diff --git a/examples/with-fela/package.json b/examples/with-fela/package.json old mode 100644 new mode 100755 index 626f321c..8a0ae30a --- a/examples/with-fela/package.json +++ b/examples/with-fela/package.json @@ -7,12 +7,12 @@ "start": "next start" }, "dependencies": { - "fela": "^5.0.1", - "fela-dom": "5.0.2", - "fela-preset-web": "^5.0.2", + "fela": "^6.1.4", + "fela-dom": "^7.0.5", + "fela-preset-web": "^8.0.4", "next": "latest", "react": "^16.0.0", "react-dom": "^16.0.0", - "react-fela": "^5.0.2" + "react-fela": "^7.0.1" } } diff --git a/examples/with-fela/pages/_document.js b/examples/with-fela/pages/_document.js old mode 100644 new mode 100755 index 834753ec..b80eee22 --- a/examples/with-fela/pages/_document.js +++ b/examples/with-fela/pages/_document.js @@ -1,12 +1,20 @@ import Document, { Head, Main, NextScript } from 'next/document' import { renderToSheetList } from 'fela-dom' -import felaRenderer from '../fela-renderer' + +import FelaProvider from '../FelaProvider' +import getFelaRenderer from '../getFelaRenderer' export default class MyDocument extends Document { static getInitialProps ({ renderPage }) { - const page = renderPage() - const sheetList = renderToSheetList(felaRenderer) - felaRenderer.clear() + const serverRenderer = getFelaRenderer() + + const page = renderPage(App => props => ( + + + + )) + + const sheetList = renderToSheetList(serverRenderer) return { ...page, @@ -15,13 +23,17 @@ export default class MyDocument extends Document { } render () { - const styleNodes = this.props.sheetList.map(({ type, media, css }) => - +

AddCount: {count}

+ + + ) + } +} + +const mapStateToProps = ({ count }) => ({ count }) + +const mapDispatchToProps = (dispatch) => { + return { + addCount: bindActionCreators(addCount, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddCount) diff --git a/examples/with-redux-wrapper/components/Clock.js b/examples/with-redux-wrapper/components/Clock.js new file mode 100644 index 00000000..d8e05cea --- /dev/null +++ b/examples/with-redux-wrapper/components/Clock.js @@ -0,0 +1,24 @@ +export default ({ lastUpdate, light }) => { + return ( +
+ {format(new Date(lastUpdate))} + +
+ ) +} + +const format = t => `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad(t.getUTCSeconds())}` + +const pad = n => n < 10 ? `0${n}` : n diff --git a/examples/with-redux-wrapper/components/Page.js b/examples/with-redux-wrapper/components/Page.js new file mode 100644 index 00000000..3532e3fb --- /dev/null +++ b/examples/with-redux-wrapper/components/Page.js @@ -0,0 +1,17 @@ +import Link from 'next/link' +import { connect } from 'react-redux' +import Clock from './Clock' +import AddCount from './AddCount' + +export default connect(state => state)(({ title, linkTo, lastUpdate, light }) => { + return ( +
+

{title}

+ + + +
+ ) +}) diff --git a/examples/with-redux-wrapper/package.json b/examples/with-redux-wrapper/package.json new file mode 100644 index 00000000..61f2be4e --- /dev/null +++ b/examples/with-redux-wrapper/package.json @@ -0,0 +1,20 @@ +{ + "name": "with-redux-wrapper", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "next-redux-wrapper": "^1.0.0", + "react": "^16.0.0", + "redux-devtools-extension": "^2.13.2", + "react-dom": "^16.0.0", + "react-redux": "^5.0.1", + "redux": "^3.6.0", + "redux-thunk": "^2.1.0" + }, + "license": "ISC" +} diff --git a/examples/with-redux-wrapper/pages/index.js b/examples/with-redux-wrapper/pages/index.js new file mode 100644 index 00000000..be66747b --- /dev/null +++ b/examples/with-redux-wrapper/pages/index.js @@ -0,0 +1,37 @@ +import React from 'react' +import { bindActionCreators } from 'redux' +import { initStore, startClock, addCount, serverRenderClock } from '../store' +import withRedux from 'next-redux-wrapper' +import Page from '../components/Page' + +class Counter extends React.Component { + static getInitialProps ({ store, isServer }) { + store.dispatch(serverRenderClock(isServer)) + store.dispatch(addCount()) + + return { isServer } + } + + componentDidMount () { + this.timer = this.props.startClock() + } + + componentWillUnmount () { + clearInterval(this.timer) + } + + render () { + return ( + + ) + } +} + +const mapDispatchToProps = (dispatch) => { + return { + addCount: bindActionCreators(addCount, dispatch), + startClock: bindActionCreators(startClock, dispatch) + } +} + +export default withRedux(initStore, null, mapDispatchToProps)(Counter) diff --git a/examples/with-redux-wrapper/pages/other.js b/examples/with-redux-wrapper/pages/other.js new file mode 100644 index 00000000..70e98f90 --- /dev/null +++ b/examples/with-redux-wrapper/pages/other.js @@ -0,0 +1,36 @@ +import React from 'react' +import { bindActionCreators } from 'redux' +import { initStore, startClock, addCount, serverRenderClock } from '../store' +import withRedux from 'next-redux-wrapper' +import Page from '../components/Page' + +class Counter extends React.Component { + static getInitialProps ({ store, isServer }) { + store.dispatch(serverRenderClock(isServer)) + store.dispatch(addCount()) + return { isServer } + } + + componentDidMount () { + this.timer = this.props.startClock() + } + + componentWillUnmount () { + clearInterval(this.timer) + } + + render () { + return ( + + ) + } +} + +const mapDispatchToProps = (dispatch) => { + return { + addCount: bindActionCreators(addCount, dispatch), + startClock: bindActionCreators(startClock, dispatch) + } +} + +export default withRedux(initStore, null, mapDispatchToProps)(Counter) diff --git a/examples/with-redux-wrapper/store.js b/examples/with-redux-wrapper/store.js new file mode 100644 index 00000000..7ad2c03f --- /dev/null +++ b/examples/with-redux-wrapper/store.js @@ -0,0 +1,44 @@ +import { createStore, applyMiddleware } from 'redux' +import { composeWithDevTools } from 'redux-devtools-extension' +import thunkMiddleware from 'redux-thunk' + +const exampleInitialState = { + lastUpdate: 0, + light: false, + count: 0 +} + +export const actionTypes = { + ADD: 'ADD', + TICK: 'TICK' +} + +// REDUCERS +export const reducer = (state = exampleInitialState, action) => { + switch (action.type) { + case actionTypes.TICK: + return Object.assign({}, state, { lastUpdate: action.ts, light: !!action.light }) + case actionTypes.ADD: + return Object.assign({}, state, { + count: state.count + 1 + }) + default: return state + } +} + +// ACTIONS +export const serverRenderClock = (isServer) => dispatch => { + return dispatch({ type: actionTypes.TICK, light: !isServer, ts: Date.now() }) +} + +export const startClock = () => dispatch => { + return setInterval(() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }), 1000) +} + +export const addCount = () => dispatch => { + return dispatch({ type: actionTypes.ADD }) +} + +export const initStore = (initialState = exampleInitialState) => { + return createStore(reducer, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware))) +} diff --git a/examples/with-redux/README.md b/examples/with-redux/README.md index daaea658..74cbd672 100644 --- a/examples/with-redux/README.md +++ b/examples/with-redux/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-redux with-redux-app +```bash +npx create-next-app --example with-redux with-redux-app +# or +yarn create next-app --example with-redux with-redux-app ``` ### Download manually @@ -37,17 +38,21 @@ now ## The idea behind the example +This example is based on [full example](https://github.com/kirill-konshin/next-redux-wrapper) which should probably be used for production. The next.js example with this library can be found [here](https://github.com/zeit/next.js/tree/canary/examples/with-redux-wrapper) + +The reason for this example is to show an easier to follow and extendable way to include Redux in a next.js app. + Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use redux that also works with our universal rendering approach. This is just a way you can do it but it's not the only one. In the first example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color than the client one. ![](http://i.imgur.com/JCxtWSj.gif) -Our page is located at `pages/index.js` so it will map the route `/`. To get the initial data for rendering we are implementing the static method `getInitialProps`, initializing the redux store and dispatching the required actions until we are ready to return the initial state to be rendered. Since the component is wrapped with `next-redux-wrapper`, the component is automatically connected to Redux and wrapped with `react-redux Provider`, that allows us to access redux state immediately and send the store down to children components so they can access to the state when required. +Our page is located at `pages/index.js` so it will map the route `/`. To get the initial data for rendering we are implementing the static method `getInitialProps`, initializing the redux store and dispatching the required actions until we are ready to return the initial state to be rendered. Since the component is wrapped with `withRedux.js`, the component is automatically connected to Redux and wrapped with `react-redux Provider`, that allows us to access redux state immediately and send the store down to children components so they can access to the state when required. For safety it is recommended to wrap all pages, no matter if they use Redux or not, so that you should not care about it anymore in all child components. -`withRedux` function accepts `makeStore` as first argument, all other arguments are internally passed to `react-redux connect()` function. `makeStore` function will receive initialState as one argument and should return a new instance of redux store each time when called, no memoization needed here. See the [full example](https://github.com/kirill-konshin/next-redux-wrapper#usage) in the Next Redux Wrapper repository. And there's another package [next-connect-redux](https://github.com/huzidaha/next-connect-redux) available with similar features. +`withRedux` function accepts `initStore` as first argument, all other arguments are internally passed to `react-redux connect()` function. `initStore` function will receive initialState as one argument and should return a new instance of redux store each time when called, no memoization needed here. To pass the initial state from the server to the client we pass it as a prop called `initialState` so then it's available when the client takes over. diff --git a/examples/with-redux/package.json b/examples/with-redux/package.json index 5186d7e2..c6b9ee9d 100644 --- a/examples/with-redux/package.json +++ b/examples/with-redux/package.json @@ -8,7 +8,6 @@ }, "dependencies": { "next": "latest", - "next-redux-wrapper": "^1.0.0", "react": "^16.0.0", "redux-devtools-extension": "^2.13.2", "react-dom": "^16.0.0", diff --git a/examples/with-redux/pages/index.js b/examples/with-redux/pages/index.js index be66747b..efcc0b89 100644 --- a/examples/with-redux/pages/index.js +++ b/examples/with-redux/pages/index.js @@ -1,7 +1,7 @@ import React from 'react' import { bindActionCreators } from 'redux' import { initStore, startClock, addCount, serverRenderClock } from '../store' -import withRedux from 'next-redux-wrapper' +import withRedux from '../utils/withRedux' import Page from '../components/Page' class Counter extends React.Component { diff --git a/examples/with-redux/pages/other.js b/examples/with-redux/pages/other.js index 70e98f90..f9f15cb0 100644 --- a/examples/with-redux/pages/other.js +++ b/examples/with-redux/pages/other.js @@ -1,7 +1,7 @@ import React from 'react' import { bindActionCreators } from 'redux' import { initStore, startClock, addCount, serverRenderClock } from '../store' -import withRedux from 'next-redux-wrapper' +import withRedux from '../utils/withRedux' import Page from '../components/Page' class Counter extends React.Component { diff --git a/examples/with-redux/utils/withRedux.js b/examples/with-redux/utils/withRedux.js new file mode 100644 index 00000000..2473e81e --- /dev/null +++ b/examples/with-redux/utils/withRedux.js @@ -0,0 +1,58 @@ +import React from 'react' +import { connect, Provider } from 'react-redux' + +const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__' + +// https://github.com/iliakan/detect-node +const checkServer = () => Object.prototype.toString.call(global.process) === '[object process]' + +const getOrCreateStore = (initStore, initialState) => { + // Always make a new store if server + if (checkServer() || typeof window === 'undefined') { + return initStore(initialState) + } + + // Store in global variable if client + if (!window[__NEXT_REDUX_STORE__]) { + window[__NEXT_REDUX_STORE__] = initStore(initialState) + } + return window[__NEXT_REDUX_STORE__] +} + +export default (...args) => (Component) => { + // First argument is initStore, the rest are redux connect arguments and get passed + const [initStore, ...connectArgs] = args + + const ComponentWithRedux = (props = {}) => { + const { store, initialProps, initialState } = props + + // Connect page to redux with connect arguments + const ConnectedComponent = connect.apply(null, connectArgs)(Component) + + // Wrap with redux Provider with store + // Create connected page with initialProps + return React.createElement( + Provider, + { store: store && store.dispatch ? store : getOrCreateStore(initStore, initialState) }, + React.createElement(ConnectedComponent, initialProps) + ) + } + + ComponentWithRedux.getInitialProps = async (props = {}) => { + const isServer = checkServer() + const store = getOrCreateStore(initStore) + + // Run page getInitialProps with store and isServer + const initialProps = Component.getInitialProps + ? await Component.getInitialProps({ ...props, isServer, store }) + : {} + + return { + store, + initialState: store.getState(), + initialProps + } + } + + return ComponentWithRedux +} diff --git a/examples/with-refnux/README.md b/examples/with-refnux/README.md index 8d5dd5a3..38d67c33 100644 --- a/examples/with-refnux/README.md +++ b/examples/with-refnux/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-refnux with-refnux-app +```bash +npx create-next-app --example with-refnux with-refnux-app +# or +yarn create next-app --example with-refnux with-refnux-app ``` ### Download manually diff --git a/examples/with-relay-modern/README.md b/examples/with-relay-modern/README.md index 3f344330..4998e121 100644 --- a/examples/with-relay-modern/README.md +++ b/examples/with-relay-modern/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-relay-modern with-relay-modern-app +```bash +npx create-next-app --example with-relay-modern with-relay-modern-app +# or +yarn create next-app --example with-relay-modern with-relay-modern-app ``` ### Download manually diff --git a/examples/with-semantic-ui/README.md b/examples/with-semantic-ui/README.md index 714307f1..02e6f106 100644 --- a/examples/with-semantic-ui/README.md +++ b/examples/with-semantic-ui/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-semantic-ui with-semantic-ui-app +```bash +npx create-next-app --example with-semantic-ui with-semantic-ui-app +# or +yarn create next-app --example with-semantic-ui with-semantic-ui-app ``` ### Download manually @@ -37,5 +38,4 @@ now ## The idea behind the example -This example shows how to use Next.js along with [Semantic UI React](http://react.semantic-ui.com). This is intended to show the integration of this -UI toolkit with the Framework, \ No newline at end of file +This example shows how to use Next.js along with [Semantic UI React](http://react.semantic-ui.com) including handling of external styles and assets. This is intended to show the integration of this UI toolkit with the Framework. diff --git a/examples/with-semantic-ui/next.config.js b/examples/with-semantic-ui/next.config.js new file mode 100644 index 00000000..9313dfca --- /dev/null +++ b/examples/with-semantic-ui/next.config.js @@ -0,0 +1,20 @@ +const withCss = require('@zeit/next-css') + +module.exports = withCss({ + webpack (config) { + config.module.rules.push({ + test: /\.(png|svg|eot|otf|ttf|woff|woff2)$/, + use: { + loader: 'url-loader', + options: { + limit: 100000, + publicPath: './', + outputPath: 'static/', + name: '[name].[ext]' + } + } + }) + + return config + } +}) diff --git a/examples/with-semantic-ui/package.json b/examples/with-semantic-ui/package.json index 0c394326..6a572e93 100644 --- a/examples/with-semantic-ui/package.json +++ b/examples/with-semantic-ui/package.json @@ -7,10 +7,14 @@ "start": "next start" }, "dependencies": { + "@zeit/next-css": "^0.1.2", + "file-loader": "^1.1.9", "next": "latest", "react": "^16.0.0", "react-dom": "^16.0.0", - "semantic-ui-react": "^0.68.0" + "semantic-ui-css": "^2.2.12", + "semantic-ui-react": "^0.78.2", + "url-loader": "^0.6.2" }, "license": "ISC" } diff --git a/examples/with-semantic-ui/pages/index.js b/examples/with-semantic-ui/pages/index.js index 93a817ce..28955fa5 100644 --- a/examples/with-semantic-ui/pages/index.js +++ b/examples/with-semantic-ui/pages/index.js @@ -1,10 +1,18 @@ +import 'semantic-ui-css/components/modal.css' +import 'semantic-ui-css/components/header.css' +import 'semantic-ui-css/components/button.css' +import 'semantic-ui-css/components/list.css' +import 'semantic-ui-css/components/icon.css' +import 'semantic-ui-css/themes/default/assets/fonts/icons.eot' +import 'semantic-ui-css/themes/default/assets/fonts/icons.woff' +import 'semantic-ui-css/themes/default/assets/fonts/icons.woff2' +import { Modal, Header, Button, List, Icon } from 'semantic-ui-react' import Head from 'next/head' -import { Modal, Header, Button, List } from 'semantic-ui-react' export default () => (
- + Show Modal}> Select a Photo @@ -34,5 +42,7 @@ export default () => ( + + Hello
) diff --git a/examples/with-sentry/README.md b/examples/with-sentry/README.md index 3b547695..7219d21d 100644 --- a/examples/with-sentry/README.md +++ b/examples/with-sentry/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-sentry with-sentry-app +```bash +npx create-next-app --example with-sentry with-sentry-app +# or +yarn create next-app --example with-sentry with-sentry-app ``` ### Download manually diff --git a/examples/with-shallow-routing/README.md b/examples/with-shallow-routing/README.md index cb2cd9fe..6fec2b11 100644 --- a/examples/with-shallow-routing/README.md +++ b/examples/with-shallow-routing/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-shallow-routing with-shallow-routing-app +```bash +npx create-next-app --example with-shallow-routing with-shallow-routing-app +# or +yarn create next-app --example with-shallow-routing with-shallow-routing-app ``` ### Download manually diff --git a/examples/with-socket.io/README.md b/examples/with-socket.io/README.md index 3183d311..15d8c475 100644 --- a/examples/with-socket.io/README.md +++ b/examples/with-socket.io/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-socket.io with-socket.io-app +```bash +npx create-next-app --example with-socket.io with-socket.io-app +# or +yarn create next-app --example with-socket.io with-socket.io-app ``` ### Download manually diff --git a/examples/with-static-export/README.md b/examples/with-static-export/README.md index 358f9e0b..b241c5a2 100644 --- a/examples/with-static-export/README.md +++ b/examples/with-static-export/README.md @@ -6,9 +6,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-static-export with-static-export-app +```bash +npx create-next-app --example with-static-export with-static-export-app +# or +yarn create next-app --example with-static-export with-static-export-app ``` ### Download manually diff --git a/examples/with-styled-components/README.md b/examples/with-styled-components/README.md index 04794019..ae9c1eec 100644 --- a/examples/with-styled-components/README.md +++ b/examples/with-styled-components/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-styled-components with-styled-components-app +```bash +npx create-next-app --example with-styled-components with-styled-components-app +# or +yarn create next-app --example with-styled-components with-styled-components-app ``` ### Download manually diff --git a/examples/with-styled-jsx-plugins/README.md b/examples/with-styled-jsx-plugins/README.md index 1089b0de..38a0de4c 100644 --- a/examples/with-styled-jsx-plugins/README.md +++ b/examples/with-styled-jsx-plugins/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-styled-jsx-plugins with-styled-jsx-plugins-app +```bash +npx create-next-app --example with-styled-jsx-plugins with-styled-jsx-plugins-app +# or +yarn create next-app --example with-styled-jsx-plugins with-styled-jsx-plugins-app ``` ### Download manually diff --git a/examples/with-styled-jsx-scss/README.md b/examples/with-styled-jsx-scss/README.md index 5224db5a..85dbb59f 100644 --- a/examples/with-styled-jsx-scss/README.md +++ b/examples/with-styled-jsx-scss/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-styled-jsx-scss with-styled-jsx-scss-app +```bash +npx create-next-app --example with-styled-jsx-scss with-styled-jsx-scss-app +# or +yarn create next-app --example with-styled-jsx-scss with-styled-jsx-scss-app ``` ### Download manually diff --git a/examples/with-styletron/README.md b/examples/with-styletron/README.md index 3f3f222c..ebdeeb5b 100644 --- a/examples/with-styletron/README.md +++ b/examples/with-styletron/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-styletron with-styletron-app +```bash +npx create-next-app --example with-styletron with-styletron-app +# or +yarn create next-app --example with-styletron with-styletron-app ``` ### Download manually diff --git a/examples/with-sw-precache/README.md b/examples/with-sw-precache/README.md index 16922532..b6c2eefe 100644 --- a/examples/with-sw-precache/README.md +++ b/examples/with-sw-precache/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-sw-precache with-sw-precache-app +```bash +npx create-next-app --example with-sw-precache with-sw-precache-app +# or +yarn create next-app --example with-sw-precache with-sw-precache-app ``` ### Download manually diff --git a/examples/with-tailwindcss/README.md b/examples/with-tailwindcss/README.md index 662b1089..6acb3903 100644 --- a/examples/with-tailwindcss/README.md +++ b/examples/with-tailwindcss/README.md @@ -10,9 +10,10 @@ This is an example of how you can include a global stylesheet in a next.js webap Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-tailwindcss with-tailwindcss-app +```bash +npx create-next-app --example with-tailwindcss with-tailwindcss-app +# or +yarn create next-app --example with-tailwindcss with-tailwindcss-app ``` ### Download manually diff --git a/examples/with-typescript/README.md b/examples/with-typescript/README.md index bb1a408e..703d7237 100644 --- a/examples/with-typescript/README.md +++ b/examples/with-typescript/README.md @@ -10,9 +10,10 @@ This is a really simple project that show the usage of Next.js with TypeScript. Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-typescript with-typescript-app +```bash +npx create-next-app --example with-typescript with-typescript-app +# or +yarn create next-app --example with-typescript with-typescript-app ``` ### Download manually diff --git a/examples/with-universal-configuration-runtime/readme.md b/examples/with-universal-configuration-runtime/README.md similarity index 80% rename from examples/with-universal-configuration-runtime/readme.md rename to examples/with-universal-configuration-runtime/README.md index e4480665..ab236a50 100644 --- a/examples/with-universal-configuration-runtime/readme.md +++ b/examples/with-universal-configuration-runtime/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-universal-configuration-runtime with-universal-configuration-runtime-app +```bash +npx create-next-app --example with-universal-configuration-runtime with-universal-configuration-runtime-app +# or +yarn create next-app --example with-universal-configuration-runtime with-universal-configuration-runtime-app ``` ### Download manually diff --git a/examples/with-universal-configuration/README.md b/examples/with-universal-configuration/README.md index c4bc7303..9b6071ee 100644 --- a/examples/with-universal-configuration/README.md +++ b/examples/with-universal-configuration/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-universal-configuration with-universal-configuration-app +```bash +npx create-next-app --example with-universal-configuration with-universal-configuration-app +# or +yarn create next-app --example with-universal-configuration with-universal-configuration-app ``` ### Download manually diff --git a/examples/with-url-object-routing/README.md b/examples/with-url-object-routing/README.md index 857c9692..5d9bd7cf 100644 --- a/examples/with-url-object-routing/README.md +++ b/examples/with-url-object-routing/README.md @@ -7,9 +7,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-url-object-routing with-url-object-routing-app +```bash +npx create-next-app --example with-url-object-routing with-url-object-routing-app +# or +yarn create next-app --example with-url-object-routing with-url-object-routing-app ``` ### Download manually diff --git a/examples/with-webpack-bundle-analyzer/README.md b/examples/with-webpack-bundle-analyzer/README.md index dd040ac2..f6578825 100644 --- a/examples/with-webpack-bundle-analyzer/README.md +++ b/examples/with-webpack-bundle-analyzer/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-webpack-bundle-analyzer with-webpack-bundle-analyzer-app +```bash +npx create-next-app --example with-webpack-bundle-analyzer with-webpack-bundle-analyzer-app +# or +yarn create next-app --example with-webpack-bundle-analyzer with-webpack-bundle-analyzer-app ``` ### Download manually diff --git a/examples/with-webpack-bundle-size-analyzer/README.md b/examples/with-webpack-bundle-size-analyzer/README.md index f494b17b..85de0577 100644 --- a/examples/with-webpack-bundle-size-analyzer/README.md +++ b/examples/with-webpack-bundle-size-analyzer/README.md @@ -8,9 +8,10 @@ Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: -``` -npm i -g create-next-app -create-next-app --example with-webpack-bundle-size-analyzer with-webpack-bundle-size-analyzer-app +```bash +npx create-next-app --example with-webpack-bundle-size-analyzer with-webpack-bundle-size-analyzer-app +# or +yarn create next-app --example with-webpack-bundle-size-analyzer with-webpack-bundle-size-analyzer-app ``` ### Download manually diff --git a/examples/with-zones/README.md b/examples/with-zones/README.md index 5349c814..eef1743e 100644 --- a/examples/with-zones/README.md +++ b/examples/with-zones/README.md @@ -10,14 +10,14 @@ We also have a set of rules defined in `rules.json` for the proxy. Now let's start two of our app using: -``` +```bash npm run home npm run blog ``` Then start the proxy: -``` +```bash npm run proxy ``` diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 00000000..1877517a --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,4 @@ +export const PHASE_EXPORT = 'phase-export' +export const PHASE_PRODUCTION_BUILD = 'phase-production-build' +export const PHASE_PRODUCTION_SERVER = 'phase-production-server' +export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server' diff --git a/lib/page-loader.js b/lib/page-loader.js index 99bb47a4..40d13449 100644 --- a/lib/page-loader.js +++ b/lib/page-loader.js @@ -74,9 +74,9 @@ export default class PageLoader { const script = document.createElement('script') const url = `${this.assetPrefix}/_next/${encodeURIComponent(this.buildId)}/page${scriptRoute}` script.src = url - script.type = 'text/javascript' script.onerror = () => { const error = new Error(`Error when loading route: ${route}`) + error.code = 'PAGE_LOAD_ERROR' this.pageRegisterEvents.emit(route, { error }) } diff --git a/lib/router/index.js b/lib/router/index.js index 30168ec4..665cde6d 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -1,5 +1,6 @@ /* global window */ import _Router from './router' +import { execOnce } from '../utils' const SingletonRouter = { router: null, // holds the actual router instance @@ -53,6 +54,18 @@ routerEvents.forEach((event) => { }) }) +const warnAboutRouterOnAppUpdated = execOnce(() => { + console.warn(`Router.onAppUpdated is removed - visit https://err.sh/next.js/no-on-app-updated-hook for more information.`) +}) + +Object.defineProperty(SingletonRouter, 'onAppUpdated', { + get () { return null }, + set () { + warnAboutRouterOnAppUpdated() + return null + } +}) + function throwIfNoRouter () { if (!SingletonRouter.router) { const message = 'No router instance found.\n' + @@ -85,15 +98,6 @@ export const createRouter = function (...args) { // Export the actual Router class, which is usually used inside the server export const Router = _Router -export function _notifyBuildIdMismatch (nextRoute) { - if (SingletonRouter.onAppUpdated) { - SingletonRouter.onAppUpdated(nextRoute) - } else { - console.warn(`An app update detected. Loading the SSR version of "${nextRoute}"`) - window.location.href = nextRoute - } -} - export function _rewriteUrlForNextExport (url) { const [, hash] = url.split('#') url = url.replace(/#.*/, '') diff --git a/lib/router/router.js b/lib/router/router.js index 8eedd6ad..36bd3b26 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -5,7 +5,14 @@ import EventEmitter from '../EventEmitter' import shallowEquals from '../shallow-equals' import PQueue from '../p-queue' import { loadGetInitialProps, getURL, warn, execOnce } from '../utils' -import { _notifyBuildIdMismatch, _rewriteUrlForNextExport } from './' +import { _rewriteUrlForNextExport } from './' + +const historyUnavailableWarning = execOnce(() => { + warn(`Warning: window.history is not available.`) +}) +const historyMethodWarning = execOnce((method) => { + warn(`Warning: window.history.${method} is not available`) +}) export default class Router { constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) { @@ -135,7 +142,7 @@ export default class Router { if (this.onlyAHashChange(as)) { this.changeState(method, url, as) this.scrollToHash(as) - return + return true } const { pathname, query } = parse(url, true) @@ -185,9 +192,17 @@ export default class Router { } changeState (method, url, as, options = {}) { - if (window.frameElement) { - execOnce(warn)(`Warning: You're using Next.js inside an iFrame. Browser history is disabled.`) - } else if (method !== 'pushState' || getURL() !== as) { + if (typeof window.history === 'undefined') { + historyUnavailableWarning() + return + } + + if (typeof window.history[method] === 'undefined') { + historyMethodWarning(method) + return + } + + if (method !== 'pushState' || getURL() !== as) { window.history[method]({ url, as, options }, null, as) } } @@ -212,20 +227,12 @@ export default class Router { this.components[route] = routeInfo } catch (err) { - if (err.buildIdMismatched) { - // Now we need to reload the page or do the action asked by the user - _notifyBuildIdMismatch(as) - // We also need to cancel this current route change. - // We do it like this. - err.cancelled = true - return { error: err } - } + if (err.code === 'PAGE_LOAD_ERROR') { + // If we can't load the page it could be one of following reasons + // 1. Page doesn't exists + // 2. Page does exist in a different zone + // 3. Internal error while loading the page - if (err.statusCode === 404) { - // If there's 404 error for the page, it could be due to two reasons. - // 1. Page is not exists - // 2. Page is exists in a different zone - // We are not sure whether this is actual 404 or exists in a different zone. // So, doing a hard reload is the proper way to deal with this. window.location.href = as diff --git a/lib/runtime-config.js b/lib/runtime-config.js new file mode 100644 index 00000000..dd007258 --- /dev/null +++ b/lib/runtime-config.js @@ -0,0 +1,9 @@ +let runtimeConfig + +export default () => { + return runtimeConfig +} + +export function setConfig (configValue) { + runtimeConfig = configValue +} diff --git a/package.json b/package.json index f639ecb0..b779fd5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "5.0.1-canary.6", + "version": "5.0.1-canary.17", "description": "Minimalistic framework for server-rendered React applications", "main": "./dist/server/next.js", "license": "MIT", @@ -19,7 +19,9 @@ "prefetch.js", "router.js", "asset.js", - "error.js" + "error.js", + "constants.js", + "config.js" ], "bin": { "next": "./dist/bin/next" @@ -50,7 +52,7 @@ "bin/*": "standard" }, "dependencies": { - "@zeit/check-updates": "1.1.0", + "@zeit/check-updates": "1.1.1", "@zeit/source-map-support": "0.6.2", "ansi-html": "0.0.7", "babel-core": "6.26.0", @@ -70,6 +72,7 @@ "cross-spawn": "5.1.0", "del": "3.0.0", "etag": "1.8.1", + "event-source-polyfill": "0.0.12", "find-up": "2.1.0", "fresh": "0.5.2", "friendly-errors-webpack-plugin": "1.6.1", @@ -79,23 +82,19 @@ "htmlescape": "1.1.1", "http-errors": "1.6.2", "http-status": "1.0.1", - "json-loader": "0.5.7", "loader-utils": "1.1.0", - "md5-file": "3.2.3", "minimist": "1.2.0", "mkdirp-then": "1.2.0", - "mv": "2.1.1", "mz": "2.7.0", "path-to-regexp": "2.1.0", - "pkg-up": "2.0.0", "prop-types": "15.6.0", "prop-types-exact": "1.1.1", - "react-hot-loader": "4.0.0-beta.23", + "react-hot-loader": "4.0.0", "recursive-copy": "2.0.6", "resolve": "1.5.0", "send": "0.16.1", "strip-ansi": "3.0.1", - "styled-jsx": "2.2.5", + "styled-jsx": "2.2.6", "touch": "3.1.0", "uglifyjs-webpack-plugin": "1.1.6", "unfetch": "3.0.0", @@ -104,10 +103,9 @@ "walk": "2.3.9", "webpack": "3.10.0", "webpack-dev-middleware": "1.12.0", - "webpack-hot-middleware": "2.21.0", + "webpack-hot-middleware": "2.19.1", "webpack-sources": "1.1.0", - "write-file-webpack-plugin": "4.2.0", - "xss-filters": "1.2.7" + "write-file-webpack-plugin": "4.2.0" }, "devDependencies": { "@taskr/babel": "1.1.0", diff --git a/readme.md b/readme.md index 6c4c1816..f42a28a8 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/zeit/next.js.svg?branch=master)](https://travis-ci.org/zeit/next.js) [![Build status](https://ci.appveyor.com/api/projects/status/gqp5hs71l3ebtx1r/branch/master?svg=true)](https://ci.appveyor.com/project/arunoda/next-js/branch/master) [![Coverage Status](https://coveralls.io/repos/zeit/next.js/badge.svg?branch=master)](https://coveralls.io/r/zeit/next.js?branch=master) -[![Slack Channel](http://zeit-slackin.now.sh/badge.svg)](https://zeit.chat) +[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/next-js) Next.js is a minimalistic framework for server-rendered React applications. @@ -62,7 +62,7 @@ Install it: npm install --save next react react-dom ``` -> Next.js 4 only supports [React 16](https://reactjs.org/blog/2017/09/26/react-v16.0.html).
+> Next.js only supports [React 16](https://reactjs.org/blog/2017/09/26/react-v16.0.html).
> We had to drop React 15 support due to the way React 16 works and how we use it. and add a script to your package.json like this: @@ -178,7 +178,7 @@ To support importing `.css` `.scss` or `.less` files you can use these modules, Create a folder called `static` in your project root directory. From your code you can then reference those files with `/static/` URLs: ```jsx -export default () => +export default () => my image ``` ### Populating `` @@ -405,7 +405,7 @@ export default () =>
Click{' '} - + image
``` @@ -428,6 +428,15 @@ export default ({ href, name }) => ``` +##### Disabling the scroll changes to top on page + +The default behaviour of `` is to scroll to the top of the page. When there is a hash defined it will scroll to the specific id, just like a normal `` tag. To prevent scrolling to the top / hash `scroll={false}` can be added to ``: + +```jsx +Disables scrolling +Changes with scrolling to top +``` + #### Imperatively

@@ -491,7 +500,6 @@ Here's a list of supported events: - `onRouteChangeComplete(url)` - Fires when a route changed completely - `onRouteChangeError(err, url)` - Fires when there's an error when changing routes - `onBeforeHistoryChange(url)` - Fires just before changing the browser's history -- `onAppUpdated(nextRoute)` - Fires when switching pages and there's a new version of the app > Here `url` is the URL shown in the browser. If you call `Router.push(url, as)` (or similar), then the value of `url` will be `as`. @@ -519,17 +527,6 @@ Router.onRouteChangeError = (err, url) => { } ``` -If you change a route while in between a new deployment, we can't navigate the app via client side. We need to do a full browser navigation. We do it automatically for you. - -But you can customize that via `Route.onAppUpdated` event like this: - -```js -Router.onAppUpdated = nextUrl => { - // persist the local state - location.href = nextUrl -} -``` - ##### Shallow Routing

@@ -563,9 +560,9 @@ componentWillReceiveProps(nextProps) { > NOTES: > -> Shallow routing works **only** for same page URL changes. For an example, let's assume we've another page called `about`, and you run this: +> Shallow routing works **only** for same page URL changes. For an example, let's assume we have another page called `about`, and you run this: > ```js -> Router.push('/about?counter=10', '/about?counter=10', { shallow: true }) +> Router.push('/?counter=10', '/about?counter=10', { shallow: true }) > ``` > Since that's a new page, it'll unload the current page, load the new one and call `getInitialProps` even though we asked to do shallow routing. @@ -608,7 +605,7 @@ The above `router` object comes with an API similar to [`next/router`](#imperati ### Prefetching Pages -(This is a production only feature) +⚠️ This is a production only feature ⚠️

Examples @@ -903,6 +900,9 @@ export default () => Pages in `Next.js` skip the definition of the surrounding document's markup. For example, you never include ``, ``, etc. To override that default behavior, you must create a file at `./pages/_document.js`, where you can extend the `Document` class: ```jsx +// _document is only rendered on the server side and not on the client side +// Event handlers like onClick can't be added to this file + // ./pages/_document.js import Document, { Head, Main, NextScript } from 'next/document' import flush from 'styled-jsx/server' @@ -1009,6 +1009,36 @@ module.exports = { } ``` +Or use a function: + +```js +module.exports = (phase, {defaultConfig}){ + // + // https://github.com/zeit/ + return { + /* config options here */ + } +} +``` + +`phase` is the current context in which the configuration is loaded. You can see all phases here: [constants](./lib/constants.js) +Phases can be imported from `next/constants`: + +```js +const {PHASE_DEVELOPMENT_SERVER} = require('next/constants') +module.exports = (phase, {defaultConfig}){ + if(phase === PHASE_DEVELOPMENT_SERVER) { + return { + /* development only config options here */ + } + } + + return { + /* config options for all phases except development here */ + } +} +``` + #### Setting a custom build directory You can specify a name to use for a custom build directory. For example, the following config will create a `build` folder instead of a `.next` folder. If no configuration is specified then next will create a `.next` folder. @@ -1020,6 +1050,17 @@ module.exports = { } ``` +#### Disabling etag generation + +You can disable etag generation for HTML pages depending on your cache strategy. If no configuration is specified then Next will generate etags for every page. + +```js +// next.config.js +module.exports = { + generateEtags: false +} +``` + #### Configuring the onDemandEntries Next exposes some options that give you some control over how the server will dispose or keep in memories pages built: @@ -1125,6 +1166,35 @@ Here's an example `.babelrc` file: } ``` +#### Exposing configuration to the server / client side + +The `config` key allows for exposing runtime configuration in your app. All keys are server only by default. To expose a configuration to both the server and client side you can use the `public` key. + +```js +// next.config.js +module.exports = { + serverRuntimeConfig: { // Will only be available on the server side + mySecret: 'secret' + }, + publicRuntimeConfig: { // Will be available on both server and client + staticFolder: '/static' + } +} +``` + +```js +// pages/index.js +import getConfig from 'next/config' +const {serverRuntimeConfig, publicRuntimeConfig} = getConfig() + +console.log(serverRuntimeConfig.mySecret) // Will only be available on the server side +console.log(publicRuntimeConfig.staticFolder) // Will be available on both server and client + +export default () =>
+ logo +
+``` + ### CDN support with Asset Prefix To set up a CDN, you can set up the `assetPrefix` setting and configure your CDN's origin to resolve to the domain that Next.js is hosted on. diff --git a/server/build/index.js b/server/build/index.js index 4b7577bd..e1f3bb91 100644 --- a/server/build/index.js +++ b/server/build/index.js @@ -3,11 +3,11 @@ import fs from 'mz/fs' import uuid from 'uuid' import webpack from 'webpack' import getConfig from '../config' +import {PHASE_PRODUCTION_BUILD} from '../../lib/constants' import getBaseWebpackConfig from './webpack' -import md5File from 'md5-file/promise' export default async function build (dir, conf = null) { - const config = getConfig(dir, conf) + const config = getConfig(PHASE_PRODUCTION_BUILD, dir, conf) const buildId = uuid.v4() try { @@ -25,7 +25,6 @@ export default async function build (dir, conf = null) { await runCompiler(configs) - await writeBuildStats(dir, config) await writeBuildId(dir, buildId, config) } catch (err) { console.error(`> Failed to build`) @@ -53,20 +52,6 @@ function runCompiler (compiler) { }) } -async function writeBuildStats (dir, config) { - // Here we can't use hashes in webpack chunks. - // That's because the "app.js" is not tied to a chunk. - // It's created by merging a few assets. (commons.js and main.js) - // So, we need to generate the hash ourself. - const assetHashMap = { - 'app.js': { - hash: await md5File(join(dir, config.distDir, 'app.js')) - } - } - const buildStatsPath = join(dir, config.distDir, 'build-stats.json') - await fs.writeFile(buildStatsPath, JSON.stringify(assetHashMap), 'utf8') -} - async function writeBuildId (dir, buildId, config) { const buildIdPath = join(dir, config.distDir, 'BUILD_ID') await fs.writeFile(buildIdPath, buildId, 'utf8') diff --git a/server/build/plugins/combine-assets-plugin.js b/server/build/plugins/combine-assets-plugin.js deleted file mode 100644 index 3e25fb9c..00000000 --- a/server/build/plugins/combine-assets-plugin.js +++ /dev/null @@ -1,33 +0,0 @@ -import { ConcatSource } from 'webpack-sources' - -// This plugin combines a set of assets into a single asset -// This should be only used with text assets, -// otherwise the result is unpredictable. -export default class CombineAssetsPlugin { - constructor ({ input, output }) { - this.input = input - this.output = output - } - - apply (compiler) { - compiler.plugin('compilation', (compilation) => { - // This is triggered after uglify and other optimizers have ran. - compilation.plugin('after-optimize-chunk-assets', (chunks) => { - const concat = new ConcatSource() - - this.input.forEach((name) => { - const asset = compilation.assets[name] - if (!asset) return - - // We add each matched asset from this.input to a new bundle - concat.add(asset) - // The original assets are kept because they show up when analyzing the bundle using webpack-bundle-analyzer - // See https://github.com/zeit/next.js/tree/canary/examples/with-webpack-bundle-analyzer - }) - - // Creates a new asset holding the concatted source - compilation.assets[this.output] = concat - }) - }) - } -} diff --git a/server/build/plugins/unlink-file-plugin.js b/server/build/plugins/unlink-file-plugin.js index bc419f78..b25d4356 100644 --- a/server/build/plugins/unlink-file-plugin.js +++ b/server/build/plugins/unlink-file-plugin.js @@ -1,5 +1,6 @@ import { join } from 'path' import { unlink } from 'mz/fs' +import { IS_BUNDLED_PAGE } from '../../utils' export default class UnlinkFilePlugin { constructor () { @@ -9,7 +10,7 @@ export default class UnlinkFilePlugin { apply (compiler) { compiler.plugin('after-emit', (compilation, callback) => { const removed = Object.keys(this.prevAssets) - .filter((a) => !compilation.assets[a]) + .filter((a) => IS_BUNDLED_PAGE.test(a) && !compilation.assets[a]) this.prevAssets = compilation.assets diff --git a/server/build/webpack.js b/server/build/webpack.js index 073b555c..28338137 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -6,7 +6,6 @@ import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin' import WriteFilePlugin from 'write-file-webpack-plugin' import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' import {getPages} from './webpack/utils' -import CombineAssetsPlugin from './plugins/combine-assets-plugin' import PagesPlugin from './plugins/pages-plugin' import NextJsSsrImportPlugin from './plugins/nextjs-ssr-import' import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin' @@ -137,7 +136,13 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer // This saves chunks with the name given via require.ensure() chunkFilename: '[name]-[chunkhash].js', strictModuleExceptionHandling: true, - devtoolModuleFilenameTemplate: '[absolute-resource-path]' + devtoolModuleFilenameTemplate (info) { + if (dev) { + return '[absolute-resource-path]' + } + + return `${info.absoluteResourcePath.replace(dir, '.').replace(nextDir, './node_modules/next')}` + } }, performance: { hints: false }, resolve: { @@ -239,35 +244,31 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer toplevel: false, typeofs: false, unused: false, - conditionals: false, + conditionals: true, dead_code: true, - evaluate: false + evaluate: true } } }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production') }), - !isServer && new CombineAssetsPlugin({ - input: ['manifest.js', 'react.js', 'commons.js', 'main.js'], - output: 'app.js' - }), !dev && new webpack.optimize.ModuleConcatenationPlugin(), !isServer && new PagesPlugin(), !isServer && new DynamicChunksPlugin(), isServer && new NextJsSsrImportPlugin(), + // In dev mode, we don't move anything to the commons bundle. + // In production we move common modules into the existing main.js bundle !isServer && new webpack.optimize.CommonsChunkPlugin({ - name: `commons`, - filename: `commons.js`, + name: 'main.js', + filename: 'main.js', minChunks (module, count) { - // We need to move react-dom explicitly into common chunks. - // Otherwise, if some other page or module uses it, it might - // included in that bundle too. - if (module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) { + // React and React DOM are used everywhere in Next.js. So they should always be common. Even in development mode, to speed up compilation. + if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) { return true } - if (module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) { + if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 0) { return true } @@ -278,6 +279,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer return false } + // commons // If there are one or two pages, only move modules to common if they are // used in all of the pages. Otherwise, move modules used in at-least // 1/2 of the total pages into commons. @@ -285,28 +287,11 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer return count >= totalPages } return count >= totalPages * 0.5 + // commons end } }), - !isServer && new webpack.optimize.CommonsChunkPlugin({ - name: 'react', - filename: 'react.js', - minChunks (module, count) { - if (dev) { - return false - } - - if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) { - return true - } - - if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 0) { - return true - } - - return false - } - }), - !isServer && new webpack.optimize.CommonsChunkPlugin({ + // We use a manifest file in development to speed up HMR + dev && !isServer && new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', filename: 'manifest.js' }) diff --git a/server/config.js b/server/config.js index 7ed863c0..ecba94de 100644 --- a/server/config.js +++ b/server/config.js @@ -10,17 +10,18 @@ const defaultConfig = { assetPrefix: '', configOrigin: 'default', useFileSystemPublicRoutes: true, + generateEtags: true, pageExtensions: ['jsx', 'js'] // jsx before js because otherwise regex matching will match js first } -export default function getConfig (dir, customConfig) { +export default function getConfig (phase, dir, customConfig) { if (!cache.has(dir)) { - cache.set(dir, loadConfig(dir, customConfig)) + cache.set(dir, loadConfig(phase, dir, customConfig)) } return cache.get(dir) } -function loadConfig (dir, customConfig) { +export function loadConfig (phase, dir, customConfig) { if (customConfig && typeof customConfig === 'object') { customConfig.configOrigin = 'server' return withDefaults(customConfig) @@ -34,6 +35,9 @@ function loadConfig (dir, customConfig) { if (path && path.length) { const userConfigModule = require(path) userConfig = userConfigModule.default || userConfigModule + if (typeof userConfigModule === 'function') { + userConfig = userConfigModule(phase, {defaultConfig}) + } userConfig.configOrigin = 'next.config.js' } diff --git a/server/document.js b/server/document.js index a68b7925..9df49972 100644 --- a/server/document.js +++ b/server/document.js @@ -40,8 +40,8 @@ export class Head extends Component { getChunkPreloadLink (filename) { const { __NEXT_DATA__ } = this.context._documentProps - let { buildStats, assetPrefix, buildId } = __NEXT_DATA__ - const hash = buildStats ? buildStats[filename].hash : buildId + let { assetPrefix, buildId } = __NEXT_DATA__ + const hash = buildId return ( @@ -144,14 +142,13 @@ export class NextScript extends Component { if (dev) { return [ this.getChunkScript('manifest.js'), - this.getChunkScript('commons.js'), this.getChunkScript('main.js') ] } // In the production mode, we have a single asset with all the JS content. // So, we can load the script with async - return [this.getChunkScript('app.js', { async: true })] + return [this.getChunkScript('main.js', { async: true })] } getDynamicChunks () { @@ -163,7 +160,6 @@ export class NextScript extends Component {