Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trouble with SSR and splitChunks - window is not defined #2270

Closed
smgilchrist opened this issue Sep 5, 2019 · 8 comments
Closed

Trouble with SSR and splitChunks - window is not defined #2270

smgilchrist opened this issue Sep 5, 2019 · 8 comments
Labels
support Questions or unspecific problems

Comments

@smgilchrist
Copy link

I am currently trying to upgrade webpacker so that we can move from webpack 3 to 4. One of the big changes is changing from CommonsChunkPlugin to splitChunks, and then using javascript_packs_with_chunks_tag instead of javascript_pack_tag.

This is the set up of our current environment.js

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

const webpack = require('webpack')

environment.loaders.delete('nodeModules')

environment.plugins.append(
  'MomentEnglishOnly',
  new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en-gb/),
)

// when we use this, SSR throws an error
environment.splitChunks((config) => Object.assign({}, config, { 
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      }
    },
    runtimeChunk: true
  }
}))

const WebpackAssetsManifest = require('webpack-assets-manifest')
environment.plugins.insert(
  'Manifest',
  new WebpackAssetsManifest({
    entrypoints: true,
    writeToDisk: true,
    publicPath: true,
  }),
)

module.exports = environment

Here is an example of one of the packs we load:

<%= javascript_packs_with_chunks_tag 'public-pack' %>

Here is a snippet from public:

import "jquery"

// BOOTSTRAP
import '../../../vendor/assets/javascripts/bootstrap.min'

// CORE
import {} from 'jquery-ujs' //= require jquery_ujs

// FLOWPLAYER
import '../bundles/Flowplayer/flowplayer'

import Enrollment from '../bundles/Enrollment/components/Enrollment';
import SeoSubjectEnrollment from '../bundles/Enrollment/components/SeoSubjectEnrollment';
import SeoExamReviewsSchool from '../bundles/Enrollment/components/SeoExamReviewsSchool';

import ReactOnRails from 'react-on-rails';

ReactOnRails.register({
  Enrollment,
  SeoSubjectEnrollment,
  SeoExamReviewsSchool,
})

The splitChunks seem to be working as expected, and the app works as intended when I turn server side rendering off for all the react components, but when I turn the server side rendering on, I keep getting a window is not defined error.

I've tried a couple suggestions on how turn OFF splitChunks for server side rendering only, but the results ended up with splitChunks being turned off for both server side and client side.

Please let me know if there is anything else I should include in this, but essentially I am trying to find a solution to not use splitChunks when it's server side.

@smgilchrist smgilchrist changed the title Trouble with SSR and splitChunks Trouble with SSR and splitChunks - window is not defined Sep 5, 2019
@jakeNiemiec jakeNiemiec added the support Questions or unspecific problems label Sep 6, 2019
@jakeNiemiec
Copy link
Member

I would create a ticket in the ReactOnRails repo. SSR is outside of webpacker's scope.

@erikt9
Copy link

erikt9 commented Oct 1, 2019

I haven't used the Webpacker configuration much so don't know the exact syntax (currently using webpack manually without webpacker integration), but it is most likely that you need to set output.globalObject = 'this' in the Webpack configuration (webpack.config.js). I had a similar problem and setting that parameter did the trick. By default webpack will think you are compiling for a browser environment and use window as its default global variable.

https://webpack.js.org/configuration/output/#outputglobalobject

To make UMD build available on both browsers and Node.js, set output.globalObject option to 'this'.

@radik
Copy link

radik commented Dec 26, 2019

@smgilchrist Did you find a solution?

@andrefox333
Copy link

andrefox333 commented Mar 11, 2020

@smgilchrist I'm running into a similar issue when I use the cacheGroups option.

cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      }

Try removing that part and see if it works for you.

But if you got it to work, please share your solution! :)

@andrefox333
Copy link

Also @smgilchrist, check out this solution from this link:

reactjs/react-rails#970 (comment)

