angular-architects / module-federation-plugin

MIT License
713 stars 189 forks source link

React MFE in Angular 14 host - import.meta may only appear in a module and container.init is not a function #272

Closed paynoattn closed 1 year ago

paynoattn commented 1 year ago

Hello,

I am trying to dynamically render a react microfrontend into my angular host app following this guide - https://www.angulararchitects.io/aktuelles/multi-framework-and-version-micro-frontends-with-module-federation-your-4-steps-guide/

When I actually load the hosted microfrontend in the article, it works. When I try to load the app using React.lazy and stating the remote explicitly in the remotes section of my host application, it also works. When I try dynamically load my react app, I get several errors and the app doesnt load:

Uncaught SyntaxError: import.meta may only appear in a module styles.js:8202:28

Unhandled Promise rejection: container.init is not a function ; Zone: <root> ; Task: Promise.then ; Value: TypeError: container.init is not a function
    initRemote Angular
    fulfilled tslib.es6.js:73
    Angular 19
    __awaiter tslib.es6.js:76
    ZoneAwarePromise Angular
    __awaiter tslib.es6.js:72
    Angular 2
    __awaiter tslib.es6.js:76
    ZoneAwarePromise Angular
    __awaiter tslib.es6.js:72
    Angular 2
    __awaiter tslib.es6.js:76
    ZoneAwarePromise Angular
    __awaiter tslib.es6.js:72
    Angular 2
    fulfilled tslib.es6.js:73
    Angular 6
 initRemote/<@http://devdb.localtest.me:4200/vendor.js:47:21
fulfilled@http://devdb.localtest.me:4200/vendor.js:8026:58
invoke@http://devdb.localtest.me:4200/polyfills.js:8166:158
run@http://devdb.localtest.me:4200/polyfills.js:7907:35
scheduleResolveOrReject/<@http://devdb.localtest.me:4200/polyfills.js:9251:28
invokeTask@http://devdb.localtest.me:4200/polyfills.js:8199:171
runTask@http://devdb.localtest.me:4200/polyfills.js:7960:37
drainMicroTaskQueue@http://devdb.localtest.me:4200/polyfills.js:8408:23
promise callback*nativeScheduleMicroTask@http://devdb.localtest.me:4200/polyfills.js:8379:18
scheduleMicroTask@http://devdb.localtest.me:4200/polyfills.js:8390:30
scheduleTask@http://devdb.localtest.me:4200/polyfills.js:8189:28
scheduleTask@http://devdb.localtest.me:4200/polyfills.js:8008:35
scheduleMicroTask@http://devdb.localtest.me:4200/polyfills.js:8033:19
scheduleResolveOrReject@http://devdb.localtest.me:4200/polyfills.js:9239:10
resolvePromise@http://devdb.localtest.me:4200/polyfills.js:9167:34
makeResolver/<@http://devdb.localtest.me:4200/polyfills.js:9073:23
wrapper/<@http://devdb.localtest.me:4200/polyfills.js:9090:25
promise callback*patchThen/Ctor.prototype.then/wrapped<@http://devdb.localtest.me:4200/polyfills.js:9537:22
ZoneAwarePromise@http://devdb.localtest.me:4200/polyfills.js:9438:21
patchThen/Ctor.prototype.then@http://devdb.localtest.me:4200/polyfills.js:9536:23
loadRemoteModuleEntry/<@http://devdb.localtest.me:4200/vendor.js:76:18
__awaiter/<@http://devdb.localtest.me:4200/vendor.js:8029:71
ZoneAwarePromise@http://devdb.localtest.me:4200/polyfills.js:9438:21
__awaiter@http://devdb.localtest.me:4200/vendor.js:8025:12
loadRemoteModuleEntry@http://devdb.localtest.me:4200/vendor.js:69:58
loadRemoteEntry/<@http://devdb.localtest.me:4200/vendor.js:63:13
__awaiter/<@http://devdb.localtest.me:4200/vendor.js:8029:71
ZoneAwarePromise@http://devdb.localtest.me:4200/polyfills.js:9438:21
__awaiter@http://devdb.localtest.me:4200/vendor.js:8025:12
loadRemoteEntry@http://devdb.localtest.me:4200/vendor.js:54:58
loadRemoteEntries/<@http://devdb.localtest.me:4200/vendor.js:222:23
__awaiter/<@http://devdb.localtest.me:4200/vendor.js:8029:71
ZoneAwarePromise@http://devdb.localtest.me:4200/polyfills.js:9438:21
__awaiter@http://devdb.localtest.me:4200/vendor.js:8025:12
loadRemoteEntries@http://devdb.localtest.me:4200/vendor.js:215:58
loadManifest/<@http://devdb.localtest.me:4200/vendor.js:185:13
fulfilled@http://devdb.localtest.me:4200/vendor.js:8026:58
invoke@http://devdb.localtest.me:4200/polyfills.js:8166:158
run@http://devdb.localtest.me:4200/polyfills.js:7907:35
scheduleResolveOrReject/<@http://devdb.localtest.me:4200/polyfills.js:9251:28
invokeTask@http://devdb.localtest.me:4200/polyfills.js:8199:171
runTask@http://devdb.localtest.me:4200/polyfills.js:7960:37
drainMicroTaskQueue@http://devdb.localtest.me:4200/polyfills.js:8408:23
zone.js:1061:24
    Angular 17
    __awaiter tslib.es6.js:76
    ZoneAwarePromise Angular
    __awaiter tslib.es6.js:72
    Angular 2
    __awaiter tslib.es6.js:76
    ZoneAwarePromise Angular
    __awaiter tslib.es6.js:72
    Angular 2
    __awaiter tslib.es6.js:76
    ZoneAwarePromise Angular
    __awaiter tslib.es6.js:72
    Angular 2
    fulfilled tslib.es6.js:73
    Angular 6

