single-spa-angularjs icon indicating copy to clipboard operation
single-spa-angularjs copied to clipboard

Question/Issue: How can I pull in an AngularJS app into a React host app

Open eldursi opened this issue 2 years ago • 1 comments

Hi all

I'm hoping someone will be able to help.

I'm trying to use single-spa-angularjs to pull in a legacy AngularJS application into a Reactapp. So the React app is the host. In the same React app, I'm also using Module Federation to pull in a separate React microfrontend.

So to sum it up, I have three apps:

  1. React app - host and container
  2. AngularJS app - legacy app using AngularJS version 1.4.8
  3. React microfrontend

All three apps are completely separate and hosted separately.

I was able to pull in the React microfrontend using Module Federation with no issues. The problem that I'm facing is pulling the AngularJS app into the host React app (app 1).

I'm getting a lot of errors in the console but I think this is the most relevant one:

xxx.js:701 Uncaught Error: application 'AngularJS' died in status LOADING_SOURCE_CODE: Module parse failed: Unexpected token (1:1)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> <!DOCTYPE html>
| <html lang="en" ng-app="MyAngularJSApp" ng-controller="MyAngularJSCtrl">
|     <head>
    at eval (index.html:1:7)
    at ./index.html (node_modules_angularjs-bootstrap-datetimepicker_node_modules_moment_locale_sync_recursive_-no-1f57b2.app.bundle.js:27:1)
    at __webpack_require__ (remoteEntry.js:44:42)
    at eval (app.entry.js:21:69)
    at ./app/app.entry.js (node_modules_angularjs-bootstrap-datetimepicker_node_modules_moment_locale_sync_recursive_-no-1f57b2.app.bundle.js:113:1)
    at __webpack_require__ (remoteEntry.js:44:42)
    at eval (container_entry:3:298)
    at u.m.<computed> (bundle.js:2:262091)
    at u (bundle.js:2:259451)

This is my current set up:

AngularJS App:

We're using webpack so we have a webpack.config.js file and the most important part is:


module.exports = function (env = {}) {
  const isProd = !!env.prod;
  const buildpath = env.buildpath ? env.buildpath : isProd ? 'dist' : 'build';

  return {
    entry: path.resolve(__dirname, 'app/app.entry.js'),
    output: {
      path: path.resolve(__dirname, buildpath),
      filename: 'js/app.bundle.js',
      clean: true,
    },
    plugins: [
      new webpack.container.ModuleFederationPlugin({
        name: 'MyAngularJSApp',
        filename: 'remoteEntry.js',
        remotes: {},
        exposes: {
          './App': path.resolve(__dirname, 'app/app.entry.js'),
        },
      }),
      new HtmlWebpackPlugin({
        inject: 'body',
        template: path.resolve(__dirname, 'index.html'),
        filename: path.resolve(buildpath, 'index.html'),
      })
    ],
    module: {
        ......
    },
    resolve: {
        ......
    },
    optimization: {
        ......
    },
  };
};

In app/app.entry.js I've added the following:

import singleSpaAngularJS from 'single-spa-angularjs';
import angular from 'angular';
import app from '../index.html'

const domElementGetter = () => document.getElementById('AngularJSApp');
const ngLifecycles = singleSpaAngularJS({
  angular: angular,
  domElementGetter,
  mainAngularModule: 'AngularJSApp',
  template: app,
});

export const bootstrap = ngLifecycles.bootstrap;
export const mount = ngLifecycles.mount;
export const unmount = ngLifecycles.unmount;

In the host/consuming React application:

webpack.config.js:

const path = require('path');
const webpack = require('webpack')
const { ModuleFederationPlugin } = webpack.container;
const HtmlWebpackPlugin = require("html-webpack-plugin");
const dependencies = require('./package.json').dependencies;
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  entry: './src/index.tsx',
  mode: 'production',
  performance: {
    hints: false,
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  },
  module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.html$/,
        exclude: /\.lazy\.html$/,
        use: [
          {
            loader: 'html-loader',
            options: {
              minimize: true,
            },
          },
        ],
      },
      // Use Babel to transpile TypeScript and TypeScript / React files to ES5
      {
        test: /\.(tsx|ts)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        reactMFE: 'reactMFE@http://localhost:3000/remoteEntry.js',
        AngularJSApp: 'AngularJSApp@http://localhost:3004/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true,
          eager: true,
          requiredVersion: dependencies.react,
        },
        'react-dom': {
          singleton: true,
          eager: true,
          requiredVersion: dependencies['react-dom'],
        }
      },
    }),
    new webpack.DefinePlugin({ // <-- key to reducing React's size
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    // new BundleAnalyzerPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

I've registered the Angular app in the entry point /src/index.tsx, the same file also imports 'bootstrap' to resolve the eager consumption error when using module federation: /src/index.tsx:


import("./bootstrap")
import {registerApplication} from 'single-spa'
registerApplication(
    'AngularJSApp',
    //@ts-ignore
    () => import('AngularJSApp/App'),
    location => location.pathname.startsWith('/')
)

/src/bootstrap.tsx:


import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
      <App />
  </React.StrictMode>,
);

I've made changes to load in the scripts for the imported apps in public/index.html:

<html>
  <head>
    <script src="http://localhost:3000/remoteEntry.js"></script>
    <script src="http://localhost:3004/remoteEntry.js"></script>
  
  </head>
  <body>
    <div>Container App</div>
    <div id="root"></div>

    {/* I've put these two lines as a last ditch attempt - not sure which one of them should be working*/}
    <div id="AngularJSApp"></div>
    <div id="single-spa-application:AngularJSApp"></div>
  </body>
</html>

Can someone advise on how I can get this working, it would be much appreciated. Thanks

eldursi avatar Jul 18 '22 09:07 eldursi

The first problem I see is that you're using a framework in the single-spa-root-config

1 React app - host and container

https://single-spa.js.org/docs/faq/#should-i-have-a-parentroot-app-and-children-apps

That means any debugging you're doing is going to be significantly harder because you have a react app that's mounting angularjs. If you separate these out so you have an easier time debugging the issue that seems to be related to module federation.

TheMcMurder avatar Jul 19 '22 20:07 TheMcMurder