// We have two entry points, application and server_rendering.
// We always want to keep server_rendering intact, and must
// exclude it from being chunked.

const notServerRendering = name => name !== 'server_rendering';

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks(chunk) {
            return notServerRendering(chunk.name);
          }
        }
      }
    }
  }
}

This worked for me.

@RiccardoMargiotta
Copy link

I have an updated version of that snippet to also create a separate vendor_react bundle to allow for longer caching. The concept is still the same, blocking server_rendering.js from being chunked.

But I haven't had any luck trying to do what the original poster is doing, where application.js gets split down further with the default chunk config - we still just have one large application bundle. 😞 I'd like to at least route-split the app JS with additional entry points, but haven't gotten that working with the react-rails gem yet.

// We have two entry points, application and server_rendering.
// We always want to keep server_rendering intact, and must
// exclude it from being chunked.

const notServerRendering = name => name !== 'server_rendering';

module.exports = {
  optimization: {
    splitChunks: {
      chunks(chunk) {
        return notServerRendering(chunk.name);
      },
      cacheGroups: {
        vendor: {
          name: 'vendor',
          priority: -10,
          test: /[\\/]node_modules[\\/]/
        },
        vendor_react: {
          name: 'vendor_react',
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/
        }
      }
    }
  }
};
  <%= javascript_pack_tag "vendor_react.js", defer: true %>
  <%= javascript_pack_tag "vendor.js", defer: true %>
  <%= javascript_pack_tag "application.js", defer: true %>

@scraton
Copy link

scraton commented Aug 30, 2020

We wanted to have multiple entrypoints (beyond just application.js and server_rendering.js), along with split chunks to share code between all the entrypoints. As mentioned above, in order for server rendering to work, it needs to not be split into chunks. So we opted to use multi compiler mode in Webpack to achieve this.

config/webpack/environment.js

const webpack = require('webpack');
const cloneDeep = require('lodash/cloneDeep');
const { Environment, environment } = require('@rails/webpacker');

function cloneEnvironment(environment) {
  const cloned = new Environment();

  cloned.loaders = cloneDeep(environment.loaders);
  cloned.plugins = cloneDeep(environment.plugins);
  cloned.config = cloneDeep(environment.config);
  cloned.entry = cloneDeep(environment.entry);
  cloned.resolvedModules = cloneDeep(environment.resolvedModules);

  return cloned;
}

// add any shared configuration between client + server first, example:
// environment.loaders.get('sass').use.splice(-1, 0, {
//   loader: 'resolve-url-loader'
// });

const client = (environment) => {
  // don't compile server rendering entrypoint for client
  environment.entry.delete('server_rendering');

  // do any other client specific configuration here, example:
  // environment.plugins.append('favicons', favicons);

  // enable split chunks
  environment.splitChunks((config) => ({
    ...config,
    optimization: {
      runtimeChunk: {
        name: 'manifest',
      },
      splitChunks: {
        cacheGroups: {
          vendor: {
            name: 'vendor',
            priority: -10,
            test: /[\\/]node_modules[\\/]/,
          },
          default: {
            minChunks: 1,
            priority: -20,
            reuseExistingChunk: true,
          },
        },
      },
    },
  }));

  return environment.toWebpackConfig();
};

const server = (environment) => {
  const dev = process.env.NODE_ENV === 'development';

  // configure output
  const outputPath = environment.config.get('output.path');
  environment.config.set('output.path', dev ? `${outputPath}/server` : '/app/vendor/server');
  environment.config.set('output.publicPath', dev ? '/packs/server/' : '/server/');

  // configure bundle to work for SSR
  environment.config.set('output.libraryTarget', 'umd');
  environment.config.set('output.globalObject', 'this');

  // configure manifest to point to new output path
  const manifest = environment.plugins.get('Manifest');
  manifest.options.output = '/app/vendor/server/manifest.json';
  manifest.options.publicPath = dev ? '/packs/server/' : '/app/vendor/server/';

  // disable devServer configuration, only the one defined in client is used
  // See: https://webpack.js.org/configuration/dev-server/#devserver
  environment.config.delete('devServer');

  const config = environment.toWebpackConfig();

  // only output the server_rendering entry
  config.entry = { server_rendering: config.entry['server_rendering'] };

  return config;
};

