diff --git a/scripts/start.js b/scripts/start.js index 1e83592..bdad71e 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -65,21 +65,21 @@ async function start() { // Configure client-side hot module replacement const clientConfig = webpackConfig.find((config) => config.name === "client"); - clientConfig.entry.client = ["./scripts/lib/webpackHotDevClient"].concat( - clientConfig.entry.client - ); - clientConfig.output.filename = clientConfig.output.filename.replace( - "contenthash", - "fullhash" - ); - clientConfig.output.chunkFilename = clientConfig.output.chunkFilename.replace( - "chunkhash", - "fullhash" - ); - clientConfig.module.rules = clientConfig.module.rules.filter( - (x) => x.loader !== "null-loader" - ); - clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); + if (clientConfig) { + clientConfig.entry.client = ["./scripts/lib/webpackHotDevClient"].concat( + clientConfig.entry.client + ); + clientConfig.output.filename = clientConfig.output.filename.replace( + "contenthash", + "fullhash" + ); + clientConfig.output.chunkFilename = + clientConfig.output.chunkFilename.replace("chunkhash", "fullhash"); + clientConfig.module.rules = clientConfig.module.rules.filter( + (x) => x.loader !== "null-loader" + ); + clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); + } // Configure server-side hot module replacement const serverConfig = webpackConfig.find((config) => config.name === "server"); @@ -102,11 +102,19 @@ async function start() { const serverCompiler = multiCompiler.compilers.find( (compiler) => compiler.name === "server" ); - const clientPromise = createCompilationPromise( - "client", - clientCompiler, - clientConfig - ); + + let clientPromise; + + if (clientConfig) { + clientPromise = createCompilationPromise( + "client", + clientCompiler, + clientConfig + ); + } else { + clientPromise = Promise.resolve(); + } + const serverPromise = createCompilationPromise( "server", serverCompiler, @@ -114,15 +122,17 @@ async function start() { ); // https://github.com/webpack/webpack-dev-middleware - server.use( - webpackDevMiddleware(clientCompiler, { - publicPath: clientConfig.output.publicPath, - writeToDisk: (filePath) => /stats.json$/.test(filePath), - }) - ); + if (clientConfig) { + server.use( + webpackDevMiddleware(clientCompiler, { + publicPath: clientConfig.output.publicPath, + writeToDisk: (filePath) => /stats.json$/.test(filePath), + }) + ); + server.use(webpackHotMiddleware(clientCompiler, { log: false })); + } // https://github.com/glenjamin/webpack-hot-middleware - server.use(webpackHotMiddleware(clientCompiler, { log: false })); let appPromise; let appPromiseResolve; diff --git a/scripts/webpack.config.js b/scripts/webpack.config.js index 5f79a7d..16d4159 100644 --- a/scripts/webpack.config.js +++ b/scripts/webpack.config.js @@ -17,6 +17,9 @@ const pkg = require("../package.json"); const isDevelopment = !process.argv.includes("--release"); +// set to true to enable CSR +const IS_SPA = false; + const isAnalyze = process.argv.includes("--analyze") || process.argv.includes("--analyse"); @@ -88,7 +91,8 @@ const configureStyleLoaders = () => ({ test: /\.(sa|sc|c)ss$/, rules: [ { - loader: isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader, + loader: + isDevelopment && IS_SPA ? "style-loader" : MiniCssExtractPlugin.loader, }, { exclude: SRC_DIR, @@ -304,8 +308,9 @@ const baseConfig = { plugins: [ new webpack.EnvironmentPlugin({ IS_DEVELOPMENT: isDevelopment, - NAME: JSON.stringify(pkg.name), - DESCRIPTION: JSON.stringify(pkg.description), + IS_SPA, + NAME: pkg.name, + DESCRIPTION: pkg.description, VERSION: JSON.stringify(pkg.version), }), new webpack.DefinePlugin({ @@ -474,7 +479,7 @@ const serverConfig = { { test: /\.(sa|sc|c)ss$/, rules: [ - ...(isDevelopment + ...(isDevelopment && IS_SPA ? [] : [ { @@ -490,7 +495,7 @@ const serverConfig = { localIdentName: isDevelopment ? "[name]-[local]-[hash:base64:5]" : "[hash:base64:5]", - exportOnlyLocals: isDevelopment, + exportOnlyLocals: IS_SPA && isDevelopment, }, importLoaders: 1, }, @@ -592,8 +597,8 @@ const serverConfig = { { type: "asset/resource", generator: { - filename: staticAssetName, - emit: false, + filename: IS_SPA ? staticAssetName : `public/${staticAssetName}`, + emit: !IS_SPA, }, }, ], @@ -603,7 +608,7 @@ const serverConfig = { type: "asset/resource", generator: { filename: "fonts/[name][ext]", - emit: false, + emit: !IS_SPA, }, }, ], @@ -614,7 +619,7 @@ const serverConfig = { new LoadablePlugin({ filename: "server-stats.json", }), - ...(isDevelopment + ...(isDevelopment && IS_SPA ? [] : [ new MiniCssExtractPlugin({ @@ -632,4 +637,7 @@ const serverConfig = { }, }; -module.exports = [clientConfig, serverConfig, legacyClientConfig]; +module.exports = [ + serverConfig, + ...(IS_SPA ? [clientConfig, legacyClientConfig] : []), +]; diff --git a/src/client.js b/src/client.js index fbd9b10..9892add 100644 --- a/src/client.js +++ b/src/client.js @@ -8,24 +8,26 @@ import Main from "./js/components/Main"; import initialReducers from "./js/reducers"; import configureDynamicStore from "./js/store"; -// grab the state from a global variable injected into the server-generated HTML -const store = configureDynamicStore( - // eslint-disable-next-line no-underscore-dangle - window.__PRELOADED_STATE__, - false, - initialReducers, - process.env.NODE_ENV !== "production" -); - -loadableReady(() => { - const routes = selectRoutes(store.getState()); - ReactDOM.hydrate( - -
- , - document.getElementById("root") +if (process.env.IS_SPA) { + // grab the state from a global variable injected into the server-generated HTML + const store = configureDynamicStore( + // eslint-disable-next-line no-underscore-dangle + window.__PRELOADED_STATE__, + false, + initialReducers, + process.env.NODE_ENV !== "production" ); -}); + + loadableReady(() => { + const routes = selectRoutes(store.getState()); + ReactDOM.hydrate( + +
+ , + document.getElementById("root") + ); + }); +} if (process.env.NODE_ENV === "production") { if ("serviceWorker" in navigator) { diff --git a/src/render.js b/src/render.js index 8f29489..784695c 100644 --- a/src/render.js +++ b/src/render.js @@ -3,7 +3,7 @@ import path from "path"; import { ChunkExtractor, ChunkExtractorManager } from "@loadable/server"; import * as React from "react"; -import { renderToString } from "react-dom/server"; +import { renderToStaticMarkup, renderToString } from "react-dom/server"; import Helmet from "react-helmet"; import { matchPath } from "react-router"; import { StaticRouter } from "react-router-dom/server"; @@ -60,38 +60,117 @@ const renderRoutesData = async ({ await Promise.all(promises); }; -const handleRender = async (req, res, next) => { - try { - const serverChunkExtractor = new ChunkExtractor({ - statsFile: path.resolve(__dirname, "./server-stats.json"), - entrypoints: ["server"], - }); - const { Main } = serverChunkExtractor.requireEntrypoint(); +const renderAsMPA = async ({ url, store, routes }) => { + const serverChunkExtractor = new ChunkExtractor({ + statsFile: path.resolve(__dirname, "./server-stats.json"), + entrypoints: ["server"], + }); + const { Main } = serverChunkExtractor.requireEntrypoint(); + + const html = renderToStaticMarkup( + + +
+ + + ); + + const inlineCss = await serverChunkExtractor.getCssString(); + + const helmet = Helmet.renderStatic(); + + // Send the rendered page back to the client using the server's view engine + return { + htmlattributes: helmet.htmlAttributes.toString() || "", + bodyattributes: helmet.bodyAttributes.toString() || "", + title: `${helmet.title}`, + head: `${helmet.meta} ${helmet.link}`, + html, + inlineCss, + }; +}; + +const renderAsSPA = async ({ url, store, routes }) => { + const serverChunkExtractor = new ChunkExtractor({ + statsFile: path.resolve(__dirname, "./server-stats.json"), + entrypoints: ["server"], + }); + const { Main } = serverChunkExtractor.requireEntrypoint(); - const modernChunkExtractor = new ChunkExtractor({ - statsFile: path.resolve(__dirname, "./public/client-stats.json"), + const modernChunkExtractor = new ChunkExtractor({ + statsFile: path.resolve(__dirname, "./public/client-stats.json"), + entrypoints: ["client"], + }); + + let legacyChunkExtractor; + if (!module.hot) { + legacyChunkExtractor = new ChunkExtractor({ + namespace: "legacy", + statsFile: path.resolve(__dirname, "./public/legacy-stats.json"), entrypoints: ["client"], }); + } - let legacyChunkExtractor; - if (!module.hot) { - legacyChunkExtractor = new ChunkExtractor({ - namespace: "legacy", - statsFile: path.resolve(__dirname, "./public/legacy-stats.json"), - entrypoints: ["client"], - }); - } + // override the default addChunk method to add the chunk to legacy and modern extractor + const clientChunkExtractor = { + addChunk(chunk) { + modernChunkExtractor.addChunk(chunk); + if (legacyChunkExtractor) { + legacyChunkExtractor.addChunk(chunk); + } + }, + }; + + const html = renderToString( + + +
+ + + ); + + const scriptElements = modernChunkExtractor.getScriptElements(); + + let legacyScriptElements; + if (legacyChunkExtractor) { + legacyScriptElements = legacyChunkExtractor.getScriptElements(); + } - // override the default addChunk method to add the chunk to legacy and modern extractor - const clientChunkExtractor = { - addChunk(chunk) { - modernChunkExtractor.addChunk(chunk); - if (legacyChunkExtractor) { - legacyChunkExtractor.addChunk(chunk); - } - }, - }; + const scripts = renderToString( + + ); + + let inlineCss = ""; + let css = ""; + if (!module.hot) { + inlineCss = await modernChunkExtractor.getCssString(); + } else { + css = modernChunkExtractor.getStyleTags(); + } + + const helmet = Helmet.renderStatic(); + + // Grab the state from our Redux store + const preloadedState = store.getState(); + + // Send the rendered page back to the client using the server's view engine + return { + htmlattributes: helmet.htmlAttributes.toString() || "", + bodyattributes: helmet.bodyAttributes.toString() || "", + title: `${helmet.title}`, + head: `${helmet.meta} ${helmet.link}`, + html, + inlineCss, + css, + scripts, + preloadedState: JSON.stringify(preloadedState), + IS_SPA: true, + }; +}; + +const handleRender = async (req, res, next) => { + try { // create a new Redux store instance and clear all dynamic reducers const store = configureDynamicStore( {}, @@ -111,50 +190,19 @@ const handleRender = async (req, res, next) => { store, }); - const html = renderToString( - - -
- - - ); - - const scriptElements = modernChunkExtractor.getScriptElements(); - - let legacyScriptElements; - if (legacyChunkExtractor) { - legacyScriptElements = legacyChunkExtractor.getScriptElements(); - } - - const scripts = renderToString( - - ); - - let inlineCss = ""; - let css = ""; - - if (!module.hot) { - inlineCss = await modernChunkExtractor.getCssString(); + if (process.env.IS_SPA) { + // Send the rendered page back to the client using the server's view engine + res.render("index", await renderAsSPA({ url: req.url, store, routes })); } else { - css = modernChunkExtractor.getStyleTags(); + res.render( + "index", + await renderAsMPA({ + url: req.url, + store, + routes, + }) + ); } - const helmet = Helmet.renderStatic(); - - // Grab the state from our Redux store - const preloadedState = store.getState(); - - // Send the rendered page back to the client using the server's view engine - res.render("index", { - htmlattributes: helmet.htmlAttributes.toString() || "", - bodyattributes: helmet.bodyAttributes.toString() || "", - title: `${helmet.title}`, - head: `${helmet.meta} ${helmet.link}`, - html, - inlineCss, - css, - scripts, - preloadedState: JSON.stringify(preloadedState), - }); } catch (err) { next(err); } diff --git a/src/server.js b/src/server.js index f7d2c7c..265befa 100644 --- a/src/server.js +++ b/src/server.js @@ -48,6 +48,22 @@ app.use( }) ); +// in case of server-generated fonts +app.use( + "/fonts", + express.static(path.join(__dirname, "/fonts"), { + maxAge: 31536000000, // in milliseconds + }) +); + +// in case of server-generated images +app.use( + "/public", + express.static(path.join(__dirname, "/public"), { + maxAge: 31536000000, // in milliseconds + }) +); + app.use("/", (req, res, next) => { res.set( "Cache-Control", diff --git a/src/templates/index.hbs b/src/templates/index.hbs index c6844dd..4ba1946 100644 --- a/src/templates/index.hbs +++ b/src/templates/index.hbs @@ -24,8 +24,10 @@
{{{ html }}}
{{{ scripts }}} + {{#if IS_SPA}} + {{/if}}