react-rails icon indicating copy to clipboard operation
react-rails copied to clipboard

Server rendering with CommonsChunkPlugin raises webpackJsonp is not defined

Open raapperez opened this issue 7 years ago • 14 comments

I started a clean project using:

$ bundle install
$ rails webpacker:install
$ rails webpacker:install:react
$ rails generate react:install
$ rails g react:component HelloWorld greeting:string

Then I added a server render tag in my view:

<%= react_component('HelloWorld', {name: 'John'}, {prerender: true}) %>

And it worked as expected.

Then I tried to setup a commons chunk plugin in the webpack/environment.js file:

const { environment } = require('@rails/webpacker')

const webpack = require('webpack');

const commonLibraries = [
  '/querobolsa/node_modules/jquery/dist/jquery.js',
];

environment.plugins.insert('CommonsChunkPlugin',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'common',
    minChunks: function (module, count) {
      if (commonLibraries.indexOf(module.resource) !== -1) return true;
    }
  })
);
module.exports = environment

It makes the server rendering stop working with the error:

ReferenceError: webpackJsonp is not defined.

It looks like the compiled file server_rendering.js requires that the common.js chunk to be loaded first.

So I tried to add the following code in config/application.rb:

# Settings for the pool of renderers:
    config.react.server_renderer_pool_size  ||= 1  # ExecJS doesn't allow more than one on MRI
    config.react.server_renderer_timeout    ||= 20 # seconds
    config.react.server_renderer = React::ServerRendering::BundleRenderer
    config.react.server_renderer_options = {
      files: ["common.js", "server_rendering.js"],       # files to load for prerendering
      replay_console: true,                 # if true, console.* will be replayed client-side
    }
    # Changing files matching these dirs/exts will cause the server renderer to reload:
    config.react.server_renderer_extensions = ["jsx", "js"]
    config.react.server_renderer_directories = ["/app/assets/javascripts", "/app/javascript/"]

And it raises the following error:

/tmp/execjs20180122-30273-6tq9dmjs:27938
/* 51 */,
        ^

SyntaxError: Unexpected token ,
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:588:28)
    at Object.Module._extensions..js (module.js:635:10)
    at Module.load (module.js:545:32)
    at tryModuleLoad (module.js:508:12)
    at Function.Module._load (module.js:500:3)
    at Function.Module.runMain (module.js:665:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

Please, can someone give me a solution to this problem? I know if I skip the server_rendering in commons chunk process it would work, but I don't know how to do this.

System configuration

Webpacker version: 3.2.1 React-Rails version: 2.4.3 Rect_UJS version: 2.4.3 Rails version: 5.1.4 Ruby version: 2.4.1

raapperez avatar Jan 22 '18 15:01 raapperez

@raapperez You might find you solution in rails/webpacker#119 or webpack/webpack#368

biw avatar Jan 30 '18 00:01 biw

I'm running into the same issue. Can you be more specific with where the solution is in those threads? They are rather long threads.

Overload119 avatar Feb 04 '18 00:02 Overload119

The simple solution is make sure your server_rendering.js isn't chunked. There is no need for doing it as chunking is only for the client's benefit, and it makes the code more complex.

The first possible solution is that there is some sort of name conflict with the chunk and file name, and the other one was look at other react with rails solutions, the next best one being https://github.com/renchap/webpacker-react incase that doesn't have the chunking issues.

BookOfGreg avatar Feb 04 '18 09:02 BookOfGreg

How can I make sure server_rendering is not chunked?

I'm currently using the plugin like this:

environment.plugins.insert(
  'CommonChunks',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'common',
    minChunks: module =>
      module.context && module.context.indexOf('node_modules') !== -1,
  }),
  {before: 'manifest'},
);

I ultimately want common chunking for the client's benefit, but not sure how to make Webpacker exclude it for server side rendering. Any ideas?

Overload119 avatar Feb 13 '18 04:02 Overload119

Note for myself: This will be relevant when it comes to a fix: https://github.com/renchap/webpacker-react/issues/3#issuecomment-368341147

BookOfGreg avatar Feb 26 '18 10:02 BookOfGreg

I managed to get this working on our main project by stopping the server_rendering.js file from being chunked.

We're using fairly standard setups for webpacker and react-rails, with application.js and server_rendering.js entry points. I began by using the Webpacker example for setting up the CommonChunksPlugin (https://github.com/rails/webpacker/blob/master/docs/webpack.md#add-common-chunks):

const { environment } = require('@rails/webpacker');
const webpack = require('webpack')

environment.plugins.append(
  'CommonsChunkVendor',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: (module) => {
      // this assumes your vendor imports exist in the node_modules directory
      return module.context && module.context.indexOf('node_modules') !== -1
    }
  })
)

environment.plugins.append(
  'CommonsChunkManifest',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    minChunks: Infinity
  })
)

module.exports = environment;

I set the first chunk to only run on the application entry point, thereby ignoring server_rendering.

Then I set the second chunk (which extracts the Webpack runtime) to only run on the chunk called vendor we just created - effectively again ignoring server_rendering, because it didn't get chunked out to vendor.

So I wound up with:

const { environment } = require('@rails/webpacker');
const webpack = require('webpack');

environment.plugins.append(
  'CommonsChunkVendor',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: module => {
      return module.context && module.context.includes('node_modules');
    },
    chunks: ['application']
  })
);

environment.plugins.append(
  'CommonsChunkManifest',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    minChunks: Infinity,
    chunks: ['vendor']
  })
);

module.exports = environment;

