How to effectively create a production build within a SingleSPA React App?

1.6k views Asked by At

Every time we access our deployed ReactJS App, we get a red square for the dev-tools, saying we're not on an optimized build for production.

The app is a SingleSPA Microfrontends web app.

The fact is that every microfrontend and the root orchestrator get built in production mode.

Follows a configuration for a single microfrontend, if you need other stuff please ask me, since I'm a little newbie with singleSPA, maybe I'm forgotting to put something

This is the command that Jenkins runs when it deploys:

"build:prod": "NODE_ENV=production webpack --mode=production --config config/webpack.config.prod.js",

This is out webpack.config.prod.js

require('./env');

const { merge } = require('webpack-merge');
const autoprefixer = require('autoprefixer');
const PostcssFlexbugsFixes = require('postcss-flexbugs-fixes');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const path = require('path');
const webpack = require('webpack');
const common = require('./webpack.common.js');
const paths = require('./paths');

// This is the development configuration.
// It is focused on developer experience and fast rebuilds.
// The production configuration is different and lives in a separate file.
module.exports = (a, b) => merge(common(a, b), {
  mode: 'production',
  devtool: 'source-map',
  resolve: {
    fallback: {
      // Some libraries import Node modules but don't use them in the browser.
      // Tell Webpack to provide empty mocks for them so importing them works.
      dgram: false,
      fs: false,
      net: false,
      tls: false,
      // eslint-disable-next-line camelcase
      child_process: false,
    },
    // This allows you to set a fallback for where Webpack should look for modules.
    // We placed these paths second because we want `node_modules` to "win"
    // if there are any conflicts. This matches Node resolution mechanism.
    // https://github.com/facebookincubator/create-react-app/issues/253
    modules: ['node_modules', paths.appNodeModules].concat(
      // It is guaranteed to exist because we tweak it in `env.js`
      process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
    ),
    // These are the reasonable defaults supported by the Node ecosystem.
    // We also include JSX as a common component filename extension to support
    // some tools, although we do not recommend using it, see:
    // https://github.com/facebookincubator/create-react-app/issues/290
    // `web` extension prefixes have been added for better support
    // for React Native Web.
    extensions: ['.web.js', '.js', '.json', '.web.jsx', '.jsx'],
    plugins: [
      // Prevents users from importing files from outside of src/ (or node_modules/).
      // This often causes confusion because we only process files within src/ with babel.
      // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
      // please link the files into your node_modules/ and let module-resolution kick in.
      // Make sure your source files are compiled, as they will not be processed in any way.
      new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
    ],
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /node_module\/dagre\/dist\/dagre.core.js/,
        use: [
          'imports?this=>window',
          'script',
        ],
      },
      // First, run the linter.
      // It's important to do this before Babel processes the JS.
      {
        test: /\.(js|jsx)$/,
        include: paths.appSrc,
      },
      {
        // "oneOf" will traverse all following loaders until one will
        // match the requirements. When no loader matches it will fall
        // back to the "file" loader at the end of the loader list.
        oneOf: [
          // "url" loader works like "file" loader except that it embeds assets
          // smaller than specified limit in bytes as data URLs to avoid requests.
          // A missing `test` is equivalent to a match.
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          // Process JS with Babel.
          {
            test: /\.(js|jsx)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: {
              // This is a feature of `babel-loader` for webpack (not Babel itself).
              // It enables caching results in ./node_modules/.cache/babel-loader/
              // directory for faster rebuilds.
              cacheDirectory: true,
            },
          },
          // "postcss" loader applies autoprefixer to our CSS.
          // "css" loader resolves paths in CSS and adds assets as dependencies.
          // "style" loader turns CSS into JS modules that inject <style> tags.
          // In production, we use a plugin to extract that CSS to a file, but
          // in development "style" loader enables hot editing of CSS.
          {
            test: /\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  // Necessary for external CSS imports to work
                  // https://github.com/facebookincubator/create-react-app/issues/2677
                  postcssOptions: {
                    plugins: () => [
                      PostcssFlexbugsFixes,
                      autoprefixer({
                        overrideBrowserslist: [
                          '>1%',
                          'last 4 versions',
                          'Firefox ESR',
                          'not ie < 9', // React doesn't support IE8 anyway
                        ],
                        flexbox: 'no-2009',
                      }),
                    ],
                  },
                },
              },
            ],
          },
          {
            test: /\.less$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                  url: false,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  postcssOptions: {
                    ident: 'postcss',
                    plugins: () => [
                      PostcssFlexbugsFixes,
                      autoprefixer({
                        overrideBrowserslist: [
                          '>1%',
                          'last 4 versions',
                          'Firefox ESR',
                          'not ie < 9', // React doesn't support IE8 anyway
                        ],
                        flexbox: 'no-2009',
                      }),
                    ],
                  },
                },
              },
              {
                loader: 'less-loader',
                options: {
                  lessOptions: {
                    relativeUrls: true,
                    javascriptEnabled: true,
                    paths: [path.resolve(__dirname, 'node_modules')],
                  },
                },
              },
            ],
          },
          {
            test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
            use: [
              {
                loader: 'babel-loader',
              },
              {
                loader: '@svgr/webpack',
                options: {
                  babel: false,
                  icon: true,
                },
              },
            ],
          },
          // "file" loader makes sure those assets get served by WebpackDevServer.
          // When you `import` an asset, you get its (virtual) filename.
          // In production, they would get copied to the `build` folder.
          // This loader don't uses a "test" so it will catch all modules
          // that fall through the other loaders.
          {
            // Exclude `js` files to keep "css" loader working as it injects
            // it's runtime that would otherwise processed through "file" loader.
            // Also exclude `html` and `json` extensions so they get processed
            // by webpacks internal loaders.
            exclude: [/\.js$/, /\.html$/, /\.json$/],
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
        ],
      },
      // ** STOP ** Are you adding a new loader?
      // Make sure to add the new loader(s) before the "file" loader.
    ],
  },
  plugins: [
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1,
    }),
  ],
  // Turn off performance hints during development because we don't do any
  // splitting or minification in interest of speed. These warnings become
  // cumbersome.
  performance: {
    hints: false,
  },
});

