18

In isomorphic react app I have myModule which should behave differently on node and browser environments. I would like configure this split point in package.json for myModule:

package.json

{
  "private": true,
  "name": "myModule",
  "main": "./myModule.server.js",
  "browser": "./myModule.client.js"
}

file structure

├── myModule
│   ├── myModule.client.js
│   ├── myModule.server.js
│   └── package.json
│ 
├── browser.js
└── server.js

So when I use myModule in node I should get only myModule.server.js:

server.js

import myModule from './myModule';
myModule(); // invoke myModule.server.js

On the browser side should build bundle only with myModule.client.js:

browser.js

import myModule from './myModule';
myModule(); // invoke myModule.client.js

react-starter-kit uses this approach but I can't figure out where is this configuration defined.


Motivation

  1. package.json is good semantic point to do this kind of splitting.
  2. Client side bundle only contain myModule.client.js.

Known solution - not an answer for me

You can have this kind of file structure:

├── myModule
│    ├── myModule.client.js
│    ├── myModule.server.js
│    └── index.js           <-- difference
│ 
├── browser.js
└── server.js

And in index.js:

if (process.browser) { // this condition can be different but you get the point
    module.exports = require('./myModule.client');
} else {
    module.exports = require('./myModule.server');
}

The main problem with this is that client bundle contains a lot of heavy kB backend code.


My webpack configuration

I include my webpack.config.js. Strangely this config always point to myModule.client.js for browser and node.

const webpack = require('webpack');
var path = require('path');
var fs = require('fs');

const DEBUG = !process.argv.includes('--release');
const VERBOSE = !process.argv.includes('--verbose');
const AUTOPREFIXER_BROWSERS = [
    'Android 2.3',
    'Android >= 4',
    'Chrome >= 35',
    'Firefox >= 31',
    'Explorer >= 9',
    'iOS >= 7',
    'Opera >= 12',
    'Safari >= 7.1',
];

let nodeModules = {};
fs.readdirSync('node_modules')
    .filter(function(x) {
        return ['.bin'].indexOf(x) === -1 ;
    })
    .forEach(function(mod) {
        nodeModules[mod] = 'commonjs ' + mod;
    });

let loaders = [
    {
        exclude: /node_modules/,
        loader: 'babel'
    },
    {
        test: [/\.scss$/,/\.css$/],
        loaders: [
            'isomorphic-style-loader',
            `css-loader?${DEBUG ? 'sourceMap&' : 'minimize&'}modules&localIdentName=` +
            `${DEBUG ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]'}`,
            'postcss-loader?parser=postcss-scss'
        ]
    },
    {
        test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/,
        loader: 'url-loader',
        query: {
            name: DEBUG ? '[name].[ext]' : '[hash].[ext]',
            limit: 10000,
        },
    },
    {
        test: /\.(eot|ttf|wav|mp3)$/,
        loader: 'file-loader',
        query: {
            name: DEBUG ? '[name].[ext]' : '[hash].[ext]',
        },
    },
    {
        test: /\.json$/,
        loader: 'json-loader',
    },
];

const common = {
    module: {
        loaders
    },
    plugins: [
        new webpack.optimize.OccurenceOrderPlugin(),
    ],
    postcss: function plugins(bundler) {
        var plugins = [
            require('postcss-import')({ addDependencyTo: bundler }),
            require('precss')(),
            require('autoprefixer')({ browsers: AUTOPREFIXER_BROWSERS }),
        ];

        return plugins;
    },
    resolve: {
        root: path.resolve(__dirname, 'src'),
        extensions: ['', '.js', '.jsx', '.json']
    }
};