Here is my host webpack.config.js

const {
    shareAll,
    withModuleFederationPlugin,
} = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
    remotes: {
        // Check this line. Is port 4201 configured?
        // marketplace: '/mfe-marketplace/remoteEntry.js',
    },

    shared: {
        ...shareAll({
            singleton: true,
            strictVersion: true,
            requiredVersion: 'auto',
        }),
    },
});

My guest webpack.config.js:

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

const moduleFederationConfig = require('./module-federation.config');

module.exports = (options) => {
  return {
    ...options,
    output: {
      path: ../../dist/apps/mfe-marketplace',
      publicPath: "auto",
      uniqueName: "marketplace",
      scriptType: 'text/javascript',
    },
    plugins: [
      ...options.plugins,
      new ModuleFederationPlugin({
        name: "marketplace",
        library: { type: "var", name: "marketplace" },
        filename: "remoteEntry.js", // <-- Meta Data
        exposes: {
          './web-components': './src/app/custom-element.tsx',
        },
        shared: ['react', 'react-dom']
     }),
    ]
  };
}

And my custom-elements.tsx

import React from 'react'
import ReactDOM from 'react-dom'

import App from './app';

class Mfe4Element extends HTMLElement {
  connectedCallback() {
    ReactDOM.render(<App/>, this);
  }
}

customElements.define('react-element', Mfe4Element);

Package.json of host angular app:

