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 버전을 사용하고 있을 경우, 바로 최신 버전으로 업그레이드를 하십시오. 해당 버전은 파이프라인 이슈로 인해 더 이상 사용되지 않습니다.
## 파이프라인 상태
[](https://dev.azure.com/MicrosoftIT/OneITVSO/_build/latest?definitionId=32881&branchName=azure-pipelines)
[](https://github.com/microsoft/redux-micro-frontend/actions/workflows/codeql-analysis.yml)
[](https://github.com/microsoft/redux-micro-frontend/actions/workflows/build-and-publish.yml)

## 개요
이 라이브러리는 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)
### 전역 액션
전역 액션은 특정 앱이 다른 마이크로 프론트엔드 앱에 의해 등록된 스토어에 액션을 디스패치할 수 있는 개념입니다. 각 마이크로 프론트엔드 앱에는 스토어와 함께 전역 액션 집합을 등록할 수 있는 기능이 있습니다. 이러한 전역 액션 집합은 다른 마이크로 프론트엔드 앱에 의해 해당 마이크로 프론트엔드의 스토어에서 디스패치될 수 있습니다. 따라서 애플리케이션 간의 통신이 가능하게 됩니다.

### 상태 간의 상호 콜백
애플리케이션 간의 커뮤니케이션은 다른 마이크로 프론트엔드의 상태에 대한 변화를 구독하는 것으로부터 이루어집니다. 각각의 마이크로 프론트엔드 앱들은 다른 상태들에 대해 읽기 권한만(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
[](https://dev.azure.com/MicrosoftIT/OneITVSO/_build/latest?definitionId=32881&branchName=azure-pipelines)
[](https://github.com/microsoft/redux-micro-frontend/actions/workflows/codeql-analysis.yml)
[](https://github.com/microsoft/redux-micro-frontend/actions/workflows/build-and-publish.yml)

## 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.

### 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
================================================
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.5 BLOCK -->
## 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).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
================================================
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
================================================
<html>
<head></head>
<body>
<div id="app"></div>
</body>
</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 (
<div>
<Counter count={this.state.global} header="Global Counter" increment={this.incrementGlobalCounter} decrement={this.decrementGlobalCounter}></Counter>
<Counter count={this.state.local} header="Local Counter" increment={this.incrementLocalCounter} decrement={this.decrementLocalCounter}></Counter>
<h2>
Todo Counter
</h2>
<span>{this.state.todo}</span>
</div>
)
}
}
================================================
FILE: sample/counterApp/src/counter.js
================================================
import React from 'react';
export class Counter extends React.Component {
render() {
return (
<div>
<h2>{this.props.header}</h2>
<span>{this.props.count}</span>
<button onClick={this.props.increment}>+</button>
<button onClick={this.props.decrement}>-</button>
</div>
)
}
}
================================================
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(<AppCounter />, 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
================================================
<html>
<head>
<script src="http://localhost:4001/main.bundle.js"></script>
<script src="http://localhost:5001/main.bundle.js"></script>
</head>
<body>
<h1>MF 1 - Counter App</h1>
<div id="counter"></div>
<hr />
<h1>MF 2 - Todo App</h1>
<div id="todo"></div>
</body>
</html>
================================================
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
================================================
<html>
<head></head>
<body>
<div id="app"></div>
</body>
</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 (
<div>
<label>Add Todo Object</label>
<input type='Text' placeholder="Enter Todo" ref={node => {
this.description = node;
}}></input>
<button onClick={this.addTodo}>Submit</button>
</div>
)
}
}
================================================
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(<TodoList />, 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(
<div onClick={() => this.props.removeTodo(this.props.id)}>
<span>{this.props.description}</span>
</div>
)
}
}
================================================
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 (
<div>
<AddTodoComponent addTodo={this.addTodo}></AddTodoComponent>
<h2>Todos</h2>
<ul>
{this.state.todos.map(todo => {
return (
<li>
<Todo id={todo.id} description={todo.description} removeTodo={this.removeTodo}/>
</li>
)
})}
</ul>
<div>
Global Counter: {this.state.globalCounter}
</div>
</div>
)
}
}
================================================
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<Payload = any> {
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<Middleware>, globalActions?: Array<string>, shouldReplaceStore?: boolean, shouldReplaceReducer?: boolean): Store<any, any>;
RegisterStore(appName: string, store: Store, globalActions?: Array<string>, shouldReplaceStore?: boolean): void
RegisterGlobalActions(appName: string, actions: Array<string>): void;
GetPlatformState(): any;
GetPartnerState(partnerName: string): any;
GetGlobalState(): any;
DispatchGlobalAction(source: string, action: IAction<any>): void;
DispatchLocalAction(source: string, action: IAction<any>): void;
DispatchAction(source: string, action: IAction<any>): 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<string, any>, 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<string> };
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<Middleware>} 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<any, any>} The new Store
*/
CreateStore(appName: string, appReducer: Reducer, middlewares?: Array<Middleware>, globalActions?: Array<string>, shouldReplaceStore: boolean = false, shouldReplaceReducer: boolean = false): Store<any, any> {
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<string>, 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<string>} actions List of global action names.
*/
RegisterGlobalActions(appName: string, actions?: Array<string>): 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<any>} action Action to be dispatched
*/
DispatchGlobalAction(source: string, action: IAction<any>): 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<any>} action Action to be dispatched
*/
DispatchLocalAction(source: string, action: IAction<any>): 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<any>} action Action to be dispatched
*/
DispatchAction(source: string, action: IAction<any>): 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<string, any>} 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<string, any>, 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<any, any> {
return this.GetPartnerStore(GlobalStore.Platform);
}
private GetPartnerStore(partnerName: string): Store<any, any> {
return this._stores[partnerName];
}
private GetGlobalMiddlewares(): Array<Middleware> {
let actionLoggerMiddleware = this._actionLogger.CreateMiddleware();
return [actionLoggerMiddleware];
}
private IsActionRegisteredAsGlobal(appName: string, action: IAction<any>): 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<any>) => {
if (!this.IsLoggingAllowed(action)) {
return next(action);
}
const dispatchedAt = new Date();
let state = store.getState();
this.LogActionDispatchStart(state, action);
let dispatchResult: IAction<any> = 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<any>) {
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((<any>globalStore)._stores).toBeDefined();
});
describe("CreateStore", () => {
let dummyPartnerReducer: Reducer<any, any> = (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((<any>globalStore)._stores).toBeDefined();
expect((<any>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((<any>globalStore)._stores).toBeDefined();
expect((<any>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((<any>globalStore)._stores).toBeDefined();
expect((<any>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((<any>globalStore)._stores).toBeDefined();
expect((<any>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((<any>globalStore)._globalActions).toBeDefined();
expect((<any>globalStore)._globalActions[partnerAppName]).toBeDefined();
let registeredActions = ((<any>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((<any>globalStore)._globalActions).toBeDefined();
expect((<any>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((<any>globalStore)._globalActions).toBeDefined();
expect((<any>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((<any>globalStore)._globalActions).toBeDefined();
expect((<any>globalStore)._globalActions[partnerAppName]).toBeDefined();
let registeredActions = ((<any>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((<any>globalStore)._globalActions).toBeDefined();
expect((<any>globalStore)._globalActions[partnerAppName]).toBeDefined();
let registeredActions = ((<any>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<any, any> = (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<any, any> = (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<any, any> = (state: string = "Default", action: IAction<any>) => {
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<any, any> = (state: string = "Default", action: IAction<any>) => {
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<any, any> = (state: string = "Default", action: IAction<any>) => {
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<any, any> = (state: string = "Default", action: IAction<any>) => {
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 = (<any>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 = (<any>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 = (<any>globalStore)._selectors[partnerAppName];
expect(api.selectStateUpperCased).toBeDefined();
expect(api.selectStateLowerCased).toBeUndefined();
})
})
describe("SelectPartnerState", () => {
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
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<any, any> = (state = null, action: IAction<any>): 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<any>);
// 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<any, any> = (state = null, action: IAction<any>): 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<any, any> = (state = null, action: IAction<any>): 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"
]
}
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
SYMBOL INDEX (70 symbols across 11 files)
FILE: sample/counterApp/src/appCounter.js
class AppCounter (line 8) | class AppCounter extends React.Component {
method constructor (line 9) | constructor(props) {
method incrementLocalCounter (line 29) | incrementLocalCounter() {
method decrementLocalCounter (line 33) | decrementLocalCounter() {
method incrementGlobalCounter (line 37) | incrementGlobalCounter() {
method decrementGlobalCounter (line 41) | decrementGlobalCounter() {
method updateState (line 45) | updateState(globalState) {
method render (line 54) | render() {
FILE: sample/counterApp/src/counter.js
class Counter (line 3) | class Counter extends React.Component {
method render (line 4) | render() {
FILE: sample/todoApp/src/addTodo.js
class AddTodo (line 3) | class AddTodo extends React.Component {
method constructor (line 5) | constructor(props) {
method addTodo (line 10) | addTodo() {
method render (line 15) | render() {
FILE: sample/todoApp/src/todo.js
class Todo (line 3) | class Todo extends React.Component {
method render (line 4) | render() {
FILE: sample/todoApp/src/todoList.js
class TodoList (line 9) | class TodoList extends React.Component {
method constructor (line 10) | constructor(props) {
method addTodo (line 36) | addTodo(description) {
method removeTodo (line 40) | removeTodo(todoId) {
method counterChanged (line 44) | counterChanged(counterState) {
method stateChanged (line 50) | stateChanged(todoState) {
method render (line 56) | render() {
FILE: src/actions/action.interface.ts
type IAction (line 1) | interface IAction<Payload = any> {
FILE: src/common/abstract.logger.ts
method constructor (line 8) | constructor(id: string) {
method LogEvent (line 18) | LogEvent(source: string, eventName: string, properties: any) {
method LogException (line 36) | LogException(source: string, error: Error, properties: any) {
method SetNextLogger (line 52) | SetNextLogger(nextLogger: AbstractLogger) {
method isLoggerLoopCreated (line 64) | private isLoggerLoopCreated(nextLogger: AbstractLogger) {
FILE: src/common/console.logger.ts
class ConsoleLogger (line 3) | class ConsoleLogger extends AbstractLogger {
method constructor (line 4) | constructor(private _debugMode: boolean = false) {
method processEvent (line 9) | processEvent(source: string, eventName: string, properties: any) {
method processException (line 19) | processException(source: string, error: Error, properties: any) {
FILE: src/common/interfaces/global.store.interface.ts
type IGlobalStore (line 5) | interface IGlobalStore {
FILE: src/global.store.ts
class GlobalStore (line 13) | class GlobalStore implements IGlobalStore {
method constructor (line 27) | private constructor(private _logger: ILogger = null) {
method Get (line 42) | public static Get(debugMode: boolean = false, logger: ILogger = null):...
method CreateStore (line 70) | CreateStore(appName: string, appReducer: Reducer, middlewares?: Array<...
method RegisterStore (line 97) | RegisterStore(appName: string, store: Store, globalActions?: Array<str...
method RegisterGlobalActions (line 118) | RegisterGlobalActions(appName: string, actions?: Array<string>): void {
method GetPlatformState (line 140) | GetPlatformState(): any {
method GetPartnerState (line 155) | GetPartnerState(partnerName: string): any {
method GetGlobalState (line 177) | GetGlobalState(): any {
method DispatchGlobalAction (line 195) | DispatchGlobalAction(source: string, action: IAction<any>): void {
method DispatchLocalAction (line 212) | DispatchLocalAction(source: string, action: IAction<any>): void {
method DispatchAction (line 231) | DispatchAction(source: string, action: IAction<any>): void {
method Subscribe (line 245) | Subscribe(source: string, callback: (state: any) => void): () => void {
method SubscribeToPlatformState (line 261) | SubscribeToPlatformState(source: string, callback: (state: any) => voi...
method SubscribeToPartnerState (line 281) | SubscribeToPartnerState(source: string, partner: string, callback: (st...
method SubscribeToGlobalState (line 312) | SubscribeToGlobalState(source: string, callback: (state: any) => void)...
method UnsubscribeEagerSubscription (line 319) | UnsubscribeEagerSubscription(source: string, partnerName: string) {
method SetLogger (line 331) | SetLogger(logger: ILogger) {
method AddSelectors (line 349) | AddSelectors(source: string, selectors: Record<string, any>, mergeSele...
method SelectPartnerState (line 369) | SelectPartnerState(partner: string, selector: string, defaultReturn?: ...
method RegisterEagerSubscriptions (line 381) | private RegisterEagerSubscriptions(appName: string) {
method InvokeGlobalListeners (line 401) | private InvokeGlobalListeners(): void {
method GetPlatformStore (line 408) | private GetPlatformStore(): Store<any, any> {
method GetPartnerStore (line 412) | private GetPartnerStore(partnerName: string): Store<any, any> {
method GetGlobalMiddlewares (line 416) | private GetGlobalMiddlewares(): Array<Middleware> {
method IsActionRegisteredAsGlobal (line 421) | private IsActionRegisteredAsGlobal(appName: string, action: IAction<an...
method LogRegistration (line 429) | private LogRegistration(appName: string, isReplaced: boolean) {
method CopyState (line 445) | private CopyState(state: any) {
FILE: src/middlewares/action.logger.ts
class ActionLogger (line 9) | class ActionLogger {
method constructor (line 11) | constructor(private _logger: ILogger) { }
method CreateMiddleware (line 16) | public CreateMiddleware(): Middleware {
method SetLogger (line 38) | public SetLogger(logger: ILogger) {
method IsLoggingAllowed (line 45) | private IsLoggingAllowed(action: IAction) {
method LogActionDispatchStart (line 53) | private LogActionDispatchStart(state: any, action: IAction<any>) {
method LogActionDispatchComplete (line 71) | private LogActionDispatchComplete(state: any, action: any, dispatchedA...
method LogActionDispatchFailure (line 92) | private LogActionDispatchFailure(action: any, dispatchedAt: Date, exce...
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (127K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 835,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/workflows/build-and-publish.yml",
"chars": 697,
"preview": "name: Build-and-Publish\n\non:\n workflow_dispatch:\n\njobs:\n build-and-publish:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2370,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/gated-build.yml",
"chars": 412,
"preview": "# This is a gated build to verify the validity of the code\n\nname: Gated-Build\non:\n push:\n branches: [ main ]\n pull_"
},
{
"path": ".gitignore",
"chars": 6174,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
},
{
"path": ".npmrc",
"chars": 74,
"preview": "registry:https://registry.npmjs.org/ \nalways-auth=true \npackage-lock=false"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 444,
"preview": "# Microsoft Open Source Code of Conduct\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://op"
},
{
"path": "CONTRIBUTING.md",
"chars": 1069,
"preview": "# Contributing\n\nThis project welcomes contributions and suggestions. Most contributions require you to\nagree to a Contri"
},
{
"path": "LICENSE",
"chars": 1141,
"preview": " MIT License\n\n Copyright (c) Microsoft Corporation.\n\n Permission is hereby granted, free of charge, to any pers"
},
{
"path": "README.ko.md",
"chars": 5936,
"preview": "# Redux Micro-Frontend\n\n## 더 이상 사용되지 않는 버전에 대한 경고\n만약 당신이 1.1.0 버전을 사용하고 있을 경우, 바로 최신 버전으로 업그레이드를 하십시오. 해당 버전은 파이프라인 이슈로 "
},
{
"path": "README.md",
"chars": 8576,
"preview": "# Redux Micro-Frontend\n\n[한국어🇰🇷](./README.ko.md)\n\n## Version Deprecation Warning\n1.1.0 - If you are using this version, p"
},
{
"path": "SECURITY.md",
"chars": 2780,
"preview": "<!-- BEGIN MICROSOFT SECURITY.MD V0.0.5 BLOCK -->\n\n## Security\n\nMicrosoft takes the security of our software products an"
},
{
"path": "SUPPORT.md",
"chars": 628,
"preview": "# Support\r\n\r\n## How to file issues and get help \r\n\r\nThis project uses GitHub Issues to track bugs and feature requests."
},
{
"path": "azure-gated-build.yml",
"chars": 1693,
"preview": "trigger: none\n\njobs:\n - job: BuildLibrary\n displayName: Build Library\n pool:\n vmImage: ubuntu-latest \n "
},
{
"path": "index.ts",
"chars": 217,
"preview": "export * from './src/actions';\nexport * from './src/global.store';\nexport * from './src/common/interfaces';\nexport * fro"
},
{
"path": "karma.conf.headless.js",
"chars": 1907,
"preview": "module.exports = function (config) {\n config.set({\n frameworks: [\"jasmine\", \"karma-typescript\"],\n files"
},
{
"path": "karma.conf.js",
"chars": 1677,
"preview": "module.exports = function (config) {\n config.set({\n frameworks: [\"jasmine\", \"karma-typescript\"],\n files"
},
{
"path": "package.json",
"chars": 2701,
"preview": "{\n \"name\": \"redux-micro-frontend\",\n \"version\": \"1.3.0\",\n \"license\": \"MIT\",\n \"description\": \"This is a librar"
},
{
"path": "sample/counterApp/.babelrc",
"chars": 56,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\"\n ]\n}"
},
{
"path": "sample/counterApp/index.html",
"chars": 85,
"preview": "<html>\n <head></head>\n\n <body>\n <div id=\"app\"></div>\n </body>\n</html>"
},
{
"path": "sample/counterApp/package.json",
"chars": 1637,
"preview": "{\n \"name\": \"redux-micro-frontend-sample-counter\",\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"description\": \"T"
},
{
"path": "sample/counterApp/src/appCounter.js",
"chars": 2495,
"preview": "import React from 'react';\nimport { Counter } from './counter';\nimport { GlobalStore } from 'redux-micro-frontend';\nimpo"
},
{
"path": "sample/counterApp/src/counter.js",
"chars": 386,
"preview": "import React from 'react';\n\nexport class Counter extends React.Component {\n render() {\n return (\n <"
},
{
"path": "sample/counterApp/src/index.js",
"chars": 367,
"preview": "import React from 'react';\nimport { render } from 'react-dom';\nimport { AppCounter } from './appCounter';\n\nconst mountCo"
},
{
"path": "sample/counterApp/src/store/counterReducer.js",
"chars": 672,
"preview": "export const CounterReducer = (state = initialState, action) => {\n if (action.type === \"INCREMENT_GLOBAL\") return { ."
},
{
"path": "sample/counterApp/src/store/global.actions.js",
"chars": 246,
"preview": "export const IncrementGlobalCounter = () => {\n return {\n type: \"INCREMENT_GLOBAL\",\n payload: null\n }"
},
{
"path": "sample/counterApp/src/store/local.actions.js",
"chars": 242,
"preview": "export const IncrementLocalCounter = () => {\n return {\n type: \"INCREMENT_LOCAL\",\n payload: null\n }\n}"
},
{
"path": "sample/counterApp/webpack.config.js",
"chars": 849,
"preview": "const path = require('path');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst HtmlWebpackPlugin = "
},
{
"path": "sample/counterApp/webpack.config.mf.js",
"chars": 719,
"preview": "const path = require('path');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst webpack = require('w"
},
{
"path": "sample/shell/index.html",
"chars": 346,
"preview": "<html>\n <head>\n <script src=\"http://localhost:4001/main.bundle.js\"></script>\n <script src=\"http://local"
},
{
"path": "sample/shell/package.json",
"chars": 1340,
"preview": "{\n \"name\": \"redux-micro-frontend-sample-shell\",\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"description\": \"Thi"
},
{
"path": "sample/shell/src/index.js",
"chars": 139,
"preview": "(function() {\n\n window[\"micro-front-end-context\"] = true;\n window[\"mountCounter\"](\"counter\");\n window[\"mountTod"
},
{
"path": "sample/shell/webpack.config.js",
"chars": 749,
"preview": "const path = require('path');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst HtmlWebpackPlugin = "
},
{
"path": "sample/todoApp/.babelrc",
"chars": 56,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\"\n ]\n}"
},
{
"path": "sample/todoApp/index.html",
"chars": 85,
"preview": "<html>\n <head></head>\n\n <body>\n <div id=\"app\"></div>\n </body>\n</html>"
},
{
"path": "sample/todoApp/package.json",
"chars": 1626,
"preview": "{\n \"name\": \"redux-micro-frontend-sample-todo\",\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"description\": \"This"
},
{
"path": "sample/todoApp/src/addTodo.js",
"chars": 634,
"preview": "import React from 'react';\n\nexport class AddTodo extends React.Component {\n\n constructor(props) {\n super(props"
},
{
"path": "sample/todoApp/src/index.js",
"chars": 350,
"preview": "import React from 'react';\nimport { render } from 'react-dom';\nimport { TodoList } from './todoList';\n\n\nconst mountTodo "
},
{
"path": "sample/todoApp/src/store/todo.actions.js",
"chars": 225,
"preview": "export const AddTodo = (description) => {\n return {\n type: 'ADD_TODO',\n payload: description\n }\n}\n\ne"
},
{
"path": "sample/todoApp/src/store/todoReducer.js",
"chars": 541,
"preview": "export const TodoReducer = (state = [], action) => {\n if (action.type === 'ADD_TODO') {\n return [...state, { i"
},
{
"path": "sample/todoApp/src/todo.js",
"chars": 264,
"preview": "import React from 'react';\n\nexport class Todo extends React.Component {\n render() {\n return(\n <div "
},
{
"path": "sample/todoApp/src/todoList.js",
"chars": 2261,
"preview": "import React from 'react';\nimport { Todo } from './todo';\nimport { createStore } from 'redux';\nimport { AddTodo as AddTo"
},
{
"path": "sample/todoApp/webpack.config.js",
"chars": 849,
"preview": "const path = require('path');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst HtmlWebpackPlugin = "
},
{
"path": "sample/todoApp/webpack.config.mf.js",
"chars": 719,
"preview": "const path = require('path');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst webpack = require('w"
},
{
"path": "src/actions/action.interface.ts",
"chars": 108,
"preview": "export interface IAction<Payload = any> {\n type: string,\n payload: Payload,\n logEnabled?: Boolean\n}"
},
{
"path": "src/actions/index.ts",
"chars": 35,
"preview": "export * from './action.interface';"
},
{
"path": "src/common/abstract.logger.ts",
"chars": 2678,
"preview": "/**\n * Summary Logs data from application. Follows a Chain of Responsibility pattern where multiple loggers can be chain"
},
{
"path": "src/common/console.logger.ts",
"chars": 811,
"preview": "import { AbstractLogger } from \"./abstract.logger\";\n\nexport class ConsoleLogger extends AbstractLogger {\n constructor"
},
{
"path": "src/common/interfaces/global.store.interface.ts",
"chars": 1610,
"preview": "import { Store, Middleware, Reducer } from \"redux\";\nimport { IAction } from \"../../actions/action.interface\";\nimport { A"
},
{
"path": "src/common/interfaces/index.ts",
"chars": 42,
"preview": "export * from './global.store.interface';\n"
},
{
"path": "src/global.store.ts",
"chars": 19626,
"preview": "import { IAction } from './actions/action.interface';\nimport { ConsoleLogger } from './common/console.logger';\nimport { "
},
{
"path": "src/middlewares/action.logger.ts",
"chars": 4235,
"preview": "import { Middleware } from 'redux';\nimport { stringify } from 'flatted';\nimport { IAction } from '../actions';\nimport { "
},
{
"path": "test/global.store.tests.ts",
"chars": 24514,
"preview": "import { createStore, Reducer } from 'redux';\nimport { GlobalStore } from '../src/global.store';\nimport { IAction } from"
},
{
"path": "test/middlewares/action.logger.tests.ts",
"chars": 3051,
"preview": "import { IAction } from '../../src/actions/action.interface';\nimport { createStore, Reducer, applyMiddleware } from 'red"
},
{
"path": "tsconfig.json",
"chars": 499,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"ES6\",\n \"sourceMap\": true,\n \"target\": \"es5\",\n \"allowJs"
}
]
About this extraction
This page contains the full source code of the microsoft/redux-micro-frontend GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (112.4 KB), approximately 28.3k tokens, and a symbol index with 70 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.