pmmmwh / react-refresh-webpack-plugin

A Webpack plugin to enable "Fast Refresh" (also previously known as Hot Reloading) for React components.
MIT License
3.15k stars 194 forks source link

IE11 url polyfilling doesn't help because new URL() is referenced in injected helpers #246

Closed wh1t3cAt1k closed 3 years ago

wh1t3cAt1k commented 4 years ago

( cc @pmmmwh )

I am using this plugin (0.4.3 latest version as of today) to support fast refresh in an IE11 environment, in conjunction with webpack@5 and webpack serve (ex. webpack-dev-server).

I took all precaution to inject all the necessary polyfills by having a polyfill chunk in my <head>, which includes the polyfill for url.

That's the source code of the chunk, it is of course compiled to ES5 by Babel.

// IE11 - One Love šŸ’•
// -
import 'core-js/stable'; // URL polyfill is included here, I also tried adding `url-polyfill` but it makes no difference (no surprise).
import 'regenerator-runtime/runtime';

It resides in a chunk called evil-decrepit-browsers-polyfill which, as I mentioned, is the first chunk injected into my index.html - nothing ever gets before it.

The problem is that dev server in the hot mode (I use a combination of inline and hot) injects react-refresh runtime and helpers even to this chunk!

(function(module, __unused_webpack_exports, __webpack_require__) {
/* provided dependency */ var __react_refresh_utils__ = __webpack_require__(/*! ../../node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js */ "../../node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js");

/* provided dependency */ var __react_refresh_error_overlay__ = __webpack_require__(/*! ../../node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js */ "../../node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js");

__webpack_require__.$Refresh$.runtime = __webpack_require__(/*! ../../node_modules/react-refresh/runtime.js */ "../../node_modules/react-refresh/runtime.js");
__webpack_require__.$Refresh$.setup(module.id);

// !!! My URI polyfilling is now below this code! !!!

And during the invocation of this injected code at the top I get an error in getSocketUrlParts(resourceQuery):

image

Object doesn't support this action

I confirm that the error happens because I haven't had a chance to polyfill this yet at the moment of code execution.

Using the below configuration doesn't help

                  new ReactRefreshWebpackPlugin({
                      exclude: /(node_modules|evil-decrepit-browsers-polyfill)/,
                  }),

This is a vicious cycle, the helpers seem to be injected into every file and I can't polyfill before the injected code executes.

Any thoughts?

If this is relevant, here are some other parts of my webpack configuration:

entry: {
        taskpane: path.resolve(sourceRoot, 'taskpane', 'index.tsx'),
        'oauth-dialog': path.resolve(
            sourceRoot,
            'oauth-dialog',
            'oauth-dialog.ts'
        ),
        // Separate entry point injected by html-webpack-plugin and HtmlWebpackInjector as the SOLE entry in the <head>, 
        // even before the 'runtime' chunk. Before any other chunks, for that matter. But it doesn't help :)
        // -
        'evil-decrepit-browsers-polyfill_head': '@velixo/web-polyfill',
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
        },
        runtimeChunk: 'single',
    },
wh1t3cAt1k commented 4 years ago

Perhaps some sort of configuration option to exclude a particular entry (i.e. polyfill) from processing altogether?

pmmmwh commented 4 years ago

Hi, is it possible to post your full webpack config?

Also, can you try excluding @velixo/web-polyfill instead?

wh1t3cAt1k commented 4 years ago

Hi Michael.

I will try excluding and will post my full config tomorrow.

Just in case, what difference would it make if I use polyfills directly in the entry point(s) (I have two) as opposed to importing the polyfill package?

AFAIU the usage of URL will still be injected and executed before any code in my entry point gets a chance to execute, nope?

On Sun, Nov 8, 2020, 14:26 Michael Mok notifications@github.com wrote:

Hi, is it possible to post your full webpack config?

Also, can you try excluding @velixo/web-polyfill instead?

ā€” You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/246#issuecomment-723563949, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABHLV3VOGZWP4YPICUUSVNDSOZ57HANCNFSM4TJ5IRIQ .

wh1t3cAt1k commented 4 years ago

@pmmmwh

Here is more of my webpack config:

By the way, the reason I used the @velixo/web-polyfill package (it is an internal one) instead of importing the polyfills directly is because I reuse it for two web-based projects in a monorepo structure, and didn't want to repeat myself. However I still don't see how direct import would help?..

const configuration: Configuration & { devServer: unknown } = {
    mode,
    devtool: 'source-map',
    entry: {
        taskpane: path.resolve(sourceRoot, 'taskpane', 'index.tsx'),
        'oauth-dialog': path.resolve(
            sourceRoot,
            'oauth-dialog',
            'oauth-dialog.ts'
        ),
    },
    output: {
        path: __dirname + '/wwwroot',
        publicPath,
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
        },
        runtimeChunk: 'single',
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.html', '.js'],
        fallback: {
            fs: require.resolve('browserify-fs'),
            timers: require.resolve('timers-browserify'),
            stream: require.resolve('stream-browserify'),
            os: require.resolve('os-browserify/browser'),
            process: require.resolve('process/browser'),
        },
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                exclude: /node_modules/,
                use: [
                    ...(isHotLoadingEnabled
                        ? [
                              {
                                  loader: 'babel-loader',
                                  options: {
                                      plugins: [
                                          require.resolve(
                                              'react-refresh/babel'
                                          ),
                                      ],
                                  },
                              },
                          ]
                        : []),
                    {
                        loader: 'ts-loader',
                        options: {
                            configFile: 'tsconfig.build.json',
                            getCustomTransformers: (): unknown => ({
                                before: [tsNameof],
                            }),
                        },
                    },
                ],
            },
            {
                test: /\.m?jsx?$/,
                include: new RegExp(
                    'node_modules\\' +
                        path.sep +
                        '...several specific packages I need to downcompile to ES5 - don't want to process whole node_modules...'
                ),
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            [
                                '@babel/preset-env',
                                {
                                    targets: {
                                        ie: '11',
                                    },
                                },
                            ],
                        ],
                    },
                },
            },
            {
                test: /\.m?js/,
                resolve: {
                    // work around a breaking change in webpack 5 that requires everyone to fully specify imports
                    fullySpecified: false,
                },
            },
            {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader'],
            },
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                use: {
                    loader: 'file-loader',
                    options: {
                        name: 'assets/[name].[ext]',
                    },
                },
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: isHotLoadingEnabled ? [] : ['**/*'],
        }),
        new CopyWebpackPlugin({
            // @ts-ignore
            patterns: [
                {
                    to: 'taskpane.css',
                    from: path.resolve(sourceRoot, 'taskpane', 'taskpane.css'),
                },
            ],
        }),
        new MiniCssExtractPlugin({
            filename: isProductionMode ? '[name].css' : '[name].[hash].css',
            chunkFilename: isProductionMode ? '[id].css' : '[id].[hash].css',
        }),
        new HtmlWebpackPlugin({
            filename: 'taskpane.html',
            template: path.resolve(sourceRoot, 'taskpane', 'taskpane.html'),
            chunks: ['taskpane'],
        }),
        new HtmlWebpackPlugin({
            filename: Constants.oauthDialogFilename,
            template: path.resolve(
                sourceRoot,
                'oauth-dialog',
                Constants.oauthDialogFilename
            ),
            chunks: ['oauth-dialog'],
        }),
        new CopyWebpackPlugin({
            // @ts-ignore
            patterns: [
                {
                    from: './assets',
                    to: 'assets',
                    globOptions: {
                        ignore: ['*.scss'],
                    },
                },
            ],
        }),
        new EnvironmentPlugin(
            EnvironmentConfiguration.getClientSideAvailableVariables()
        ),
        ...(shouldAnalyzeWebpackBundle
            ? [new BundleAnalyzerPlugin({ analyzerMode: 'static' })]
            : []),
        ...(isHotLoadingEnabled
            ? [
                  new ReactRefreshWebpackPlugin({
                      forceEnable: true,
                  }),
              ]
            : []),
    ],
    target: ['web', 'es5'],
    devServer: {
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
        port: 5000,
        https: false,
        hot: isHotLoadingEnabled,
        inline: isHotLoadingEnabled,
        clientLogLevel: 'trace',
        publicPath,
        contentBase: outDir,
        contentBasePublicPath: publicPath,
    },
};

