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

Code splitting doesn't work in production (because only main chunk gets copied to meteor folder) #138

Open
darkadept opened this issue Apr 1, 2016 · 12 comments

Comments

@darkadept
Copy link

The following works fine in dev mode, but does not work in prod mode.

I'm using react-router with getComponent to dynamically load a component when the route changes using the following code:

<Provider store={store}>
  <Router history={history}>
    <Route path="/" component={AppContainer}>
      <Route path="test" getComponent={(loc, cb) => {
        require.ensure([], require => {
          cb(null, require('TestComponent'));
        });
      }}
      />
    </Route>
  </Router>
</Provider>

Webpack successfully compiles my TestComponent as a separate chunk, usually 0.client.bundle.js or 1.client.bundle.js. When I try to browse to the test path (/test) I get the following error in my browser console:

Uncaught SyntaxError: Unexpected token <                           1.client.bundle.js

I guess Webpack is telling the browser to load this chunk on demand. The problem is that Meteor does not serve this chunk as JS but as an HTML page. This is the HTML it serves:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" type="text/css" class="__meteor-css__" href="/05bff44090d5f70f8d97870a9b747d3274af0c50.css?meteor_css_resource=true">  <link rel="stylesheet" type="text/css" class="__meteor-css__" href="/28408ed221754045c692118700b61a38695f33ca.css?meteor_css_resource=true">
<script type="text/javascript">__meteor_runtime_config__ = JSON.parse(decodeURIComponent("%7B%22meteorRelease%22%3A%22METEOR%401.2.1%22%2C%22PUBLIC_SETTINGS%22%3A%7B%22env%22%3A%22PROD%22%2C%22foo%22%3A%22bar%22%7D%2C%22ROOT_URL%22%3A%22http%3A%2F%2Flocalhost%3A3000%2F%22%2C%22ROOT_URL_PATH_PREFIX%22%3A%22%22%2C%22appId%22%3A%22sqjge32snhctxiwoe%22%2C%22autoupdateVersion%22%3A%220bba65b19bdec09864f836dd696caefa33ead55e%22%2C%22autoupdateVersionRefreshable%22%3A%22468d1a60ad6b5a9f8b370283a6bfc8ca9c3fe376%22%2C%22autoupdateVersionCordova%22%3A%22none%22%7D"));</script>
  <script type="text/javascript" src="/a25388218b6661007b61be8836458c55ccf2d96a.js?meteor_js_resource=true"></script>
<title>THR2</title>
</head>
<body>
<div id="dialog" class="ui inverted bottom sidebar"></div>
<div id="root" class="pusher">
  <header>
    <h1>Loading...</h1>
  </header>
</div>
<div id="modals" class="ui dimmer modals">
</div>
</body>
</html>

So Meteor is taking over and wrapping my request for the chunk in it's own Meteor page. The chunk code is actually in the /a25388218b6661007b61be8836458c55ccf2d96a.js?meteor_js_resource=true file.

Is there a way to get Webpack to ask for the correct file or for Meteor to serve up the chunk correctly?

@jedwards1211
Copy link
Owner

Anytime Meteor can't find the requested file, it just serves up the index page, which is not a great idea, in my opinion. That happens if I navigate to the meteor port instead of the webpack port.

Since this skeleton project isn't configured to put the Webpack bundle at a path like that, it seems you've tweaked the configuration somehow.
What's also strange is that there are no meteor package scripts in that header.

Also, you unfortunately need to use cb(null, require('TestComponent')).default (note .default) to get the default export from a Babel 6-transpiled module.

@darkadept
Copy link
Author

Hmm. Ok, so what you're saying is that it should work but my configuration is off? I have modified it but I'm not sure what I would have changed to do that. I'll clone your repo again and put together as basic of a test as possible and report back here. Thanks.

(And yeah, I actually do have the .default in my code. I forgot to include it above.)

@darkadept
Copy link
Author

