React not found from Webpack generated micro-frontend using @patternfly/react-core

Hi PatternFly community, this is a cross-post from SO. Hope that’s ok:

Inside an existing, Angular based, container app, with multiple Angular micro-frontends, I want to load another one based on React and Atlasmap. However, I keep getting the following exception:

zone.js:209 Uncaught TypeError: Class extends value undefined is not a constructor or null
    at Module../node_modules/@patternfly/react-core/dist/esm/components/FormSelect/FormSelect.js (FormSelect.js:15)
    at __webpack_require__ (bootstrap:24)
    at fn (hot module replacement:61)
    at Module../node_modules/@patternfly/react-core/dist/esm/components/InputGroup/InputGroup.js (main.bundle.js:25253)
    at __webpack_require__ (bootstrap:24)
    at fn (hot module replacement:61)
    at Module../node_modules/@atlasmap/atlasmap/dist/atlasmap.esm.js (main.bundle.js:245)
    at __webpack_require__ (bootstrap:24)
    at fn (hot module replacement:61)
    at Module../src/App.tsx (index.js:176)

Which indicates that somehow React is not loaded. What I’ve tried so far:

  • Adding React to the import map
  • Overriding the module import with the import map dev tool
  • Trying to include React inside the webpack generated bundle
  • Yarn instead of npm
  • Adding React directly to the <head> of the container app
  • The same with Pika’s “ESM” React build

All don’t seem to work, so it probably boils down to an incorrect Webpack configuration. The config is getting pretty big, so I’ll post it below, it’s based on several blogposts and the patternfly/react Webpack config recommendation

Versions in use:
“single-spa-react”: “^4.0.0”,
“ts-config-single-spa”: “^2.0.0”,
“typescript”: “^4.1.2”,
“webpack”: “^5.8.0”,
“webpack-cli”: “^4.2.0”,
“webpack-config-single-spa-react”: “^2.0.0”,
“webpack-config-single-spa-react-ts”: “^2.0.0”,
“webpack-config-single-spa-ts”: “^2.0.0”,
“react”: “^16.13.1”
“react-dom”: “^16.13.1”
@atlasmap/atlasmap”: “^2.2.2”,

const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa-react-ts");
const path = require("path");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const BG_IMAGES_DIRNAME = "bgimages";
const ASSET_PATH = process.env.ASSET_PATH || "/";