{
  "name": "angular-suite",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "build": "node --max_old_space_size=10000 node_modules/@angular/cli/bin/ng build --project=web-lightning --configuration=development && node --max_old_space_size=10000 node_modules/@angular/cli/bin/ng build --project=web-request-portal --configuration=development",
    "build:lib-grease": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease --configuration=production",
    "build:web-admin": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-admin --configuration=production",
    "build:web-lightning": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease --configuration=production && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-lightning --configuration=production",
    "build:web-operator-companion": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease --configuration=production && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-operator-companion --configuration=production",
    "build:web-request-portal": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease --configuration=production && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-request-portal --configuration=production",
    "build:web-vendor-portal": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease --configuration=production && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-vendor-portal --configuration=production",
    "dev:web-admin": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-admin --configuration=development",
    "dev:web-lightning": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-lightning --configuration=development",
    "dev:web-operator-companion": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-operator-companion --configuration=development",
    "dev:web-request-portal": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-request-portal --configuration=development",
    "dev:web-vendor-portal": "node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build lib-grease && node --max_old_space_size=14336 node_modules/@angular/cli/bin/ng build web-vendor-portal --configuration=development",
    "dev:docker-start": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve web-lightning --configuration=development  --open --host 0.0.0.0 --port 4200 --disable-host-check",
    "watch": "start cmd.exe /c npm run watch:web-lightning && npm run watch:web-request-portal",
    "watch:web-lightning": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng build --project=web-lightning --watch=true --configuration=development",
    "watch:web-request-portal": "ng build --project=web-request-portal --watch=true --configuration=development",
    "watch:web-admin": "ng build --project=web-admin --watch=true --configuration=development",
    "watch:web-operator-companion": "ng build --project=web-operator-companion --watch=true --configuration=development",
    "watch:web-vendor-portal": "ng build --project=web-vendor-portal --watch=true --configuration=development",
    "test": "ng test",
    "lint": "ng lint",
    "run:all": "node node_modules/@angular-architects/module-federation/src/server/mf-dev-server.js"
  },
  "private": true,
  "dependencies": {
    "@angular-architects/module-federation": "^14.3.0",
    "@angular-architects/module-federation-tools": "^14.3.0",
    "@angular/animations": "^14.2.1",
    "@angular/cdk": "^14.2.1",
    "@angular/common": "^14.2.1",
    "@angular/compiler": "^14.2.1",
    "@angular/core": "^14.2.1",
    "@angular/forms": "^14.2.1",
    "@angular/material": "^14.2.1",
    "@angular/platform-browser": "^14.2.1",
    "@angular/platform-browser-dynamic": "^14.2.1",
    "@angular/router": "^14.2.1",
    "@angular/service-worker": "^14.2.1",
    "@microsoft/signalr": "^3.1.10",
    "@ngrx/effects": "^14.3.1",
    "@ngrx/router-store": "^14.3.1",
    "@ngrx/store": "^14.3.1",
    "@okta/okta-angular": "^5.1.1",
    "@okta/okta-auth-js": "^6.0.0",
    "@types/jquery": "^3.5.14",
    "adal-angular4": "^3.0.13",
    "angular-froala-wysiwyg": "^2.9.6",
    "bootstrap": "^3.4.1",
    "core-js": "^2.6.11",
    "date-fns": "^2.16.1",
    "devextreme": "^20.2.3",
    "devextreme-angular": "^20.2.3",
    "devextreme-aspnet-data-nojquery": "^2.6.2",
    "fast-json-patch": "^2.2.1",
    "font-awesome": "^4.7.0",
    "froala-editor": "2.8.5",
    "ng-recaptcha": "^10.0.0",
    "quagga": "^0.12.1",
    "react": "18.0.0",
    "react-dom": "18.0.0",
    "rxjs": "^6.6.3",
    "stream": "0.0.2",
    "toastr": "^2.1.4",
    "tslib": "^2.4.0",
    "@okta/okta-angular": "^5.1.1",
    "@okta/okta-auth-js": "^6.0.0",
    "zone.js": "~0.11.4"
  },
  "devDependencies": {
    "@angular-builders/custom-webpack": "^14.0.1",
    "@angular-builders/dev-server": "^7.3.1",
    "@angular-builders/jest": "^9.0.1",
    "@angular-devkit/build-angular": "^14.2.2",
    "@angular/cli": "^14.2.2",
    "@angular/compiler-cli": "^14.2.1",
    "@angular/language-service": "^14.2.1",
    "@babel/core": "^7.18.2",
    "@types/react": "18.0.1",
    "@types/react-dom": "18.0.0",
    "babel-loader": "^8.2.5",
    "concurrently": "^7.1.0",
    "devextreme-cli": "^1.3.2",
    "devextreme-themebuilder": "^20.2.11",
    "jest": "^26.6.3",
    "ng-packagr": "^14.2.1",
    "ngx-build-plus": "^14.0.0",
    "ts-node": "~7.0.0",
    "tslint": "~6.1.0",
    "typescript": "~4.6.4",
    "webpack": "^5.50.0"
  }
}

