diff --git a/lib/api/getRibbitStore.js b/lib/api/getRibbitStore.js new file mode 100644 index 0000000..bfd3a69 --- /dev/null +++ b/lib/api/getRibbitStore.js @@ -0,0 +1,20 @@ +import { createStore, applyMiddleware } from 'redux'; + +const getRibbitStore = (combinedReducers, ...middleware) => { + let store; + // check if rendering is happening on the client or the server. + + if (typeof window !== 'undefined') { + // get state from window + const preloadedState = window.RIBBIT_PRELOADED_STATE; + delete window.RIBBIT_PRELOADED_STATE; + + // pass state into redux store + store = createStore(combinedReducers, preloadedState, applyMiddleware(...middleware)); + } else { + store = createStore(combinedReducers, applyMiddleware(...middleware)); + } + return store; +}; + +export default getRibbitStore; diff --git a/lib/api/preloadActions.js b/lib/api/preloadActions.js new file mode 100644 index 0000000..24a749a --- /dev/null +++ b/lib/api/preloadActions.js @@ -0,0 +1,13 @@ +function preloadPush() { + fetch('http://localhost:5000/preload-push') + .then(() => console.log('preload push')) + .catch(err => console.log(err)); +} + +function preloadPop() { + fetch('http://localhost:5000/preload-pop') + .then(() => console.log('preload pop')) + .catch(err => console.log(err)); +} + +module.exports = { preloadPush, preloadPop }; diff --git a/lib/api/ribbitPreload.js b/lib/api/ribbitPreload.js new file mode 100644 index 0000000..d043190 --- /dev/null +++ b/lib/api/ribbitPreload.js @@ -0,0 +1,23 @@ +const { preloadPush } = require('./preloadActions'); + +const ribbitRequestFactory = () => { + const fetched = {}; + + function ribbitPreload(cb, component) { + // only perform fetch if running on the server + if (typeof window === 'undefined') { + // dont let component continue to send requests after initial preload + if (!fetched[component]) { + fetched[component] = true; + cb(); + preloadPush(); + } + } + } + + return ribbitPreload; +}; + +const ribbitPreload = ribbitRequestFactory(); + +export default ribbitPreload; diff --git a/lib/api/ribbitStore.js b/lib/api/ribbitStore.js new file mode 100644 index 0000000..fabbb2b --- /dev/null +++ b/lib/api/ribbitStore.js @@ -0,0 +1,15 @@ +const { preloadPop } = require('./preloadActions'); +// re-render page with updated state on the server +const ribbitStore = routes => { + routes.forEach(route => { + fetch(`http://localhost:5000${route}`) + .then(response => response.json()) + .then(() => { + console.log('Re-render success!'); + preloadPop(); + }) + .catch(err => console.log(err)); + }); +}; + +export default ribbitStore; diff --git a/lib/commands/build.js b/lib/commands/build.js index a1d54a7..11f6092 100644 --- a/lib/commands/build.js +++ b/lib/commands/build.js @@ -7,7 +7,13 @@ function build() { { cwd: __dirname }, - () => {} + () => { + const TEMP_STATIC_FILE_FOLDER = `${__dirname}/../../dist`; + rimraf(TEMP_STATIC_FILE_FOLDER, error => { + if (error) process.stdout(error); + process.kill(process.pid, 'SIGINT'); + }); + } ); child.stdout.on('data', data => { @@ -18,14 +24,6 @@ function build() { process.stdout.write(data); }); - child.stdout.on('finish', () => { - const TEMP_STATIC_FILE_FOLDER = `${__dirname}/../../dist`; - rimraf(TEMP_STATIC_FILE_FOLDER, error => { - if (error) process.stdout(error); - process.kill(process.pid, 'SIGINT'); - }); - }); - child.stderr.on('exit', data => { console.log('Error starting server: ', data); }); diff --git a/package.json b/package.json index 2ab5627..e51ca29 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@babel/preset-env": "^7.2.3", "@babel/preset-react": "^7.0.0", "babel-plugin-import-inspector": "^2.0.0", + "body-parser": "^1.18.3", "chalk": "^2.4.2", "commander": "^2.19.0", "css-loader": "^2.1.0", @@ -44,7 +45,9 @@ "isomorphic-fetch": "^2.2.1", "react": "^16.7.0", "react-dom": "^16.7.0", + "react-redux": "^6.0.0", "react-router-dom": "^4.3.1", + "redux": "^4.0.1", "rimraf": "^2.6.3", "webpack": "^4.28.4", "webpack-cli": "^3.2.1" diff --git a/server/controllers/htmlTemplate.js b/server/controllers/htmlTemplate.js index 3d69320..9e0921e 100644 --- a/server/controllers/htmlTemplate.js +++ b/server/controllers/htmlTemplate.js @@ -5,7 +5,7 @@ const { StringDecoder } = require('string_decoder'); const decoder = new StringDecoder('utf8'); function htmlTemplate(req, res, next) { - const { appParentDirectory, componentRoute, jsx } = res.locals; + const { appParentDirectory, componentRoute, jsx, preLoadedState } = res.locals; const ribbitConfig = require(path.join(appParentDirectory, '/ribbit.config.js')); const reactStream = renderToNodeStream(jsx); @@ -14,6 +14,8 @@ function htmlTemplate(req, res, next) { reactStream.on('data', data => { reactDom += decoder.write(data); }); + + // inject state into HTML template reactStream.on('end', () => { const html = ` @@ -24,8 +26,13 @@ function htmlTemplate(req, res, next) { -
${reactDom}
- +
${reactDom}
+ + `; diff --git a/server/helpers/buildRoutesCliCommand.js b/server/helpers/buildRoutesCliCommand.js index 1fc4d32..ad0ce6f 100644 --- a/server/helpers/buildRoutesCliCommand.js +++ b/server/helpers/buildRoutesCliCommand.js @@ -2,12 +2,6 @@ function buildRoutesCliCommand(command, routes, appParentDirectory, appRoot) { let homeComponent; const appRootFile = appRoot ? `/${appRoot}` : ''; routes.forEach(route => { - // if (route.assetName) { - // const routeComponentPair = `${ - // route.assetName - // }=${appParentDirectory}${appRootFile}${route.component.slice(1)} `; - // command += routeComponentPair; - // } else { if (route.route === '/') { const index = route.component.lastIndexOf('/'); homeComponent = route.component.substring(index, route.component.length - 3); @@ -21,8 +15,8 @@ function buildRoutesCliCommand(command, routes, appParentDirectory, appRoot) { }=${appParentDirectory}${appRootFile}${route.component.slice(1)} `; command += routeComponentPair; } - // } }); + return { command, homeComponent }; } diff --git a/server/server.js b/server/server.js index 148c67d..1598e6c 100644 --- a/server/server.js +++ b/server/server.js @@ -1,16 +1,17 @@ // react imports import { StaticRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; const React = require('react'); - const express = require('express'); const path = require('path'); const fs = require('fs'); const { exec } = require('child_process'); require('isomorphic-fetch'); +const bodyParser = require('body-parser'); +// const hasPreloadRan = require('../lib/api/hasPreloadRan.js'); const app = express(); - // User app directory is received from arguments const appParentDirectory = process.argv[2]; const ribbitConfig = require(path.join(appParentDirectory, '/ribbit.config.js')); @@ -18,9 +19,7 @@ const ribbitRoutes = require(path.join( appParentDirectory, `${ribbitConfig.appRoot}/ribbit.routes.json` )); - const appFile = `${appParentDirectory}/${ribbitConfig.appRoot}/${ribbitConfig.app}`; - // Helper functions imports const buildRoutesCliCommand = require('./helpers/buildRoutesCliCommand'); const sendFetches = require('./helpers/sendFetches'); @@ -53,23 +52,50 @@ const routesCliCommand = buildRoutesCliCommand( ribbitConfig.appRoot ); +const preloadArray = []; + +app.use(bodyParser.json()); + +app.get(['/preload-push', '/preload-pop'], (req, res) => { + const arrayCommand = req.url.substring(req.url.lastIndexOf('-') + 1); + if (arrayCommand === 'push') { + preloadArray.push(1); + res.end(); + } else if (arrayCommand === 'pop') { + preloadArray.pop(); + res.end(); + } +}); + app.get( routeArray, (req, res, next) => { const CompiledApp = require(`../dist/App.js`).default; - const context = {}; + + // user exports their store from wherever they created it + // user must give the path to their store file + const { store } = require(`../dist/App.js`); + + const context = { data: {}, head: [], req }; let componentRoute = req.url; + // pull state out of store + const preLoadedState = store.getState(); + const jsx = ( - - - + // wrap static router in redux Provider in order to user redux state + + + + + ); if (componentRoute === '/') componentRoute = routesCliCommand.homeComponent; res.locals = { ...res.locals, + preLoadedState, appParentDirectory, componentRoute, jsx, @@ -80,18 +106,13 @@ app.get( htmlTemplate, writeFile ); -app.use(express.static(ribbitConfig.bundleRoot)); -// Create a new child process, that executes the passed in 'cli command' -// Child starts webpack and copies components over to the Ribbit directory +app.use(express.static(ribbitConfig.bundleRoot)); const webpackChild = exec(`${routesCliCommand.command}`, () => { - // start server in callback (after webpack finishes running) - app.listen(4000, () => { - console.log('Listening on port 4000'); - // Send fetch request to all routes - const fetchArray = sendFetches(ribbitRoutes, 4000); - + app.listen(5000, () => { + console.log('Listening on port 5000'); + const fetchArray = sendFetches(ribbitRoutes, 5000); Promise.all(fetchArray) .then(arrayOfRoutes => { const ribbitManifest = arrayOfRoutes.reduce((acc, curr) => { @@ -105,10 +126,19 @@ const webpackChild = exec(`${routesCliCommand.command}`, () => { `${appParentDirectory}/ribbit.manifest.json`, JSON.stringify(ribbitManifest) ); - unlinkUserDeps(ribbitConfig, appParentDirectory); - process.kill(process.pid, 'SIGINT'); + + function killServer() { + if (preloadArray.length === 0) { + console.log('KILL THE SERVER GENTS!!!!'); + process.kill(process.pid, 'SIGINT'); + } else { + console.log('NOT READY TO KILL SERVER'); + setTimeout(killServer, 500); + } + } + killServer(); }) - .catch(); + .catch(err => console.log(err)); }); });