environment.toWebpackConfigs = () => ([
  client(cloneEnvironment(environment)),
  server(cloneEnvironment(environment)),
]);

module.exports = environment;

config/webpack/production.js

process.env.NODE_ENV = process.env.NODE_ENV || 'production';

const environment = require('./environment');

module.exports = environment.toWebpackConfigs();

config/webpack/development.js

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

const environment = require('./environment');

module.exports = environment.toWebpackConfigs();

You will want to change the /app/vendor/server paths to something sensible to your environment. You can leave these alone, but we didn't want to expose the server_rendering.js file to a publicly accessible path (except in development, which is required to work with webpack-dev-server).

The configuration for changing the server manifest output path is also crucial. If you use multiple configurations, you will overwrite the manifest.json file with either just the client outputs or the server outputs, breaking the Rails integration.

In order for the react-rails gem to use this alternative manifest, we needed to implement our own asset_container_class. This is based on React::ServerRendering::WebpackerManifestContainer. You could probably write something better and less copy-pastey. :)

lib/assets/server_manifest_container.rb

module Assets
  class ServerManifestContainer < ::React::ServerRendering::WebpackerManifestContainer
    def refresh_manifest
      @manifest_data = load_manifest
    end

    def find_asset(logical_path)
      if Webpacker.dev_server.running?
        ds = Webpacker.dev_server

        asset_path = manifest_lookup(logical_path).to_s
        asset_path.slice!("#{ds.protocol}://#{ds.host_with_port}")

        dev_server_asset = URI.open("#{ds.protocol}://#{ds.host_with_port}#{asset_path}").read
        dev_server_asset.sub!(CLIENT_REQUIRE, '//\0')
        dev_server_asset
      else
        File.read(file_path(logical_path))
      end
    end

    def file_path(path)
      manifest_lookup!(path)
    end

    private

    def manifest_data
      if config.cache_manifest?
        @manifest_data ||= load_manifest
      else
        refresh_manifest
      end
    end

    def manifest_lookup(name, pack_type = {})
      manifest_find(full_pack_name(name, pack_type[:type]))
    end

    def manifest_lookup!(name, pack_type = {})
      manifest_lookup(name, pack_type)
    end

    def manifest_find(name)
      manifest_data[name.to_s].presence
    end

    def full_pack_name(name, pack_type)
      return name unless File.extname(name.to_s).empty?
      "#{name}.#{manifest_type(pack_type)}"
    end

    def manifest_type(pack_type)
      case pack_type
      when :javascript then "js"
      when :stylesheet then "css"
      else pack_type.to_s
      end
    end

    def server_manifest_path
      # should match `manifest.options.output` defined in environment.js
      ::Rails.root.join('vendor', 'server', 'manifest.json')
    end

    def load_manifest
      if server_manifest_path.exist?
        JSON.parse server_manifest_path.read
      else
        {}
      end
    end
  end
end

config/initializers/react.rb

React::ServerRendering::BundleRenderer.asset_container_class = ::Assets::ServerManifestContainer

Then you can reference the chunks in your templates as normal:

<%= stylesheet_packs_with_chunks_tag 'application', 'other_chunk' %>
<%= javascript_packs_with_chunks_tag 'application', 'other_chunk', async: true %>

And server rendering should work just fine as well.

I hope this is helpful to anyone else out there trying to do SSR with split chunks!

@guillaumebriday
Copy link
Member

Can this issue be closed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
support Questions or unspecific problems
Projects
None yet
Development

No branches or pull requests

8 participants