module.exports = [
    Object.assign({} , common, { // client
        entry: [
            'babel-polyfill',
            './src/client.js'
        ],
        output: {
            path: __dirname + '/public/',
            filename: 'bundle.js'
        },
        target: 'web',
        node: {
            fs: 'empty',
        },
        devtool: DEBUG ? 'cheap-module-eval-source-map' : false,
        plugins: [
            ...common.plugins,
            new webpack.DefinePlugin({'process.env.BROWSER': true }),
        ],
    }),
    Object.assign({} , common, { // server
        entry: [
            'babel-polyfill',
            './src/server.js'
        ],
        output: {
            path: __dirname + '',
            filename: 'server.js'
        },
        target: 'node',
        plugins: [
            ...common.plugins,
            new webpack.DefinePlugin({'process.env.BROWSER': false }),
        ],
        node: {
            console: false,
            global: false,
            process: false,
            Buffer: false,
            __filename: false,
            __dirname: false,
        },
        externals: nodeModules,

    })
];
Everettss
  • 15,475
  • 9
  • 72
  • 98
  • It seems that the `react-starter-kit` also has different webpack configurations for the client and server as well. Not sure if this is what is making the magic happen, but I guess you could take a look: https://github.com/kriasoft/react-starter-kit/blob/master/tools/webpack.config.js#L202 and https://github.com/kriasoft/react-starter-kit/blob/master/tools/webpack.config.js#L261 – Vasil Dininski Jun 27 '16 at 13:51
  • @VasilDininski I have also two configs (only looks little different). In `module.exports` I merge two configs in array. The same is in `react-starter-kit` https://github.com/kriasoft/react-starter-kit/blob/master/tools/webpack.config.js#L309 – Everettss Jun 27 '16 at 13:57
  • Is the problem that ./server.js is not output, or it has the same contents as ./public/bundle.js? As far as 'main' vs 'browser' in package.json - I don't believe webpack or the browser know or care about package.json for the most part, so you should just set it to the built server js. – Michael Pratt Jun 28 '16 at 15:57

4 Answers4

14

The behavior is standardized here: https://github.com/defunctzombie/package-browser-field-spec

Although this specification is unofficial, many Javascript bundlers follow it, including Webpack, Browserify, and the React Native packager. The browser field not only allows you to change your module entry point, but to also replace or ignore individual files within your module. It's quite powerful.

Since Webpack bundles code for the web by default, you need to manually disable the browser field if you want to use Webpack for your server build. You can do that using the target config option to do this: https://webpack.js.org/concepts/targets/

William Swanson
  • 1,314
  • 11
  • 17
6

It has been a long time since this question was asked. I just want to clarify the previous answer.

If you look at tools/webpack.config.js in React Starter Kit you will see that it exports two Webpack configurations that slightly differ, e.g. module.exports = [clientConfig, sererConfig]. The server-side bundle config has this field target set to node (by default it's web).

It seems this webpack beheavior is not documented, but webpack automatically takes 'main' entry when target is 'node' and takes 'browser' entry when target is 'web'.

mPrinC
  • 9,147
  • 2
  • 32
  • 31
Juan Je García
  • 611
  • 1
  • 8
  • 14
  • 3
    Jumping in even later here - I think this documentation is what we were looking for: https://webpack.js.org/configuration/resolve/#resolvemainfields So when `target: node` is set in your webpack config, webpack will by default first search for a `module` field - and if that doesn't exist, it will 'take' the `main` field Similarly, the precedence order for `target: web` is browser, then module, then main – Bernard Leech Jul 22 '20 at 19:03
1

If you look at tools/webpack.config.js in React Starter Kit you will see that it exports two Webpack configurations that slightly differ, e.g. module.exports = [clientConfig, sererConfig]. The server-side bundle config has this field target set to node (by default it's web).

https://webpack.github.io/docs/configuration.html#target

The approach that you described works great for modules that have exactly the same API but different implementations, like in the case with HTTP client utility that uses XMLHttpRequest in its browser-specific implementation and Node's http module in its server implementation:

https://github.com/kriasoft/react-starter-kit/tree/master/src/core/fetch

Konstantin Tarkus
  • 37,618
  • 14
  • 135
  • 121
0

To have a different entry point for client and server in a Node Module, you can use process.browser flag and handle the same

if (process.browser) {
  // load client entry point
} else {
  // load server entry point
}
ᴄʀᴏᴢᴇᴛ
  • 2,939
  • 26
  • 44
neeraj-dixit27
  • 2,812
  • 2
  • 15
  • 6