Package.json of guest React app:

{
  "name": "@managerplus/asset-settings",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "start": "nx serve",
    "marketplace-bff": "nx serve svc-marketplace-bff",
    "build": "nx build",
    "test": "nx test",
    "prepare": "husky install"
  },
  "private": true,
  "dependencies": {
    "@apollo/client": "^3.6.2",
    "@faker-js/faker": "^6.3.1",
    "@nestjs/apollo": "^10.0.11",
    "@nestjs/common": "^8.0.0",
    "@nestjs/config": "^2.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/graphql": "^10.0.11",
    "@nestjs/platform-express": "^8.0.0",
    "apollo-server-express": "^3.7.0",
    "axios": "^0.27.2",
    "axios-cookiejar-support": "^3.0.0",
    "core-js": "^3.6.5",
    "express": "^4.17.1",
    "graphql": "^16.4.0",
    "prop-types": "^15.8.1",
    "react": "18.0.0",
    "react-dom": "18.0.0",
    "react-is": "^18.1.0",
    "react-router-dom": "6.3.0",
    "rebass": "^4.0.7",
    "reflect-metadata": "^0.1.13",
    "regenerator-runtime": "0.13.7",
    "rxjs": "^7.0.0",
    "styled-components": "^5.3.5",
    "tough-cookie": "^4.0.0",
    "tslib": "^2.3.0",
    "uuid": "^8.3.2"
  },
  "devDependencies": {
    "@babel/core": "^7.8",
    "@babel/preset-env": "^7",
    "@nestjs/schematics": "^8.0.0",
    "@nestjs/testing": "^8.0.0",
    "@nrwl/cli": "14.0.3",
    "@nrwl/cypress": "14.0.3",
    "@nrwl/eslint-plugin-nx": "14.0.3",
    "@nrwl/jest": "14.0.3",
    "@nrwl/linter": "14.0.3",
    "@nrwl/nest": "^14.0.3",
    "@nrwl/node": "14.0.3",
    "@nrwl/react": "14.0.3",
    "@nrwl/web": "14.0.3",
    "@nrwl/workspace": "14.0.3",
    "@runeffective/eslint-plugin": "0.1.9",
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "13.1.1",
    "@types/jest": "27.4.1",
    "@types/node": "16.11.7",
    "@types/react": "18.0.1",
    "@types/react-dom": "18.0.0",
    "@types/react-is": "17.0.3",
    "@types/react-router-dom": "5.3.3",
    "@types/rebass": "^4.0.10",
    "@types/styled-components": "5.1.25",
    "@types/tough-cookie": "^4.0.2",
    "@types/uuid": "^8.3.4",
    "@typescript-eslint/eslint-plugin": "^5.44.0",
    "@typescript-eslint/parser": "^5.44.0",
    "babel-jest": "27.5.1",
    "babel-loader": "^8.0.2",
    "babel-plugin-styled-components": "1.10.7",
    "cypress": "^9.1.0",
    "eslint": ">=7",
    "eslint-config-airbnb": "19.0.4",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-cypress": "^2.10.3",
    "eslint-plugin-import": "^2.25.3",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-prefer-arrow": "^1.2.3",
    "eslint-plugin-react": "^7.28.0",
    "eslint-plugin-react-hooks": "^4.3.0",
    "husky": ">=6",
    "jest": "27.5.1",
    "lint-staged": ">=10",
    "nx": "14.0.3",
    "prettier": ">=2",
    "react-test-renderer": "18.0.0",
    "ts-jest": "27.1.4",
    "ts-node": "9.1.1",
    "typescript": "~4.6.2",
    "webpack": "^5.0.0"
  },
  "lint-staged": {
    "*.{js,ts,tsx,jsx}": "eslint --cache --fix",
    "*.{js,css,md}": "prettier --write"
  }
}

