preactjs / preact-compat

ATTENTION: The React compatibility layer for Preact has moved to the main preact repo.
http://npm.im/preact-compat
MIT License
949 stars 148 forks source link

Webpack 4 "--mode production" breaks #484

Closed EdmundMai closed 6 years ago

EdmundMai commented 6 years ago

For some reason, when I start the server with

cross-env NODE_ENV=production webpack --config='config/webpack.config.js' --mode production

it breaks and ReactDOM.render(...) returns <undefined></undefined>. Changing it to --mode development fixes it.

cross-env NODE_ENV=production webpack --config='config/webpack.config.js' --mode development

Webpack config:

  module.exports = merge(common, {
    bail: true,
    devtool: shouldUseSourceMap ? "source-map" : false,
    devtool: "cheap-module-source-map",
    entry: paths.appSrc + "/script.js",
    output: {
      path: paths.appBuild,
      filename: `static/js/oyster-webnode-${APP_VERSION}.min.js`,
      chunkFilename: "static/js/[name].chunk.js",
      publicPath: publicPath,
      devtoolModuleFilenameTemplate: info =>
        path
          .relative(paths.appSrc, info.absoluteResourcePath)
          .replace(/\\/g, "/")
    },
    resolve: {
      modules: ["node_modules", paths.appNodeModules].concat(
        process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
      ),
      extensions: [".web.js", ".mjs", ".js", ".json", ".web.jsx", ".jsx"],
      alias: {
        react: "preact-compat",
        "react-dom": "preact-compat"
      },
      plugins: [new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])]
    },
    plugins: [
      new BundleAnalyzerPlugin({
        generateStatsFile: generateStatsFile
      }),
      new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml,
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true
        }
      }),
      new InterpolateHtmlPlugin(env.raw)
    ]
  });
nehaabrol87 commented 6 years ago

@developit This is the same issue I was facing 3 weeks ago. As I mentioned on the issue there are 5 plugins that mode = production automatically adds I bet its one of those 5

Provides process.env.NODE_ENV with value production. Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin and UglifyJsPlugin

tadeuszwojcik commented 6 years ago

Happens for me as well, in production mode, VNode lacks properties defined via Object.defineProperty, this is what I got:

{
  "children": [...],
  "attributes": {...},
  "preactCompatUpgraded": true,
  "preactCompatNormalized": true
}

There is no type of props there. Everything looks fine in dev mode.

Narrowed it down to UglifyJsPlugin plugin. Turning it off helps, except bundle is size is much larger. I've noticed that when minified preact-compat createElement isn't called at all. Setting unused : false seems to be the temp solution at least for now.

wongjn commented 6 years ago

Analysis

I looked into this further and it seems (at least in preact.ems.js) the initial VNode class declaration gets mangled into the h() function when run through the UglifyJSPlugin with default options:

Before:

/** Virtual DOM Node */
function VNode() {}

// Skipped code

function h(nodeName, attributes) {
    // Skipped code

    var p = new VNode();
    // Skipped code

    return p;
}

After (interpreted by me):

/*** No class declaration here ***/

function h(nodeName, attributes) {
    // Skipped code

       // `function VNode() {}` becomes the below anonymous class
    var p = new function() {};
    // Skipped code

    return p;
}

As a side-node, even renaming it to anything but still kept the in top-level scope would still work:

/** Virtual DOM Node */
function fooBar() {}

// Skipped code

function h(nodeName, attributes) {
    // Skipped code

    var p = new fooBar();
    // Skipped code

    return p;
}

So it seems the class declarations is being used as a global somewhere or there is some identity conditional that never evaluates to true (since a new instance is created in the mangled form, instead of re-using the top-level declaration).

Workaround

A work around for this is to use reduce_funcs which stops the function getting mangled inside the h() function. The above comment about unused: false also works because again, it keeps the function in the global scope.

Here is a webpack config that can be merged into an existing config (I use WebPack merge to do this). It splits out preact into its own chunk file so that only it gets applied the slightly differing Uglify options (this is because Uglify exclude/include options matches filenames after concatenation, not source chunk names). If you wanted to keep preact in your main bundle, then you can simply change the include/exclude options to the chunk with preact in and get rid of the preact cacheGroup and the chunkFilename setting.

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  output: {
    // Add chunk name to filename so Uglify's include/exclude can target it.
    chunkFilename: '[chunkhash].[name].js',
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        // Create a separate chunk for preact to apply slightly different
        // Uglify options on it.
        preact: {
          name: 'preact',
          chunks: 'all',
          minSize: 0,
          test: /[\\/]preact[\\/]/,
          priority: 99,
        },
      },
    },
    minimizer: [
      // Prevent function reduction in preact for preact-compat to work.
      new UglifyJsPlugin({
        include: /preact\.js$/,
        uglifyOptions: {
          compress: {
            reduce_funcs: false,
          },
        },
      }),
      // Normal uglifying for everything else.
      new UglifyJsPlugin({
        exclude: /preact\.js$/,
        cache: true,
        parallel: true,
      }),
    ],
  },
};
gerhardsletten commented 6 years ago

I did also had this problem where I was using preact in production. But found that I would need the reduce_vars setting for UglifyJSPlugin to making react-router work:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
// and this config
optimization: {
  minimizer: [
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          reduce_vars: false // see https://github.com/developit/preact/issues/961
        }
      }
    })
  ]
}

And with another package redux-connect there was a problem because it in the transpiled files was require babel-runtime/helpers/jsx instead of react

var _jsx2 = require('babel-runtime/helpers/jsx');

I am no babel-expert but it looks to me like babel-runtime is a deprecated method to use, so I upgraded the package and after switching to this my project works fine with preact-compat and webpack 4

developit commented 6 years ago

Sorry for the absence. I believe we may have merged a workaround for this Uglify bug in Preact itself, which will fix the issues described here. Sorry for the really difficult debugging!

Here's the PR to Preact: https://github.com/developit/preact/pull/1153

We'll release it as a patch update shortly. As mentioned by @gerhardsletten, a temporary workaround is to disable reduce_funcs which contains the constructor miscategorization issue.

developit commented 6 years ago

This is fixed in Preact 8.3.0. Update and enjoy!