Ok. The problem is with require in the require.ensure section. It doesn't work to require a file. It works fine under development: node dev.js but not if you deploy. Under production you get the following error in the browser console:

Uncaught SyntaxError: Unexpected token <

I'll include my deploy steps at the end of this post.
I checked out the master branch and made these changes.
In main_server.js comment out these two lines: (Disabling SSR for now)

// import App from './components/App.jsx';
...
  // console.log('React SSR:', React.renderToString(<App/>));

Create Sample.jsx in the components folder:

import React, {Component} from 'react';
export default class Sample extends Component
{
  render() {
    return (
      <div>Hello World!</div>
    );
  }
}

Modify App.jsx to look like this:

/* global ReactMeteorData */
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import reactMixin from 'react-mixin';
import BlazeTemplate from './BlazeTemplate';
import {Users, Posts} from 'collections';
import './App.css';

Meteor.call('sayHello', function(err, res) {
  console.log(res);
});

@reactMixin.decorate(ReactMeteorData)
export default class App extends Component {

  componentDidMount() {
    var el = this.refs.container;
    require.ensure([], (require) => {
      var Sample = require('./Sample').default;
      ReactDOM.render(React.createElement(Sample), el);
    });
  }

  getMeteorData() {
    return {
      users: Users.find().fetch()
    };
  }

  render() {
    let userCount = Users.find().fetch().length;
    let postsCount = Posts.find().fetch().length;
    return (
      <div className="App">
        {Meteor.isClient && <BlazeTemplate template={Template.loginButtons} />}
        <h1>Hello Webpack!</h1>
        <p>There are {userCount} users in the Minimongo  (login to change)</p>
        <p>There are {postsCount} posts in the Minimongo  (autopublish removed)</p>
          <div ref="container"/>
      </div>
    );
  }
}

My deploy steps are:

# This builds everything but doesn't actually use mup or whatever.
node deploy.js blah
cd meteor_core
meteor build ../build
cd ../build
mv meteor_core.tar.gz /opt/mydeploy
cd /opt/mydeploy
tar -zxf meteor_core.tar.gz
cd bundle/programs/server
nvm exec 0.10 npm install
cd ../..
export MONGO_URL='mongodb://mymongoserver:27017/test'
export ROOT_URL='http://localhost:3000'
export PORT=3000
nvm exec 0.10 node main.js

I'm running dev and building with Node 5.10.0 and NPM 3.8.3.
I'm using NVM to execute the production with Node 0.10.44 and NPM 2.15.0.

@jedwards1211
Copy link
Owner

Ah, the reason is that the script doesn't copy anything but the main client bundle into the meteor folder. It works in dev mode because all bundles are being served up straight from webpack-dev-server.

The webpack-meteor-tools branch might work with code splitting in prod. I know at least that in my own project, where I'm using the load-entry-chunks-only branch of webpack-meteor-tools, code splitting works in prod. I haven't had time to clean this up for general use yet.

@jedwards1211 jedwards1211 changed the title Meteor serving Webpack JS chunk as HTML when using React Router getComponent Code splitting doesn't work in production (because only main chunk gets copied to meteor folder) Apr 5, 2016
@darkadept
Copy link
Author

Ok. That makes a lot of sense now. I assume using webpack-meteor-tools would be a better approach but as a small workaround would it not work to modify the predeploy.js script to copy the necessary chunks over?

In my example above I noticed that webpack produced the 1.client.bundle.js chunk in webpack/assets. I copied that file to meteor_core/public/ and now production works.

So I'm going to modify predeploy.js to symlink any x.client.bundle.js to the meteor public folder. Am I missing anything to this approach? The x.client.bundle.js chunks seem to be minified already.

@jedwards1211
Copy link
Owner

Yes, certainly you can do that as well. Probably that would be a good stopgap fix for this project skeleton too

@darkadept
Copy link
Author

