Repository: microsoft/redux-micro-frontend Branch: main Commit: 5a2d96347f8f Files: 56 Total size: 112.4 KB Directory structure: gitextract_bpdac_6z/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── build-and-publish.yml │ ├── codeql-analysis.yml │ └── gated-build.yml ├── .gitignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.ko.md ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── azure-gated-build.yml ├── index.ts ├── karma.conf.headless.js ├── karma.conf.js ├── package.json ├── sample/ │ ├── counterApp/ │ │ ├── .babelrc │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── appCounter.js │ │ │ ├── counter.js │ │ │ ├── index.js │ │ │ └── store/ │ │ │ ├── counterReducer.js │ │ │ ├── global.actions.js │ │ │ └── local.actions.js │ │ ├── webpack.config.js │ │ └── webpack.config.mf.js │ ├── shell/ │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.js │ │ └── webpack.config.js │ └── todoApp/ │ ├── .babelrc │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── addTodo.js │ │ ├── index.js │ │ ├── store/ │ │ │ ├── todo.actions.js │ │ │ └── todoReducer.js │ │ ├── todo.js │ │ └── todoList.js │ ├── webpack.config.js │ └── webpack.config.mf.js ├── src/ │ ├── actions/ │ │ ├── action.interface.ts │ │ └── index.ts │ ├── common/ │ │ ├── abstract.logger.ts │ │ ├── console.logger.ts │ │ └── interfaces/ │ │ ├── global.store.interface.ts │ │ └── index.ts │ ├── global.store.ts │ └── middlewares/ │ └── action.logger.ts ├── test/ │ ├── global.store.tests.ts │ └── middlewares/ │ └── action.logger.tests.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/build-and-publish.yml ================================================ name: Build-and-Publish on: workflow_dispatch: jobs: build-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Dependencies run: npm install - name: Build run: npm run build - name: Test run: npm run test - name: Copy Files run: npm run copyfiles - name: Change Directory to lib run: cd lib - name: Setup Node uses: actions/setup-node@v2 with: node-version: '12.x' registry-url: 'https://registry.npmjs.org' - name: Publish run: cd lib && npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main, Min_Reviewer_Check ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '30 18 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .github/workflows/gated-build.yml ================================================ # This is a gated build to verify the validity of the code name: Gated-Build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Dependencies run: npm install - name: Build run: npm run build - name: Test run: npm run test ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Build bits /lib/**/**.* # Code Coverage /coverage/**/**.* # Ignoring dist folders /sample/counterApp/dist/**.* /sample/todoApp/dist/**/**.* /sample/shell/dist/**/**.* ================================================ FILE: .npmrc ================================================ registry:https://registry.npmjs.org/ always-auth=true package-lock=false ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our CLA. If you are adding new features please add unit test cases. For the modification of any existing feature ensure all existing test cases are running. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: README.ko.md ================================================ # Redux Micro-Frontend ## 더 이상 사용되지 않는 버전에 대한 경고 만약 당신이 1.1.0 버전을 사용하고 있을 경우, 바로 최신 버전으로 업그레이드를 하십시오. 해당 버전은 파이프라인 이슈로 인해 더 이상 사용되지 않습니다. ## 파이프라인 상태 [![Build Status](https://dev.azure.com/MicrosoftIT/OneITVSO/_apis/build/status/Compliant/Core%20Services%20Engineering%20and%20Operations/Corporate%20Functions%20Engineering/Professional%20Services/Foundational%20PS%20Services/Field%20Experience%20Platform/PS-FPSS-FExP-GitHub-Redux-Micro-Frontend?branchName=azure-pipelines)](https://dev.azure.com/MicrosoftIT/OneITVSO/_build/latest?definitionId=32881&branchName=azure-pipelines) [![CodeQL](https://github.com/microsoft/redux-micro-frontend/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/microsoft/redux-micro-frontend/actions/workflows/codeql-analysis.yml) [![Build-and-Publish](https://github.com/microsoft/redux-micro-frontend/actions/workflows/build-and-publish.yml/badge.svg?branch=main)](https://github.com/microsoft/redux-micro-frontend/actions/workflows/build-and-publish.yml) ![npm](https://img.shields.io/npm/dt/redux-micro-frontend) ## 개요 이 라이브러리는 Redux를 마이크로 프론트엔드 기반의 아키텍쳐에서 사용하기 위해 만들어졌습니다. 마이크로 프론트엔드는 모노리스 프론트엔드 애플리케이션을 벗어나 다루고 쉽고, 분리된, 또 작은 단위의 어플리케이션을 구현하기 위한 아키텍쳐 패턴입니다. 각 애플리케이션은 독립적이고 자립적인 하나의 유닛이 됩니다. 일반적으로 껍데기(shell) 애플리케이션은 엔드 유저들에게 비슷한 경험을 제공하기 위해 이러한 작은 유닛들에 대한 호스트로 사용되도록 구현됩니다. `Redux`는 예측가능한 상태 관리를 위해 만들어진 유명한 라이브러리들 중 하나입니다. 하지만, 일반적으로 리덕스를 사용할 때 하나의 store를 두고 하나의 상태 객체를 가지게 됩니다. 이 접근은 모든 마이크로 프론트엔드가 하나의 상태를 공유할 수 있다는 것을 의미합니다. 하지만 마이크로 프론트엔드 기반의 아키텍쳐에서 각 애플리케이션은 해당 스토어에 대해 독립적으로 운영되기 때문에 이러한 요소는 마이크로 프론트엔드 아키텍쳐에서 문제 요소로 작용합니다. 몇몇의 개발자는 마이크로 프론트엔드의 각 애플리케이션의 분리 수준을 제공하기 위해 `combineReducer()`를 사용하기도 합니다. 마이크로 프론트엔드를 구현하기 위해 분리된 리듀서를 작성하고, 이들을 하나의 거대한 리듀서로 통합하기 위해서 말이지요. 이러한 것들이 어느 정도의 문제를 해결할 수는 있지만, 이는 단 하나의 상태 객체가 모든 각각의 앱들에 대해 공유된다는 것을 의미합니다. 이는 충분한 조치가 없을 경우, 각각의 앱들이 뜻하지 않게 다른 상태 객체들을 재정의(override)할 수도 있습니다. 마이크로 프론트엔드 아키텍쳐에서, 각각의 애플리케이션은 다른 앱들의 상태에 접근하여 상태값을 수정해서는 안됩니다. 그럼에도 각각의 앱들은 다른 앱들의 상태를 알아야 할 경우가 있습니다. 애플리케이션 간의 커뮤니케이션을 가능케하는 선에서 그들은 이벤트 또는 액션을 다른 스토어에 전달할 수 있습니다. 나아가, 다른 앱들 사이의 상태 변화를 감지할 수도 있습니다. 이 라이브러리는 각 애플리케이션의 분리와 애플리케이션 간의 커뮤니케이션 두 개의 마이크로 프론트엔드 아키텍쳐 요구사항을 만족시킬 수 있는 라이브러리입니다. ## 개념 `전역 스토어`의 개념은 다양한 `리덕스 스토어`들을 가상으로 병합하기 위해 도입되었습니다. 엄밀히 말하면, `전역 스토어`는 스토어는 아닙니다. 오히려 이는 다양한 독립적인 `리덕스 스토어`들의 집합입니다. 각각의 물리적인 `리덕스 스토어`는 각각의 앱이 사용하는 독립된 스토어를 참조합니다. `전역 스토어`에 접근하는 마이크로 프론트엔드 앱들은 `getState()`, `dispatch()` 그리고 `subscribe()`와 같은 각각의 `리덕스 스토어`에 대한 모든 작업들을 수행할 수 있습니다. 각각의 마이크로 프론트엔드 앱들은 그들만의 `리덕스 스토어`를 가질 수 있습니다. 각각의 앱들은 그들의 `리덕스 스토어`를 만들고 `전역 스토어`에 등록할 수 있습니다. 그 `전역 스토어`는 이러한 개별 리덕스 스토어를 사용하여 다른 모든 스토어의 상태를 조합한 전역 상태(Global State)를 투영합니다. 모든 마이크로 프론트엔드 앱들은 이 `전역 스토어`에 접근할 수 있고, 다른 마이크로 프론트엔드의 상태를 볼 수 있습니다. 하지만 그들을 수정할 수는 없죠. 앱에서 디스패치된 작업은 앱에서 등록된 스토어 내에서 제한되어 있어 다른 스토어에 디스패치되지 않으므로 컴포넌트화 또는 분리가 가능합니다. ### 더 읽어볼 것들 [상태 관리의 기본](https://www.devcompost.com/post/state-management-for-front-end-applications-part-i-what-and-why) ### 전역 액션 전역 액션은 특정 앱이 다른 마이크로 프론트엔드 앱에 의해 등록된 스토어에 액션을 디스패치할 수 있는 개념입니다. 각 마이크로 프론트엔드 앱에는 스토어와 함께 전역 액션 집합을 등록할 수 있는 기능이 있습니다. 이러한 전역 액션 집합은 다른 마이크로 프론트엔드 앱에 의해 해당 마이크로 프론트엔드의 스토어에서 디스패치될 수 있습니다. 따라서 애플리케이션 간의 통신이 가능하게 됩니다. ![Global Store](https://github.com/microsoft/redux-micro-frontend/blob/main/assets/Global_Store_Dispatch.png) ### 상태 간의 상호 콜백 애플리케이션 간의 커뮤니케이션은 다른 마이크로 프론트엔드의 상태에 대한 변화를 구독하는 것으로부터 이루어집니다. 각각의 마이크로 프론트엔드 앱들은 다른 상태들에 대해 읽기 권한만(read-only) 가지고 있기 때문에, 그들은 상태 변화를 읽기 위해 콜백을 붙일 수 있습니다. 콜백들은 각각 스토어 레벨 또는 전역 레벨로 붙여질 수 있습니다. (전역 레벨 콜백은 어떤 스토어이던 상태 변화가 발생할 경우 콜백을 일으킨다는 것을 의미합니다.) ## 단일 상태 공유의 문제점 - 실수로 다른 앱의 상태를 재정의할 수 있습니다. (중복된 액션들이 여러 앱에 의해 디스패치되는 경우) - 앱들은 다른 마이크로 프론트엔드들을 알고 있어야 합니다. - 공유된 미들웨어들. 하나의 스토어만 유지되기 때문에, 모든 마이크로 프론트엔드들이 동일한 미들웨어를 공유해야 합니다. 따라서 어떤 앱은 `redux-saga`를 쓰고, 어떤 앱은 `redux-thunk`를 하는 등의 작업은 할 수 없습니다. ## 설치 ```sh npm install redux-micro-frontend --save ``` ## 빠른 적용 ### 전역 스토어의 인스턴스 얻기 ```javascript import { GlobalStore } from 'redux-micro-frontend'; ... this.globalStore = GlobalStore.Get(); ``` ### 스토어를 생성하고 등록하기 ```javascript let appStore = createStore(AppReducer); // 리덕스 스토어 this.globalStore.RegisterStore("App1", appStore); this.globalStore.RegisterGlobalActions("App1", ["Action-1", "Action-2"]); // 이 액션들은 다른 앱들에 의해 이 스토어에 디스패치될 수 있습니다. ``` ### 액션 디스패치하기 ```javascript let action = { type: 'Action-1', payload: 'Some data' } this.globalStore.DispatchAction("App1", action); // 이는 현재 앱의 스토어뿐만 아니라, 'Action-1'을 전역 액션으로 등록한 다른 스토어로도 액션이 전송됩니다. ``` ### 상태 구독하기 ```javascript // 모든 앱들의 상태 변화 this.globalStore.Subscribe("App1", localStateChanged); // 현재 앱의 상태 변화 this.globalStore.SubscribeToGlobalState("App1", globalStateChanged); // App2의 상태 변화 this.globalStore.SubscribeToPartnerState("App1", "App2", app2StateChanged); ... localStateChanged(localState) { // 새로운 상태에 대한 작업을 수행하세요. } globalStateChanged(stateChanged) { // 전역 상태는 스토어에 등록된 모든 앱들을 위한 분리된 어트리뷰트를 가질 수 있습니다. let app1State = globalState.App1; let app2State = globalState.App2; } app2StateChanged(app2State) { // app2의 새로운 상태에 대한 작업을 수행하세요. } ``` ## 샘플 앱 위치: https://github.com/microsoft/redux-micro-frontend/tree/main/sample 샘플 앱을 실행하기 위한 안내서 1. sample/counterApp으로 가서 `npm i`을 실행하세요. 그리고 `npm run start`을 하세요. 2. sample/todoApp으로 가서 `npm i`을 실행하세요. 그리고 `npm run start`을 하세요. 3. sample/shell으로 가서 `npm i`을 실행하세요. 그리고 `npm run start`을 하세요. 4. http://localhost:6001을 여세요. ## 문서 [Github 위키](https://github.com/microsoft/redux-micro-frontend/wiki) ## 부록 - Redux의 기본기를 배우기 위해서는 Redux 공식 문서를 확인하세요. - https://redux.js.org/. - 마이크로 프론트엔드 아키텍쳐에 대해 더 알고 싶다면, [martinfowler.com](http://martinfowler.com/)에서 만든 [이 아티클](https://martinfowler.com/articles/micro-frontends.html)을 확인하세요. ## Trademarks 이 프로젝트에는 프로젝트, 제품 또는 서비스의 상표 또는 로고가 포함될 수 있습니다. Microsoft 상표 또는 로고의 승인된 사용은 Microsoft의 상표 & 브랜드 지침의 적용을 받으며 반드시 따라야 합니다. 이 프로젝트의 수정된 버전에서 Microsoft 상표 또는 로고를 사용하면 Microsoft의 후원이 혼동되거나 암시되어서는 안 됩니다. 타사 상표 또는 로고는 해당 타사 정책의 적용을 받습니다. ================================================ FILE: README.md ================================================ # Redux Micro-Frontend [한국어🇰🇷](./README.ko.md) ## Version Deprecation Warning 1.1.0 - If you are using this version, please upgrade to latest immediately. This version has been deprecated due to a pipeline issue. ## Pipeline Status [![Build Status](https://dev.azure.com/MicrosoftIT/OneITVSO/_apis/build/status/Compliant/Core%20Services%20Engineering%20and%20Operations/Corporate%20Functions%20Engineering/Professional%20Services/Foundational%20PS%20Services/Field%20Experience%20Platform/PS-FPSS-FExP-GitHub-Redux-Micro-Frontend?branchName=azure-pipelines)](https://dev.azure.com/MicrosoftIT/OneITVSO/_build/latest?definitionId=32881&branchName=azure-pipelines) [![CodeQL](https://github.com/microsoft/redux-micro-frontend/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/microsoft/redux-micro-frontend/actions/workflows/codeql-analysis.yml) [![Build-and-Publish](https://github.com/microsoft/redux-micro-frontend/actions/workflows/build-and-publish.yml/badge.svg?branch=main)](https://github.com/microsoft/redux-micro-frontend/actions/workflows/build-and-publish.yml) ![npm](https://img.shields.io/npm/dt/redux-micro-frontend) ## Overview This library can be used for using Redux in a Micro Frontend based architecture. Micro Frontends is an architectural pattern for breaking up a monolith Frontend application into manageable, decoupled and smaller applications. Each application is a self-contained and isolated unit. Generally, a common shell/platform application is used to host these small units to provide a common experience for the end-users. `Redux` is one of the most popular libraries for predictable state management. However, the general practice in using Redux is to have a single store, thereby having a single state object. This approach would mean that all the Micro Frontends would have a shared state. This is a violation of the Micro Frontend based architecture since each App is supposed to be a self-contained unit having its store. To provide a level of isolation some developers use `combineReducer()` to write a separate reducer for each Micro Frontend and then combine them into one big reducer. Although it would solve some problems this would still imply that a single state object is shared across all the apps. In the absence of sufficient precautions, apps might accidentally override each other's state. In a Micro Frontend architecture, an individual application should not be able to modify the state of other apps. However, they should be able to see the state of other apps. Along the same line for enabling cross-application communication, they should also be able to send events/actions to other Stores and also get notified of changes in other apps' state. This library aims to attain that sweet spot between providing isolation and cross-application communication. ## Concept A concept of `Global Store` is introduced which is a virtual amalgamation of multiple `Redux Stores`. Strictly speaking, the `Global Store` is not an actual store, rather it's a collection of multiple isolated `Redux Stores`. Each physical `Redux Store` here refers to the isolated store that each app uses. Micro frontends having access to the `Global Store` would be able to perform all operations that are allowed on an individual `Redux Store` including `getState()`, `dispatch()` and `subscribe()`. Each Micro Frontend would have the capability to have its own `Redux Store`. Each app would create and register their `Redux Store` with the `Global Store`. The `Global Store` then uses these individual stores to project a Global State which is a combination of the state from all the other Stores. All the Micro Frontends would have access to the Global Store and would be able to see the state from the other Micro Frontends but won't be able to modify them. Actions dispatched by an app remains confined within the store registered by the app and is not dispatched to the other stores, thereby providing componentization and isolation. ### Read more - [State Management Basics](https://www.devcompost.com/post/state-management-for-front-end-applications-part-i-what-and-why) - [State Management in Micro Frontends](https://www.devcompost.com/post/state-management-ii-world-of-micro-frontends) ### Global Actions A concept of `Global Action` is available which allows other apps to dispatch actions to stores registered by other Micro Frontends. Each Micro Frontend has the capability to register a set of global actions along with the store. These set of global actions can be dispatched in this Micro Frontend's store by other Micro Frontends. This enables cross-application communication. ![Global Store](https://github.com/microsoft/redux-micro-frontend/blob/main/assets/Global_Store_Dispatch.png) ### Cross-state callbacks Cross-application communication can also be achieved by subscribing to change notifications in other Micro Frontend's state. Since each micro-frontend has read-only permission to other states, they can also attach callbacks for listening to state changes. The callbacks can be attached either at an individual store level or at a global level (this would mean that state change in any store would invoke the callback). ## Problems of a single shared state - Accidental override of state of other apps (in case duplicate actions are dispatched by multiple apps) - Apps would have to be aware of other Micro Frontends - Shared middlewares. Since only a single store is maintained, all the Micro Frontends would have to share the same middlewares. So in situations where one app wants to use `redux-saga` and other wants to use `redux-thunk` is not possible. ## Installation ```sh npm install redux-micro-frontend --save ``` ## Quick Guide ### Get an instance of Global Store ```javascript import { GlobalStore } from 'redux-micro-frontend'; ... this.globalStore = GlobalStore.Get(); ``` ### Create/Register Store ```javascript let appStore = createStore(AppReducer); // Redux Store this.globalStore.RegisterStore("App1", appStore); this.globalStore.RegisterGlobalActions("App1", ["Action-1", "Action-2"]); // These actions can be dispatched by other apps to this store ``` ### Dispatch Action ```javascript let action = { type: 'Action-1', payload: 'Some data' } this.globalStore.DispatchAction("App1", action); // This will dispatch the action to current app's store as well other stores who might have registered 'Action-1' as a global action ``` ### Subscribe to State ```javascript // State change in any of the apps this.globalStore.Subscribe("App1", localStateChanged); // State change in the current app this.globalStore.SubscribeToGlobalState("App1", globalStateChanged); // State change in app2's state this.globalStore.SubscribeToPartnerState("App1", "App2", app2StateChanged); ... localStateChanged(localState) { // Do something with the new state } globalStateChanged(stateChanged) { // The global state has a separate attribute for all the apps registered in the store let app1State = globalState.App1; let app2State = globalState.App2; } app2StateChanged(app2State) { // Do something with the new state of app 2 } ``` ## Sample App Location: https://github.com/microsoft/redux-micro-frontend/tree/main/sample Instruction for running Sample App 1. Go to sample/counterApp and run `npm i` and then `npm run start` 2. Go to sample/todoApp and run `npm i` and then `npm run start` 3. Go to sample/shell and run `npm i` and then `npm run start` 4. Browse http://localhost:6001 ## Documentation [See Github wiki](https://github.com/microsoft/redux-micro-frontend/wiki) ## Appendix - To learn the basics for Redux check for [official documentation of Redux](https://redux.js.org/) - https://redux.js.org/. - To know more about [Micro Front-end](https://martinfowler.com/articles/micro-frontends.html) style of architecture check [this article](https://martinfowler.com/articles/micro-frontends.html) from [martinfowler.com](https://martinfowler.com/articles/micro-frontends.html). ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). ================================================ FILE: SUPPORT.md ================================================ # Support ## How to file issues and get help This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER CHANNEL. WHERE WILL YOU HELP PEOPLE?**. ## Microsoft Support Policy Support for this **PROJECT or PRODUCT** is limited to the resources listed above. ================================================ FILE: azure-gated-build.yml ================================================ trigger: none jobs: - job: BuildLibrary displayName: Build Library pool: vmImage: ubuntu-latest steps: - task: NodeTool@0 inputs: versionSpec: '12.x' checkLatest: true - task: Npm@1 displayName: Install Dependencies inputs: command: 'install' - task: Npm@1 displayName: Build Library inputs: command: 'custom' customCommand: 'run build' - task: Npm@1 displayName: Test inputs: command: 'custom' customCommand: 'run test' - job: BuildSample displayName: Build Sample Projects pool: vmImage: ubuntu-latest steps: - task: NodeTool@0 inputs: versionSpec: '12.x' checkLatest: true - task: Npm@1 displayName: Install Counter App Dependencies inputs: command: 'install' workingDir: 'sample/counterApp' - task: Npm@1 displayName: Build Counter App inputs: command: 'custom' customCommand: 'run build' workingDir: 'sample/counterApp' - task: Npm@1 displayName: Install Todo App Dependencies inputs: command: 'install' workingDir: 'sample/todoApp' - task: Npm@1 displayName: Build Todo App inputs: command: 'custom' customCommand: 'run build' workingDir: 'sample/todoApp' - task: Npm@1 displayName: Install Shell Dependencies inputs: command: 'install' workingDir: 'sample/shell' - task: Npm@1 displayName: Build Shell inputs: command: 'custom' customCommand: 'run build' workingDir: 'sample/shell' ================================================ FILE: index.ts ================================================ export * from './src/actions'; export * from './src/global.store'; export * from './src/common/interfaces'; export * from './src/common/abstract.logger'; export * from './src/common/interfaces/global.store.interface'; ================================================ FILE: karma.conf.headless.js ================================================ module.exports = function (config) { config.set({ frameworks: ["jasmine", "karma-typescript"], files: [ { pattern: "src/**/*.ts" }, { pattern: "test/**/*.ts" } ], preprocessors: { "src/**/*.ts": ["karma-typescript"], "test/**/*.ts": "karma-typescript" }, reporters: ["progress"], customLaunchers: { ChromeHeadlessCustom: { base: 'ChromeHeadless', flags: ['--no-sandbox', '--disable-gpu'] } }, browsers: ["ChromeHeadlessCustom"], karmaTypescriptConfig: { coverageReporter: { instrumenterOptions: { istanbul: { noCompact: true } } }, bundlerOptions: { transforms: [ require("karma-typescript-es6-transform")({ presets: [ ["env", { targets: { chrome: "60" } }] ] }) ] }, compilerOptions: { module: "commonjs", sourceMap: true, target: "es6", allowJs: false, declaration: true, moduleResolution: "node", skipLibCheck: true, lib: ["es2017", "DOM"], downlevelIteration: true }, typeRoots: [ "node_modules/@types" ], exclude: [ "node_modules/**/*" ] }, singleRun: true, autoWatch: false, plugins: ['karma-jasmine', 'karma-chrome-launcher', 'karma-typescript'] }); }; ================================================ FILE: karma.conf.js ================================================ module.exports = function (config) { config.set({ frameworks: ["jasmine", "karma-typescript"], files: [ { pattern: "src/**/*.ts" }, { pattern: "test/**/*.ts" } ], preprocessors: { "src/**/*.ts": ["karma-typescript"], "test/**/*.ts": "karma-typescript" }, reporters: ["progress", "karma-typescript"], browsers: ["Chrome"], karmaTypescriptConfig: { coverageReporter: { instrumenterOptions: { istanbul: { noCompact: true } } }, bundlerOptions: { transforms: [ require("karma-typescript-es6-transform")({ presets: [ ["env", { targets: { chrome: "60" } }] ] }) ] }, compilerOptions: { module: "commonjs", sourceMap: true, target: "es6", allowJs: false, declaration: true, moduleResolution: "node", skipLibCheck: true, lib: ["es2017", "DOM"], downlevelIteration: true }, typeRoots: [ "node_modules/@types" ], exclude: [ "node_modules/**/*" ] }, plugins: ['karma-jasmine', 'karma-chrome-launcher', 'karma-typescript'] }); }; ================================================ FILE: package.json ================================================ { "name": "redux-micro-frontend", "version": "1.3.0", "license": "MIT", "description": "This is a library for using Redux to managing state for self-contained apps in a Micro-Frontend architecture. Each self-contained isolated app can have its own isolated and decoupled Redux store. The componentized stores interact with a global store for enabling cross-application communication.", "author": { "name": "Pratik Bhattacharya", "email": "pratikb@microsoft.com", "url": "https://www.devcompost.com/" }, "homepage": "https://github.com/microsoft/redux-micro-frontend", "keywords": [ "redux", "micro frontend", "microfrontend", "microfrontends", "micro frontends", "state", "statemanagement" ], "bugs": { "url": "https://github.com/microsoft/redux-micro-frontend/issues", "email": "pratikb@microsoft.com" }, "repository": { "type": "git", "url": "https://github.com/microsoft/redux-micro-frontend" }, "scripts": { "build": "tsc", "test-chrome": "karma start karma.conf.js", "test": "karma start karma.conf.headless.js", "release:pre": "npm run build && npm version prerelease && npm run copyfiles:publish-beta", "release:patch": "npm run build && npm version patch && npm run copyfiles:publish", "release:minor": "npm run build && npm version minor && npm run copyfiles:publish", "release:major": "npm run build && npm version major && npm run copyfiles:publish", "copyfiles": "npm run copy:packagejson && npm run copy:npmrc", "copyfiles:publish": "npm run copy:packagejson && npm run copy:npmrc && cd lib && npm publish", "copyfiles:publish-beta": "npm run copy:packagejson && npm run copy:npmrc && cd lib && npm publish --tag beta", "copy:packagejson": "cpr package.json lib/package.json -o", "copy:npmrc": "cpr .npmrc lib/.npmrc -o", "clean": "rimraf node_modules", "rebuild": "npm run clean && npm i && npm build" }, "private": false, "dependencies": { "flatted": "^2.0.2", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8" }, "devDependencies": { "@types/jasmine": "^3.5.14", "cpr": "^3.0.1", "jasmine": "^3.6.2", "karma": "^6.3.4", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", "karma-jasmine": "^3.3.1", "karma-typescript": "^5.5.1", "karma-typescript-es6-transform": "^4.1.1", "puppeteer": "^5.5.0", "rimraf": "^3.0.2", "typescript": "^3.5.3" } } ================================================ FILE: sample/counterApp/.babelrc ================================================ { "presets": [ "@babel/preset-react" ] } ================================================ FILE: sample/counterApp/index.html ================================================
================================================ FILE: sample/counterApp/package.json ================================================ { "name": "redux-micro-frontend-sample-counter", "version": "0.0.0", "license": "MIT", "description": "This is a sample app with counter", "author": { "name": "Pratik Bhattacharya", "email": "pratikb@microsoft.com", "url": "https://www.devcompost.com/" }, "homepage": "https://github.com/microsoft/redux-micro-frontend", "keywords": [ "redux", "micro frontend", "microfrontend", "microfrontends", "micro frontends", "state", "statemanagement" ], "bugs": { "url": "https://github.com/microsoft/redux-micro-frontend/issues", "email": "pratikb@microsoft.com" }, "repository": { "type": "git", "url": "https://github.com/microsoft/redux-micro-frontend" }, "scripts": { "build": "webpack", "start": "webpack serve", "start-component": "webpack serve --config ./webpack.config.mf.js" }, "private": false, "devDependencies": { "@babel/cli": "^7.12.1", "@babel/core": "^7.12.3", "@babel/preset-env": "^7.12.1", "@babel/preset-react": "^7.12.1", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", "css-loader": "^5.0.0", "html-webpack-plugin": "^4.5.0", "path-parse": "^1.0.7", "react": "^16.14.0", "react-dom": "^16.14.0", "redux-micro-frontend": "^1.1.1", "style-loader": "^2.0.0", "webpack": "^5.1.3", "webpack-cli": "^4.1.0", "webpack-dev-server": "^3.11.2" } } ================================================ FILE: sample/counterApp/src/appCounter.js ================================================ import React from 'react'; import { Counter } from './counter'; import { GlobalStore } from 'redux-micro-frontend'; import { CounterReducer } from './store/counterReducer'; import { IncrementLocalCounter, DecrementLocalCounter } from './store/local.actions'; import { IncrementGlobalCounter, DecrementGlobalCounter } from './store/global.actions'; export class AppCounter extends React.Component { constructor(props) { super(props); this.state = { local: 0, global: 0, todo: 0 }; this.incrementLocalCounter = this.incrementLocalCounter.bind(this); this.decrementLocalCounter = this.decrementLocalCounter.bind(this); this.incrementGlobalCounter = this.incrementGlobalCounter.bind(this); this.decrementGlobalCounter = this.decrementGlobalCounter.bind(this); this.updateState = this.updateState.bind(this); this.globalStore = GlobalStore.Get(false); this.store = this.globalStore.CreateStore("CounterApp", CounterReducer, []); this.globalStore.RegisterGlobalActions("CounterApp", ["INCREMENT_GLOBAL", "DECREMENT_GLOBAL", "ADD_TODO", "REMOVE_TODO"]); this.globalStore.SubscribeToGlobalState("CounterApp", this.updateState) } incrementLocalCounter() { this.globalStore.DispatchAction("CounterApp", IncrementLocalCounter()); } decrementLocalCounter() { this.globalStore.DispatchAction("CounterApp", DecrementLocalCounter()); } incrementGlobalCounter() { this.globalStore.DispatchAction("CounterApp", IncrementGlobalCounter()); } decrementGlobalCounter() { this.globalStore.DispatchAction("CounterApp", DecrementGlobalCounter()); } updateState(globalState) { this.setState({ local: globalState.CounterApp.local, global: globalState.CounterApp.global, todo: globalState.CounterApp.todo }); } render() { return (

Todo Counter

{this.state.todo}
) } } ================================================ FILE: sample/counterApp/src/counter.js ================================================ import React from 'react'; export class Counter extends React.Component { render() { return (

{this.props.header}

{this.props.count}
) } } ================================================ FILE: sample/counterApp/src/index.js ================================================ import React from 'react'; import { render } from 'react-dom'; import { AppCounter } from './appCounter'; const mountCounter = (elementId) => { const renderElemement = document.getElementById(elementId); render(, renderElemement); } window["mountCounter"] = mountCounter; if (!(window["micro-front-end-context"])) { mountCounter("app"); } ================================================ FILE: sample/counterApp/src/store/counterReducer.js ================================================ export const CounterReducer = (state = initialState, action) => { if (action.type === "INCREMENT_GLOBAL") return { ...state, global: state.global + 1 }; if (action.type === "DECREMENT_GLOBAL") return { ...state, global: state.global - 1 }; if (action.type === "INCREMENT_LOCAL") return { ...state, local: state.local + 1 }; if (action.type === "DECREMENT_LOCAL") return { ...state, local: state.local - 1 }; if (action.type === "ADD_TODO") return { ...state, todo: state.todo + 1 }; if (action.type === "REMOVE_TODO") return { ...state, todo: state.todo - 1 }; return state; } var initialState = { global: 0, local: 0, todo: 0 } ================================================ FILE: sample/counterApp/src/store/global.actions.js ================================================ export const IncrementGlobalCounter = () => { return { type: "INCREMENT_GLOBAL", payload: null } } export const DecrementGlobalCounter = () => { return { type: "DECREMENT_GLOBAL", payload: null } } ================================================ FILE: sample/counterApp/src/store/local.actions.js ================================================ export const IncrementLocalCounter = () => { return { type: "INCREMENT_LOCAL", payload: null } } export const DecrementLocalCounter = () => { return { type: "DECREMENT_LOCAL", payload: null } } ================================================ FILE: sample/counterApp/webpack.config.js ================================================ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', mode: 'development', devtool: 'cheap-module-source-map', plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "React", template: 'index.html' }) ], module: { rules:[{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }] }, devServer: { contentBase: './dist', port: 4001 }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } } ================================================ FILE: sample/counterApp/webpack.config.mf.js ================================================ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { entry: './src/index.js', mode: 'production', devtool: 'cheap-module-source-map', plugins: [ new CleanWebpackPlugin() ], module: { rules:[{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }] }, devServer: { contentBase: './dist', port: 4001 }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } } ================================================ FILE: sample/shell/index.html ================================================

