UNPKG

48.2 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 // verbosity.
372 // when set to true, outputs Webpack stats to the console
373 // in development mode on each incremental build.
374 // (i don't know who might need that info)
375 //
376 // verbose: true, // is false by default
377
378 // By default it creates 'webpack-assets.json' file at
379 // webpack_configuration.context (which is your project folder).
380 // You can change the assets file path as you wish
381 // (therefore changing both folder and filename).
382 //
383 // (relative to webpack_configuration.context which is your project folder)
384 //
385 webpack_assets_file_path: 'webpack-assets.json',
386
387 // By default, when running in debug mode, it creates 'webpack-stats.json' file at
388 // webpack_configuration.context (which is your project folder).
389 // You can change the stats file path as you wish
390 // (therefore changing both folder and filename).
391 //
392 // (relative to webpack_configuration.context which is your project folder)
393 //
394 webpack_stats_file_path: 'webpack-stats.json',
395
396 // Makes `webpack-isomorphic-tools` aware of Webpack aliasing feature
397 // (if you use it)
398 // https://webpack.github.io/docs/resolving.html#aliasing
399 //
400 // The `alias` parameter corresponds to `resolve.alias`
401 // in your Webpack configuration.
402 //
403 alias: webpack_configuration.resolve.alias, // is {} by default
404
405 // here you can define all your asset types
406 //
407 assets:
408 {
409 // asset_type will appear in:
410 // * webpack-assets.json
411 // * .assets() method call result
412 // * .regular_expression(asset_type) method call
413 //
414 asset_type:
415 {
416 // which file types belong to this asset type
417 //
418 extension: 'png', // or extensions: ['png', 'jpg', ...],
419
420 // here you are able to add some file paths
421 // for which the require() call will bypass webpack-isomorphic-tools
422 // (relative to the project base folder, e.g. ./sources/server/kitten.jpg.js)
423 // (also supports regular expressions, e.g. /^\.\/node_modules\/*/,
424 // and functions(path) { return true / false })
425 //
426 exclude: [],
427
428 // here you can specify manually the paths
429 // for which the require() call will be processed by webpack-isomorphic-tools
430 // (relative to the project base folder, e.g. ./sources/server/kitten.jpg.js)
431 // (also supports regular expressions, e.g. /^\.\/node_modules\/*/,
432 // and functions(path) { return true / false }).
433 // in case of `include` only included paths will be processed by webpack-isomorphic-tools.
434 //
435 include: [],
436
437 // [optional]
438 //
439 // determines which webpack stats modules
440 // belong to this asset type
441 //
442 // arguments:
443 //
444 // module - a webpack stats module
445 //
446 // (to understand what a "module" is
447 // read the "What's a "module"?" section of this readme)
448 //
449 // regular_expression - a regular expression
450 // composed of this asset type's extensions
451 // e.g. /\.scss$/, /\.(ico|gif)$/
452 //
453 // options - various options
454 // (development mode flag,
455 // debug mode flag,
456 // assets base url,
457 // project base folder,
458 // regular_expressions{} for each asset type (by name),
459 // webpack stats json object)
460 //
461 // log
462 //
463 // returns: a Boolean
464 //
465 // by default is: "return regular_expression.test(module.name)"
466 //
467 // premade utility filters:
468 //
469 // Webpack_isomorphic_tools_plugin.style_loader_filter
470 // (for use with style-loader + css-loader)
471 //
472 filter: function(module, regular_expression, options, log)
473 {
474 return regular_expression.test(module.name)
475 },
476
477 // [optional]
478 //
479 // transforms a webpack stats module name
480 // to an asset path (usually is the same thing)
481 //
482 // arguments:
483 //
484 // module - a webpack stats module
485 //
486 // (to understand what a "module" is
487 // read the "What's a "module"?" section of this readme)
488 //
489 // options - various options
490 // (development mode flag,
491 // debug mode flag,
492 // assets base url,
493 // project base folder,
494 // regular_expressions{} for each asset type (by name),
495 // webpack stats json object)
496 //
497 // log
498 //
499 // returns: a String
500 //
501 // by default is: "return module.name"
502 //
503 // premade utility path extractors:
504 //
505 // Webpack_isomorphic_tools_plugin.style_loader_path_extractor
506 // (for use with style-loader + css-loader)
507 //
508 path: function(module, options, log)
509 {
510 return module.name
511 },
512
513 // [optional]
514 //
515 // parses a webpack stats module object
516 // for an asset of this asset type
517 // to whatever you need to get
518 // when you require() these assets
519 // in your code later on.
520 //
521 // this is what you'll see as the asset value in webpack-assets.json:
522 // { ..., path(): compile(parser()), ... }
523 //
524 // can be a CommonJS module source code:
525 // module.exports = ...what you export here is
526 // what you get when you require() this asset...
527 //
528 // if the returned value is not a CommonJS module source code
529 // (it may be a string, a JSON object, whatever)
530 // then it will be transformed into a CommonJS module source code.
531 //
532 // in other words:
533 //
534 // // making of webpack-assets.json
535 // for each type of configuration.assets
536 // modules.filter(type.filter).for_each (module)
537 // assets[type.path()] = compile(type.parser(module))
538 //
539 // // requiring assets in your code
540 // require(path) = (path) => return assets[path]
541 //
542 // arguments:
543 //
544 // module - a webpack stats module
545 //
546 // (to understand what a "module" is
547 // read the "What's a "module"?" section of this readme)
548 //
549 // options - various options
550 // (development mode flag,
551 // debug mode flag,
552 // assets base url,
553 // project base folder,
554 // regular_expressions{} for each asset type (by name),
555 // webpack stats json object)
556 //
557 // log
558 //
559 // returns: whatever (could be a filename, could be a JSON object, etc)
560 //
561 // by default is: "return module.source"
562 //
563 // premade utility parsers:
564 //
565 // Webpack_isomorphic_tools_plugin.url_loader_parser
566 // (for use with url-loader or file-loader)
567 // require() will return file URL
568 // (is equal to the default parser, i.e. no parser)
569 //
570 // Webpack_isomorphic_tools_plugin.css_loader_parser
571 // (for use with css-loader when not using "modules" feature)
572 // require() will return CSS style text
573 //
574 // Webpack_isomorphic_tools_plugin.css_modules_loader_parser
575 // (for use with css-loader when using "modules" feature)
576 // require() will return a JSON object map of style class names
577 // which will also have a `_style` key containing CSS style text
578 //
579 parser: function(module, options, log)
580 {
581 log.info('# module name', module.name)
582 log.info('# module source', module.source)
583 log.info('# project path', options.project_path)
584 log.info('# assets base url', options.assets_base_url)
585 log.info('# regular expressions', options.regular_expressions)
586 log.info('# debug mode', options.debug)
587 log.info('# development mode', options.development)
588 log.debug('debugging')
589 log.warning('warning')
590 log.error('error')
591 }
592 },
593 ...
594 },
595 ...]
596}
597```
598
599## Configuration examples
600
601#### url-loader / file-loader (images, fonts, etc)
602
603```javascript
604{
605 assets:
606 {
607 images:
608 {
609 extensions: ['png', 'jpg']
610 },
611
612 fonts:
613 {
614 extensions: ['woff', 'ttf']
615 }
616 }
617}
618```
619
620#### style-loader (standard CSS stylesheets)
621
622```javascript
623{
624 assets:
625 {
626 styles:
627 {
628 extensions: ['less', 'scss'],
629
630 // which `module`s to parse CSS from:
631 filter: function(module, regular_expression, options, log)
632 {
633 if (options.development)
634 {
635 // In development mode there's Webpack "style-loader",
636 // which outputs `module`s with `module.name == asset_path`,
637 // but those `module`s do not contain CSS text.
638 //
639 // The `module`s containing CSS text are
640 // the ones loaded with Webpack "css-loader".
641 // (which have kinda weird `module.name`)
642 //
643 // Therefore using a non-default `filter` function here.
644 //
645 return webpack_isomorphic_tools_plugin.style_loader_filter(module, regular_expression, options, log)
646 }
647
648 // In production mode there will be no CSS text at all
649 // because all styles will be extracted by Webpack Extract Text Plugin
650 // into a .css file (as per Webpack configuration).
651 //
652 // Therefore in production mode `filter` function always returns non-`true`.
653 },
654
655 // How to correctly transform kinda weird `module.name`
656 // of the `module` created by Webpack "css-loader"
657 // into the correct asset path:
658 path: webpack_isomorphic_tools_plugin.style_loader_path_extractor,
659
660 // How to extract these Webpack `module`s' javascript `source` code.
661 // basically takes `module.source` and modifies `module.exports` a little.
662 parser: webpack_isomorphic_tools_plugin.css_loader_parser
663 }
664 }
665}
666```
667
668#### style-loader (CSS stylesheets with "CSS modules" feature)
669
670```javascript
671{
672 assets:
673 {
674 style_modules:
675 {
676 extensions: ['less', 'scss'],
677
678 // which `module`s to parse CSS style class name maps from:
679 filter: function(module, regex, options, log)
680 {
681 if (options.development)
682 {
683 // In development mode there's Webpack "style-loader",
684 // which outputs `module`s with `module.name == asset_path`,
685 // but those `module`s do not contain CSS text.
686 //
687 // The `module`s containing CSS text are
688 // the ones loaded with Webpack "css-loader".
689 // (which have kinda weird `module.name`)
690 //
691 // Therefore using a non-default `filter` function here.
692 //
693 return webpack_isomorphic_tools_plugin.style_loader_filter(module, regex, options, log)
694 }
695
696 // In production mode there's no Webpack "style-loader",
697 // so `module.name`s of the `module`s created by Webpack "css-loader"
698 // (those which contain CSS text)
699 // will be simply equal to the correct asset path
700 return regex.test(module.name)
701 },
702
703 // How to correctly transform `module.name`s
704 // into correct asset paths
705 path: function(module, options, log)
706 {
707 if (options.development)
708 {
709 // In development mode there's Webpack "style-loader",
710 // so `module.name`s of the `module`s created by Webpack "css-loader"
711 // (those picked by the `filter` function above)
712 // will be kinda weird, and this path extractor extracts
713 // the correct asset paths from these kinda weird `module.name`s
714 return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
715 }
716
717 // in production mode there's no Webpack "style-loader",
718 // so `module.name`s will be equal to correct asset paths
719 return module.name
720 },
721
722 // How to extract these Webpack `module`s' javascript `source` code.
723 // Basically takes `module.source` and modifies its `module.exports` a little.
724 parser: function(module, options, log)
725 {
726 if (options.development)
727 {
728 // In development mode it adds an extra `_style` entry
729 // to the CSS style class name map, containing the CSS text
730 return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
731 }
732
733 // In production mode there's Webpack Extract Text Plugin
734 // which extracts all CSS text away, so there's
735 // only CSS style class name map left.
736 return module.source
737 }
738 }
739 }
740}
741```
742
743## What are webpack-assets.json?
744
745This 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).
746
747Pseudocode:
748
749```
750// requiring assets in your code
751require(path) = (path) => return assets[path]
752```
753
754Therefore, if you get such a message in the console:
755
756```
757[webpack-isomorphic-tools] [error] asset not found: ./~/react-toolbox/lib/font_icon/style.scss
758```
759
760Then 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`?
761
762Pseudocode:
763
764```
765// making of webpack-assets.json inside the Webpack plugin
766for each type of configuration.assets
767 modules.filter(type.filter).for_each (module)
768 assets[type.path()] = compile(type.parser(module))
769```
770
771Therefore, 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?
772
773## What are Webpack stats?
774
775[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.
776
777## What's a "module"?
778
779**This is an advanced topic on Webpack internals**
780
781A "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.
782
783For 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.
784
785```javascript
786{
787 ...
788
789 "modules": [
790 {
791 "id": 0,
792 ...
793 },
794 {
795 "id": 1,
796 "name": "./~/fbjs/lib/invariant.js",
797 "source": "module.exports = global[\"undefined\"] = require(\"-!G:\\\\work\\\\isomorphic-demo\\\\node_modules\\\\fbjs\\\\lib\\\\invariant.js\");",
798
799 // the rest of the fields are irrelevant
800
801 "chunks": [
802 0
803 ],
804 "identifier": "G:\\work\\isomorphic-demo\\node_modules\\expose-loader\\index.js?undefined!G:\\work\\isomorphic-demo\\node_modules\\fbjs\\lib\\invariant.js",
805 "index": 27,
806 "index2": 7,
807 "size": 117,
808 "cacheable": true,
809 "built": true,
810 "optional": false,
811 "prefetched": false,
812 "assets": [],
813 "issuer": "G:\\work\\isomorphic-demo\\node_modules\\react\\lib\\ReactInstanceHandles.js",
814 "failed": false,
815 "errors": 0,
816 "warnings": 0,
817
818 "reasons": [
819 {
820 "moduleId": 418,
821 "moduleIdentifier": "G:\\work\\isomorphic-demo\\node_modules\\react\\lib\\ReactInstanceHandles.js",
822 "module": "./~/react/lib/ReactInstanceHandles.js",
823 "moduleName": "./~/react/lib/ReactInstanceHandles.js",
824 "type": "cjs require",
825 "userRequest": "fbjs/lib/invariant",
826 "loc": "17:16-45"
827 },
828 ...
829 {
830 "moduleId": 483,
831 "moduleIdentifier": "G:\\work\\isomorphic-demo\\node_modules\\react\\lib\\traverseAllChildren.js",
832 "module": "./~/react/lib/traverseAllChildren.js",
833 "moduleName": "./~/react/lib/traverseAllChildren.js",
834 "type": "cjs require",
835 "userRequest": "fbjs/lib/invariant",
836 "loc": "19:16-45"
837 }
838 ]
839 },
840
841 ...
842 ]
843}
844```
845
846Judging 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
847
848```javascript
849var invariant = require('fbjs/lib/invariant')
850```
851
852Every 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":
853
854```javascript
855// Webpack configuration
856module.exports =
857{
858 ...
859
860 module:
861 {
862 loaders:
863 [
864 ...
865
866 {
867 test : /\.jpg$/,
868 loader : 'url-loader'
869 }
870 ]
871 },
872
873 ...
874}
875```
876
877This 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).
878
879The `module` object for an image would look like this
880
881```javascript
882{
883 ...
884 "name": "./assets/images/husky.jpg",
885 "source": "module.exports = __webpack_public_path__ + \"9059f094ddb49c2b0fa6a254a6ebf2ad.jpg\""
886}
887```
888
889Therefore, in this simple case, in `webpack-isomorphic-tools` configuration file we create an "images" asset type with extension "jpg" and these parameters:
890
891* the `filter` function would be `module => module.name.ends_with('.jpg')` (and it's the default `filter` if no `filter` is specified)
892* the `path` parser function would be `module => module.name` (and it's the default `path` parser if no `path` parser is specified)
893* the `parser` function would be `module => module.source` (and it's the default `parser` if no `parser` is specified)
894
895When 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`:
896
897```
898{
899 ...
900 assets:
901 {
902 "./assets/images/husky.jpg": "/assets/9059f094ddb49c2b0fa6a254a6ebf2ad.jpg",
903 ...
904 }
905}
906```
907
908And so when you later `require("./assets/images/husky.jpg")` in your server code it will return `"/assets/9059f094ddb49c2b0fa6a254a6ebf2ad.jpg"` and that's it.
909
910## API
911
912#### Constructor
913
914(both Webpack plugin and server tools)
915
916Takes an object with options (see [Configuration](#configuration) section above)
917
918#### .development(true or false or undefined -> true)
919
920(both Webpack plugin instance and server tools instance)
921
922Is 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.
923
924#### .regular_expression(asset_type)
925
926(Webpack plugin instance)
927
928Returns the regular expression for this asset type (based on this asset type's `extension` (or `extensions`))
929
930#### Webpack_isomorphic_tools_plugin.url_loader_parser
931
932(Webpack plugin)
933
934A 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.
935
936#### .server(project_path, [callback])
937
938(server tools instance)
939
940Initializes 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.
941
942When 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`.
943
944To 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.
945
946#### .refresh()
947
948(server tools instance)
949
950Refreshes your assets info (re-reads `webpack-assets.json` from disk) and also flushes cache for all the previously `require()`d assets
951
952#### .assets()
953
954(server tools instance)
955
956Returns the contents of `webpack-assets.json` which is created by `webpack-isomorphic-tools` in your project base folder
957
958## Configuration example
959
960### .gitignore
961
962Make sure you add this to your `.gitignore`
963
964```
965# webpack-isomorphic-tools
966/webpack-stats.json
967/webpack-assets.json
968```
969
970### Require() vs import
971
972In the image requiring examples above we could have wrote it like this:
973
974```
975import picture from './cat.jpg'
976```
977
978That 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:
979
980* 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
981* it's smart enough to resolve cyclic dependencies
982* 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
983* 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
984* it's simple, it's transparent, it's sane
985
986If 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?
987
988```javascript
989if (_development_)
990{
991 webpack_isomorhic_tools.refresh()
992}
993```
994
995It 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.
996
997I also read on the internets that ES6 supports dynamic module loading too and it looks something like this:
998
999```javascript
1000System.import('some_module')
1001.then(some_module =>
1002{
1003 // Use some_module
1004})
1005.catch(error =>
1006{
1007 ...
1008})
1009```
1010
1011I'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.
1012
1013Also 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.
1014
1015### Cannot find module
1016
1017If 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`.
1018
1019### SyntaxError: Unexpected token ILLEGAL
1020
1021This probably means that in some asset module source there's a `require()` call to some file extension that isn't specified in
1022
1023## References
1024
1025Initially based on the code from [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) by Erik Rasmussen
1026
1027Also the same codebase (as in the project mentioned above) can be found in [isomorphic500](https://github.com/gpbl/isomorphic500) by Giampaolo Bellavite
1028
1029Also uses `require()` hooking techniques from [node-hook](https://github.com/bahmutov/node-hook) by Gleb Bahmutov
1030
1031## Contributing
1032
1033After cloning this repo, ensure dependencies are installed by running:
1034
1035```sh
1036npm install
1037```
1038
1039This module is written in ES6 and uses [Babel](http://babeljs.io/) for ES5
1040transpilation. Widely consumable JavaScript can be produced by running:
1041
1042```sh
1043npm run build
1044```
1045
1046Once `npm run build` has run, you may `import` or `require()` directly from
1047node.
1048
1049After developing, the full test suite can be evaluated by running:
1050
1051```sh
1052npm test
1053```
1054
1055While actively developing, one can use (personally I don't use it)
1056
1057```sh
1058npm run watch
1059```
1060
1061in a terminal. This will watch the file system and run tests automatically
1062whenever you save a js file.
1063
1064When you're ready to test your new functionality on a real project, you can run
1065
1066```sh
1067npm pack
1068```
1069
1070It will `build`, `test` and then create a `.tgz` archive which you can then install in your project folder
1071
1072```sh
1073npm install [module name with version].tar.gz
1074```
1075
1076## To do
1077
1078 * Proper testing for `log` (output to a variable rather than `console`)
1079 * Proper testing for `notify_stats` (output to a `log` variable)
1080 * Proper testing for parsers (using `eval()` CommonJS module compilation)
1081 * Proper testing for `require('./node_modules/whatever.jpg')` test case
1082
1083## License
1084
1085[MIT](LICENSE)
1086[npm-image]: https://img.shields.io/npm/v/webpack-isomorphic-tools.svg
1087[npm-url]: https://npmjs.org/package/webpack-isomorphic-tools
1088[travis-image]: https://img.shields.io/travis/halt-hammerzeit/webpack-isomorphic-tools/master.svg
1089[travis-url]: https://travis-ci.org/halt-hammerzeit/webpack-isomorphic-tools
1090[downloads-image]: https://img.shields.io/npm/dm/webpack-isomorphic-tools.svg
1091[downloads-url]: https://npmjs.org/package/webpack-isomorphic-tools
1092[coveralls-image]: https://img.shields.io/coveralls/halt-hammerzeit/webpack-isomorphic-tools/master.svg
1093[coveralls-url]: https://coveralls.io/r/halt-hammerzeit/webpack-isomorphic-tools?branch=master
1094
1095<!---
1096[gratipay-image]: https://img.shields.io/gratipay/dougwilson.svg
1097[gratipay-url]: https://gratipay.com/dougwilson/
1098-->
\No newline at end of file