Based on other issues posted here, I have tried the following things:

SandeepThomas commented 1 year ago

@paynoattn

Have you tried with type: script in the route config for remote?

import { WebComponentWrapper, WebComponentWrapperOptions } from '@angular-architects/module-federation-tools';

 export const APP_ROUTES: Routes = [
     [...]
     {
         path: 'react',
         component: WebComponentWrapper,
         data: {
             type: 'script',
             remoteEntry: 'https://witty-wave-0a695f710.azurestaticapps.net/remoteEntry.js',
             remoteName: 'react',
             exposedModule: './web-components',
             elementName: 'react-element'
         } as WebComponentWrapperOptions
     },
     [...]
 ]

If this doesn't work, is there any way you could provide a sample repo that reproduces this?

paynoattn commented 1 year ago

Hello, I added the type: script to the app and am still experiencing the issue.

I'll create an example repo if there is no other suggestions.

paynoattn commented 1 year ago

I created a repo with the just the basic HTML / components of my two apps here.

I apologize if there is any confusion about the directory structure as i'm trying to smash two monorepos together. Right now for business reasons, I cannot migrate the angular app into the nx workspace.

SandeepThomas commented 1 year ago

@paynoattn, I tried to find the root cause of this issue and it seems like the issue is coming from @nrwl/webpack which is creating esm build (due to scriptType: module) for the remoteEntry.js.

image

Here is my test code which i used to debug this issue: https://github.com/SandeepThomas/angular-react-mfe

I had tested with both nx-react and also normal react app build using create-react-app/craco. The nx-react is generating esm for the remoteEntry.js while the craco one creates normal script file which works fine.

related issue: https://github.com/nrwl/nx/issues/13628

Update: nx 15.4.6 added support to modify webpack config for react apps, but is having an open issue when creating react apps https://github.com/nrwl/nx/issues/14344.

keithcarter5 commented 1 year ago

For angular 13 and higher, type should be ‘module’. Even so, I’m still getting stick on InitRemote(), where it calls container.init()...saying “container.init() is not a function”.

paynoattn commented 1 year ago

Based on @SandeepThomas I tried upgrading to nx 15.4.6, which did not work, I got the same error.

Based on @keithcarter5's recommendation, I tried setting library: { type: module } in the options for ModuleFederationPlugin (as well asexperiment: { outputModule: true } in the base webpack config), and am now getting an error: SyntaxError: export declarations may only appear at top level of a module

SandeepThomas commented 1 year ago

@paynoattn

With the latest nx update (15.6.3) i was able to override the webpack config for react remoteEntry file to be normal script instead of module.

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
const { withModuleFederation } = require('@nrwl/react/module-federation');
const baseConfig = require('./module-federation.config');
const config = {
  ...baseConfig,
};

const changeModuleFederationToScript = () => {
  return (config, ctx) => {
    for (let plugin of config.plugins) {
      if (plugin instanceof ModuleFederationPlugin) {
        plugin._options.library = {
          type: 'var',
          name: plugin._options.name,
        };
      }
    }
    config.output.scriptType = 'text/javascript';
    return config;
  };
};

// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
  withNx(),
  withReact(),
  withModuleFederation(config),
  changeModuleFederationToScript()
);

You can checkout my github repo to see the full configuration.

Thanks to @bryantcj52 for the idea (https://github.com/nrwl/nx/issues/10110#issuecomment-1256336849)

paynoattn commented 1 year ago

Thanks @SandeepThomas this was trully a huge help. I was able to get this working as welll.

paynoattn commented 1 year ago

Another bug I found related to this. You still get the container is undefined bug once you do a nx build --prod. You have to turn optimization to false in the project.json of your production build configuration.