{"id":19777337,"url":"https://github.com/catamphetamine/universal-webpack","last_synced_at":"2025-04-12T18:44:55.703Z","repository":{"id":57129781,"uuid":"59322692","full_name":"catamphetamine/universal-webpack","owner":"catamphetamine","description":"Isomorphic Webpack: both on client and server","archived":false,"fork":false,"pushed_at":"2023-01-09T13:24:33.000Z","size":575,"stargazers_count":683,"open_issues_count":6,"forks_count":48,"subscribers_count":15,"default_branch":"master","last_synced_at":"2025-04-04T03:02:47.036Z","etag":null,"topics":["isomorphic","webpack"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/catamphetamine.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-05-20T19:57:56.000Z","updated_at":"2025-03-02T14:06:30.000Z","dependencies_parsed_at":"2023-02-08T12:01:32.827Z","dependency_job_id":null,"html_url":"https://github.com/catamphetamine/universal-webpack","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catamphetamine%2Funiversal-webpack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catamphetamine%2Funiversal-webpack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catamphetamine%2Funiversal-webpack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catamphetamine%2Funiversal-webpack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/catamphetamine","download_url":"https://codeload.github.com/catamphetamine/universal-webpack/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248617252,"owners_count":21134190,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["isomorphic","webpack"],"created_at":"2024-11-12T05:24:29.310Z","updated_at":"2025-04-12T18:44:55.682Z","avatar_url":"https://github.com/catamphetamine.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# universal-webpack\r\n\r\n[![NPM Version][npm-badge]][npm]\r\n[![Test Coverage][coveralls-badge]][coveralls]\r\n\r\n\u003c!-- Travis builds error beause of `npm install crlf -g` is not available there --\u003e\r\n\u003c!--[![Build Status][travis-badge]][travis]--\u003e\r\n\r\n**For beginners:** consider trying [Next.js](https://github.com/zeit/next.js) first: it's user-friendly and is supposed to be a good start for people not wanting to deal with configuring Webpack manually. On the other hand, if you're an experienced Webpack user then setting up `universal-webpack` shouldn't be too difficult.\r\n\r\nThis library generates client-side and server-side configuration for Webpack therefore enabling seamless client-side/server-side Webpack builds. Requires some initial set up and some prior knowledge of Webpack.\r\n\r\n## Install\r\n\r\n```\r\nnpm install universal-webpack --save-dev\r\n```\r\n\r\n## Example project\r\n\r\nYou may refer to [this sample project](https://github.com/catamphetamine/webpack-react-redux-server-side-render-example) as a reference example of using this library (see `webpack` directory, `package.json` and `rendering-service/main.js`).\r\n\r\n## Use\r\n\r\nSuppose you have a typical `webpack.config.js` file. Create two new files called `webpack.config.client.babel.js` and `webpack.config.server.babel.js` with the following contents:\r\n\r\n### webpack.config.client.babel.js\r\n\r\n```js\r\nimport { client } from 'universal-webpack/config'\r\nimport settings from './universal-webpack-settings'\r\nimport configuration from './webpack.config'\r\n\r\n// Create client-side Webpack config.\r\nexport default client(configuration, settings)\r\n```\r\n\r\n### webpack.config.server.babel.js\r\n\r\n```js\r\nimport { server } from 'universal-webpack/config'\r\nimport settings from './universal-webpack-settings'\r\nimport configuration from './webpack.config'\r\n\r\n// Create server-side Webpack config.\r\nexport default server(configuration, settings)\r\n```\r\n\r\nWhere `./universal-webpack-settings.json` is a configuration file for `universal-webpack` (see below, leave empty for now).\r\n\r\nNow, use `webpack.config.client.babel.js` instead of the old `webpack.config.js` for client side Webpack builds. Your setup also most likely differentiates between a \"development\" client side Webpack build and a \"production\" one, in which case `webpack.config.client.babel.js` is further split into two files — `webpack.config.client.dev.babel.js` and `webpack.config.client.prod.babel.js` — each of which inherits from `webpack.config.client.babel.js` and makes the necessary changes to it as defined by your particular setup.\r\n\r\nAnd, `webpack.config.server.babel.js` file will be used for server-side Webpack builds. And, analogous to the client-side config, it also most likely is gonna be split into \"development\" and \"production\" configs, as defined by your particular setup.\r\n\r\nSetting up the server side requires an additional step: creating the \"entry\" file for running the server. The reason is that client-side config is created from `webpack.config.js` which already has Webpack [\"entry\"](https://webpack.js.org/concepts/entry-points/) defined. Usually it's something like `./src/index.js` which is the \"main\" file for the client-side application. Server-side needs such a \"main\" file too and the path to it must be configured in `./universal-webpack-settings.json` as `server.input`:\r\n\r\n### universal-webpack-settings.json\r\n\r\n```js\r\n{\r\n  \"server\":\r\n  {\r\n    \"input\": \"./source/server.js\",\r\n    \"output\": \"./build/server/server.js\"\r\n  }\r\n}\r\n```\r\n\r\nWith the server-side \"entry\" file path configured, the server-side config created by this library will have the Webpack \"entry\" parameter set up properly. A \"server-side\" Webpack build will now produce a \"server-side\" bundle (`./build/server/server.js`) which can be run using Node.js. An example of an \"entry\" file:\r\n\r\n### source/server.js\r\n\r\n```js\r\nimport path from 'path'\r\nimport http from 'http'\r\nimport express from 'express'\r\nimport httpProxy from 'http-proxy'\r\n\r\n// React routes.\r\n// (shared with the client side)\r\nimport routes from '../client/routes.js'\r\n\r\n// Redux reducers.\r\n// (shared with the client side)\r\nimport reducers from '../client/reducers.js'\r\n\r\n// Starts the server.\r\nfunction startServer()\r\n{\r\n\t// Create HTTP server.\r\n\tconst app = new express()\r\n\tconst server = new http.Server(app)\r\n\r\n\t// Serve static files.\r\n\tapp.use(express.static(path.join(__dirname, '..', 'build/assets')))\r\n\r\n\t// Proxy API calls to API server.\r\n\tconst proxy = httpProxy.createProxyServer({ target: 'http://localhost:xxxx' })\r\n\tapp.use('/api', (req, res) =\u003e proxy.web(req, res))\r\n\r\n\t// React application rendering.\r\n\tapp.use((req, res) =\u003e {\r\n\t\t// Match current URL to the corresponding React page.\r\n\t\trouterMatchURL(routes, req.originalUrl).then((error, routingResult) =\u003e {\r\n\t\t\tif (error) {\r\n\t\t\t\tthrow error\r\n\t\t\t}\r\n\t\t\t// Render React page.\r\n\t\t\tconst page = createPageElement(routingResult, reducers)\r\n\t\t\tres.status(200)\r\n\t\t\tres.send('\u003c!doctype html\u003e\u003chtml\u003e...' + ReactDOM.renderToString(page) + '...\u003c/html\u003e')\r\n\t\t})\r\n\t\t.catch((error) =\u003e {\r\n\t\t\tres.status(500)\r\n\t\t\treturn res.send('Server error')\r\n\t\t})\r\n\t})\r\n\r\n\t// Start the HTTP server.\r\n\tserver.listen()\r\n}\r\n\r\n// Run the server.\r\nstartServer()\r\n```\r\n\r\nThe main use-case for `universal-webpack` though is most likely \"Server-Side Rendering\" which means that the server is somehow gonna need to know the actual URLs for the compiled javascript and CSS files (which contain random-generated md5 hashes). Specifically for this case this library provides a special \"runner\" for the server-side bundle which requires that the server-side bundle just exports a \"start server\" function, without actually running the server, and then such \"start server\" function will be called with a special `parameters` argument which holds the actual URLs for the compiled javascript and CSS files (see the \"Chunks\" section below).\r\n\r\nSo in this case the changes to the server file are gonna be:\r\n\r\n### source/server.js\r\n\r\n```js\r\n...\r\nexport default function startServer(parameters) {\r\n\t...\r\n}\r\n// Don't start the server manually.\r\n// // Run the server.\r\n// startServer()\r\n```\r\n\r\nAnd the server-side runner will be called like this:\r\n\r\n### source/start-server.js\r\n\r\n```js\r\n// The runner.\r\nvar startServer = require('universal-webpack/server')\r\n\r\n// The server-side bundle path info.\r\nvar settings = require('../universal-webpack-settings')\r\n\r\n// Only `configuration.context` and `configuration.output.path`\r\n// parameters are used from the whole Webpack config.\r\nvar configuration = require('../webpack.config')\r\n\r\n// Run the server.\r\nstartServer(configuration, settings)\r\n```\r\n\r\nRunning `node source/start-server.js` will basically call the function exported from `source/server.js` with the `parameters` argument.\r\n\r\nFinally, to run all the things required for \"development\" mode (in parallel):\r\n\r\n```bash\r\n# Client-side build.\r\nwebpack-dev-server --hot --config ./webpack.config.client.dev.babel.js\r\n```\r\n\r\n```bash\r\n# Server-side build.\r\nwebpack --watch --config ./webpack.config.server.dev.babel.js --colors --display-error-details\r\n```\r\n\r\n```bash\r\n# Run the server.\r\nnodemon ./source/start-server --watch ./build/server\r\n```\r\n\r\nFor production mode the command sequence would be:\r\n\r\n```bash\r\n# Build the client.\r\nwebpack --config \"./webpack.config.client.babel.js\" --colors --display-error-details\r\n# Build the server.\r\nwebpack --config \"./webpack.config.server.babel.js\" --colors --display-error-details\r\n# Run the server.\r\nnode \"./source/start-server\"\r\n```\r\n\r\n## Chunks\r\n\r\nThis library will pass the `chunks()` function parameter (inside the `parameters` argument of the server-side function) which returns webpack-compiled chunks filename info:\r\n\r\n### build/webpack-chunks.json\r\n\r\n```js\r\n{\r\n\tjavascript:\r\n\t{\r\n\t\tmain: `/assets/main.785f110e7775ec8322cf.js`\r\n\t},\r\n\r\n\tstyles:\r\n\t{\r\n\t\tmain: `/assets/main.785f110e7775ec8322cf.css`\r\n\t}\r\n}\r\n```\r\n\r\nThese filenames are required for `\u003cscript src=.../\u003e` and `\u003clink rel=\"style\" href=.../\u003e` tags in case of isomorphic (universal) rendering on the server-side.\r\n\r\n## Gotchas\r\n\r\n* It emits no assets on the server side so make sure you include all assets on the client side (e.g. \"favicon\").\r\n* `resolve.root` won't work out-of-the-box while `resolve.alias`es do. For those using `resolve.root` I recommend switching to `resolve.alias`. By default no \"modules\" are bundled in a server-side bundle except for `resolve.alias`es and `excludeFromExternals` matches (see below).\r\n\r\n## Using `mini-css-extract-plugin`\r\n\r\nThe third argument – `options` object – may be passed to `client()` configuration function. If `options.development` is set to `false`, then it will apply `mini-css-extract-plugin` to CSS styles automatically, i.e. it will extract all CSS styles into separate `*.css` files (one for each Webpack \"chunk\"): this is considered a slightly better approach for production deployment instead of just leaving all CSS in `*.js` chunk files (due to static file caching in a browser). Using `options.development=false` option is therefore just a convenience shortcut which one may use instead of adding `mini-css-extract-plugin` to production client-side webpack configuration manually.\r\n\r\n```js\r\nimport { clientConfiguration } from 'universal-webpack'\r\nimport settings from './universal-webpack-settings'\r\nimport baseConfiguration from './webpack.config'\r\n\r\nconst configuration = clientConfiguration(baseConfiguration, settings, {\r\n  // Extract all CSS into separate `*.css` files (one for each chunk)\r\n  // using `mini-css-extract-plugin`\r\n  // instead of leaving that CSS embedded directly in `*.js` chunk files.\r\n  development: false\r\n})\r\n```\r\n\r\n## Advanced configuration\r\n\r\n`./universal-webpack-settings.json` configuration file also supports the following optional configuration parameters:\r\n\r\n```js\r\n{\r\n\t// By default, all `require()`d packages\r\n\t// (e.g. everything from `node_modules`, `resolve.modules`),\r\n\t// except for `resolve.alias`ed ones,\r\n\t// are marked as `external` for server-side Webpack build\r\n\t// which means they won't be processed and bundled by Webpack,\r\n\t// instead being processed and `require()`d at runtime by Node.js.\r\n\t//\r\n\t// With this setting one can explicitly define which modules\r\n\t// aren't gonna be marked as `external` dependencies.\r\n\t// (and therefore are gonna be compiled and bundled by Webpack)\r\n\t//\r\n\t// Can be used, for example, for ES6-only `node_modules`.\r\n\t// ( a more intelligent solution would be accepted\r\n\t//   https://github.com/catamphetamine/universal-webpack/issues/10 )\r\n\t//\r\n\texcludeFromExternals:\r\n\t[\r\n\t\t'lodash-es',\r\n\t\t/^some-other-es6-only-module(\\/.*)?$/\r\n\t],\r\n\r\n\t// As stated above, all files inside `node_modules`, when `require()`d,\r\n\t// would be resolved as \"externals\" which means Webpack wouldn't use\r\n\t// loaders to process them, and therefore `require()`ing them\r\n\t// would result in an error when running the server-side bundle.\r\n\t//\r\n\t// E.g. for CSS files Node.js would just throw `SyntaxError: Unexpected token .`\r\n\t// because these CSS files need to be compiled by Webpack's `css-loader` first.\r\n\t//\r\n\t// Hence the \"exclude from externals\" file extensions list\r\n\t// which by default is initialized with some common asset types:\r\n\t//\r\n\tloadExternalModuleFileExtensions:\r\n\t[\r\n\t\t'css',\r\n\t\t'png',\r\n\t\t'jpg',\r\n\t\t'svg',\r\n\t\t'xml'\r\n\t],\r\n\r\n\t// Enable `silent` flag to prevent client side webpack build\r\n\t// from outputting chunk stats to the console.\r\n\tsilent: true,\r\n\r\n\t// By default, chunk_info_filename is `webpack-chunks.json`\r\n\tchunk_info_filename: 'submodule-webpack-chunks.json'\r\n}\r\n```\r\n\r\n## Source maps\r\n\r\nI managed to get source maps working in my Node.js server-side code using [`source-map-support`](https://github.com/evanw/node-source-map-support) module.\r\n\r\n### source/start-server.js\r\n\r\n```js\r\n// Enables proper source map support in Node.js\r\nrequire('source-map-support/register')\r\n\r\n// The rest is the same as in the above example\r\n\r\nvar startServer = require('universal-webpack/server')\r\nvar settings = require('../universal-webpack-settings')\r\nvar configuration = require('../webpack.config')\r\n\r\nstartServer(configuration, settings)\r\n```\r\n\r\nWithout `source-map-support` enabled it would give me `No element indexed by XXX` error (which [means](https://github.com/mozilla/source-map/issues/76) that by default Node.js thinks there are references to other source maps and tries to load them but there are no such source maps).\r\n\r\n[`devtool`](https://webpack.github.io/docs/configuration.html#devtool) is set to `source-map` for server-side builds.\r\n\r\n## Nodemon\r\n\r\nI recommend using [nodemon](https://github.com/remy/nodemon) for running server-side Webpack bundle. Nodemon has a `--watch \u003cdirectory\u003e` command line parameter which restarts Node.js process each time the `\u003cdirectory\u003e` is updated (e.g. each time any file in that directory is modified).\r\n\r\nIn other words, Nodemon will relaunch the server every time the code is rebuilt with Webpack.\r\n\r\nThere's one little gotcha though: for the `--watch` feature to work the watched folder needs to exist by the time Nodemon is launched. That means that the server must be started only after the `settings.server.output` path folder has been created.\r\n\r\nTo accomplish that this library provides a command line tool: `universal-webpack`. No need to install in globally: it is supposed to work locally through npm scripts. Usage example:\r\n\r\n### package.json\r\n\r\n```js\r\n...\r\n  \"scripts\": {\r\n    \"start\": \"npm-run-all prepare-server-build start-development-workflow\",\r\n    \"start-development-workflow\": \"npm-run-all --parallel development-webpack-build-for-client development-webpack-build-for-server development-start-server\",\r\n    \"prepare-server-build\": \"universal-webpack --settings ./universal-webpack-settings.json prepare\",\r\n    ...\r\n```\r\n\r\nThe `prepare` command creates `settings.server.output` path folder, or clears it if it already exists.\r\n\r\nNote: In a big React project server restart times can reach ~10 seconds.\r\n\r\n## Flash of unstyled content\r\n\r\nA \"flash of unstyled content\" is a well-known dev-mode Webpack phenomenon. One can observe it when refreshing the page in development mode: because Webpack's `style-loader` adds styles to the page dynamically there's a short period of time (a second maybe) when there are no CSS styles applied to the webpage (in production mode `mini-css-extract-plugin` is used instead of `style-loader` so there's no \"flash of unstyled content\").\r\n\r\nIt's not really a bug, because it's only for development mode. Still, if you're a perfectionist then it can be annoying. The most basic workaround for this is to simply show a white \"smoke screen\" and then hide it after a pre-defined timeout.\r\n\r\n```js\r\nimport { smokeScreen, hideSmokeScreenAfter } from 'universal-webpack'\r\n\r\n\u003cbody\u003e\r\n  ${smokeScreen}\r\n\u003c/body\u003e\r\n\r\n\u003cscript\u003e\r\n  ${hideSmokeScreenAfter(100)}\r\n\u003c/script\u003e\r\n```\r\n\r\n## resolve.moduleDirectories\r\n\r\nIf you were using `resolve.moduleDirectories` for global paths instead of relative paths in your code then consider using `resolve.alias` instead\r\n\r\n```js\r\nresolve:\r\n{\r\n  alias:\r\n  {\r\n    components: path.resolve(__dirname, '../src/components'),\r\n    ...\r\n  }\r\n}\r\n```\r\n\r\n## `universal-webpack` vs `webpack-isomorphic-tools`\r\n\r\nNote: If you never heard of `webpack-isomorphic-tools` then you shouldn't read this section.\r\n\r\n`webpack-isomorphic-tools` runs on the server-side and hooks into Node.js `require()` function with the help of `require-hacker` and does what needs to be done.\r\n\r\n`universal-webpack` doesn't hook into `require()` function - it's just a helper for transforming client-side Webpack configuration to a server-side Webpack configuration. It doesn't run on the server-side or something. It's just a Webpack configuration generator - turned out that Webpack has a `target: \"node\"` parameter which makes it output code that runs on Node.js without any issues.\r\n\r\nI wrote `webpack-isomorphic-tools` before `universal-webpack`, so `universal-webpack` is the recommended tool. However many people still use `webpack-isomorphic-tools` (including me) and find it somewhat less complicated for beginners.\r\n\r\n## GitHub\r\n\r\nOn March 9th, 2020, GitHub, Inc. silently [banned](https://medium.com/@catamphetamine/how-github-blocked-me-and-all-my-libraries-c32c61f061d3) my account (and all my libraries) without any notice. I opened a support ticked but they didn't answer. Because of that, I had to move all my libraries to [GitLab](https://gitlab.com/catamphetamine).\r\n\r\n## License\r\n\r\n[MIT](LICENSE)\r\n\r\n[npm]: https://www.npmjs.org/package/universal-webpack\r\n[npm-badge]: https://img.shields.io/npm/v/universal-webpack.svg?style=flat-square\r\n\r\n[travis]: https://travis-ci.org/catamphetamine/universal-webpack\r\n[travis-badge]: https://img.shields.io/travis/catamphetamine/universal-webpack/master.svg?style=flat-square\r\n\r\n[coveralls]: https://coveralls.io/r/catamphetamine/universal-webpack?branch=master\r\n[coveralls-badge]: https://img.shields.io/coveralls/catamphetamine/universal-webpack/master.svg?style=flat-square\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatamphetamine%2Funiversal-webpack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcatamphetamine%2Funiversal-webpack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatamphetamine%2Funiversal-webpack/lists"}