In application.html.erb, we now load three JS files on the client side:

  <%= javascript_pack_tag "manifest.js" %>
  <%= javascript_pack_tag "vendor.js" %>
  <%= javascript_pack_tag "application.js" %>

Hope that helps! For reference, I ran bin/webpack --display-entrypoints for more detailed output:

                                       Asset       Size  Chunks                    Chunk Names
              vendor-e5362835eab5e8423e38.js    1.09 MB       0  [emitted]  [big]  vendor
         application-369071402e939381b3b2.js     1.1 MB       1  [emitted]  [big]  application
    server_rendering-7b68441c3dc38a040cd3.js    2.19 MB    2, 0  [emitted]  [big]  server_rendering
            manifest-92c7bc2fbbeb9c585893.js    5.97 kB       3  [emitted]         manifest
          vendor-e5362835eab5e8423e38.js.map    1.23 MB       0  [emitted]         vendor
     application-369071402e939381b3b2.js.map     674 kB       1  [emitted]         application
server_rendering-7b68441c3dc38a040cd3.js.map     1.9 MB    2, 0  [emitted]         server_rendering
        manifest-92c7bc2fbbeb9c585893.js.map    6.02 kB       3  [emitted]         manifest
                               manifest.json  606 bytes          [emitted]
Entrypoint application [big] = manifest-92c7bc2fbbeb9c585893.js manifest-92c7bc2fbbeb9c585893.js.map vendor-e5362835eab5e8423e38.js vendor-e5362835eab5e8423e38.js.map application-369071402e939381b3b2.js application-369071402e939381b3b2.js.map
Entrypoint server_rendering [big] = server_rendering-7b68441c3dc38a040cd3.js server_rendering-7b68441c3dc38a040cd3.js.map

So you see application gets split into three, while server_rendering stays whole.

RiccardoMargiotta avatar Mar 20 '18 21:03 RiccardoMargiotta

I should also mention that it was wingrunr21's post and gist that helped me get on the right track, so thanks for linking it, @BookOfGreg. 😃

RiccardoMargiotta avatar Mar 20 '18 21:03 RiccardoMargiotta

A little update for those curious - since it's so small, I wound up inlining the contents of manifest.js in a script tag in the head of the page. That saves a request, so now I just request vendor.js and application.js client-side.

RiccardoMargiotta avatar Apr 05 '18 13:04 RiccardoMargiotta

I wound up inlining the contents of manifest.js in a script tag in the head of the page

@RiccardoMargiotta, good idea. It feels convoluted how I have it here; do you have a better way. Thanks

module ApplicationHelper
  def manifest_tag
    path = File.join(Rails.root, 'public/packs/manifest.json')
    content = File.read(path)
    data = JSON.parse(content)
    filename = data["manifest.js"]
    filepath = File.join(Rails.root, 'public', filename)

    javascript_tag "window.webpackManifest = #{File.read(filepath)}"
  end
end
<%= manifest_tag %>

jDeppen avatar Sep 28 '18 16:09 jDeppen

@jDeppen We never came up with a particularly elegant way of doing this, here's what it looks like on our project:

<%= inline_js asset_pack_path "manifest.js" %>
module AssetsHelper
  def inline_js path
    javascript_tag(File.read(File.join(Rails.root, 'public', asset_path(path))))
  end
end

RiccardoMargiotta avatar Oct 01 '18 09:10 RiccardoMargiotta

@RiccardoMargiotta, thanks. Are you getting this in the terminal?

Started GET "/manifest-6ba779bac5026446ba70.js.map" for 127.0.0.1 at 2018-10-03 11:41:51 -0400  
ActionController::RoutingError (No route matches [GET] "/manifest-6ba779bac5026446ba70.js.map"):

It looks like that's happening because the sourceMappingURL is relative //# sourceMappingURL=manifest-6ba779bac5026446ba70.js.map

This doesn't occur when making the extra request rather than inlining. <%= javascript_pack_tag "manifest" %>

I'm going to throw the JS requests right above </body> for now since I'm prerendering.

jDeppen avatar Oct 03 '18 15:10 jDeppen

@jDeppen Ah, we're not actually using sourcemaps, we've updated our webpack development.js and production.js configs to remove them. I'm not sure if you'd be able to get around that while still inlining the file.

RiccardoMargiotta avatar Oct 03 '18 21:10 RiccardoMargiotta

I took @RiccardoMargiotta's solution and made it a bit more dynamic. This is what I ended up with, in case it helps anyone:

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

const bundlesPath = path.join(__dirname, '..', '..', 'app', 'javascript', 'packs');
const bundles = fs.readdirSync(bundlesPath).map((file) => {
  return file.replace('.js', '');
}).filter((file) => file !== 'server-bundle'); // This could be server_rendering, etc

// Extract common vendor libraries into a vendor pack
environment.plugins.append(
  'CommonsChunkVendor',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: (module) => {
      // this assumes your vendor imports exist in the node_modules directory
      return module.context && module.context.indexOf('node_modules') !== -1;
    },
    chunks: bundles
  })
);

// Extract webpack bootstrap into its own file. This allows a file that has not
// been changed to keep its hash when other files change.
environment.plugins.append(
  'CommonsChunkManifest',
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    minChunks: Infinity,
    chunks: ['vendor']
  })
);

brandoncc avatar May 19 '19 18:05 brandoncc

Possibly related to https://github.com/reactjs/react-rails/issues/970

BookOfGreg avatar Jun 08 '19 13:06 BookOfGreg

As per the discussion in the above thread, we can close this issue. #970 is a different issue we need to look after.

alkesh26 avatar Nov 02 '22 13:11 alkesh26