React not found from Webpack generated micro-frontend

Viewed 921

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" build of React

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.

/EDIT more info

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;
0 Answers
Related