angular-architects / module-federation-plugin

MIT License
725 stars 196 forks source link

Native federation angular and firebase #650

Open raliand opened 1 month ago

raliand commented 1 month ago

I am trying to create an angular project to work with the Firebase (@angular/fire) and Native Federation in an NX Monorepo.

This is my setup: tsconfig.base.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "ES2022",
    "module": "esnext",
    "lib": ["ES2022", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "baseUrl": ".",
    "paths": {
      "@ethi/common": ["libs/common/src/index.ts"],
      "@ethi/schema": ["libs/schema/src/index.ts"]
    }
  },
  "exclude": ["node_modules", "tmp"]
}

package.json

{
  "name": "@ethi/source",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {},
  "private": true,
  "dependencies": {
    "@angular/animations": "~18.2.0",
    "@angular/common": "~18.2.0",
    "@angular/compiler": "~18.2.0",
    "@angular/core": "~18.2.0",
    "@angular/fire": "^18",
    "@angular/forms": "~18.2.0",
    "@angular/platform-browser": "~18.2.0",
    "@angular/platform-browser-dynamic": "~18.2.0",
    "@angular/router": "~18.2.0",
    "es-module-shims": "^1.5.12",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.14.3"
  },
  "devDependencies": {
    "@angular-architects/native-federation": "^18.2.2",
    "@angular-devkit/build-angular": "~18.2.0",
    "@angular-devkit/core": "~18.2.0",
    "@angular-devkit/schematics": "~18.2.0",
    "@angular-eslint/eslint-plugin": "^18.0.1",
    "@angular-eslint/eslint-plugin-template": "^18.0.1",
    "@angular-eslint/template-parser": "^18.0.1",
    "@angular/cli": "~18.2.0",
    "@angular/compiler-cli": "~18.2.0",
    "@angular/language-service": "~18.2.0",
    "@nx/angular": "19.7.3",
    "@nx/eslint": "19.7.3",
    "@nx/eslint-plugin": "19.7.3",
    "@nx/jest": "19.7.3",
    "@nx/js": "19.7.3",
    "@nx/node": "19.7.3",
    "@nx/web": "19.7.3",
    "@nx/workspace": "19.7.3",
    "@schematics/angular": "~18.2.0",
    "@swc-node/register": "~1.9.1",
    "@swc/core": "~1.5.7",
    "@swc/helpers": "~0.5.11",
    "@types/jest": "^29.5.12",
    "@types/node": "18.16.9",
    "@typescript-eslint/eslint-plugin": "^7.16.0",
    "@typescript-eslint/parser": "^7.16.0",
    "@typescript-eslint/utils": "^7.16.0",
    "eslint": "~8.57.0",
    "eslint-config-prettier": "^9.0.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "jest-environment-node": "^29.7.0",
    "jest-preset-angular": "~14.1.0",
    "nx": "19.7.3",
    "prettier": "^2.6.2",
    "ts-jest": "^29.1.0",
    "ts-node": "10.9.1",
    "typescript": "~5.5.2"
  }
}

app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(appRoutes), 
    provideFirebaseApp(() => initializeApp(environments.firebase), 
    provideAuth(() => getAuth()), 
    provideFirestore(() => getFirestore()), 
    provideFunctions(() => getFunctions()),
  ],
};

app.routes.ts

const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['login']);
const redirectLoggedInToHome = () => redirectLoggedInTo(['home']);

export const appRoutes: Routes = [
    {
        path: '',
        component: HomeComponent,
        pathMatch: 'full',
        canActivate: [AuthGuard],
        data: { authGuardPipe: redirectUnauthorizedToLogin },
    },
    {
        path: 'home',
        component: HomeComponent,
        pathMatch: 'full',
        canActivate: [AuthGuard],
        data: { authGuardPipe: redirectUnauthorizedToLogin },
    },
    {
        path: 'login',
        component: LoginPageComponent,
        canActivate: [AuthGuard],
        data: { authGuardPipe: redirectLoggedInToHome },
    },
];

app.component.ts

