UNPKG

48 kBMarkdownView Raw
1# webpack-isomorphic-tools
2
3[![NPM Version][npm-image]][npm-url]
4[![NPM Downloads][downloads-image]][downloads-url]
5[![Build Status][travis-image]][travis-url]
6[![Test Coverage][coveralls-image]][coveralls-url]
7
8<!---
9[![Gratipay][gratipay-image]][gratipay-url]
10-->
11
12Is a small helper module providing support for isomorphic (universal) rendering when using Webpack.
13
14## Topics
15
16- [What it does and why is it needed?](#what-it-does-and-why-is-it-needed)
17- [Getting down to business](#getting-down-to-business)
18- [Installation](#installation)
19- [Usage](#usage)
20- [A working example](#a-working-example)
21- [Configuration](#configuration)
22- [Configuration examples](#configuration-examples)
23- [What are webpack-assets.json?](#what-are-webpack-assetsjson)
24- [What are Webpack stats?](#what-are-webpack-stats)
25- [What's a "module"?](#whats-a-module)
26- [API](#api)
27- [Gotchas](#gotchas)
28- [References](#references)
29- [Contributing](#contributing)
30
31## What it does and why is it needed?
32
33What is a web application? I would define it as a box with a bunch of inputs (keyboard events, mouse events) and a display as an output. A user walks into your website and your web application renders a "page" on his display.
34
35At first all the rendering used to happen on the server. But then "AJAX" came (in 2005) and it opened a possibility of moving all rendering logic to the client (user's web browser) leaving the server with just serving API calls (data fetching, data modification, etc).
36
37And so numerous javascript frameworks emerged to serve the purpose of client side rendering and routing. But then everybody realised that this new way of building web applications broke search engine indexing because the search engines didn't talk any javascript.
38
39Then the age of super-responsive websites came and also the iPhone emerged and the battle for milliseconds began. And everybody noticed that client side rendering introduced unnecessary data fetching roundtrips on the first page load: the web browser loaded markup templates and scripts first and then asked the server for the actual data to display.
40
41So it became obvious that web applications need to be "isomorphic" ("universal"), i.e. be able to render both on the client and the server, depending on circumstances. It was quite manageable: one just had to write the rendering logic in such a programming language that is able to run both on client and server. One such language is javascript.
42
43Time moved on and then another new technology emerged - bundling: web applications became so sophisticated and big that the programmers needed a software to take control of the development, testing and building process and to manage all the heterogeneous components of the system. Currently the most popular and well-thought bundler is Webpack.
44
45But Webpack is made for client side code development only: it finds all `require()` calls inside your code and replaces them with various kinds of magic to make the things work. If you try to run the same source code outside of Webpack - for example, on a Node.js server - you'll get a ton of `SyntaxError`s with `Unexpected token`s. That's because on a Node.js server there's no Webpack `require()` magic happening and it simply tries to `require()` all the "assets" (styles, images, fonts, OpenGL shaders, etc) as if they were proper javascript-coded modules hence the error message.
46
47This module - `webpack-isomorphic-tools` - aims to solve these issues and make the client-side code work on the server too therefore reclaiming isomorphic (universal) rendering capabilities. It provides the missing `require()` magic - same as Webpack does on client-side - when running your code on the server. With the help of `webpack-isomorphic-tools` one can fix all those webpack-ish `require()`s of assets and make them work on the server instead of throwing `SyntaxError`s.
48
49## Getting down to business
50
51For example, consider images. Images are `require()`d in React components and then used like this:
52
53```javascript
54// alternatively one can use import, but in this case hot reloading won't work
55// import image_path from '../image.png'
56
57// you just `src` your image inside your `render()` method
58class Photo extends React.Component
59{
60 render()
61 {
62 // when Webpack url-loader finds this `require()` call
63 // it will copy `image.png` to your build folder
64 // and name it something like `9059f094ddb49c2b0fa6a254a6ebf2ad.png`,
65 // because we are using the `[hash]` file naming feature of Webpack url-loader
66 // which (feature) is required to make browser caching work correctly
67 const image_path = require('../image.png')
68
69 return <img src={image_path}/>
70 }
71}
72```
73
74It works on the client because Webpack intelligently replaces all the `require()` calls for you.
75But it wouldn't work on the server because Node.js only knows how to `require()` javascript modules. It would just throw a `SyntaxError`.
76
77To solve this issue you use `webpack-isomorphic-tools` in your application and what it does is it makes the code above work on the server too so that you can have your isomorphic (universal) rendering working.
78
79In this particular case the `require()` call will return the real path to the image on the disk. It would be something like `../../build/9059f094ddb49c2b0fa6a254a6ebf2ad.png`. How did `webpack-isomorphic-tools` know this weird real file path? It's just a bit of magic.
80
81So, you get the idea now?
82
83Aside all of that, `webpack-isomorphic-tools` is highly extensible, and finding the real paths for your assets is just the simplest example of what you can do. Using [custom configuration](#configuration) one can make `require()` calls (on the server) return whatever is needed (not just a String; it may be a JSON object, for example).
84
85For example, if you're using Webpack [css-loader](https://github.com/webpack/css-loader) modules feature (also referred to as ["local styles"](https://medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284)) you can make `require(*.css)` calls return JSON objects with generated CSS class names like they do in [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example#styles) (it's just a demonstration of what one can do with `webpack-isomorphic-tools`, and I'm not using this "modules" feature of `ccs-plugin` in my projects).
86
87## Installation
88
89`webpack-isomorphic-tools` are required both for development and production
90
91```bash
92$ npm install webpack-isomorphic-tools --save
93```
94
95## Usage
96
97First you add `webpack_isomorphic_tools` plugin to your Webpack configuration.
98
99### webpack.config.js
100
101```javascript
102var Webpack_isomorphic_tools_plugin = require('webpack-isomorphic-tools/plugin')
103
104var webpack_isomorphic_tools_plugin =
105 // webpack-isomorphic-tools settings reside in a separate .js file
106 // (because they will be used in the web server code too).
107 new Webpack_isomorphic_tools_plugin(require('./webpack-isomorphic-tools-configuration'))
108 // also enter development mode since it's a development webpack configuration
109 // (see below for explanation)
110 .development()
111
112// usual Webpack configuration
113module.exports =
114{
115 context: '(required) your project path here',
116
117 output:
118 {
119 publicPath: '(required) web path for static files here'
120 },
121
122 module:
123 {
124 loaders:
125 [
126 ...,
127 {
128 test: webpack_isomorphic_tools_plugin.regular_expression('images'),
129 loader: 'url-loader?limit=10240', // any image below or equal to 10K will be converted to inline base64 instead
130 }
131 ]
132 },
133
134 plugins:
135 [
136 ...,
137
138 webpack_isomorphic_tools_plugin
139 ]
140
141 ...
142}
143```
144
145What does `.development()` method do? It enables development mode. In short, when in development mode, it disables asset caching (and enables asset hot reload). Just call it if you're developing your project with `webpack-dev-server` using this config (and, conversely, don't call it for your production webpack build - obvious enough).
146
147For each asset type managed by `webpack_isomorphic_tools` there should be a corresponding loader in your Webpack configuration. For this reason `webpack_isomorphic_tools/plugin` provides a `.regular_expression(asset_type)` method. The `asset_type` parameter is taken from your `webpack-isomorphic-tools` configuration:
148
149### webpack-isomorphic-tools-configuration.js
150
151```javascript
152import Webpack_isomorphic_tools_plugin from 'webpack-isomorphic-tools/plugin'
153
154export default
155{
156 assets:
157 {
158 images:
159 {
160 extensions: ['png', 'jpg', 'gif', 'ico', 'svg']
161 }
162 }
163}
164```
165
166That's it for the client side. Next, the server side. You create your server side instance of `webpack-isomorphic-tools` in the very main server javascript file (and your web application code will reside in some `server.js` file which is `require()`d in the bottom)
167
168### main.js
169
170```javascript
171var Webpack_isomorphic_tools = require('webpack-isomorphic-tools')
172
173// this must be equal to your Webpack configuration "context" parameter
174var project_base_path = require('path').resolve(__dirname, '..')
175
176// this global variable will be used later in express middleware
177global.webpack_isomorphic_tools = new Webpack_isomorphic_tools(require('./webpack-isomorphic-tools-configuration'))
178// enter development mode if needed
179// (you may also prefer to use a Webpack DefinePlugin variable)
180.development(process.env.NODE_ENV === 'development')
181// initializes a server-side instance of webpack-isomorphic-tools
182// (the first parameter is the base path for your project
183// and is equal to the "context" parameter of you Webpack configuration)
184// (if you prefer Promises over callbacks
185// you can omit the callback parameter
186// and then it will return a Promise instead)
187.server(project_base_path, function()
188{
189 // webpack-isomorphic-tools is all set now.
190 // here goes all your web application code:
191 require('./server')
192})
193```
194
195Then you, for example, create an express middleware to render your pages on the server
196
197```javascript
198import React from 'react'
199
200// html page markup
201import Html from './html'
202
203// will be used in express_application.use(...)
204export function page_rendering_middleware(request, response)
205{
206 // clear require() cache if in development mode
207 // (makes asset hot reloading work)
208 if (_development_)
209 {
210 webpack_isomorphic_tools.refresh()
211 }
212
213 // for react-router example of determining current page by URL take a look at this:
214 // https://github.com/halt-hammerzeit/webapp/blob/master/code/server/webpage%20rendering.js
215 const page_component = [determine your page component here using request.path]
216
217 // for a Redux Flux store implementation you can see the same example:
218 // https://github.com/halt-hammerzeit/webapp/blob/master/code/server/webpage%20rendering.js
219 const flux_store = [initialize and populate your flux store depending on the page being shown]
220
221 // render the page to string and send it to the browser as text/html
222 response.send('<!doctype html>\n' +
223 React.renderToString(<Html assets={webpack_isomorphic_tools.assets()} component={page_component} store={flux_store}/>))
224}
225```
226
227And finally you use the `assets` inside the `Html` component's `render()` method
228
229```javascript
230import React, {Component, PropTypes} from 'react'
231import serialize from 'serialize-javascript'
232
233export default class Html extends Component
234{
235 static propTypes =
236 {
237 assets : PropTypes.object,
238 component : PropTypes.object,
239 store : PropTypes.object
240 }
241
242 // a sidenote for "advanced" users:
243 // (you may skip this)
244 //
245 // this file is usually not included in your Webpack build
246 // because this React component is only needed for server side React rendering.
247 //
248 // so, if this React component is not `require()`d from anywhere in your client code,
249 // then Webpack won't ever get here
250 // which means Webpack won't detect and parse any of the `require()` calls here,
251 // which in turn means that if you `require()` any unique assets here
252 // you should also `require()` those assets somewhere in your client code,
253 // otherwise those assets won't be present in your Webpack bundle and won't be found.
254 //
255 render()
256 {
257 const { assets, component, store } = this.props
258
259 // "import" will work here too
260 // but if you want hot reloading to work while developing your project
261 // then you need to use require()
262 // because import will only be executed a single time
263 // (when the application launches)
264 // you can refer to the "Require() vs import" section for more explanation
265 const picture = require('../assets/images/cat.jpg')
266
267 // favicon
268 const icon = require('../assets/images/icon/32x32.png')
269
270 const html =
271 (
272 <html lang="en-us">
273 <head>
274 <meta charSet="utf-8"/>
275 <title>xHamster</title>
276
277 {/* favicon */}
278 <link rel="shortcut icon" href={icon} />
279
280 {/* styles (will be present only in production with webpack extract text plugin) */}
281 {Object.keys(assets.styles).map((style, i) =>
282 <link href={assets.styles[style]} key={i} media="screen, projection"
283 rel="stylesheet" type="text/css"/>)}
284
285 {/* resolves the initial style flash (flicker) on page load in development mode */}
286 { Object.keys(assets.styles).is_empty() ? <style dangerouslySetInnerHTML={{__html: require('../assets/styles/main_style.css')}}/> : null }
287 </head>
288
289 <body>
290 {/* image requiring demonstration */}
291 <img src={picture}/>
292
293 {/* rendered React page */}
294 <div id="content" dangerouslySetInnerHTML={{__html: React.renderToString(component)}}/>
295
296 {/* Flux store data will be reloaded into the store on the client */}
297 <script dangerouslySetInnerHTML={{__html: `window._flux_store_data=${serialize(store.getState())};`}} />
298
299 {/* javascripts */}
300 {/* (usually one for each "entry" in webpack configuration) */}
301 {/* (for more informations on "entries" see https://github.com/petehunt/webpack-howto/) */}
302 {Object.keys(assets.javascript).map((script, i) =>
303 <script src={assets.javascript[script]} key={i}/>
304 )}
305 </body>
306 </html>
307 )
308
309 return html
310 }
311}
312```
313
314`assets` in the code above are simply the contents of `webpack-assets.json` which is created by `webpack-isomorphic-tools` in your project base folder. `webpack-assets.json` (in the simplest case) keeps track of the real paths to your assets, e.g.
315
316```javascript
317{
318 "javascript":
319 {
320 "main": "/assets/main-d8c29e9b2a4623f696e8.js"
321 },
322
323 "styles":
324 {
325 "main": "/assets/main-d8c29e9b2a4623f696e8.css"
326 },
327
328 "assets":
329 {
330 "./assets/images/cat.jpg": "http://localhost:3001/assets/9059f094ddb49c2b0fa6a254a6ebf2ad.jpg",
331
332 "./assets/images/icon/32x32.png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQffBhcWAg6gFw6bAAAB60lEQVRIx+3UTUjUQRzG8c+u/n2BDe3lIJtQSuYhsPTQG+TFYLulguStoA5dPHYogoKigoi8dIsOCd0iiC4JFYFQBAVZEUgklWVQqam4vu1uF111d1310qWe0/yemfnyzPyG4b8KllQl6jWqNuX3nFNun/0qjJpYGRB1TkyRWu0C76Q0uKhOkT1aDfqSP0uxTpetR1i9e2Iq3HVUCQKt7tuWP0GDmDOGkfJd3GEbhFwzg6T3alR5lg0Ip0fVPhhKV2+UqfNcMu28sjlXggVAXEQoXZVKmlC2aGXETH5Ary3q026zPg8dtGnOKXPIi/x3MCJwUtyUqBN2uarXTi1+Cql1yqibuTKElsCaHBFBn1v6sU67RoGkHl3GciVYDNiuWVSphDEJYaSkRBSbNqLHI7PZgML0qNIFrz3OwqZAuQ6BB8KqRL01nA3YbdCVRW3L1KxGTx1zQMI3p01nAkqN5NnOkBrXJZw1qlOlj5mAlTQuqluXcRGTSrOPsJJeajOQzphaOyDucy47vGrAMvqLgCLlS97HmgH17mgRzFWhbEAq43/M1EYF2p1XoVAgMW8vdKFfmx0+LbO9WJNut3W44Ze4r/MTC6cKHBczutDhJSrxwyWDAntt9cRANoCwqLKcgJApAyZXfV//mP4AWg969geZ6qgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDYtMjNUMjI6MDI6MTQrMDI6MDBG88r0AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA2LTIzVDIyOjAyOjE0KzAyOjAwN65ySAAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAASUVORK5CYII="
333 }
334}
335```
336
337And that's it, now you can `require()` your assets "isomorphically" (both on client and server).
338
339## A working example
340
341`webpack-isomorphic-tools` are featured in [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example/blob/master/webpack/webpack-isomorphic-tools.js#L64-L96). There it is used to `require()` images and CSS styles (in the form of CSS `modules`).
342
343Also for a comprehensive example of isomorphic React rendering you can look at [this sample project](https://github.com/halt-hammerzeit/webapp) (see the Quick Start section of the readme). There it is used to `require()` images and CSS stylesheet contents.
344
345Some source code guidance for the aforementioned project:
346
347* [webpack-isomorphic-tools configuration](https://github.com/halt-hammerzeit/webapp/blob/master/webpack/isomorphic.js)
348* [webpack-isomorphic-tools client initialization](https://github.com/halt-hammerzeit/webapp/blob/master/webpack/development%20server.js#L48)
349* [webpack-isomorphic-tools server initialization](https://github.com/halt-hammerzeit/webapp/blob/master/code/page-server/entry.js#L9-L17)
350* [webpage rendering express middleware](https://github.com/halt-hammerzeit/webapp/blob/master/code/page-server/webpage%20rendering.js)
351* [the Html file](https://github.com/halt-hammerzeit/webapp/blob/master/code/page-server/html.js)
352
353## Configuration
354
355Available configuration parameters:
356
357```javascript
358{
359 // sets "development" mode flag.
360 // see the API section below for method .development()
361 // for more explanation about what "development" mode does
362 // and when is it needed.
363 //
364 development: true, // is false by default
365
366 // debug mode.
367 // when set to true, lets you see debugging messages in the console.
368 //
369 debug: true, // is false by default
370
371 // By default it creates 'webpack-assets.json' file at
372 // webpack_configuration.context (which is your project folder).
373 // You can change the assets file path as you wish
374 // (therefore changing both folder and filename).
375 //
376 // (relative to webpack_configuration.context which is your project folder)
377 //
378 webpack_assets_file_path: 'webpack-assets.json',
379
380 // By default, when running in debug mode, it creates 'webpack-stats.json' file at
381 // webpack_configuration.context (which is your project folder).
382 // You can change the stats file path as you wish
383 // (therefore changing both folder and filename).
384 //
385 // (relative to webpack_configuration.context which is your project folder)
386 //
387 webpack_stats_file_path: 'webpack-stats.json',
388
389 // Makes `webpack-isomorphic-tools` aware of Webpack aliasing feature
390 // (if you use it)
391 // https://webpack.github.io/docs/resolving.html#aliasing
392 //
393 // The `alias` parameter corresponds to `resolve.alias`
394 // in your Webpack configuration.
395 //
396 alias: webpack_configuration.resolve.alias, // is {} by default
397
398 // here you can define all your asset types
399 //
400 assets:
401 {
402 // asset_type will appear in:
403 // * webpack-assets.json
404 // * .assets() method call result
405 // * .regular_expression(asset_type) method call
406 //
407 asset_type:
408 {
409 // which file types belong to this asset type
410 //
411 extension: 'png', // or extensions: ['png', 'jpg', ...],
412
413 // here you are able to add some file paths
414 // for which the require() call will bypass webpack-isomorphic-tools
415 // (relative to the project base folder, e.g. ./sources/server/kitten.jpg.js)
416 // (also supports regular expressions, e.g. /^\.\/node_modules\/*/,
417 // and functions(path) { return true / false })
418 //
419 exclude: [],
420
421 // here you can specify manually the paths
422 // for which the require() call will be processed by webpack-isomorphic-tools
423 // (relative to the project base folder, e.g. ./sources/server/kitten.jpg.js)
424 // (also supports regular expressions, e.g. /^\.\/node_modules\/*/,
425 // and functions(path) { return true / false }).
426 // in case of `include` only included paths will be processed by webpack-isomorphic-tools.
427 //
428 include: [],
429
430 // [optional]
431 //
432 // determines which webpack stats modules
433 // belong to this asset type
434 //
435 // arguments:
436 //
437 // module - a webpack stats module
438 //
439 // (to understand what a "module" is
440 // read the "What's a "module"?" section of this readme)
441 //
442 // regular_expression - a regular expression
443 // composed of this asset type's extensions
444 // e.g. /\.scss$/, /\.(ico|gif)$/
445 //
446 // options - various options
447 // (development mode flag,
448 // debug mode flag,
449 // assets base url,
450 // project base folder,
451 // regular_expressions{} for each asset type (by name),
452 // webpack stats json object)
453 //
454 // log
455 //
456 // returns: a Boolean
457 //
458 // by default is: "return regular_expression.test(module.name)"
459 //
460 // premade utility filters:
461 //
462 // Webpack_isomorphic_tools_plugin.style_loader_filter
463 // (for use with style-loader + css-loader)
464 //
465 filter: function(module, regular_expression, options, log)
466 {
467 return regular_expression.test(module.name)
468 },
469
470 // [optional]
471 //
472 // transforms a webpack stats module name
473 // to an asset path (usually is the same thing)
474 //
475 // arguments:
476 //
477 // module - a webpack stats module
478 //
479 // (to understand what a "module" is
480 // read the "What's a "module"?" section of this readme)
481 //
482 // options - various options
483 // (development mode flag,
484 // debug mode flag,
485 // assets base url,
486 // project base folder,
487 // regular_expressions{} for each asset type (by name),
488 // webpack stats json object)
489 //
490 // log
491 //
492 // returns: a String
493 //
494 // by default is: "return module.name"
495 //
496 // premade utility path extractors:
497 //
498 // Webpack_isomorphic_tools_plugin.style_loader_path_extractor
499 // (for use with style-loader + css-loader)
500 //
501 path: function(module, options, log)
502 {
503 return module.name
504 },
505
506 // [optional]
507 //
508 // parses a webpack stats module object
509 // for an asset of this asset type
510 // to whatever you need to get
511 // when you require() these assets
512 // in your code later on.
513 //
514 // this is what you'll see as the asset value in webpack-assets.json:
515 // { ..., path(): compile(parser()), ... }
516 //
517 // can be a CommonJS module source code:
518 // module.exports = ...what you export here is
519 // what you get when you require() this asset...
520 //
521 // if the returned value is not a CommonJS module source code
522 // (it may be a string, a JSON object, whatever)
523 // then it will be transformed into a CommonJS module source code.
524 //
525 // in other words:
526 //
527 // // making of webpack-assets.json
528 // for each type of configuration.assets
529 // modules.filter(type.filter).for_each (module)
530 // assets[type.path()] = compile(type.parser(module))
531 //
532 // // requiring assets in your code
533 // require(path) = (path) => return assets[path]
534 //
535 // arguments:
536 //
537 // module - a webpack stats module
538 //
539 // (to understand what a "module" is
540 // read the "What's a "module"?" section of this readme)
541 //
542 // options - various options
543 // (development mode flag,
544 // debug mode flag,
545 // assets base url,
546 // project base folder,
547 // regular_expressions{} for each asset type (by name),
548 // webpack stats json object)
549 //
550 // log
551 //
552 // returns: whatever (could be a filename, could be a JSON object, etc)
553 //
554 // by default is: "return module.source"
555 //
556 // premade utility parsers:
557 //
558 // Webpack_isomorphic_tools_plugin.url_loader_parser
559 // (for use with url-loader or file-loader)
560 // require() will return file URL
561 // (is equal to the default parser, i.e. no parser)
562 //
563 // Webpack_isomorphic_tools_plugin.css_loader_parser
564 // (for use with css-loader when not using "modules" feature)
565 // require() will return CSS style text
566 //
567 // Webpack_isomorphic_tools_plugin.css_modules_loader_parser
568 // (for use with css-loader when using "modules" feature)
569 // require() will return a JSON object map of style class names
570 // which will also have a `_style` key containing CSS style text
571 //
572 parser: function(module, options, log)
573 {
574 log.info('# module name', module.name)
575 log.info('# module source', module.source)
576 log.info('# project path', options.project_path)
577 log.info('# assets base url', options.assets_base_url)
578 log.info('# regular expressions', options.regular_expressions)
579 log.info('# debug mode', options.debug)
580 log.info('# development mode', options.development)
581 log.debug('debugging')
582 log.warning('warning')
583 log.error('error')
584 }
585 },
586 ...
587 },
588 ...]
589}
590```
591
592## Configuration examples
593
594#### url-loader / file-loader (images, fonts, etc)
595
596```javascript
597{
598 assets:
599 {
600 images:
601 {
602 extensions: ['png', 'jpg']
603 },
604
605 fonts:
606 {
607 extensions: ['woff', 'ttf']
608 }
609 }
610}
611```
612
613#### style-loader (standard CSS stylesheets)
614
615```javascript
616{
617 assets:
618 {
619 styles:
620 {
621 extensions: ['less', 'scss'],
622
623 // which `module`s to parse CSS from:
624 filter: function(module, regular_expression, options, log)
625 {
626 if (options.development)
627 {
628 // In development mode there's Webpack "style-loader",
629 // which outputs `module`s with `module.name == asset_path`,
630 // but those `module`s do not contain CSS text.
631 //
632 // The `module`s containing CSS text are
633 // the ones loaded with Webpack "css-loader".
634 // (which have kinda weird `module.name`)
635 //
636 // Therefore using a non-default `filter` function here.
637 //
638 return webpack_isomorphic_tools_plugin.style_loader_filter(module, regular_expression, options, log)
639 }
640
641 // In production mode there will be no CSS text at all
642 // because all styles will be extracted by Webpack Extract Text Plugin
643 // into a .css file (as per Webpack configuration).
644 //
645 // Therefore in production mode `filter` function always returns non-`true`.
646 },
647
648 // How to correctly transform kinda weird `module.name`
649 // of the `module` created by Webpack "css-loader"
650 // into the correct asset path:
651 path: webpack_isomorphic_tools_plugin.style_loader_path_extractor,
652
653 // How to extract these Webpack `module`s' javascript `source` code.
654 // basically takes `module.source` and modifies `module.exports` a little.
655 parser: webpack_isomorphic_tools_plugin.css_loader_parser
656 }
657 }
658}
659```
660
661#### style-loader (CSS stylesheets with "CSS modules" feature)
662
663```javascript
664{
665 assets:
666 {
667 style_modules:
668 {
669 extensions: ['less', 'scss'],
670
671 // which `module`s to parse CSS style class name maps from:
672 filter: function(module, regex, options, log)
673 {
674 if (options.development)
675 {
676 // In development mode there's Webpack "style-loader",
677 // which outputs `module`s with `module.name == asset_path`,
678 // but those `module`s do not contain CSS text.
679 //
680 // The `module`s containing CSS text are
681 // the ones loaded with Webpack "css-loader".
682 // (which have kinda weird `module.name`)
683 //
684 // Therefore using a non-default `filter` function here.
685 //
686 return webpack_isomorphic_tools_plugin.style_loader_filter(module, regex, options, log)
687 }
688
689 // In production mode there's no Webpack "style-loader",
690 // so `module.name`s of the `module`s created by Webpack "css-loader"
691 // (those which contain CSS text)
692 // will be simply equal to the correct asset path
693 return regex.test(module.name)
694 },
695
696 // How to correctly transform `module.name`s
697 // into correct asset paths
698 path: function(module, options, log)
699 {
700 if (options.development)
701 {
702 // In development mode there's Webpack "style-loader",
703 // so `module.name`s of the `module`s created by Webpack "css-loader"
704 // (those picked by the `filter` function above)
705 // will be kinda weird, and this path extractor extracts
706 // the correct asset paths from these kinda weird `module.name`s
707 return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
708 }
709
710 // in production mode there's no Webpack "style-loader",
711 // so `module.name`s will be equal to correct asset paths
712 return module.name
713 },
714
715 // How to extract these Webpack `module`s' javascript `source` code.
716 // Basically takes `module.source` and modifies its `module.exports` a little.
717 parser: function(module, options, log)
718 {
719 if (options.development)
720 {
721 // In development mode it adds an extra `_style` entry
722 // to the CSS style class name map, containing the CSS text
723 return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
724 }
725
726 // In production mode there's Webpack Extract Text Plugin
727 // which extracts all CSS text away, so there's
728 // only CSS style class name map left.
729 return module.source
730 }
731 }
732 }
733}
734```
735
736## What are webpack-assets.json?
737
738This file is needed for `webpack-isomorphic-tools` operation on server. It is created by a custom Webpack plugin and is then read from the filesystem by `webpack-isomorphic-tools` server instance. When you `require(path_to_an_asset)` an asset on server then what you get is simply what's there in this file corresponding to this `path_to_an_asset` key (under the `assets` section).
739
740Pseudocode:
741
742```
743// requiring assets in your code
744require(path) = (path) => return assets[path]
745```
746
747Therefore, if you get such a message in the console:
748
749```
750[webpack-isomorphic-tools] [error] asset not found: ./~/react-toolbox/lib/font_icon/style.scss
751```
752
753Then it means that the asset you requested (`require()`d) is absent from your `webpack-assets.json` which in turn means that you haven't placed this asset to your `webpack-assets.json` in the first place. How to place an asset into `webpack-assets.json`?
754
755Pseudocode:
756
757```
758// making of webpack-assets.json inside the Webpack plugin
759for each type of configuration.assets
760 modules.filter(type.filter).for_each (module)
761 assets[type.path()] = compile(type.parser(module))
762```
763
764Therefore, if you get the "asset not found" error, first check your `webpack-assets.json` and second check your `webpack-isomorphic-tools` configuration section for this asset type: are your `filter`, `path` and `parser` functions correct?
765
766## What are Webpack stats?
767
768[Webpack stats](https://github.com/webpack/docs/wiki/node.js-api#stats) are a description of all the modules in a Webpack build. When running in debug mode Webpack stats are output to a file named `webpack-stats.json` in the same folder as your `webpack-assets.json` file. One may be interested in the contents of this file when writing custom `filter`, `path` or `parser` functions. This file is not needed for operation, it's just some debugging information.
769
770## What's a "module"?
771
772**This is an advanced topic on Webpack internals**
773
774A "module" is a Webpack entity. One of the main features of Webpack is code splitting. When Webpack builds your code it splits it into "chunks" - large portions of code which can be downloaded separately later on (if needed) therefore reducing the initial page load time for your website visitor. These big "chunks" aren't monolithic and in their turn are composed of "modules" which are: standard CommonJS javascript modules you `require()` every day, pictures, stylesheets, etc. Every time you `require()` something (it could be anything: an npm module, a javascript file, or a css style, or an image) a `module` entry is created by Webpack. And the file where this `require()` call originated is called a `reason` for this `require()`d `module`. Each `module` entry has a `name` and a `source` code, along with a list of `chunks` it's in and a bunch of other miscellaneous irrelevant properties.
775
776For example, here's a piece of an example `webpack-stats.json` file (which is generated along with `webpack-assets.json` in debug mode). Here you can see a random `module` entry created by Webpack.
777
778```javascript
779{
780 ...
781
782 "modules": [
783 {
784 "id": 0,
785 ...
786 },
787 {
788 "id": 1,
789 "name": "./~/fbjs/lib/invariant.js",
790 "source": "module.exports = global[\"undefined\"] = require(\"-!G:\\\\work\\\\isomorphic-demo\\\\node_modules\\\\fbjs\\\\lib\\\\invariant.js\");",
791
792 // the rest of the fields are irrelevant
793
794 "chunks": [
795 0
796 ],
797 "identifier": "G:\\work\\isomorphic-demo\\node_modules\\expose-loader\\index.js?undefined!G:\\work\\isomorphic-demo\\node_modules\\fbjs\\lib\\invariant.js",
798 "index": 27,
799 "index2": 7,
800 "size": 117,
801 "cacheable": true,
802 "built": true,
803 "optional": false,
804 "prefetched": false,
805 "assets": [],
806 "issuer": "G:\\work\\isomorphic-demo\\node_modules\\react\\lib\\ReactInstanceHandles.js",
807 "failed": false,
808 "errors": 0,
809 "warnings": 0,
810
811 "reasons": [
812 {
813 "moduleId": 418,
814 "moduleIdentifier": "G:\\work\\isomorphic-demo\\node_modules\\react\\lib\\ReactInstanceHandles.js",
815 "module": "./~/react/lib/ReactInstanceHandles.js",
816 "moduleName": "./~/react/lib/ReactInstanceHandles.js",
817 "type": "cjs require",
818 "userRequest": "fbjs/lib/invariant",
819 "loc": "17:16-45"
820 },
821 ...
822 {
823 "moduleId": 483,
824 "moduleIdentifier": "G:\\work\\isomorphic-demo\\node_modules\\react\\lib\\traverseAllChildren.js",
825 "module": "./~/react/lib/traverseAllChildren.js",
826 "moduleName": "./~/react/lib/traverseAllChildren.js",
827 "type": "cjs require",
828 "userRequest": "fbjs/lib/invariant",
829 "loc": "19:16-45"
830 }
831 ]
832 },
833
834 ...
835 ]
836}
837```
838
839Judging by its `reasons` and their `userRequest`s one can deduce that this `module` is `require()`d by many other `module`s in this project and the code triggering this `module` entry creation could look something like this
840
841```javascript
842var invariant = require('fbjs/lib/invariant')
843```
844
845Every time you `require()` anything in your code, Webpack detects it during build process and the `require()`d `module` is "loaded" (decorated, transformed, replaced, etc) by a corresponding module "loader" (or loaders) specified in Webpack configuration file (`webpack.conf.js`) under the "module.loaders" path. For example, say, all JPG images in a project are configured to be loaded with a "url-loader":
846
847```javascript
848// Webpack configuration
849module.exports =
850{
851 ...
852
853 module:
854 {
855 loaders:
856 [
857 ...
858
859 {
860 test : /\.jpg$/,
861 loader : 'url-loader'
862 }
863 ]
864 },
865
866 ...
867}
868```
869
870This works on client: `require()` calls will return URLs for JPG images. The next step is to make `require()` calls to these JPG images behave the same way when this code is run on the server, with the help of `webpack-isomorphic-tools`. So, the fields of interest of the `module` object would be `name` and `source`: first you find the modules of interest by their `name`s (in this case, the module `name`s would end in ".jpg") and then you parse the `source`s of those modules to extract the information you need (in this case that would be the real path to an image).
871
872The `module` object for an image would look like this
873
874```javascript
875{
876 ...
877 "name": "./assets/images/husky.jpg",
878 "source": "module.exports = __webpack_public_path__ + \"9059f094ddb49c2b0fa6a254a6ebf2ad.jpg\""
879}
880```
881
882Therefore, in this simple case, in `webpack-isomorphic-tools` configuration file we create an "images" asset type with extension "jpg" and these parameters:
883
884* the `filter` function would be `module => module.name.ends_with('.jpg')` (and it's the default `filter` if no `filter` is specified)
885* the `path` parser function would be `module => module.name` (and it's the default `path` parser if no `path` parser is specified)
886* the `parser` function would be `module => module.source` (and it's the default `parser` if no `parser` is specified)
887
888When the javascript `source` code returned by this `parser` function gets compiled by `webpack-isomorphic-tools` it will yeild a valid CommonJS javascript module which will return the URL for this image, resulting in the following piece of `webpack-assets.json`:
889
890```
891{
892 ...
893 assets:
894 {
895 "./assets/images/husky.jpg": "/assets/9059f094ddb49c2b0fa6a254a6ebf2ad.jpg",
896 ...
897 }
898}
899```
900
901And so when you later `require("./assets/images/husky.jpg")` in your server code it will return `"/assets/9059f094ddb49c2b0fa6a254a6ebf2ad.jpg"` and that's it.
902
903## API
904
905#### Constructor
906
907(both Webpack plugin and server tools)
908
909Takes an object with options (see [Configuration](#configuration) section above)
910
911#### .development(true or false or undefined -> true)
912
913(both Webpack plugin instance and server tools instance)
914
915Is it development mode or is it production mode? By default it's production mode. But if you're instantiating `webpack-isomorphic-tools/plugin` for use in Webpack development configuration, or if you're instantiating `webpack-isomorphic-tools` on server when you're developing your project, then you should call this method to enable asset hot reloading (and disable asset caching). It should be called right after the constructor.
916
917#### .regular_expression(asset_type)
918
919(Webpack plugin instance)
920
921Returns the regular expression for this asset type (based on this asset type's `extension` (or `extensions`))
922
923#### Webpack_isomorphic_tools_plugin.url_loader_parser
924
925(Webpack plugin)
926
927A parser (see [Configuration](#configuration) section above) for Webpack [url-loader](https://github.com/webpack/url-loader), also works for Webpack [file-loader](https://github.com/webpack/file-loader). Use it for your images, fonts, etc.
928
929#### .server(project_path, [callback])
930
931(server tools instance)
932
933Initializes a server-side instance of `webpack-isomorphic-tools` with the base path for your project and makes all the server-side `require()` calls work. The `project_path` parameter must be identical to the `context` parameter of your Webpack configuration and is needed to locate `webpack-assets.json` (contains the assets info) which is output by Webpack process.
934
935When you're running your project in development mode for the very first time the `webpack-assets.json` file doesn't exist yet because in development mode `webpack-dev-server` and your application server are run concurrently and by the time the application server starts the `webpack-assets.json` file hasn't yet been generated by Webpack and `require()` calls for your assets would return `undefined`.
936
937To fix this you can put your application server code into a `callback` and pass it as a second parameter and it will be called as soon as `webpack-assets.json` file is detected. If not given a `callback` this method will return a `Promise` which is fulfilled as soon as `webpack-assets.json` file is detected (in case you prefer `Promise`s over `callback`s). When choosing a `Promise` way you won't be able to get the `webpack-isomorphic-tools` instance variable reference out of the `.server()` method call result, so your code can be a bit more verbose in this case.
938
939#### .refresh()
940
941(server tools instance)
942
943Refreshes your assets info (re-reads `webpack-assets.json` from disk) and also flushes cache for all the previously `require()`d assets
944
945#### .assets()
946
947(server tools instance)
948
949Returns the contents of `webpack-assets.json` which is created by `webpack-isomorphic-tools` in your project base folder
950
951## Configuration example
952
953### .gitignore
954
955Make sure you add this to your `.gitignore`
956
957```
958# webpack-isomorphic-tools
959/webpack-stats.json
960/webpack-assets.json
961```
962
963### Require() vs import
964
965In the image requiring examples above we could have wrote it like this:
966
967```
968import picture from './cat.jpg'
969```
970
971That would surely work. Much simpler and more modern. But, the disadvantage of the new ES6 module `import`ing is that by design it's static as opposed to dynamic nature of `require()`. Such a design decision was done on purpose and it's surely the right one:
972
973* it's static so it can be optimized by the compiler and you don't need to know which module depends on which and manually reorder them in the right order because the compiler does it for you
974* it's smart enough to resolve cyclic dependencies
975* it can load modules both synchronously and asynchronously if it wants to and you'll never know because it can do it all by itself behind the scenes without your supervision
976* the `export`s are static which means that your IDE can know exactly what each module is gonna export without compiling the code (and therefore it can autocomplete names, detect syntax errors, check types, etc); the compiler too has some benefits such as improved lookup speed and syntax and type checking
977* it's simple, it's transparent, it's sane
978
979If you wrote your code with just `import`s it would work fine. But imagine you're developing your website, so you're changing files constantly, and you would like it all refresh automagically when you reload your webpage (in development mode). `webpack-isomorphic-tools` gives you that. Remember this code in the express middleware example above?
980
981```javascript
982if (_development_)
983{
984 webpack_isomorhic_tools.refresh()
985}
986```
987
988It does exactly as it says: it refreshes everything on page reload when you're in development mode. And to leverage this feature you need to use dynamic module loading as opposed to static one through `import`s. This can be done by `require()`ing your assets, and not at the top of the file where all `require()`s usually go but, say, inside the `reder()` method for React components.
989
990I also read on the internets that ES6 supports dynamic module loading too and it looks something like this:
991
992```javascript
993System.import('some_module')
994.then(some_module =>
995{
996 // Use some_module
997})
998.catch(error =>
999{
1000 ...
1001})
1002```
1003
1004I'm currently unfamiliar with ES6 dynamic module loading system because I didn't research this question. Anyway it's still a draft specification so I guess good old `require()` is just fine to the time being.
1005
1006Also it's good to know that the way all this `require('./asset.whatever_extension')` magic is based on [Node.js require hooks](http://bahmutov.calepin.co/hooking-into-node-loader-for-fun-and-profit.html) and it works with `import`s only when your ES6 code is transpiled by Babel which simply replaces all the `import`s with `require()`s. For now, everyone out there uses Babel, both on client and server. But when the time comes for ES6 to be widely natively adopted, and when a good enough ES6 module loading specification is released, then I (or someone else) will port this "require hook" to ES6 to work with `import`s.
1007
1008### Cannot find module
1009
1010If encountered when run on server, this error means that the `require()`d path doesn't exist in the filesystem (all the `require()`d assets must exist in the filesystem when run on server). If encountered during Webpack build, this error means that the `require()`d path is absent from `webpack-stats.json`.
1011
1012### SyntaxError: Unexpected token ILLEGAL
1013
1014This probably means that in some asset module source there's a `require()` call to some file extension that isn't specified in
1015
1016## References
1017
1018Initially based on the code from [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) by Erik Rasmussen
1019
1020Also the same codebase (as in the project mentioned above) can be found in [isomorphic500](https://github.com/gpbl/isomorphic500) by Giampaolo Bellavite
1021
1022Also uses `require()` hooking techniques from [node-hook](https://github.com/bahmutov/node-hook) by Gleb Bahmutov
1023
1024## Contributing
1025
1026After cloning this repo, ensure dependencies are installed by running:
1027
1028```sh
1029npm install
1030```
1031
1032This module is written in ES6 and uses [Babel](http://babeljs.io/) for ES5
1033transpilation. Widely consumable JavaScript can be produced by running:
1034
1035```sh
1036npm run build
1037```
1038
1039Once `npm run build` has run, you may `import` or `require()` directly from
1040node.
1041
1042After developing, the full test suite can be evaluated by running:
1043
1044```sh
1045npm test
1046```
1047
1048While actively developing, one can use (personally I don't use it)
1049
1050```sh
1051npm run watch
1052```
1053
1054in a terminal. This will watch the file system and run tests automatically
1055whenever you save a js file.
1056
1057When you're ready to test your new functionality on a real project, you can run
1058
1059```sh
1060npm pack
1061```
1062
1063It will `build`, `test` and then create a `.tgz` archive which you can then install in your project folder
1064
1065```sh
1066npm install [module name with version].tar.gz
1067```
1068
1069## To do
1070
1071 * Proper testing for `log` (output to a variable rather than `console`)
1072 * Proper testing for `notify_stats` (output to a `log` variable)
1073 * Proper testing for parsers (using `eval()` CommonJS module compilation)
1074 * Proper testing for `require('./node_modules/whatever.jpg')` test case
1075
1076## License
1077
1078[MIT](LICENSE)
1079[npm-image]: https://img.shields.io/npm/v/webpack-isomorphic-tools.svg
1080[npm-url]: https://npmjs.org/package/webpack-isomorphic-tools
1081[travis-image]: https://img.shields.io/travis/halt-hammerzeit/webpack-isomorphic-tools/master.svg
1082[travis-url]: https://travis-ci.org/halt-hammerzeit/webpack-isomorphic-tools
1083[downloads-image]: https://img.shields.io/npm/dm/webpack-isomorphic-tools.svg
1084[downloads-url]: https://npmjs.org/package/webpack-isomorphic-tools
1085[coveralls-image]: https://img.shields.io/coveralls/halt-hammerzeit/webpack-isomorphic-tools/master.svg
1086[coveralls-url]: https://coveralls.io/r/halt-hammerzeit/webpack-isomorphic-tools?branch=master
1087
1088<!---
1089[gratipay-image]: https://img.shields.io/gratipay/dougwilson.svg
1090[gratipay-url]: https://gratipay.com/dougwilson/
1091-->
\No newline at end of file