UNPKG

react-static

Version:

A progressive static site generator for React

275 lines (236 loc) 8.11 kB
/* eslint-disable import/no-dynamic-require, react/no-danger, import/no-mutable-exports */ import webpack from 'webpack' import chalk from 'chalk' import io from 'socket.io' import WebpackDevServer from 'webpack-dev-server' // import makeWebpackConfig from './makeWebpackConfig' import getRouteData from '../getRouteData' import plugins from '../plugins' import { findAvailablePort, time, timeEnd } from '../../utils' import fetchSiteData from '../fetchSiteData' let devServer let latestState let buildDevRoutes = () => {} export const reloadClientData = () => { if (reloadClientData.current) { reloadClientData.current() } } // Starts the development server export default async function runDevServer(state) { // TODO check config.devServer for changes and notify user // if the server needs to be restarted for changes to take // effect. // If the server is already running, trigger a refresh to the client if (devServer) { await buildDevRoutes(state) await reloadClientData() } else { state = await runExpressServer(state) } return state } async function runExpressServer(state) { // Default to localhost:3000, or use a custom combo if defined in static.config.js // or environment variables const intendedPort = Number(state.config.devServer.port) const port = await findAvailablePort(intendedPort) let defaultMessagePort = 4000 if (process.env.REACT_STATIC_MESSAGE_SOCKET_PORT) { defaultMessagePort = process.env.REACT_STATIC_MESSAGE_SOCKET_PORT } // Find an available port for messages, as long as it's not the devServer port const messagePort = await findAvailablePort(defaultMessagePort, [port]) const messageHost = process.env.REACT_STATIC_MESSAGE_SOCKET_HOST || 'http://localhost' if (intendedPort !== port) { console.log( chalk.red( `Warning! Port ${intendedPort} is not available. Using port ${chalk.green( port )} instead!` ) ) } state = { ...state, config: { ...state.config, devServer: { ...state.config.devServer, port, }, }, } const devConfig = makeWebpackConfig(state) const devCompiler = webpack(devConfig) const devServerConfig = { contentBase: [state.config.paths.PUBLIC, state.config.paths.DIST], publicPath: '/', historyApiFallback: true, compress: false, clientLogLevel: 'warning', overlay: true, stats: 'errors-only', noInfo: true, ...state.config.devServer, hotOnly: true, proxy: { '/socket.io': { target: `${messageHost}:${messagePort}`, ws: true, }, ...(state.config.devServer ? state.config.devServer.proxy || {} : {}), }, watchOptions: { ...(state.config.devServer ? state.config.devServer.watchOptions || {} : {}), ignored: [ /node_modules/, ...((state.config.devServer.watchOptions || {}).ignored || []), ], }, before: app => { // Since routes may change during dev, this function can rebuild all of the config // routes. It also references the original config when possible, to make sure it // uses any up to date getData callback generated from new or replacement routes. buildDevRoutes = async newState => { latestState = await fetchSiteData(newState) app.get('/__react-static__/siteData', async (req, res, next) => { try { res.send(latestState.siteData) } catch (err) { res.status(500) res.send(err) next(err) } }) // Serve each routes data latestState.routes.forEach(({ path: routePath }) => { app.get( `/__react-static__/routeInfo/${encodeURI( routePath === '/' ? '' : routePath )}`, async (req, res, next) => { // Make sure we have the most up to date route from the config, not // an out of date object. let route = latestState.routes.find(d => d.path === routePath) try { if (!route) { const err = new Error( `Route could not be found for: ${routePath} If you removed this route, disregard this error. If this is a dynamic route, consider adding it to the prefetchExcludes list: addPrefetchExcludes(['${routePath}']) ` ) delete err.stack throw err } route = await getRouteData(route, latestState) route = await plugins.routeInfo(route, state) // Don't use any hashProp, just pass all the data in dev res.json(route) } catch (err) { res.status(404) next(err) } } ) }) return new Promise(resolve => setTimeout(resolve, 1)) } buildDevRoutes(state) if (state.config.devServer && state.config.devServer.before) { state.config.devServer.before(app) } return app }, } let first = true const startedAt = Date.now() let skipLog = false console.log('Bundling Application...') time(chalk.green('[\u2713] Application Bundled')) devCompiler.hooks.invalid.tap( { name: 'React-Static', }, (file, changed) => { // If a file is changed within the first two seconds of // the server starting, we don't bark about it. Less // noise is better! skipLog = changed - startedAt < 2000 if (!skipLog) { console.log('File changed:', file.replace(state.config.paths.ROOT, '')) console.log('Updating bundle...') time(chalk.green('[\u2713] Bundle Updated')) } } ) devCompiler.hooks.done.tap( { name: 'React-Static', }, stats => { const messages = stats.toJson({}, true) const isSuccessful = !messages.errors.length const hasWarnings = messages.warnings.length if (isSuccessful && !skipLog) { if (first) { // Print out any dev compiler warnings if (hasWarnings) { console.log( chalk.yellowBright( `\n[\u0021] There were ${messages.warnings.length} warnings during compilation\n` ) ) messages.warnings.forEach((message, index) => { console.warn(`[warning ${index}]: ${message}\n`) }) } timeEnd(chalk.green('[\u2713] Application Bundled')) const protocol = state.config.devServer.https ? 'https' : 'http' console.log( `${chalk.green('[\u2713] App serving at')} ${chalk.blue( `${protocol}://${state.config.devServer.host}:${state.config.devServer.port}` )}` ) } else { timeEnd(chalk.green('[\u2713] Bundle Updated')) } } else if (!skipLog) { console.log(chalk.redBright('[\u274C] Application bundling failed')) console.error(chalk.redBright(messages.errors.join('\n'))) console.warn(chalk.yellowBright(messages.warnings.join('\n'))) } first = false } ) // Start the webpack dev server devServer = new WebpackDevServer(devCompiler, devServerConfig) // Start the messages socket const socket = io() reloadClientData.current = async () => { latestState = await fetchSiteData(latestState) socket.emit('message', { type: 'reloadClientData' }) } await new Promise((resolve, reject) => { devServer.listen(port, null, err => { if (err) { console.error(`Listening on ${port} failed: ${err}`) return reject(err) } resolve() }) }) // Make sure we start listening on the message port after the dev server. // We do this mostly to appease codesandbox.io, since they autobind to the first // port that opens up for their preview window. socket.listen(messagePort) console.log('Running plugins...') state = await plugins.afterDevServerStart(state) return state }