MF 1 - Counter App


MF 2 - Todo App

================================================ FILE: sample/shell/package.json ================================================ { "name": "redux-micro-frontend-sample-shell", "version": "0.0.0", "license": "MIT", "description": "This is a sample app as the MF shell", "author": { "name": "Pratik Bhattacharya", "email": "pratikb@microsoft.com", "url": "https://www.devcompost.com/" }, "homepage": "https://github.com/microsoft/redux-micro-frontend", "keywords": [ "redux", "micro frontend", "microfrontend", "microfrontends", "micro frontends", "state", "statemanagement" ], "bugs": { "url": "https://github.com/microsoft/redux-micro-frontend/issues", "email": "pratikb@microsoft.com" }, "repository": { "type": "git", "url": "https://github.com/microsoft/redux-micro-frontend" }, "scripts": { "build": "webpack", "start": "webpack serve" }, "private": false, "dependencies": { "react": "^16.14.0", "react-dom": "^16.14.0", "redux-micro-frontend": "^1.1.1" }, "devDependencies": { "clean-webpack-plugin": "^3.0.0", "css-loader": "^5.0.0", "html-webpack-plugin": "^4.5.0", "style-loader": "^2.0.0", "webpack": "^5.1.3", "webpack-cli": "^4.1.0", "webpack-dev-server": "^3.11.2" } } ================================================ FILE: sample/shell/src/index.js ================================================ (function() { window["micro-front-end-context"] = true; window["mountCounter"]("counter"); window["mountTodo"]("todo"); })(); ================================================ FILE: sample/shell/webpack.config.js ================================================ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', mode: 'development', devtool: 'cheap-module-source-map', plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "Micro-Front end Sample", template: 'index.html' }) ], module: { rules:[{ test: /\.css$/, use: ['style-loader', 'css-loader'] }] }, devServer: { contentBase: './dist', port: 6001 }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } } ================================================ FILE: sample/todoApp/.babelrc ================================================ { "presets": [ "@babel/preset-react" ] } ================================================ FILE: sample/todoApp/index.html ================================================
================================================ FILE: sample/todoApp/package.json ================================================ { "name": "redux-micro-frontend-sample-todo", "version": "0.0.0", "license": "MIT", "description": "This is a sample app with counter", "author": { "name": "Pratik Bhattacharya", "email": "pratikb@microsoft.com", "url": "https://www.devcompost.com/" }, "homepage": "https://github.com/microsoft/redux-micro-frontend", "keywords": [ "redux", "micro frontend", "microfrontend", "microfrontends", "micro frontends", "state", "statemanagement" ], "bugs": { "url": "https://github.com/microsoft/redux-micro-frontend/issues", "email": "pratikb@microsoft.com" }, "repository": { "type": "git", "url": "https://github.com/microsoft/redux-micro-frontend" }, "scripts": { "build": "webpack", "start": "webpack serve", "start-component": "webpack serve --config ./webpack.config.mf.js" }, "private": false, "dependencies": {}, "devDependencies": { "@babel/cli": "^7.12.1", "@babel/core": "^7.12.3", "@babel/preset-env": "^7.12.1", "@babel/preset-react": "^7.12.1", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", "css-loader": "^5.0.0", "html-webpack-plugin": "^4.5.0", "react": "^16.14.0", "react-dom": "^16.14.0", "redux-micro-frontend": "^1.1.1", "style-loader": "^2.0.0", "webpack": "^5.1.3", "webpack-cli": "^4.1.0", "webpack-dev-server": "^3.11.2" } } ================================================ FILE: sample/todoApp/src/addTodo.js ================================================ import React from 'react'; export class AddTodo extends React.Component { constructor(props) { super(props); this.addTodo = this.addTodo.bind(this); } addTodo() { this.props.addTodo(this.description.value); this.description.value = ''; } render() { return (
{ this.description = node; }}>
) } } ================================================ FILE: sample/todoApp/src/index.js ================================================ import React from 'react'; import { render } from 'react-dom'; import { TodoList } from './todoList'; const mountTodo = (elementId) => { const renderElemement = document.getElementById(elementId); render(, renderElemement); } window["mountTodo"] = mountTodo; if (!(window["micro-front-end-context"])) { mountTodo("app"); } ================================================ FILE: sample/todoApp/src/store/todo.actions.js ================================================ export const AddTodo = (description) => { return { type: 'ADD_TODO', payload: description } } export const RemoveTodo = (id) => { return { type: 'REMOVE_TODO', payload: id } } ================================================ FILE: sample/todoApp/src/store/todoReducer.js ================================================ export const TodoReducer = (state = [], action) => { if (action.type === 'ADD_TODO') { return [...state, { id: state.length + 1, description: action.payload }]; } if (action.type === 'REMOVE_TODO') { var todoId = action.payload; var index = state.findIndex(todo => todo.id === todoId); if (index === undefined || index === null || index < 0) return state; return [ ...state.slice(0, index), ...state.slice(index + 1) ]; } return state; } ================================================ FILE: sample/todoApp/src/todo.js ================================================ import React from 'react'; export class Todo extends React.Component { render() { return(
this.props.removeTodo(this.props.id)}> {this.props.description}
) } } ================================================ FILE: sample/todoApp/src/todoList.js ================================================ import React from 'react'; import { Todo } from './todo'; import { createStore } from 'redux'; import { AddTodo as AddTodoComponent } from './addTodo'; import { TodoReducer } from './store/todoReducer'; import { GlobalStore } from 'redux-micro-frontend'; import { AddTodo, RemoveTodo } from './store/todo.actions'; export class TodoList extends React.Component { constructor(props) { super(props); this.state = { todos: [], globalCounter: 0 }; this.addTodo = this.addTodo.bind(this); this.removeTodo = this.removeTodo.bind(this); this.counterChanged = this.counterChanged.bind(this); this.stateChanged = this.stateChanged.bind(this); this.globalStore = GlobalStore.Get(); this.store = createStore(TodoReducer); this.globalStore.RegisterStore("TodoApp", this.store, [GlobalStore.AllowAll]); try { this.globalStore.SubscribeToPartnerState("TodoApp", "CounterApp", this.counterChanged) } catch (error) { //Since } this.globalStore.Subscribe("TodoApp", this.stateChanged); } addTodo(description) { this.globalStore.DispatchAction("TodoApp", AddTodo(description)); } removeTodo(todoId) { this.globalStore.DispatchAction("TodoApp", RemoveTodo(todoId)); } counterChanged(counterState) { this.setState({ globalCounter: counterState.global }); } stateChanged(todoState) { this.setState({ todos: todoState }); } render() { return (

Todos

    {this.state.todos.map(todo => { return (
  • ) })}
Global Counter: {this.state.globalCounter}
) } } ================================================ FILE: sample/todoApp/webpack.config.js ================================================ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', mode: 'development', devtool: 'cheap-module-source-map', plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "React", template: 'index.html' }) ], module: { rules:[{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }] }, devServer: { contentBase: './dist', port: 5001 }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } } ================================================ FILE: sample/todoApp/webpack.config.mf.js ================================================ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { entry: './src/index.js', mode: 'production', devtool: 'cheap-module-source-map', plugins: [ new CleanWebpackPlugin() ], module: { rules:[{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }] }, devServer: { contentBase: './dist', port: 5001 }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } } ================================================ FILE: src/actions/action.interface.ts ================================================ export interface IAction { type: string, payload: Payload, logEnabled?: Boolean } ================================================ FILE: src/actions/index.ts ================================================ export * from './action.interface'; ================================================ FILE: src/common/abstract.logger.ts ================================================ /** * Summary Logs data from application. Follows a Chain of Responsibility pattern where multiple loggers can be chained. */ export abstract class AbstractLogger { LoggerIdentity: String; NextLogger: AbstractLogger; constructor(id: string) { this.LoggerIdentity = id; } /** * Summary Logs an event * @param source Location from where the log is sent * @param eventName Name of the event that has occurred * @param properties Properties (KV pair) associated with the event */ LogEvent(source: string, eventName: string, properties: any) { try { this.processEvent(source, eventName, properties); if (this.NextLogger !== undefined && this.NextLogger !== null) { this.NextLogger.LogEvent(source, eventName, properties); } } catch { // DO NOT THROW AN EXCEPTION WHEN LOGGING FAILS } } /** * Summary Logs an error in the system * @param source Location where the error has occurred * @param error Error * @param properties Custom properties (KV pair) */ LogException(source: string, error: Error, properties: any) { try { this.processException(source, error, properties); if (this.NextLogger !== undefined && this.NextLogger !== null) { this.NextLogger.LogException(source, error, properties); } } catch { // DO NOT THROW AN EXCEPTION WHEN LOGGING FAILS } } /** * Summary Sets the next logger in the chain. If the next logger is already filled then its chained to the last logger of the chain * @param nextLogger Next Logger to be set in the chain */ SetNextLogger(nextLogger: AbstractLogger) { if (nextLogger === undefined || nextLogger === null) return; if (!this.isLoggerLoopCreated(nextLogger)) { if (this.NextLogger === undefined || this.NextLogger === null) { this.NextLogger = nextLogger; } else { this.NextLogger.SetNextLogger(nextLogger); } } } private isLoggerLoopCreated(nextLogger: AbstractLogger) { let tmpLogger = {...nextLogger}; do { if (tmpLogger.LoggerIdentity === this.LoggerIdentity) return true; tmpLogger = tmpLogger.NextLogger; } while (tmpLogger !== null && tmpLogger !== undefined) return false; } abstract processEvent(source: string, eventName: string, properties: any); abstract processException(source, error: Error, properties: any); } ================================================ FILE: src/common/console.logger.ts ================================================ import { AbstractLogger } from "./abstract.logger"; export class ConsoleLogger extends AbstractLogger { constructor(private _debugMode: boolean = false) { super("DEFAULT_CONSOLE_LOGGER"); this.NextLogger = null; } processEvent(source: string, eventName: string, properties: any) { try { if (!this._debugMode) return; console.log(`EVENT : ${eventName}. (${source})`); } catch { // DO NOT THROW AN EXCEPTION WHEN LOGGING FAILS } } processException(source: string, error: Error, properties: any) { try { if (!this._debugMode) return; console.error(error); } catch { // DO NOT THROW AN EXCEPTION WHEN LOGGING FAILS } } } ================================================ FILE: src/common/interfaces/global.store.interface.ts ================================================ import { Store, Middleware, Reducer } from "redux"; import { IAction } from "../../actions/action.interface"; import { AbstractLogger as ILogger } from "../abstract.logger"; export interface IGlobalStore { CreateStore(appName: string, appReducer: Reducer, middlewares?: Array, globalActions?: Array, shouldReplaceStore?: boolean, shouldReplaceReducer?: boolean): Store; RegisterStore(appName: string, store: Store, globalActions?: Array, shouldReplaceStore?: boolean): void RegisterGlobalActions(appName: string, actions: Array): void; GetPlatformState(): any; GetPartnerState(partnerName: string): any; GetGlobalState(): any; DispatchGlobalAction(source: string, action: IAction): void; DispatchLocalAction(source: string, action: IAction): void; DispatchAction(source: string, action: IAction): void; Subscribe(source: string, callback: (state: any) => void): () => void; SubscribeToPlatformState(source: string, callback: (state: any) => void): () => void; SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void): () => void; SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void, eager: boolean): () => void; SubscribeToGlobalState(source: string, callback: (state: any) => void): () => void; AddSelectors(source: string, selectors: Record, mergeSelectors?: boolean): void; SelectPartnerState(partner: string, selector: string, defaultReturn?: any): any; SetLogger(logger: ILogger): void; }; ================================================ FILE: src/common/interfaces/index.ts ================================================ export * from './global.store.interface'; ================================================ FILE: src/global.store.ts ================================================ import { IAction } from './actions/action.interface'; import { ConsoleLogger } from './common/console.logger'; import { ActionLogger } from './middlewares/action.logger'; import { composeWithDevTools } from 'redux-devtools-extension'; import { AbstractLogger as ILogger } from './common/abstract.logger'; import { IGlobalStore } from './common/interfaces/global.store.interface'; import { Store, Reducer, Middleware, createStore, applyMiddleware } from 'redux'; /** * Summary Global store for all Apps and container shell (Platform) in Micro-Frontend application. * Description Singleton class to be used all all Apps for registering the isolated App States. The platform-level and global-level store can be accessed from this class. */ export class GlobalStore implements IGlobalStore { public static readonly Platform: string = "Platform"; public static readonly AllowAll: string = "*"; public static readonly InstanceName: string = "GlobalStoreInstance"; public static DebugMode: boolean = false; private _stores: { [key: string]: Store }; private _globalActions: { [key: string]: Array }; private _globalListeners: Array<(state: any) => void>; private _eagerPartnerStoreSubscribers: { [key: string]: { [key: string]: (state) => void } } private _eagerUnsubscribers: { [key: string]: { [key: string]: () => void } } private _actionLogger: ActionLogger = null; private _selectors: { [key: string]: any }; private constructor(private _logger: ILogger = null) { this._stores = {}; this._globalActions = {}; this._globalListeners = []; this._eagerPartnerStoreSubscribers = {}; this._eagerUnsubscribers = {}; this._actionLogger = new ActionLogger(_logger); this._selectors = {}; } /** * Summary Gets the singleton instance of the Global Store. * * @param {ILogger} logger Logger service. */ public static Get(debugMode: boolean = false, logger: ILogger = null): IGlobalStore { if (debugMode) { this.DebugMode = debugMode; } if (debugMode && (logger === undefined || logger === null)) { logger = new ConsoleLogger(debugMode); } let globalGlobalStoreInstance: IGlobalStore = window[GlobalStore.InstanceName] || null; if (globalGlobalStoreInstance === undefined || globalGlobalStoreInstance === null) { globalGlobalStoreInstance = new GlobalStore(logger); window[GlobalStore.InstanceName] = globalGlobalStoreInstance; } return globalGlobalStoreInstance; } /** * Summary: Creates and registers a new store * * @access public * * @param {string} appName Name of the App for whom the store is getting creating. * @param {Reducer} appReducer The root reducer of the App. If partner app is using multiple reducers, then partner App must use combineReducer and pass the root reducer * @param {Array} middlewares List of redux middlewares that the partner app needs. * @param {boolean} shouldReplaceStore Flag to indicate if the Partner App wants to replace an already created/registered store with the new store. * @param {boolean} shouldReplaceReducer Flag to indicate if the Partner App wants to replace the existing root Reducer with the given reducer. Note, that the previous root Reducer will be replaced and not updated. If the existing Reducer needs to be used, then partner app must do the append the new reducer and pass the combined root reducer. * * @returns {Store} The new Store */ CreateStore(appName: string, appReducer: Reducer, middlewares?: Array, globalActions?: Array, shouldReplaceStore: boolean = false, shouldReplaceReducer: boolean = false): Store { let existingStore = this._stores[appName]; if (existingStore === null || existingStore === undefined || shouldReplaceStore) { if (middlewares === undefined || middlewares === null) middlewares = []; let appStore = createStore(appReducer, GlobalStore.DebugMode ? composeWithDevTools(applyMiddleware(...middlewares)) : applyMiddleware(...middlewares)); this.RegisterStore(appName, appStore, globalActions, shouldReplaceStore); return appStore; } if (shouldReplaceReducer) { console.warn(`The reducer for ${appName} is getting replaced`); existingStore.replaceReducer(appReducer); this.RegisterStore(appName, existingStore, globalActions, true); } return existingStore; } /** * Summary: Registers an isolated app store * * @access public * * @param {string} appName Name of the App. * @param {Store} store Instance of the store. * @param {boolean} shouldReplace Flag to indicate if the an already registered store needs to be replaced. */ RegisterStore(appName: string, store: Store, globalActions?: Array, shouldReplaceExistingStore: boolean = false): void { let existingStore = this._stores[appName]; if (existingStore !== undefined && existingStore !== null && shouldReplaceExistingStore === false) return; this._stores[appName] = store; store.subscribe(this.InvokeGlobalListeners.bind(this)); this.RegisterGlobalActions(appName, globalActions); this.RegisterEagerSubscriptions(appName); this.LogRegistration(appName, (existingStore !== undefined && existingStore !== null)); } /** * Summary: Registers a list of actions for an App that will be made Global. * Description: Global actions can be dispatched on the App Store by any Partner Apps. If partner needs to make all actions as Global, then pass "*" in the list. If no global actions are registered then other partners won't be able to dispatch any action on the App Store. * * @access public * * @param {string} appName Name of the app. * @param {Array} actions List of global action names. */ RegisterGlobalActions(appName: string, actions?: Array): void { if (actions === undefined || actions === null || actions.length === 0) { return; } let registeredActions = this._globalActions[appName]; if (registeredActions === undefined || registeredActions === null) { registeredActions = []; this._globalActions[appName] = []; } let uniqueActions = actions.filter(action => registeredActions.find(registeredAction => action === registeredAction) === undefined); uniqueActions = [...new Set(uniqueActions)]; // Removing any duplicates this._globalActions[appName] = [...this._globalActions[appName], ...uniqueActions]; } /** * Summary: Gets the current state of the Platform * * @access public * * @returns Current Platform State (App with name Platform) */ GetPlatformState(): any { let platformStore = this.GetPlatformStore(); if (platformStore === undefined || platformStore === null) return null; return this.CopyState(platformStore.getState()); } /** * Summary: Gets the current state of the given Partner. * Description: A read-only copy of the Partner state is returned. The state cannot be mutated using this method. For mutation dispatch actions. In case the partner hasn't been registered or the partner code hasn't loaded, the method will return null. * * @param partnerName Name of the partner whose state is needed * * @returns {any} Current partner state. */ GetPartnerState(partnerName: string): any { let partnerStore = this.GetPartnerStore(partnerName); if (partnerStore === undefined || partnerStore === null) return null; let partnerState = partnerStore.getState(); return this.CopyState(partnerState); } /** * Summary: Gets the global store. * Description: The global store comprises of the states of all registered partner's state. * Format * { * Platform: { ...Platform_State }, * Partner_Name_1: { ...Partner_1_State }, * Partner_Name_2: { ...Partner_2_State } * } * * @access public * * @returns {any} Global State. */ GetGlobalState(): any { let globalState = {}; for (let partner in this._stores) { let state = this._stores[partner].getState(); globalState[partner] = state; }; return this.CopyState(globalState); } /** * Summary: Dispatches an action on all the Registered Store (including Platform level store). * Description: The action will be dispatched only if the Partner App has declated the action to be global at it's store level. * * @access public * * @param {string} source Name of app dispatching the Actions * @param {IAction} action Action to be dispatched */ DispatchGlobalAction(source: string, action: IAction): void { for (let partner in this._stores) { let isActionRegisteredByPartner = this.IsActionRegisteredAsGlobal(partner, action); if (isActionRegisteredByPartner) { this._stores[partner].dispatch(action); } } } /** * Summary: Dispatched an action of the local store * * @access public * * @param {string} source Name of app dispatching the Actions * @param {IAction} action Action to be dispatched */ DispatchLocalAction(source: string, action: IAction): void { let localStore = this._stores[source]; if (localStore === undefined || localStore === null) { let error = new Error(`Store is not registered`); if (this._logger !== undefined && this._logger !== null) this._logger.LogException(source, error, {}); throw error; } localStore.dispatch(action); } /** * Summary: Dispatches an action at a local as well global level * * @access public * * @param {string} source Name of app dispatching the Actions * @param {IAction} action Action to be dispatched */ DispatchAction(source: string, action: IAction): void { this.DispatchGlobalAction(source, action); let isActionGlobal = this.IsActionRegisteredAsGlobal(source, action); if (!isActionGlobal) this.DispatchLocalAction(source, action); } /** * Summary: Subscribe to current store's state changes * * @param {string} source Name of the application * @param {(state: any) => void} callback Callback method to be invoked when state changes */ Subscribe(source: string, callback: (state: any) => void): () => void { let store = this.GetPartnerStore(source); if (store === undefined || store === null) { throw new Error(`ERROR: Store for ${source} hasn't been registered`); } return store.subscribe(() => callback(store.getState())); } /** * Summary: Subscribe to any change in the Platform's state. * * @param {string} source Name of application subscribing to the state changes. * @param {(state: any) => void} callback Callback method to be called for every platform's state change. * * @returns {() => void} Unsubscribe method. Call this method to unsubscribe to the changes. */ SubscribeToPlatformState(source: string, callback: (state: any) => void): () => void { let platformStore = this.GetPlatformStore(); return platformStore.subscribe(() => callback(platformStore.getState())); } /** * Summary: Subscribe to any change in the Partner App's state. * * @access public * * * @param {string} source Name of the application subscribing to the state changes. * @param {string} partner Name of the Partner application to whose store is getting subscribed to. * @param {(state: any) => void} callback Callback method to be called for every partner's state change. * @param {boolean} eager Allows subscription to store that's yet to registered * * @throws Error when the partner is yet to be registered/loaded or partner doesn't exist. * * @returns {() => void} Unsubscribe method. Call this method to unsubscribe to the changes. */ SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void, eager: boolean = true): () => void { let partnerStore = this.GetPartnerStore(partner); if (partnerStore === undefined || partnerStore === null) { if (!eager) { throw new Error(`ERROR: ${source} is trying to subscribe to partner ${partner}. Either ${partner} doesn't exist or hasn't been loaded yet`); } if (this._eagerPartnerStoreSubscribers[partner]) { this._eagerPartnerStoreSubscribers[partner].source = callback; } else { this._eagerPartnerStoreSubscribers[partner] = { source: callback } } return () => { this.UnsubscribeEagerSubscription(source, partner); } } return partnerStore.subscribe(() => callback(partnerStore.getState())); } /** * Summary: Subscribe to any change in the Global State, including Platform-level and Partner-level changes. * * @access public * * @param {string} source Name of the application subscribing to the state change. * @param {(state: any) => void} callback Callback method to be called for every any change in the global state. * * @returns {() => void} Unsubscribe method. Call this method to unsubscribe to the changes. */ SubscribeToGlobalState(source: string, callback: (state: any) => void): () => void { this._globalListeners.push(callback) return () => { this._globalListeners = this._globalListeners.filter(globalListener => globalListener !== callback); } } UnsubscribeEagerSubscription(source: string, partnerName: string) { if (!partnerName || !source) return; if (!this._eagerUnsubscribers[partnerName]) return; let unsubscriber = this._eagerUnsubscribers[partnerName].source; if (unsubscriber) unsubscriber(); } SetLogger(logger: ILogger) { if (this._logger === undefined || this._logger === null) this._logger = logger; else this._logger.SetNextLogger(logger); this._actionLogger.SetLogger(logger); } /** * Summary: Expose a collection of Selecotrs from a Partner-level that other partners can later consume. This allows partners to derive data without forcing partners to know the state structure. * * @access public * * @param {string} source Name of the application exposing an derived state API * @param {Record} selectors The collection of APIs of derived state selectors. * @param {boolean} mergeSelectors If the source application already exposed an API set, merge the new API being passed in. * */ AddSelectors(source: string, selectors: Record, mergeSelectors = false) { if (this._selectors[source] == undefined) { this._selectors[source] = selectors; } if (this._selectors[source] != undefined && mergeSelectors) { this._selectors[source] = Object.assign({}, this._selectors[source], selectors); } } /** * Summary: Select derived state from a partner app using the selector name * * @access public * * @param {string} partner Name of the partner application to select derived data from * @param {string} selector The name of the API to select * @param {any} defaultReturn If the partner app does not have that API exposed, return this default value instead of undefined. * */ SelectPartnerState(partner: string, selector: string, defaultReturn?: any) { if (this._selectors[partner] == undefined) { throw new Error(`ERROR: ${partner} not exposed any selectors.`); } if (this._selectors[partner][selector] == undefined) { console.warn(`${partner} has not exposed a selector with the name: ${selector}`); return defaultReturn; } return this._selectors[partner][selector](); } private RegisterEagerSubscriptions(appName: string) { let eagerCallbacksRegistrations = this._eagerPartnerStoreSubscribers[appName]; if (eagerCallbacksRegistrations === undefined || eagerCallbacksRegistrations === undefined) return; let registeredApps = Object.keys(eagerCallbacksRegistrations); registeredApps.forEach(sourceApp => { let callback = eagerCallbacksRegistrations[sourceApp]; if (callback) { let unregistrationCallback = this.SubscribeToPartnerState(sourceApp, appName, callback, false); if (this._eagerPartnerStoreSubscribers[appName]) { this._eagerPartnerStoreSubscribers[appName].sourceApp = unregistrationCallback; } else { this._eagerPartnerStoreSubscribers[appName] = { sourceApp: unregistrationCallback }; } } }); } private InvokeGlobalListeners(): void { let globalState = this.GetGlobalState(); this._globalListeners.forEach(globalListener => { globalListener(globalState); }); } private GetPlatformStore(): Store { return this.GetPartnerStore(GlobalStore.Platform); } private GetPartnerStore(partnerName: string): Store { return this._stores[partnerName]; } private GetGlobalMiddlewares(): Array { let actionLoggerMiddleware = this._actionLogger.CreateMiddleware(); return [actionLoggerMiddleware]; } private IsActionRegisteredAsGlobal(appName: string, action: IAction): boolean { let registeredGlobalActions = this._globalActions[appName]; if (registeredGlobalActions === undefined || registeredGlobalActions === null) { return false; } return registeredGlobalActions.some(registeredAction => registeredAction === action.type || registeredAction === GlobalStore.AllowAll); } private LogRegistration(appName: string, isReplaced: boolean) { try { let properties = { "AppName": appName, "IsReplaced": isReplaced.toString() }; if (this._logger) this._logger.LogEvent("Store.GlobalStore", "StoreRegistered", properties); } catch (error) { // Gulp the error console.error(`ERROR: There was an error while logging registration for ${appName}`); console.error(error); } } private CopyState(state: any) { if (state === undefined || state === null || typeof state !== 'object') { return state; } else { return { ...state } } } } ================================================ FILE: src/middlewares/action.logger.ts ================================================ import { Middleware } from 'redux'; import { stringify } from 'flatted'; import { IAction } from '../actions'; import { AbstractLogger as ILogger } from '../common/abstract.logger'; /** * Summary Logs action and its impact on the state */ export class ActionLogger { constructor(private _logger: ILogger) { } /** * Summary Creates as Redux middleware for logging the actions and its impact on the State */ public CreateMiddleware(): Middleware { return store => next => (action: IAction) => { if (!this.IsLoggingAllowed(action)) { return next(action); } const dispatchedAt = new Date(); let state = store.getState(); this.LogActionDispatchStart(state, action); let dispatchResult: IAction = null; try { dispatchResult = next(action); } catch (error) { this.LogActionDispatchFailure(action, dispatchedAt, error); throw error; } state = store.getState(); this.LogActionDispatchComplete(state, action, dispatchedAt); return dispatchResult; } } public SetLogger(logger: ILogger) { if (this._logger === undefined || this._logger === null) this._logger = logger; else this._logger.SetNextLogger(logger); } private IsLoggingAllowed(action: IAction) { return action.logEnabled !== undefined && action.logEnabled !== null && action.logEnabled === true && this._logger !== undefined && this._logger !== null; } private LogActionDispatchStart(state: any, action: IAction) { try { var properties = { "OldState": stringify(state), "ActionName": action.type, "DispatchStatus": "Dispatched", "DispatchedOn": new Date().toISOString(), "Payload": stringify(action.payload) }; this._logger.LogEvent("Fxp.Store.ActionLogger", `${action.type} :: DISPATCHED`, properties); } catch (error) { // Gulp the error console.error("ERROR: There was an error while trying to log the Dispatch Complete event"); console.error(error); } } private LogActionDispatchComplete(state: any, action: any, dispatchedAt: Date) { try { let currentTime = new Date(); const timeTaken = currentTime.getTime() - dispatchedAt.getTime(); var properties = { "NewState": stringify(state), "ActionName": action.type, "DispatchStatus": "Completed", "DispatchedOn": new Date().toISOString(), "Payload": stringify(action.payload), "TimeTaken": timeTaken.toString() }; this._logger.LogEvent("Fxp.Store.ActionLogger", `${action.type} :: COMPLETED`, properties); } catch (error) { // Gulp the error console.error("ERROR: There was an error while trying to log the Dispatch Complete event"); console.error(error); } } private LogActionDispatchFailure(action: any, dispatchedAt: Date, exception: Error) { try { let currentTime = new Date(); const timeTaken = currentTime.getTime() - dispatchedAt.getTime(); var properties = { "ActionName": action.type, "DispatchStatus": "Failed", "DispatchedOn": new Date().toISOString(), "Payload": stringify(action.payload), "TimeTaken": timeTaken.toString() }; this._logger.LogEvent("Fxp.Store.ActionLogger", `${action.type} :: FAILED`, properties); this._logger.LogException("Fxp.Store.ActionLogger", exception, properties); console.error(exception); } catch (error) { // Gulp the error console.error("ERROR: There was an error while trying to log the Dispatch Failure event"); console.error(error); } } } ================================================ FILE: test/global.store.tests.ts ================================================ import { createStore, Reducer } from 'redux'; import { GlobalStore } from '../src/global.store'; import { IAction } from '../src/actions/action.interface'; import { AbstractLogger as ILogger } from '../src/common/abstract.logger'; describe("Global Store", () => { let mockLogger = { LogEvent: function (source, event, properties) { }, LogException: function (source, error, properties) { } } as ILogger; beforeEach(() => { spyOn(mockLogger, "LogEvent").and.callThrough(); }); let globalStore = GlobalStore.Get(true, mockLogger); it("Should get created", () => { expect(globalStore).toBeDefined(); }); it("Should get created as a singleton", () => { let globalStoreDuplicate = GlobalStore.Get(); expect(globalStore).toBe(globalStoreDuplicate); }); it("Should get created with Platform State", () => { expect((globalStore)._stores).toBeDefined(); }); describe("CreateStore", () => { let dummyPartnerReducer: Reducer = (state: any = {}, action) => { return state; }; it("Should create a new partner store without custom middlewares and no global actions", () => { // Arrange let partnerAppName = "SamplePartner_1"; // Act let store = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, []); // Assert expect(store).toBeDefined(); expect((globalStore)._stores).toBeDefined(); expect((globalStore)._stores[partnerAppName]).toBeDefined(); }); it("Should re-register partner store when ShouldReplace is true", () => { // Arrange let partnerAppName = "SamplePartner_1"; // Act let store = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], null, true); // Assert expect(store).toBeDefined(); expect((globalStore)._stores).toBeDefined(); expect((globalStore)._stores[partnerAppName]).toBeDefined(); expect(mockLogger.LogEvent).toHaveBeenCalled(); }); it("Should replace partner reducer when ShouldUpdate is true", () => { // Arrange let partnerAppName = "SamplePartner_1"; // Act let store = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], null, false, true); // Assert expect(store).toBeDefined(); expect((globalStore)._stores).toBeDefined(); expect((globalStore)._stores[partnerAppName]).toBeDefined(); expect(mockLogger.LogEvent).toHaveBeenCalled(); expect(mockLogger.LogEvent).toHaveBeenCalledTimes(1); }); it("Should not re-register partner store when ShouldReplace is false", () => { // Arrange let partnerAppName = "SamplePartner_2"; let store = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], null, true); // Act store = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], null, false); // Assert expect(store).toBeDefined(); expect((globalStore)._stores).toBeDefined(); expect((globalStore)._stores[partnerAppName]).toBeDefined(); expect(mockLogger.LogEvent).toHaveBeenCalled(); expect(mockLogger.LogEvent).toHaveBeenCalledTimes(1); }); }); describe("RegisterGlobalActions", () => { it("Should register global actions for a new partner", () => { // Arrange let partnerAppName = "SamplePartner-1"; let partnerGlobalActions = ["ga-1", "ga-2"]; // Act globalStore.RegisterGlobalActions(partnerAppName, partnerGlobalActions); // Assert expect((globalStore)._globalActions).toBeDefined(); expect((globalStore)._globalActions[partnerAppName]).toBeDefined(); let registeredActions = ((globalStore)._globalActions[partnerAppName] as string[]); registeredActions.forEach(registeredAction => { expect(partnerGlobalActions.some(action => action === registeredAction)).toBeTruthy(); }) }); it("Should not register global actions for a new partner when actions is null", () => { // Arrange let partnerAppName = "SamplePartner-2"; // Act globalStore.RegisterGlobalActions(partnerAppName, null); // Assert expect((globalStore)._globalActions).toBeDefined(); expect((globalStore)._globalActions[partnerAppName]).toBeUndefined(); }); it("Should not register global actions for a new partner when actions is empty", () => { // Arrange let partnerAppName = "SamplePartner-3"; // Act globalStore.RegisterGlobalActions(partnerAppName, []); // Assert expect((globalStore)._globalActions).toBeDefined(); expect((globalStore)._globalActions[partnerAppName]).toBeUndefined(); }); it("Should not register already registered global actions for a partner", () => { // Arrange let partnerAppName = "SamplePartner-4"; let duplicateActionName = "ga-2"; let partnerGlobalActions_1 = ["ga-1", duplicateActionName]; let partnerGlobalActions_2 = [duplicateActionName, "ga-3"]; // Act globalStore.RegisterGlobalActions(partnerAppName, partnerGlobalActions_1); globalStore.RegisterGlobalActions(partnerAppName, partnerGlobalActions_2); // Assert expect((globalStore)._globalActions).toBeDefined(); expect((globalStore)._globalActions[partnerAppName]).toBeDefined(); let registeredActions = ((globalStore)._globalActions[partnerAppName] as string[]); expect(registeredActions.length).toBe(3); expect(registeredActions.filter(a => a === duplicateActionName).length).toBe(1); }); it("Should not register duplicate global actions for a partner", () => { // Arrange let partnerAppName = "SamplePartner-4"; let duplicateActionName = "ga-2"; let partnerGlobalActions = ["ga-1", duplicateActionName, duplicateActionName, duplicateActionName, "ga-3"]; // Act globalStore.RegisterGlobalActions(partnerAppName, partnerGlobalActions); // Assert expect((globalStore)._globalActions).toBeDefined(); expect((globalStore)._globalActions[partnerAppName]).toBeDefined(); let registeredActions = ((globalStore)._globalActions[partnerAppName] as string[]); expect(registeredActions.length).toBe(3); expect(registeredActions.filter(a => a === duplicateActionName).length).toBe(1); }); }); describe("GetPlatformState", () => { it("Should return Platform state", () => { // Act let platformState = globalStore.GetPlatformState(); // Assert expect(platformState).toBeDefined(); }) }); describe("GetPartnerState", () => { let dummyPartnerReducer: Reducer = (state: string = null, action) => { return action.payload; }; it("Should return Partner state", () => { // Arrange let partnerAppName = "SamplePartner-10"; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], [GlobalStore.AllowAll], false, false); let actionText = "ACTION!!!"; // Act globalStore.DispatchGlobalAction("TEST", { type: "Sample", payload: actionText }); let partnerState = globalStore.GetPartnerState(partnerAppName); // Assert expect(partnerState).toBeDefined(); expect(partnerState).toBe(actionText); }); it("Should not return Partner state when partner is not registered", () => { // Arrange let partnerAppName = "SamplePartner-11"; // Act let partnerState = globalStore.GetPartnerState(partnerAppName); // Assert expect(partnerState).toBeNull(); }); }); describe("GetGlobalState", () => { let dummyPartnerReducer: Reducer = (state: string = null, action) => { return action.payload; }; it("Should formulate global state", () => { // Arrange let partnerAppName = "SamplePartner-20"; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], [GlobalStore.AllowAll], false, false); let actionText = "ACTION!!!"; // Act globalStore.DispatchGlobalAction("TEST", { type: "Sample", payload: actionText }); let partnerState = globalStore.GetGlobalState(); // Assert expect(partnerState).toBeDefined(); expect(partnerState[partnerAppName]).toBeDefined(); expect(partnerState[partnerAppName]).toBe(actionText); }); }); describe("DispatchGlobalAction", () => { let dummyPartnerReducer: Reducer = (state: string = "Default", action: IAction) => { switch (action.type) { case "Local": return "Local"; case "Global": return "Global"; } }; it("Should dispatch globally registered action on a partner store", () => { // Arrange let partnerAppName = "SamplePartner-30"; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Act globalStore.DispatchGlobalAction("Test", { type: "Global", payload: null }); let partnerState = globalStore.GetGlobalState(); // Assert expect(partnerState).toBeDefined(); expect(partnerState[partnerAppName]).toBeDefined(); expect(partnerState[partnerAppName]).toBe("Global"); }); it("Should not dispatch non-globally registered action on a partner store", () => { // Arrange let partnerAppName = "SamplePartner-31"; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Act globalStore.DispatchGlobalAction("Test", { type: "Local", payload: null }); let partnerState = globalStore.GetGlobalState(); // Assert expect(partnerState).toBeDefined(); expect(partnerState[partnerAppName]).toBeUndefined(); }); }); describe("SubscribeToPartnerState", () => { let dummyPartnerReducer: Reducer = (state: string = "Default", action: IAction) => { switch (action.type) { case "Local": return "Local"; case "Global": return "Global"; } }; it("Should invoke callback when partner state changes", () => { // Arrange let partnerAppName = "SamplePartner-40"; let isPartnerStateChanged = false; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Act globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => { isPartnerStateChanged = true; }); globalStore.DispatchGlobalAction("Test", { type: "Global", payload: null }); // Assert expect(isPartnerStateChanged).toBeTruthy(); }); it("Should allow registering to non-registered store in eager mode", () => { // Arrange let partnerAppName = "SamplePartner-100"; // Act let exceptionThrown = false; try { globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => { }, true); } catch { exceptionThrown = true; } // Assert expect(exceptionThrown).not.toBeTruthy(); }); it("Should throw exception when registering to non-registered store in non-eager mode", () => { // Arrange let partnerAppName = "SamplePartner-101"; // Act let exceptionThrown = false; try { globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => { }, false); } catch { exceptionThrown = true; } // Assert expect(exceptionThrown).toBeTruthy(); }); it("Should attach eager subscriber", () => { // Arrange let partnerAppName = "SamplePartner-102"; let isPartnerStateChanged = false; // Act globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => { isPartnerStateChanged = true; }, true); globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); globalStore.DispatchGlobalAction("Test", { type: "Global", payload: null }); // Assert expect(isPartnerStateChanged).toBeTruthy(); }); it("Should attach eager multiple subscribers", () => { // Arrange let partnerAppName_1 = "SamplePartner-112"; let isPartnerStateChanged_1 = false; let partnerAppName_2 = "SamplePartner-114"; let isPartnerStateChanged_2 = false; // Act globalStore.SubscribeToPartnerState("Test", partnerAppName_1, (state) => { isPartnerStateChanged_1 = true; }, true); globalStore.SubscribeToPartnerState("Test", partnerAppName_2, (state) => { isPartnerStateChanged_2 = true; }, true); globalStore.CreateStore(partnerAppName_1, dummyPartnerReducer, [], ["Global"], false, false); globalStore.CreateStore(partnerAppName_2, dummyPartnerReducer, [], ["Global"], false, false); globalStore.DispatchGlobalAction("Test", { type: "Global", payload: null }); // Assert expect(isPartnerStateChanged_1).toBeTruthy(); expect(isPartnerStateChanged_2).toBeTruthy(); }); }); describe("SubscribeToGlobalState", () => { let dummyPartnerReducer: Reducer = (state: string = "Default", action: IAction) => { switch (action.type) { case "Local": return "Local"; case "Global": return "Global"; } }; it("Should invoke callback when global state changes due to partner change", () => { // Arrange let partnerAppName = "SamplePartner-40"; let isGlobalStateChanged = false; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Act globalStore.SubscribeToGlobalState("Test", (state) => { isGlobalStateChanged = true; }); globalStore.DispatchGlobalAction("Test", { type: "Global", payload: null }); // Assert expect(isGlobalStateChanged).toBeTruthy(); }); it("Should invoke callback when global state changes due to partner change - CreateStore", () => { // Arrange let partnerAppName = "SamplePartner-40"; let isGlobalStateChanged = false; let store = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Act globalStore.SubscribeToGlobalState("Test", (state) => { isGlobalStateChanged = true; }); store.dispatch( { type: "Global", payload: null }); // Assert expect(isGlobalStateChanged).toBe(true); }); it("Should invoke callback when global state changes due to partner change - ResigerStore", () => { // Arrange let partnerAppName = "SamplePartner-41"; let isGlobalStateChanged = false; let store = createStore(dummyPartnerReducer); globalStore.RegisterStore(partnerAppName, store, ["Global"], false); // Act globalStore.SubscribeToGlobalState("Test", (state) => { isGlobalStateChanged = true; }); store.dispatch( { type: "Global", payload: null }); // Assert expect(isGlobalStateChanged).toBe(true); }); }); describe("AddSelectors", () => { let dummyPartnerReducer: Reducer = (state: string = "Default", action: IAction) => { switch (action.type) { case "Local": return "Local"; case "Global": return "Global"; } }; it("Should successfully expose derived state API", () => { let partnerAppName = "SamplePartner-2022"; const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Arrange globalStore.AddSelectors(partnerAppName, { selectStateUpperCased: () => { const state = partnerStore.getState(); return state.toUpperCase() }, selectStateLowerCased: () => { const state = partnerStore.getState(); return state.toLowerCase() } }); // Assert const api = (globalStore)._selectors[partnerAppName]; expect(api.selectStateUpperCased).toBeDefined(); expect(api.selectStateLowerCased).toBeDefined(); }); it("Should successfully merge derived state API", () => { let partnerAppName = "SamplePartner-2023"; const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Arrange globalStore.AddSelectors(partnerAppName, { selectStateUpperCased: () => { const state = partnerStore.getState(); return state.toUpperCase() }, }); globalStore.AddSelectors(partnerAppName, { selectStateLowerCased: () => { const state = partnerStore.getState(); return state.toLowerCase() } }, true); // Assert const api = (globalStore)._selectors[partnerAppName]; expect(api.selectStateUpperCased).toBeDefined(); expect(api.selectStateLowerCased).toBeDefined(); }); it("Should not merge derived state API", () => { let partnerAppName = "SamplePartner-2024"; const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); // Arrange globalStore.AddSelectors(partnerAppName, { selectStateUpperCased: () => { const state = partnerStore.getState(); return state.toUpperCase() }, }); globalStore.AddSelectors(partnerAppName, { selectStateLowerCased: () => { const state = partnerStore.getState(); return state.toLowerCase() } }); // Assert const api = (globalStore)._selectors[partnerAppName]; expect(api.selectStateUpperCased).toBeDefined(); expect(api.selectStateLowerCased).toBeUndefined(); }) }) describe("SelectPartnerState", () => { let dummyPartnerReducer: Reducer = (state: string = "Default", action: IAction) => { switch (action.type) { case "Local": return "Local"; case "Global": return "Global"; default: return state; } }; it("Should be able to request a piece of derived state with valid key", () => { // Arrange let partnerAppName = "SamplePartner-2012"; const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); globalStore.AddSelectors(partnerAppName, { selectStateUpperCased: () => { const state = partnerStore.getState(); return state.toUpperCase() }, selectStateLowerCased: () => { const state = partnerStore.getState(); return state.toLowerCase() } }); // Act const partnerStateComputedUpperCase = globalStore.SelectPartnerState(partnerAppName, "selectStateUpperCased"); expect(partnerStateComputedUpperCase).toEqual("DEFAULT"); const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased"); expect(partnerStateComputedLowerCase).toEqual("default"); }); it("Should be return undefined if derived state key is not defined", () => { // Arrange let partnerAppName = "SamplePartner-2013"; const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); globalStore.AddSelectors(partnerAppName, { selectStateUpperCased: () => { const state = partnerStore.getState(); return state.toUpperCase() }, }); // Act const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased"); expect(partnerStateComputedLowerCase).toEqual(undefined); }); it("Should be return default value if derived state key is not defined", () => { // Arrange let partnerAppName = "SamplePartner-2013"; const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); globalStore.AddSelectors(partnerAppName, { selectStateUpperCased: () => { const state = partnerStore.getState(); return state.toUpperCase() }, }); // Act const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased", "I am a default value"); expect(partnerStateComputedLowerCase).toEqual("I am a default value"); }); it("Should throw error if partner has not exposed any derived", () => { // Arrange let partnerAppName = "SamplePartner-2014"; let exceptionThrown = false; globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false); try { globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased"); } catch { exceptionThrown = true; } // Assert expect(exceptionThrown).toBeTruthy(); }); }); }); ================================================ FILE: test/middlewares/action.logger.tests.ts ================================================ import { IAction } from '../../src/actions/action.interface'; import { createStore, Reducer, applyMiddleware } from 'redux'; import { ActionLogger } from '../../src/middlewares/action.logger'; import { AbstractLogger as ILogger } from '../../src/common/abstract.logger'; describe("ActionLoggerMiddleware", () => { let mockLogger = { LogEvent: function (source, event, properties) { }, LogException: function (source, exception, properties) { } } as ILogger; it("Should get created", () => { expect(new ActionLogger(mockLogger)).toBeDefined(); }); it("Should log action start and action end", () => { // Arrange spyOn(mockLogger, "LogEvent").and.callThrough(); let reducer: Reducer = (state = null, action: IAction): any => { return action.payload; }; let middleware = new ActionLogger(mockLogger).CreateMiddleware(); let store = createStore(reducer, applyMiddleware(middleware)); // Act store.dispatch({ type: "Action", payload: "Dummy", logEnabled: true } as IAction); // Assert expect(mockLogger.LogEvent).toHaveBeenCalledTimes(2); }); it("Should log action start and action failure", () => { // Arrange spyOn(mockLogger, "LogEvent").and.callThrough(); spyOn(mockLogger, "LogException").and.callThrough(); let dummyError = new Error("Dummy Error"); let reducer: Reducer = (state = null, action: IAction): any => { if (action.type === "FaultAction") { throw dummyError; } return state; }; let middleware = new ActionLogger(mockLogger).CreateMiddleware(); let store = createStore(reducer, applyMiddleware(middleware)); // Act try { store.dispatch({ type: "FaultAction", payload: "Dummy", logEnabled: true }); expect(true).toBeFalsy(); // Control should not come here } catch (error) { // Assert expect(error.message).toBe(dummyError.message); expect(mockLogger.LogEvent).toHaveBeenCalledTimes(2); expect(mockLogger.LogException).toHaveBeenCalledTimes(1); } }); it("Should not throw exception when logger fails", () => { // Arrange spyOn(mockLogger, "LogEvent").and.throwError("Some dummy error"); let reducer: Reducer = (state = null, action: IAction): any => { return action.payload; }; let middleware = new ActionLogger(mockLogger).CreateMiddleware(); let store = createStore(reducer, applyMiddleware(middleware)); // Act store.dispatch({ type: "Action", payload: "Dummy", logEnabled: true }); // Assert expect(mockLogger.LogEvent).toHaveBeenCalledTimes(2); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "ES6", "sourceMap": true, "target": "es5", "allowJs": false, "outDir": "lib", "declaration": true, "moduleResolution": "node", "skipLibCheck": true, "lib": ["es2017", "DOM"], "downlevelIteration": true }, "typeRoots": [ "node_modules/@types" ], "files": [ "index.ts" ], "exclude": [ "node_modules/**/*", "**/*.spec.ts" ] }