diff --git a/src/package-lock.json b/src/package-lock.json index 2a57c13..825530f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -22,7 +22,8 @@ "react-router-dom": "^6.3.0", "react-router-hash-link": "^2.4.3", "redux": "^4.1.2", - "webpack": "^5.72.0" + "webpack": "^5.72.0", + "webpack-manifest-plugin": "^5.0.0" }, "devDependencies": { "@babel/core": "^7.17.9", @@ -31,6 +32,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "babel-loader": "^8.2.4", "clean-webpack-plugin": "^4.0.0", + "compression-webpack-plugin": "^9.2.0", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^3.4.1", "mini-css-extract-plugin": "^2.6.0", @@ -3206,6 +3208,79 @@ "node": ">= 0.8.0" } }, + "node_modules/compression-webpack-plugin": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz", + "integrity": "sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/compression-webpack-plugin/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/compression-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/compression-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/compression-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7346,6 +7421,11 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -8328,6 +8408,41 @@ "strip-ansi": "^6.0.0" } }, + "node_modules/webpack-manifest-plugin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.0.tgz", + "integrity": "sha512-8RQfMAdc5Uw3QbCQ/CBV/AXqOR8mt03B6GJmRbhWopE8GzRfEpn+k0ZuWywxW+5QZsffhmFDY1J6ohqJo+eMuw==", + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "webpack": "^5.47.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webpack-merge": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", @@ -10892,6 +11007,57 @@ } } }, + "compression-webpack-plugin": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz", + "integrity": "sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==", + "dev": true, + "requires": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -13894,6 +14060,11 @@ "websocket-driver": "^0.7.4" } }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -14601,6 +14772,31 @@ "strip-ansi": "^6.0.0" } }, + "webpack-manifest-plugin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.0.tgz", + "integrity": "sha512-8RQfMAdc5Uw3QbCQ/CBV/AXqOR8mt03B6GJmRbhWopE8GzRfEpn+k0ZuWywxW+5QZsffhmFDY1J6ohqJo+eMuw==", + "requires": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, "webpack-merge": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", diff --git a/src/package.json b/src/package.json index d351309..c38f2c6 100644 --- a/src/package.json +++ b/src/package.json @@ -36,7 +36,8 @@ "react-router-dom": "^6.3.0", "react-router-hash-link": "^2.4.3", "redux": "^4.1.2", - "webpack": "^5.72.0" + "webpack": "^5.72.0", + "webpack-manifest-plugin": "^5.0.0" }, "devDependencies": { "@babel/core": "^7.17.9", @@ -45,6 +46,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "babel-loader": "^8.2.4", "clean-webpack-plugin": "^4.0.0", + "compression-webpack-plugin": "^9.2.0", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^3.4.1", "mini-css-extract-plugin": "^2.6.0", diff --git a/src/server/getHashManifest.js b/src/server/getHashManifest.js new file mode 100644 index 0000000..9ae2c7e --- /dev/null +++ b/src/server/getHashManifest.js @@ -0,0 +1,11 @@ +import fs from 'fs'; + +const getHashManifest = () => { + try { + return JSON.parse(fs.readFileSync(`${__dirname}/../build/assets/manifest-hash.json`)) + }catch(err){ + console.error(err) + } +} + +export default getHashManifest \ No newline at end of file diff --git a/src/server/server.js b/src/server/server.js index d4969ae..2ba295c 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -19,6 +19,8 @@ import { createStore } from 'redux'; //, applyMiddleware import { Provider } from 'react-redux'; import reducer from '../frontend/reducers'; import initialState from '../frontend/reducers/initialState'; +//Get Hashes +import getHashManifest from './getHashManifest'; //App import App from '../frontend/components/App'; @@ -41,13 +43,29 @@ if(env === 'development'){ })); }else{ app + .use((req, res, next) => { + if(!req.hashManifest) req.hashManifest = getHashManifest(); + next(); + }) .use(express.static(`${__dirname}/../build`)) .use(helmet()) .use(helmet.permittedCrossDomainPolicies()) + .use(helmet({ + contentSecurityPolicy: { + directives: { + ...helmet.contentSecurityPolicy.getDefaultDirectives(), + "script-src": ["'self'", "'unsafe-inline'"],//"example.com" + }, + }, + })) .disable('x-powered-by'); } -const setResponse = (html, preloadedState) => { +const setResponse = (html, preloadedState, manifest) => { + const mainStyles = manifest ? manifest['main.css'] : 'assets/app.css'; + const mainBuild = manifest ? manifest['main.js'] : 'assets/app.js'; + const vendorBuild = manifest ? manifest['vendors.js'] : 'assets/vendor.js'; + return(` @@ -55,7 +73,7 @@ const setResponse = (html, preloadedState) => { - + App @@ -63,7 +81,8 @@ const setResponse = (html, preloadedState) => { - + + `) @@ -79,7 +98,7 @@ const renderApp = (req, res) => { ) - res.send(setResponse(html, preloadedState)); + res.send(setResponse(html, preloadedState, req.hashManifest)); }; app.get('*', renderApp) diff --git a/src/webpack.config.dev.js b/src/webpack.config.dev.js index 5ebd41c..ed93fbf 100644 --- a/src/webpack.config.dev.js +++ b/src/webpack.config.dev.js @@ -48,4 +48,23 @@ module.exports = { 'process.env': JSON.stringify(dotenv.parsed), }), ], + optimization: { + splitChunks: { + chunks: 'async', + cacheGroups: { + vendors: { + name: 'vendors', + chunks: 'all', + reuseExistingChunk: true, + priority: 1, + filename: 'assets/vendor.js', + enforce: true, + test (module, chunks){ + const name = module.nameForCondition && module.nameForCondition(); + return chunks.name !== 'vendors' && /[\\/]node_modules[\\/]/.test(name); + }, + }, + }, + }, + }, } diff --git a/src/webpack.config.js b/src/webpack.config.js index 84df14c..9d89a08 100644 --- a/src/webpack.config.js +++ b/src/webpack.config.js @@ -1,16 +1,18 @@ const path = require('path'); const dotenv = require('dotenv').config(); const webpack = require('webpack'); +const CompressionWebpackPlugin = require('compression-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); +const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: './frontend/index.js', output: { path: path.resolve(__dirname, 'build'), - filename: 'assets/app.js', + filename: 'assets/app-[fullhash].js', publicPath: '/', }, resolve: { @@ -41,8 +43,15 @@ module.exports = { ], }, plugins: [ + new CompressionWebpackPlugin({ + test: /\.(js|css)$/, + filename: '[path][base].gz', + }), new MiniCssExtractPlugin({ - filename: 'assets/app.css', + filename: 'assets/app-[fullhash].css', + }), + new WebpackManifestPlugin({ + fileName: 'assets/manifest-hash.json', }), new CleanWebpackPlugin(), new webpack.DefinePlugin({ @@ -55,5 +64,22 @@ module.exports = { new CssMinimizerPlugin(), new TerserPlugin(), ], + splitChunks: { + chunks: 'async', + cacheGroups: { + vendors: { + name: 'vendors', + chunks: 'all', + reuseExistingChunk: true, + priority: 1, + filename: 'assets/vendor-[fullhash].js', + enforce: true, + test (module, chunks){ + const name = module.nameForCondition && module.nameForCondition(); + return chunks.name !== 'vendors' && /[\\/]node_modules[\\/]/.test(name); + }, + }, + }, + }, }, }