module.exports = (webpackConfigEnv, argv) => {
  const defaultConfig = singleSpaDefaults({
    orgName: "",
    projectName: "data-mapping-mf",
    webpackConfigEnv,
    argv,
  });

  delete defaultConfig.devServer.disableHostCheck;
  // for debugging uncomment the line below
  //  console.log(JSON.stringify(defaultConfig));

  return merge(defaultConfig, {
    module: {
      rules: [
        {
          test: /\.(tsx|ts|jsx)?$/,
          use: [
            {
              loader: "ts-loader",
              options: {
                transpileOnly: true,
                experimentalWatchApi: true,
              },
            },
          ],
        },
        {
          test: /\.(svg|ttf|eot|woff|woff2)$/,
          // only process modules with this loader
          // if they live under a 'fonts' or 'pficon' directory
          include: [
            path.resolve(__dirname, "node_modules/patternfly/dist/fonts"),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-core/dist/styles/assets/fonts"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-core/dist/styles/assets/pficon"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/patternfly/assets/fonts"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/patternfly/assets/pficon"
            ),
          ],
          use: {
            loader: "file-loader",
            options: {
              // Limit at 50k. larger files emited into separate files
              limit: 5000,
              outputPath: "fonts",
              name: "[name].[ext]",
            },
          },
        },
        {
          test: /\.svg$/,
          include: (input) => input.indexOf("background-filter.svg") > 1,
          use: [
            {
              loader: "url-loader",
              options: {
                limit: 5000,
                outputPath: "svgs",
                name: "[name].[ext]",
              },
            },
          ],
        },
        {
          test: /\.svg$/,
          // only process SVG modules with this loader if they live under a 'bgimages' directory
          // this is primarily useful when applying a CSS background using an SVG
          include: (input) => input.indexOf(BG_IMAGES_DIRNAME) > -1,
          use: {
            loader: "svg-url-loader",
            options: {},
          },
        },
        {
          test: /\.svg$/,
          // only process SVG modules with this loader when they don't live under a 'bgimages',
          // 'fonts', or 'pficon' directory, those are handled with other loaders
          include: (input) =>
            input.indexOf(BG_IMAGES_DIRNAME) === -1 &&
            input.indexOf("fonts") === -1 &&
            input.indexOf("background-filter") === -1 &&
            input.indexOf("pficon") === -1,
          use: {
            loader: "raw-loader",
            options: {},
          },
        },
        {
          test: /\.(jpg|jpeg|png|gif)$/i,
          include: [
            path.resolve(__dirname, "src"),
            path.resolve(__dirname, "node_modules/patternfly"),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/patternfly/assets/images"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-styles/css/assets/images"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-core/dist/styles/assets/images"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images"
            ),
            path.resolve(
              __dirname,
              "node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images"
            ),
          ],
          use: [
            {
              loader: "url-loader",
              options: {
                limit: 5000,
                outputPath: "images",
                name: "[name].[ext]",
              },
            },
          ],
        },
      ],
    },
    output: {
      filename: "[name].bundle.js",
      path: path.resolve(__dirname, "dist"),
      publicPath: ASSET_PATH,
    },
    resolve: {
      extensions: [".js", ".ts", ".tsx", ".jsx"],
      plugins: [
        new TsconfigPathsPlugin({
          configFile: path.resolve(__dirname, "./tsconfig.json"),
        }),
      ],
      symlinks: false,
      cacheWithContext: false,
    },
  });
};

It’s a complex problem with lots of moving parts, but if not for any solutions, I would already be grateful for pointers to try.

Please let me know if you need more info.

Although it could be cleaned up, I find it unlikely the problem is with your Webpack config. Do you import React from 'react'; at the top of each file you’re using JSX (aka the <div prop={value} /> syntax) and set "jsx": "react" in your tsconfig?

If that doesn’t work can you link a repo?

Thanks for the suggestions. I can answer both with YES :slight_smile: Regarding cleaning up: I agree, I’ve been hacking at this thing like a machete through the jungle. Let me first provide you with the files below, if that’s not enough, I’m happy to create a GitHub repo for you.

tsconfig.json:

 {
  "extends": "ts-config-single-spa",
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react",
    "baseUrl": "."
  },
  "include": ["src/**/*", "node_modules/@types"],
  "exclude": ["src/**/*.test*"]
}

SphereMall-data-mapping-mf.tsx (lifecycle creating for single spa)

import React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import App from "./App";

const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: App,
  errorBoundary(err, info, props) {
    return (
      <ul>
        <li>Error: ${err}</li>
        <li>Info: ${info}</li>
        <li>Props: ${props}</li>
      </ul>
    );
  },
});

export const { bootstrap, mount, unmount } = lifecycles;

App.tsx (copied and adapted from https://github.com/atlasmap/atlasmap/blob/master/ui/packages/atlasmap-standalone/src/App.tsx and https://github.com/atlasmap/atlasmap/blob/master/ui/packages/atlasmap-standalone/src/index.tsx)

import { Atlasmap, AtlasmapProvider } from "@atlasmap/atlasmap";
import React from "react";

const App: React.FC = () => {
  return (
    <AtlasmapProvider
      baseJavaInspectionServiceUrl={"/v2/atlas/java/"}
      baseXMLInspectionServiceUrl={"/v2/atlas/xml/"}
      baseJSONInspectionServiceUrl={"/v2/atlas/json/"}
      baseCSVInspectionServiceUrl={"/v2/atlas/csv/"}
      baseMappingServiceUrl={"/v2/atlas/"}
    >
      <Atlasmap />
    </AtlasmapProvider>
  );
};

export default App;