So would you use this project skeleton for a production app or are there other resources you would focus on? You mentioned webpack-meteor-tools and I have tried webpack:webpack but it's very slow when developing. (BTW, I love how fast HMR, especially in this project skeleton.)

Anyways, I'll include my predeploy.js script here in case someone else stumbles upon this problem. I'm not sure if it's PR worthy.

require('shelljs/global');

var fs = require('fs');
var path = require('path');
var dirs = require('./dirs');
var webpack = require('webpack');
var statsOptions = require('./statsOptions');

var makeConfig = require(path.join(dirs.webpack, 'make-webpack-config'));

var serverConfig = makeConfig({target: 'server', mode: 'production'});
var clientConfig = makeConfig({target: 'client', mode: 'production'});

var serverBundlePath = path.join(dirs.assets, 'server.bundle.js');
var clientBundlePath = path.join(dirs.assets, 'client.bundle.js');
var serverBundleLink = path.join(dirs.meteor, 'server/server.bundle.min.js');
var clientBundleLink = path.join(dirs.meteor, 'client/client.bundle.min.js');
var loadClientBundleLink = path.join(dirs.meteor, 'client/loadClientBundle.html');
var publicPath = path.join(dirs.meteor, 'public');
var chunkRegex = /\d+.client.bundle.js/i;

module.exports = function(callback) {
  exec('node core-js-custom-build.js');

  if (!process.env.NODE_ENV) {
    process.env.NODE_ENV = env.NODE_ENV = 'production';
  }

  if (fs.existsSync(loadClientBundleLink)) rm(loadClientBundleLink);
  if (fs.existsSync(serverBundleLink)) rm(serverBundleLink);

  if (!fs.existsSync(publicPath)) mkdir(publicPath);

  fs.readdirSync(publicPath).forEach(function(filename){
    if (chunkRegex.test(filename)) {
      rm(path.join(publicPath, filename));
    }
  });

  var serverCompiler = webpack(serverConfig);

  serverCompiler.run(function(err, stats) {
    if (err) {
      console.error(error);
      return callback(err);
    }
    console.log(stats.toString(statsOptions));
    if (stats.toJson().errors.length) {
      return callback(new Error('Webpack reported compilation errors'));
    }
    ln('-sf', serverBundlePath, serverBundleLink);
    compileClient();
  });

  function compileClient() {
    var clientCompiler = webpack(clientConfig);
    clientCompiler.run(function(err, stats) {
      if (err) {
        console.error(error);
        return callback(err);
      }
      console.log(stats.toString(statsOptions));
      if (stats.toJson().errors.length) {
        return callback(new Error('Webpack reported compilation errors'));
      }
      ln('-sf', clientBundlePath, clientBundleLink);

      fs.readdirSync(dirs.assets).forEach(function(filename){
        if (chunkRegex.test(filename)) {
          ln('-sf', path.join(dirs.assets, filename), path.join(publicPath, filename));
        }
      });

      return callback();
    });
  }
};

@jedwards1211
Copy link
Owner

In my production projects I'm using the load-entry-chunks-only-branch of webpack-meteor-tools with more complex build scripts than this skeleton project (we have to build multiple targets each with their own client/server bundles). Most of my work time is devoted to company projects so this repo is always lagging behind what we're doing. My production projects do have extensive code splitting.

@darkadept
Copy link
Author

Awesome! Well. I guess that closes this bug, at least for now. Thanks!

@jedwards1211
Copy link
Owner

Well, I'll leave it open, because while you have a solution, the current master still doesn't support this use case. Thanks for bringing it up!

@jedwards1211
Copy link
Owner

Hey @darkadept, I came out with a much better app skeleton that works with Meteor 1.4: jedwards1211/crater. Check it out!

@darkadept
Copy link
Author

Awesome! I'm super excited about this! I'll check it out asap. I hope I can help with finding bugs, etc.

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

No branches or pull requests

2 participants