export default configuration;
pmmmwh commented 4 years ago

Hi! I'll take a look at the webpack config and test things tomorrow.

However to answer some of your questions:

I will try excluding

Did that work for you?

AFAIK the usage of URL will still be injected and executed before any code in my entry point gets a chance to execute, nope?

In the plugin we do inject entries BEFORE your entries, so when the order is implicit (like how you would do it within an HTML which is out of Webpack's control) the detetion of order is not guaranteed. However I believe Webpack 5's Entry Descriptor feature does solve this problem by making the dependency explicit.

Just in case, what difference would it make if I use polyfills directly in the entry point(s) (I have two) as opposed to importing the polyfill package?

There is none.

pmmmwh commented 4 years ago

An update to this - the next minor (major?) release of the plugin will contain an option to polyfill the URL api (with core-js/features/url) - we will ensure that all code dealing with URL from the plugin will use the imported version instead.

I'm thinking to have it work a bit like a "ponyfill", where the functionality will only be patched within the scope of the plugin but not globally.

wafflepie commented 3 years ago

In case anyone wants to make this awesome plugin work with IE11 before the fix is ready, here's my solution. Assuming you are using HtmlWebpackPlugin, put this into the <head> element in your template:

    <% if (webpackConfig.mode !== 'production') { %>
        <!--
            HACK: Until https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/246 is resolved,
            we need to polyfill window.URL manually outside of the webpack bundle, otherwise an error
            is thrown in IE 11. Polyfilling via webpack is not an option, because all the entry points
            get wrapped by the React Refresh plugin.
        -->
        <script src="https://polyfill.io/v3/polyfill.min.js?features=URL"></script>
    <% } %>

Works just fine with webpack@5 and webpack-plugin-serve. I couldn't get webpack-dev-server to work with webpack@5, because the 4-beta.0 version doesn't support IE11 due to https://github.com/webpack/webpack-dev-server/issues/2904 and related issues, and the latest stable version requires target: 'web' (see https://github.com/webpack/webpack-dev-server/issues/2758) and target: ['es5', 'web'] (see webpackConfig.target docs) simultaneously to fix all the issues.

pmmmwh commented 3 years ago

Fixed in #278 šŸŽ‰