This is our webpack.common.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { merge } = require('webpack-merge');

const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const path = require('path');
const singleSpaDefaults = require('webpack-config-single-spa-react');
const webpack = require('webpack');

const dotenv = require('dotenv')
  .config({ path: `.env.${process.env.NODE_ENV}` });

module.exports = (webpackConfigEnv, argv) => {
  const defaultConfig = singleSpaDefaults({
    orgName: 'xxx',
    projectName: 'yyy',
    webpackConfigEnv,
    argv,
  });

  return merge(defaultConfig, {
    module: {
      rules: {
        test: /\.(bmp|png|jpg|jpeg|gif|webp)$/i,
        exclude: /node_modules/,
        type: 'asset/resource',
      },
    },
    resolve: {
      fallback: {
        https: false,
        http: false,
      },
      alias: {
        '@Api': path.resolve(__dirname, '../src/api/'),
        '@Components': path.resolve(__dirname, '../src/components/'),
        '@Container': path.resolve(__dirname, '../src/container/'),
        '@Img': path.resolve(__dirname, '../src/resources/images/'),
        '@Helpers': path.resolve(__dirname, '../src/helpers/'),
        '@Src': path.resolve(__dirname, '../src/'),
        '@State': path.resolve(__dirname, '../src/store/state/'),
        '@Store': path.resolve(__dirname, '../src/store/'),
        'react-dom': '@hot-loader/react-dom',
      },
    },
    plugins: [
      new CaseSensitivePathsPlugin(),
      new CleanWebpackPlugin(),
      new webpack.DefinePlugin({
        'process.env': JSON.stringify(dotenv.parsed),
      }),
      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
    ],
    externals: {
      lodash: 'lodash',
      moment: 'moment',
      react: 'react',
      'react-dom': 'react-dom',
    },
  });
};

This is our env.js

/* eslint-disable */

const fs = require('fs');
const path = require('path');
const paths = require('./paths');

// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];

const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
  throw new Error(
    'The NODE_ENV environment variable is required but was not specified.'
  );
}

// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
  `${paths.dotenv}.${NODE_ENV}.mock`,
  `${paths.dotenv}.${NODE_ENV}.development`,
  `${paths.dotenv}.${NODE_ENV}.production`,
  //`${paths.dotenv}.${NODE_ENV}.local`,
  `${paths.dotenv}.${NODE_ENV}`,
  // Don't include `.env.local` for `test` environment
  // since normally you expect tests to produce the same
  // results for everyone
  paths.dotenv,
].filter(Boolean);

// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set.
// https://github.com/motdotla/dotenv
dotenvFiles.forEach((dotenvFile) => {
  if (fs.existsSync(dotenvFile)) {
    require('dotenv-expand')(
      require('dotenv').config({
        path: dotenvFile,
      })
    );
  }
});

// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebookincubator/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
  .split(path.delimiter)
  .filter((folder) => folder && !path.isAbsolute(folder))
  .map((folder) => path.resolve(appDirectory, folder))
  .join(path.delimiter);

// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i;

function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    // .filter((key) => REACT_APP.test(key))
    .reduce((env, key) => {
      env[key] = process.env[key];
      return env;
    },
    {
      // Useful for determining whether we’re running in production mode.
      // Most importantly, it switches React into the correct mode.
      NODE_ENV: process.env.NODE_ENV || 'development',
      // Useful for resolving the correct path to static assets in `public`.
      // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
      // This should only be used as an escape hatch. Normally you would put
      // images into the `src` and `import` them in code to get their paths.
      PUBLIC_URL: publicUrl,
    }
  );
  // Stringify all values so we can feed into Webpack DefinePlugin
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
  };

  return { raw, stringified };
}

module.exports = getClientEnvironment;
1

There are 1 answers

0
Marcus R On

This doesn't seem to be a configuration issue with the build or configuration. It sounds like you might be using the importmap overrides library that comes with single-spa. Using the dev-libs property when using this library like causes it do use development bundles.

<import-map-overrides-full
   show-when-local-storage="devtools"
   dev-libs

></import-map-overrides-full>

The dev-libs attribute indicates that you prefer using development versions of third party libraries like react when the import-map-overrides ui is active. The presence of that attribute turns on this feature. For example, if you have react in your import map pointing to https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js the dev-libs attribute will automatically override it to https://cdn.jsdelivr.net/npm/react/umd/react.development.js. You can also turn this feature on/off via localStorage. localStorage.setItem('import-map-overrides-dev-libs', false) will forcibly turn this feature off, while calling it with true will turn it on.