@Component({
  standalone: true,
  imports: [CommonModule, RouterModule],
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  title = 'broker-bi';
}

project.json

{
  "name": "broker-bi",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "projectType": "application",
  "prefix": "app",
  "sourceRoot": "apps/broker-bi/src",
  "tags": [],
  "targets": {
    "build": {
      "executor": "@angular-architects/native-federation:build",
      "options": {},
      "configurations": {
        "production": {
          "target": "broker-bi:esbuild:production"
        },
        "development": {
          "target": "broker-bi:esbuild:development",
          "dev": true
        }
      },
      "defaultConfiguration": "production"
    },
    "serve": {
      "executor": "@angular-architects/native-federation:build",
      "options": {
        "target": "broker-bi:serve-original:development",
        "rebuildDelay": 0,
        "dev": true,
        "port": 0
      }
    },
    "extract-i18n": {
      "executor": "@angular-devkit/build-angular:extract-i18n",
      "options": {
        "buildTarget": "broker-bi:build"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint"
    },
    "test": {
      "executor": "@nx/jest:jest",
      "outputs": [
        "{workspaceRoot}/coverage/{projectRoot}"
      ],
      "options": {
        "jestConfig": "apps/broker-bi/jest.config.ts"
      }
    },
    "serve-static": {
      "executor": "@nx/web:file-server",
      "options": {
        "buildTarget": "broker-bi:build",
        "port": 4200,
        "staticFilePath": "dist/apps/broker-bi/browser",
        "spa": true
      }
    },
    "esbuild": {
      "executor": "@angular-devkit/build-angular:application",
      "outputs": [
        "{options.outputPath}"
      ],
      "options": {
        "outputPath": "dist/apps/broker-bi",
        "index": "apps/broker-bi/src/index.html",
        "browser": "apps/broker-bi/src/main.ts",
        "polyfills": [
          "zone.js",
          "es-module-shims"
        ],
        "tsConfig": "apps/broker-bi/tsconfig.app.json",
        "assets": [
          {
            "glob": "**/*",
            "input": "apps/broker-bi/public"
          }
        ],
        "styles": [
          "apps/broker-bi/src/styles.css"
        ],
        "scripts": []
      },
      "configurations": {
        "production": {
          "budgets": [
            {
              "type": "initial",
              "maximumWarning": "500kb",
              "maximumError": "1mb"
            },
            {
              "type": "anyComponentStyle",
              "maximumWarning": "2kb",
              "maximumError": "4kb"
            }
          ],
          "outputHashing": "all"
        },
        "development": {
          "optimization": false,
          "extractLicenses": false,
          "sourceMap": true
        }
      },
      "defaultConfiguration": "production"
    },
    "serve-original": {
      "executor": "@angular-devkit/build-angular:dev-server",
      "configurations": {
        "production": {
          "buildTarget": "broker-bi:esbuild:production"
        },
        "development": {
          "buildTarget": "broker-bi:esbuild:development"
        }
      },
      "defaultConfiguration": "development",
      "options": {
        "port": 1100
      }
    }
  }
}

When I run the application through nx run broker-bi:serve-original it runs as expected but with no native federation. When I run it through nx run broker-bi:serve I get the following error:

FirebaseError: Firebase: No Firebase App '[DEFAULT]' has been created - call initializeApp() first (app/no-app).
    at getApp (_angular_fire_auth.r5LpLF_iJr-dev.js:1538:25)
    at getAuth (_angular_fire_auth.r5LpLF_iJr-dev.js:9174:24)
    at _angular_fire.hX3GNn6leI-dev.js:1316:44
    at _angular_fire.hX3GNn6leI-dev.js:1263:57
    at _ZoneDelegate.invoke (zone.js:369:28)
    at _ZoneImpl.run (zone.js:111:43)
    at _NgZone.runOutsideAngular (_angular_core.lGU3keLAQ5-dev.js:4134:24)
    at runOutsideAngular (_angular_fire.hX3GNn6leI-dev.js:1263:33)
    at _angular_fire.hX3GNn6leI-dev.js:1316:17
    at app.config.ts:24:23

What am I dong wrong?

vladimirs-puzanovs-vr commented 3 days ago

@raliand

I've run into this issue recently and I solved it by adding firebase/app and firebase/messaging to the skip array in federation.config.js like this:

const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({

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

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    'firebase/app',
    'firebase/messaging',
    // Add further packages you don't need at runtime
  ]
});

You will still be able to import and use firebase functions where necessary in your project, e.g.:

import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';

The reason for this issue occuring might be that firebase is being loaded twice - once when federation plugin sets up all the dependencies via withNativeFederation call and then again when you actually import it somewhere. Same error occurs when using @angular/fire, by the way. @manfredsteyer worth looking into, perhaps?

raliand commented 2 days ago

@vladimirs-puzanovs-vr Thank you for the suggestion. The point though, is to initialize the firebase app once when bootstrapping the skeleton app and use it across all the federated apps. We do this with module federation and Angular 14 and it works really well. I am trying to understand if it will be possible to do this with an app where we use only standalone components.