---
# System prepended metadata

title: 【NOTE】Single Spa
tags: [note]

---

# 【NOTE】single-spa
> [name=呂芯萍 Aka] [time=2025 05 27] 
> Mail: NT96153@cathaybk.com.tw

:::spoiler 學習歷程
* 2023/11/22
> create-single-spa root-config
* 2023/11/27
> create-single-spa application
* 2024/03/07
> props
* 2024/05/12
> single-spa-angular
* 2024/07/05
> single-spa-react
* 2024/07/18
> import config from deploy
* 2025/02/11
> Module Federation in single-spa
* 2025/05/27
> Angular 18 init error
:::

:::spoiler 參考資料
* 主要
> [single-spa 官網文件](https://single-spa.js.org/docs/)
* 次要
> [single-spa: props](https://single-spa.js.org/docs/layout-definition/#props)
> [lifecycle function bootstrap did not return a promise](https://single-spa.js.org/error/?code=15&arg=application&arg=&arg=bootstrap&arg=1)
> [Single SPA with React](https://mrbytebuster.medium.com/single-spa-with-react-85491b437759)
> [single-spa-login-example-with-npm-packages | Github](https://github.com/jualoppaz/single-spa-login-example-with-npm-packages)
> [single-spa import Module Federation | single-spa](https://single-spa.js.org/docs/recommended-setup/#module-federation)
:::

## 成為 Single-spa 要注意的地方
* Add single-spa
* Change api Interceptor (call apiUrl from config.json)
* Auth call share function
* Add route parent (base url)
* Unsubscribe all oberservable on ngOnDestroy
* Image url call deployUrl from config.json

## 環境建置
### 下載 single-spa CLI
* 執行下列任一項即可
    * 全域下載
    ```npm install --global create-single-spa```

    * 該目錄下下載（執行下列任一項即可）
    ```npm init single-spa```
    ```npx create-single-spa```

    :::danger
    * 使用 ```npm init single-spa``` 且 single-spa 6.0.3 建立時，會無法將 Application 連結進 root-config，建議使用 ```npx create-single-spa```
        * 2025.05.29 測試
    :::

### 建立 root config
* 執行 ```create-single-spa --moduleType root-config```
    ![image](https://hackmd.io/_uploads/rJAtemoE6.png)
    * 替專案目錄命名
    * 選擇使用的套件管理器（建議使用 npm）
    * 選擇是否使用 Typescript（建議選用）
    * 選擇是否使用 single-spa的 Layout 框架（建議選用）
    * 替專案命名（會自動加上尾綴「root-config」）

    ![image](https://hackmd.io/_uploads/rkbJZmjVp.png)
* 執行 ```npm run start```，至瀏覽器輸入預設網址「[http://localhost:9000/](http://localhost:9000/)」可以看是否有初始成功
    ![image](https://hackmd.io/_uploads/rkWDQmoVa.png)

### 建立 Application（Angular）
* 執行 create-single-spa
    * 方法一：執行 ```create-single-spa --moduleType app-parcel```
        ![image](https://hackmd.io/_uploads/ByqSO2bHp.png)
        ![image](https://hackmd.io/_uploads/H1j8Mn-ST.png)
        * 替專案目錄命名（建議為空，目錄才不會變兩層）
        * 選擇使用的前端框架
        * 替專案命名
        * 其他該框架的詢問（Angular 會詢問是否使用 Routing、使用的 Style、是否下載相關套件）
        * 專案 Port（預設 4200）
        ![image](https://hackmd.io/_uploads/H1TUtnbBT.png)
    * 方法二：執行 ```create-single-spa --framework angular```
    :::info
    * 如出現「Package "single-spa-angular" was found but does not support schematics.」為版本不符
        * 如為 angular 14
            ```ng add single-spa-angular@7```
            ```npm i```
        * [single-spa-angular 對應版本](https://single-spa.js.org/docs/ecosystem-angular/)
    :::
* 更改執行指令
    ```json
    // package.json
    {
        // ...
        "scripts": {
            // ...
            "start": "npm run serve:single-spa:single-spa-demo-main",
            // ...
        },
    }
    ```
* 執行 ```npm run start```，至瀏覽器輸入預設網址「[http://localhost:4200/](http://localhost:4200/)」可以看是否有初始成功
    ![image](https://hackmd.io/_uploads/HkPtA3WBa.png)
  
  :::info
  * 如建立 Angular14+ 版本，需手動調整成 Standalone 方式
      * 確定 AppComponent 為 Standalone
          ```typescript
            @Component({
              standalone: true,
              // ...
            })
            export class AppComponent {}
          ```
      * 修改 main.single-spa.ts，使用 bootstrapApplication() 而非 platformBrowserDynamic().bootstrapModule()
          ```typescript
            import { AppComponent } from './app/app.component';
            import { bootstrapApplication } from '@angular/platform-browser';
            import { routes } from './app/app.routes';

            const lifecycles = singleSpaAngular({
              bootstrapFunction: () =>
                bootstrapApplication(AppComponent, {
                  providers: [provideRouter(routes)], // if has routes
                }),
              template: '<app-root />',
              NgZone,
            });

            export const bootstrap = lifecycles.bootstrap;
            export const mount = lifecycles.mount;
            export const unmount = lifecycles.unmount;
          ```
      * 移除 angular.json 中的 browser
          ```json
            "architect": {
              "build": {
                "builder": "@angular-devkit/build-angular:browser",
                "options": {
                  //"browser": "src/main.single-spa.ts", // remove
                  "main": "src/main.single-spa.ts",
                  // ...
                },
          ```
  :::
  
  :::info
  * 如出現以下錯誤，執行 ```npm i``` 下載 @angular-builders 等套件
      ![image](https://hackmd.io/_uploads/Hy0-A2brT.png)
  * 如出現以下錯誤，自行添加 environments/environment.ts
      ![image](https://hackmd.io/_uploads/rygzTnZra.png)
      
      ![image](https://hackmd.io/_uploads/By9hTnWS6.png)
  * 如出現以下錯誤，於檔案「tsconfig.json」添加「"skipLibCheck": true」 
      ![image](https://hackmd.io/_uploads/r1uvaK1T1g.png)

      ![image](https://hackmd.io/_uploads/BkP2aKypyx.png)
  * 如出現以下錯誤，為 angular.json 有多餘項目，移除即可（此範例為多了 browser 項目）
      * Error: Schema validation failed with the following errors: Data path "" must NOT have additional properties(browser).
  :::
* 連結進 root config
    * 加入 importmap
        ```html
        <!-- single-spa-demo-root-config/src/index.ejs -->
        <script type="systemjs-importmap">
          {
            "imports": {
              ...
              "@single-spa-demo/main": "//localhost:4200/main.js"
            }
          }
        </script>
        ```
    * 加入 zone
        ```html
        <!-- single-spa-demo-root-config/src/index.ejs -->
        <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script>
        ```
    * 加入 router
        ```html
        <!-- single-spa-demo-root-config/src/microfrontend-layout.html -->
        <single-spa-router>        
            <main>
                <route default>
                  <application name="@single-spa-demo/main"></application>
                </route>
            </main>
        </single-spa-router>
        ```
    :::info
    * 如有 APP_BASE_HREF error，加上 APP_BASE_HREF
        ```javascript
        // single-spa-demo-main\src\app\app-routing.module.ts
        @NgModule({
          //...
          providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
        })
        export class AppRoutingModule { }
        ```
    :::

### 建立 Application（React）
* 匯入套件 single-spa、single-spa-react、react-router-dom
    ```npm i -D single-spa single-spa-react react-router-dom```
* 重構進入點
    * 調整 src/index.tsx
        ```typescript
        import React from "react";
        import App from "./App";
        import { Provider } from "react-redux";
        import { createStore } from "redux";
        import rootReducer from "./reducer/root.reducer";
        import { BrowserRouter } from "react-router-dom";

        import "./index.css";
        import "./styles.scss";

        const store = createStore(rootReducer);

        export default function Root() {
          return (
            <BrowserRouter basename="/app-react">
              <Provider store={store}>
                <App />
              </Provider>
            </BrowserRouter>
          );
        }
        ```
    
    * 建立檔案 src/singleSpaEntry.tsx
        ```javascript
        import React from 'react';
        import ReactDOM from 'react-dom';
        import singleSpaReact from 'single-spa-react';
        import Root from './index';
        const reactLifecycles = singleSpaReact({
          React,
          ReactDOM,
          rootComponent: Root,
        });
        export const { bootstrap, mount, unmount } = reactLifecycles;
        ```
        
    * 調整執行 script
        * 匯入套件 craco
            ```npm i --save-dev @craco/craco```
        * 建立檔案 src/craco.config.js
            ```javascript
            const path = require("path");
            module.exports = {
                webpack: {
                  configure: (config, { paths }) => {
                    config.output.libraryTarget = 'umd';
                    config.output.filename = 'static/js/[name].js';
                    config.output.library = 'single-spa-demo-app-react';
                    config.output.publicPath = '/';
                    config.entry= ['./src/singleSpaEntry.tsx'];
                    paths.appBuild = config.output.path = path.resolve("dist", "single-spa-demo-app-react"); // 統一輸出成 dist
                    
                    // 打包 style
                    const oneOfRule = webpackConfig.module.rules.find(rule => Array.isArray(rule.oneOf)).oneOf;
                    oneOfRule.forEach(rule => {
                      if (rule.test && rule.test.toString().includes('scss')) {
                        rule.use = [
                         'style-loader',
                         {
                           loader: 'css-loader',
                           options: {
                              importLoaders: 1,
                           }
                         },
                          'sass-loader'
                        ]
                      }
                    })

                    return config
                  }
                }
            }
            ```
        * 更改 script
            ```json
            // package.json
            {
                "scripts": {
                    "start": "cross-env PORT=4201 craco start",
                    "build": "craco build",
                    // ...
                }
            }
            ```
            
* 執行 ```npm run start```，至瀏覽器輸入網址「[http://localhost:4201/static/js/main.js](http://localhost:4201/static/js/main.js)」可以看是否有初始成功

* 連結進 root config

    * 加入 importmap
        ```html
        <!-- single-spa-demo-root-config/src/index.ejs -->
        <script type="systemjs-importmap">
          {
            "imports": {
              ...
              "@single-spa-demo/app-react": "//localhost:4201/static/js/main.js"
            }
          }
        </script>
        ```
    * 加入 zone
        ```html
        <!-- single-spa-demo-root-config/src/index.ejs -->
        <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script>
        ```
    * 加入 router
        ```html
        <!-- single-spa-demo-root-config/src/microfrontend-layout.html -->
        <single-spa-router>        
            <main>
                <route path="app-react">
                  <application name="@single-spa-demo/app-react"></application>
                </route>
            </main>
        </single-spa-router>
        ```

* 至瀏覽器輸入網址「[http://localhost:9000/app-react](http://localhost:9000/app-react)」可以看是否有初始成功



### 建立 in-browser utility module
* 執行 init single-spa
    ```npm init single-spa```
    * 替專案目錄命名
    * 選擇使用 in-browser utility module
    * 使用框架選擇（建議使用 none）
    * 套件管理器選擇（建議使用 npm）
    * 是否使用 Typescript（建議使用）
    * 使用打包方式選擇（建議使用 esm）
    * 主專案名稱（single-spa-demo）
    * 子專案名稱（auth）

* 設定 port=9001
    ```json
    // pakage.json
    {
      // ...
      "scripts": {
        // ...
        "start": "webpack serve --port=9001",
      }
    }
    ```
* 執行 ```npm run start```，至瀏覽器輸入預設網址「[http://localhost:9001/single-spa-demo-auth.js](http://localhost:9001/single-spa-demo-auth.js)」可以看是否有初始成功

* 連結進 root config
    * 加入 importmap
        ```html
        <!-- single-spa-demo-root-config/src/index.ejs -->
        <script type="systemjs-importmap">
          {
            "imports": {
              ...
              "@single-spa-demo/auth": "//localhost:9001/single-spa-demo-auth.js"
            }
          }
        </script>
        ```
* Application Import (Angular)
    * 新增 declarations.d.ts
        ```javascript
        declare module '@single-spa-demo/auth';
        ```
    
    * 設定 import
        ```javascript
        // extra-webpack.config.js
        module.exports = (config, options) => {
          const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
          singleSpaWebpackConfig.externals = [/^@single-spa-demo\/auth$/];
          return singleSpaWebpackConfig;
        };
        ```
    * 測試 Export/Import
        ```javascript
        // single-spa-demo-auth\src\single-spa-demo-auth.ts
        export const SS_DEMO_USERINFO_KEY = "SS-DEMO-USER-INFO";       
        ```
        
        ```javascript
        // single-spa-demo-main\src\app\app.componet.ts
        import { SS_DEMO_USERINFO_KEY } from '@single-spa-demo/auth';
        
        console.log(SS_DEMO_USERINFO_KEY);
        ```

* Application Import (React)
    * 新增 declarations.d.ts
        ```javascript
        declare module '@single-spa-demo/auth';
        ```
    
    * 設定 import
        ```javascript
        // craco.config.js
        module.exports = {
          webpack: {
            configure: (config) => {
              // ...
              config.externals= ["@single-spa-demo/auth"];
              return config;
            },
        },
        ```
    
    * 測試 Export/Import
        ```javascript
        // single-spa-demo-auth\src\single-spa-demo-auth.ts
        export const SS_DEMO_USERINFO_KEY = "SS-DEMO-USER-INFO";       
        ```
        
        ```javascript
        // App.tsx
        import { SS_DEMO_USERINFO_KEY } from '@single-spa-demo/auth';
        
        console.log(SS_DEMO_USERINFO_KEY);
        ```
        
## Import config.json/importmap.json from deploy

### Import and Export importmap.json (root config、in-browser utility module)

* root config
    * 建立 importmap.json
        ```json
        // src/assets/config/importmap.json
        {
          "imports": {
            "@single-spa-demo/root-config": "/root-config.bundle.js",
            "@single-spa-demo/auth": "//localhost:9001/single-spa-demo-auth.js",
            "@single-spa-demo/main": "//localhost:4200/main.js",
            "@single-spa-demo/app-react": "//localhost:4201/static/js/main.js"
          }
        }
        ```

    * 匯入檔案
        ```html
        <!--index.ejs-->
        <script type="systemjs-importmap" src="/assets/config/importmap.json"></script>
        ```

* in-browser utility module (export deployUrls_init$ and deployUrls$)
    * 建立 config.ts
        ```typescript
        import { BehaviorSubject, Subject, Observable } from "rxjs";

        const deployUrlsBehaviorSubject$ = new BehaviorSubject<any[]>(undefined);
        const deployUrlsSubject$ = new Subject<any[]>();
        export const deployUrls_init$: Observable<any[]> = deployUrlsBehaviorSubject$.asObservable();
        export const deployUrls$: Observable<any[]> = deployUrlsSubject$.asObservable(); // for browser reload

        export function loadDeployUrls() {
          return fetch(`${window.location.origin}/assets/config/importmap.json`)
            .then(async (response) => {
              if (!response.ok) {
                throw {
                  status: response.status,
                  body: await response.json(),
                };
              }
              return await response.json();
            })
            .then((importmap) => {
              let deployUrls;
              Object.keys(importmap.imports).forEach((key) => {
                const url = importmap.imports[key];
                const a = document.createElement("a");
                a.href = url;

                deployUrls = {
                  ...deployUrls,
                  [key]: a.origin,
                };
              });
              deployUrlsBehaviorSubject$.next(deployUrls);
              deployUrlsSubject$.next(deployUrls);
            });
        }
        ```
    
    * 呼叫方法
        ```typescript
        // single-spa-demo-auth.ts
        import * as config from "./config";
        export const DEMO_CONFIG_SERVICE = config; // 匯出給其他 Application 使用
        DEMO_CONFIG_SERVICE.loadDeployUrls(); // 讀取所有系統網址
        ```

### Import config.json （Application）
* Angular
    * 新增 declarations.d.ts
        ```javascript
        declare module '@single-spa-demo/auth';
        ```
        
    * 設定 import
        ```javascript
        // extra-webpack.config.js
        module.exports = (config, options) => {
          const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
          singleSpaWebpackConfig.externals = [/^@single-spa-demo\/auth$/];
          return singleSpaWebpackConfig;
        };
        ```

    * 建立 config.json
        ```json
        // src\assets\config\config.json
        {
          "apiUrl": "...",
          "deployUrl": "..."
        }
        ```
        
    * 建立 app-config.service.ts
        ```typescript
        // src\app\core\services\app-config.service.ts
        import { Injectable } from '@angular/core';
        import { DEMO_CONFIG_SERVICE } from '@single-spa-demo/auth';
        import { Observable, of } from 'rxjs';
        import { first, switchMap } from 'rxjs/operators';

        @Injectable({
          providedIn: 'root',
        })
        export class AppConfigService {
          config: any;

          constructor() {}

          load(): Observable<any> {
            return DEMO_CONFIG_SERVICE.deployUrls_init$.pipe(
              first(),
              switchMap((deployUrls: any) => {
                if (!deployUrls) {
                  // for browser reload
                  return DEMO_CONFIG_SERVICE.deployUrls$.pipe(first());
                }
                return of(deployUrls);
              }),
              switchMap((deployUrls: any) => {
                return fetch(`${deployUrls['@single-spa-demo/main']}/assets/config/config.json`)
                  .then((res) => res.json())
                  .then((data) => {
                    this.config = data;
                  });
              })
            );
          }
        }
        ```
        
    * 設定 APP_INITIALIZER
        ```typescript
        // src\app\app.module.ts
        import { APP_INITIALIZER, NgModule } from '@angular/core';
        import { AppConfigService } from './core/services/app-config.service';
        export function initializeApp(AppConfigService: AppConfigService) {
          return () => AppConfigService.load();
        }

        @NgModule({
          declarations: [
            //...
          ],
          imports: [
            //...
          ],
          providers: [
            {
              provide: APP_INITIALIZER,
              useFactory: initializeApp,
              deps: [AppConfigService],
              multi: true,
            },
            //...
          ],
          bootstrap: [AppComponent],
        })
        export class AppModule {}
        ```

    * 加入方法 assetUrl 取代讀取圖片路徑
        * 建立 Service
            ```typescript
            // src\app\core\services\image.service.ts
            import { Injectable } from '@angular/core';
            import { AppConfigService } from '../services/app-config.service';
            import { DEMO_ICON } from '../constants/icon';

            @Injectable({
              providedIn: 'root',
            })
            export class ImageService {
              demoIcon = this.assetUrl(DEMO_ICON);

              constructor(private appConfigService: AppConfigService) {}

              assetUrl(url: string): string {
                const publicPath =
                  // @ts-ignore
                  this.appConfigService.config.deployUrl || __webpack_public_path__;
                const publicPathSuffix = publicPath.endsWith('/') ? '' : '/';
                const urlPrefix = url.startsWith('/') ? '' : '/';

                return `${publicPath}${publicPathSuffix}assets${urlPrefix}${url}`;
              }
            }
            ```
        * 圖片路徑皆需經過 assetUrl 方法
            ```typescript
            // example
            import { ImageService } from 'src/app/core/services/image.service';
            constructor(private imageService: ImageService) {}

            demoIcon = this.imageService.demoIcon;
            
            ```
* React
    * 加入全域變數 config、方法 assetUrl 取代讀取圖片路徑
        * 建立檔案 src/assets/config/config.json
            ```json
            {
                "deployUrl": "http://localhost:4201/"
            }
            ```
        * 建立檔案 src/single-spa/config-service.ts
            ```typescript
            import { DEMO_CONFIG_SERVICE } from "@single-spa-demo/auth";
            import { of } from "rxjs";
            import { first, switchMap } from "rxjs/operators";

            const config = {}
            export default config;

            // 讀取 ConfigMap 檔
            export async function load() {
              return await DEMO_CONFIG_SERVICE.deployUrls_init$
                .pipe(
                  first(),
                  switchMap((deployUrls: any) => {
                    if (!deployUrls) {
                      // for browser reload
                      return DEMO_CONFIG_SERVICE.deployUrls$.pipe(first());
                    }
                    return of(deployUrls);
                  }),
                  switchMap((deployUrls: any) => {
                    return fetch(
                      `${deployUrls[`@single-spa-demo/app-react`]}/assets/config/config.json`
                    )
                      .then((res) => res.json())
                      .then((data) => {
                        config = data;
                      });
                  })
                )
                .subscribe();
            }

            // 使用 config 中路徑讀取圖片
            export function assetUrl(url: string): string {                
              // @ts-ignore
              const publicPath = config.deployUrl || __webpack_public_path__;
              const publicPathSuffix = publicPath.endsWith('/') ? '' : '/';
              const urlPrefix = url.startsWith('/') ? '' : '/';
              return `${publicPath}${publicPathSuffix}assets${urlPrefix}${url}`;
            }
            ```
        
        * 進入點需呼叫讀取 config 檔
            ```typescript
            // index.tsx
            import { load as loadConfig } from './single-spa/config-service';
            function Root() {
              // 確認 config fetch 完成後再渲染 App.tsx
              const [doRen, setDoRen] = useState<boolean>(false);
              useEffect(() => {
                let doCon = loadConfig().subscribe({
                  next: () => {
                    if(!!config){setDoRen(true);} 
                  }
                });
                return () =>{
                  setDoRen(false);
                  doCon.unsubscribe();
                } 
              }, [])
              // ...
              {doRen ? <App /> : null}
              // ...
            }
            ```
        
        * 取代讀取圖片路徑
            * 圖片路徑皆需經過 assetUrl 方法

            ```typescript
            // example
            import NoticeImg from './assets/images/Img_Notice_Horizontal.svg';
            import { assetUrl } from './single-spa/config-service';
            function Test() {
              return (
                <div>
                    <img src={assetUrl(NoticeImg)} />
                </div>
              );
            }
            ```

## Import Local Folders
* 安裝套件 [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin)、[clean-webpack-plugin](https://www.npmjs.com/package/clean-webpack-plugin)
    ```npm install copy-webpack-plugin --save-dev```
    ```npm install clean-webpack-plugin --save-dev```
    
* 設置 patterns
    ```javascript
    plugins: [
      //...
      new CopyPlugin({
        patterns: [
          {
            from: "assets/js",
            to: "assets/js",
          },
          {
            from: "assets",
            to: "assets",
          },
        ],
      }),
      new CleanWebpackPlugin({
        verbose: true,
      }),
    ],
    ```

## Import packge styles
* 一般
    ```css
    // style.css
    @import "node_modules/<package>";
    ```

* 省略 node_modules

    ```css
    // src/style.css
    @import "<package>";
    ```
    
    ```json
    // angular.json
    "build": {
      //...
      "options": {
        "stylePreprocessorOptions": {
          "includePaths": ["node_modules"]
        },
      //...
      }
    }
    ```

## Use one style file when builded
* 安裝套件 [mini-css-extract-plugin](https://awdr74100.github.io/2020-03-02-webpack-minicssextractplugin/)
    ```npm install mini-css-extract-plugin --save-dev```

* 設置 rules
    ```javascript
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = (webpackConfigEnv, argv) => {
      //...

      return merge(defaultConfig, {
        plugins: [
          new MiniCssExtractPlugin(),
          //...
        ],
        module: {
          rules: [
            {
              test: /\.s[ac]ss$/i,
              use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
            },
          ],
        },
      });
    };
    ```

* Import main.css
    ```html
    <!--index.ejs-->
    <head>
        <!--...-->
        <link rel="stylesheet" href="/main.css">
        <!--...-->
    </head>
    ```
## Change output file name in webpack
* webpack 預設 build 完的靜態檔案（圖片、字型檔）檔名為 hash 值
* 方法一：webpack
    * 更改檔案檔名

        ```javascript
        module.exports = (webpackConfigEnv, argv) => {
          // ...
          return merge(defaultConfig, {
            // ...
            output: {
              // 入口檔案檔名
              filename: "[name].js",
              // 靜態檔案檔名
              assetModuleFilename: "[path][name][ext]",
            },
          });
        };
        ```

* 方法二：使用套件 craco（React）
    * 匯入套件 craco
        ```npm i --save-dev @craco/craco```
    * 建立檔案 src/craco.config.js
        ```javascript
        module.exports = {
            webpack: {
              configure: (config) => {
                config.output.libraryTarget = 'umd';
                config.output.filename = 'static/js/[name].js';
                config.output.library = 'single-spa-demo-app-react';
                config.output.publicPath = '/';
                return config
              }
            }
        }
        ```
    * 更改 script
        ```json
        // package.json
        {
            "scripts": {
                "start": "cross-env PORT=4201 craco start",
                "build": "craco build",
                // ...
            }
        }
        ```

## Prop
* defind
    ![image](https://hackmd.io/_uploads/HJgPvH66a.png)

* use
    ![image](https://hackmd.io/_uploads/rkBWOH6pT.png)

* get
    * not work...

## Import Module Federation
> [【NOTE】Module Federation](/-LHmfJBrTqi3H1hgBN-WbQ)

### Application（Angular）
* 使用 webpackMerge，修改檔案 extra-webpack.config.ts
    ```typescript
    const singleSpaAngularWebpack =
    require("single-spa-angular/lib/webpack").default;
    const webpackMerge = require("webpack-merge");
    const { ModuleFederationPlugin } = require("webpack").container;
    const { shareAll } = require("@angular-architects/module-federation/webpack");

    module.exports = (config, options) => {
      const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);

      return webpackMerge.default(singleSpaWebpackConfig, {
        plugins: [
          new ModuleFederationPlugin({
            name: "angular-app-name",
            filename: "remoteEntry.js",
            remotes: {
              modal: "layout@http://localhost:4201/remoteEntry.js",
            },
            shared: {
              ...shareAll({
                singleton: true,
                strictVersion: true,
                requiredVersion: "auto",
                eager: true, // 如 angular 版本太舊，需開啟預先載入
              }),
            },
          }),
        ],
      });
    };
    ```

    :::info
    * References for 'eager'
        * [Webpack Troubleshooting: Shared module is not available for eager consumption](https://webpack.js.org/concepts/module-federation/#troubleshooting)
        * [babel-loader version is too old](https://alexocallaghan.com/module-not-available-for-eager-consumption)
    * 疑難排解可參考 [【NOTE】Module Federation - 疑難排解](/-LHmfJBrTqi3H1hgBN-WbQ#疑難排解)
    :::
    

* 需使用動態載入，於欲使用 Module Federation 的元件中加入
    ```html
    <div #header></div>
    ```

    ```typescript
    import { loadRemoteModule } from '@angular-architects/module-federation';

    @ViewChild('header', { read: ViewContainerRef })
  viewContainer!: ViewContainerRef;

    async loadHeader() {
        const m = await loadRemoteModule({
           type: 'module',
           remoteEntry: 'http://localhost:4201/remoteEntry.js',
           exposedModule: './Component/Header'
         });
        const ref = this.viewContainer.createComponent(m.HeaderComponent);
        // const compInstance = ref.instance;
        // compInstance.ngOnInit()
      }
    ```
