max-remotes-helper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/dist/components/RemoteWrapper.d.ts +7 -0
- package/dist/components/Secured.d.ts +7 -0
- package/dist/context/RemoteAuthContext.d.ts +9 -0
- package/dist/hooks/useRemoteConfig.d.ts +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/index.js.LICENSE.txt +9 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/utils/moduleUtils.d.ts +24 -0
- package/dist/utils/tokenUtils.d.ts +24 -0
- package/package.json +44 -0
- package/src/components/RemoteWrapper.tsx +22 -0
- package/src/components/Secured.tsx +28 -0
- package/src/context/RemoteAuthContext.tsx +125 -0
- package/src/hooks/useRemoteConfig.ts +42 -0
- package/src/index.ts +37 -0
- package/src/types/index.ts +56 -0
- package/src/utils/moduleUtils.ts +73 -0
- package/src/utils/tokenUtils.ts +98 -0
- package/tsconfig.json +32 -0
- package/webpack.config.js +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# bo-remotes-helper
|
|
2
|
+
|
|
3
|
+
Helper library for Module Federation remotes in the BO ecosystem.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`bo-remotes-helper` provides common functionality for remote modules in the BO system, specifically designed to work with Module Federation without the heavy Node.js dependencies that `bo-library` has.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔐 **Lightweight Authentication Context** - No Node.js polyfills required
|
|
12
|
+
- 🛡️ **Secured Component** - Permission-based conditional rendering
|
|
13
|
+
- 🔧 **Remote Utilities** - Common functions for remote modules
|
|
14
|
+
- 📦 **Module Federation Ready** - Pre-configured shared dependencies
|
|
15
|
+
- 💾 **Token Management** - Browser-compatible token handling
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install bo-remotes-helper
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Basic Setup
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { RemoteWrapper, useRemoteAuth, Secured } from 'bo-remotes-helper';
|
|
29
|
+
|
|
30
|
+
// Wrap your remote component
|
|
31
|
+
export const MyRemoteComponent = ({ token }) => {
|
|
32
|
+
return (
|
|
33
|
+
<RemoteWrapper token={token}>
|
|
34
|
+
<MyContent />
|
|
35
|
+
</RemoteWrapper>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Use authentication context
|
|
40
|
+
const MyContent = () => {
|
|
41
|
+
const { state, hasPermission } = useRemoteAuth();
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<h1>Welcome {state.user?.name}</h1>
|
|
46
|
+
|
|
47
|
+
<Secured permission="edit">
|
|
48
|
+
<button>Edit Content</button>
|
|
49
|
+
</Secured>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Module Federation Configuration
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import { useRemoteConfig } from 'bo-remotes-helper';
|
|
59
|
+
|
|
60
|
+
const { getWebpackConfig } = useRemoteConfig();
|
|
61
|
+
|
|
62
|
+
const config = getWebpackConfig('my-remote', {
|
|
63
|
+
'./MyComponent': './src/MyComponent',
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
### Components
|
|
70
|
+
|
|
71
|
+
#### `RemoteWrapper`
|
|
72
|
+
Main wrapper component that provides authentication context.
|
|
73
|
+
|
|
74
|
+
#### `Secured`
|
|
75
|
+
Conditional rendering component based on permissions.
|
|
76
|
+
|
|
77
|
+
### Hooks
|
|
78
|
+
|
|
79
|
+
#### `useRemoteAuth()`
|
|
80
|
+
Access authentication state and methods.
|
|
81
|
+
|
|
82
|
+
#### `useRemoteConfig()`
|
|
83
|
+
Get standard Module Federation configuration.
|
|
84
|
+
|
|
85
|
+
### Utils
|
|
86
|
+
|
|
87
|
+
#### Token utilities
|
|
88
|
+
- `decodeToken(token)` - Decode JWT safely
|
|
89
|
+
- `isTokenExpired(token)` - Check token expiration
|
|
90
|
+
- `tokenStorage` - Browser storage utilities
|
|
91
|
+
|
|
92
|
+
#### Module utilities
|
|
93
|
+
- `loadRemoteModule()` - Dynamic remote loading
|
|
94
|
+
- `isRemoteAvailable()` - Check remote availability
|
|
95
|
+
|
|
96
|
+
## Differences from bo-library
|
|
97
|
+
|
|
98
|
+
| Feature | bo-library | bo-remotes-helper |
|
|
99
|
+
|---------|------------|-------------------|
|
|
100
|
+
| Authentication | Full JWT verification | Client-side decode only |
|
|
101
|
+
| Node.js APIs | Required (crypto, etc.) | Browser-only |
|
|
102
|
+
| Bundle size | Large | Lightweight |
|
|
103
|
+
| Use case | Host applications | Remote modules |
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Install dependencies
|
|
109
|
+
npm install
|
|
110
|
+
|
|
111
|
+
# Build
|
|
112
|
+
npm run build
|
|
113
|
+
|
|
114
|
+
# Watch mode
|
|
115
|
+
npm run dev
|
|
116
|
+
|
|
117
|
+
# Test
|
|
118
|
+
npm run test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Integration with bo-module-react-template
|
|
122
|
+
|
|
123
|
+
This library is designed to be the standard authentication solution for all remote modules created from `bo-module-react-template`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AuthProviderProps } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper component that provides authentication context to remote components
|
|
5
|
+
* This is the main component that remote applications should use
|
|
6
|
+
*/
|
|
7
|
+
export declare const RemoteWrapper: React.FC<AuthProviderProps>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { RemoteAuthContextValue } from '../types';
|
|
3
|
+
interface RemoteAuthProviderProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
initialToken?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const RemoteAuthProvider: React.FC<RemoteAuthProviderProps>;
|
|
8
|
+
export declare const useRemoteAuth: () => RemoteAuthContextValue;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SharedDependencies } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Hook that provides standard Module Federation configuration for remotes
|
|
4
|
+
*/
|
|
5
|
+
export declare const useRemoteConfig: () => {
|
|
6
|
+
sharedDependencies: SharedDependencies;
|
|
7
|
+
getWebpackConfig: (remoteName: string, exposedComponents: Record<string, string>) => {
|
|
8
|
+
name: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
exposes: Record<string, string>;
|
|
11
|
+
shared: SharedDependencies;
|
|
12
|
+
};
|
|
13
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { RemoteAuthProvider, useRemoteAuth } from './context/RemoteAuthContext';
|
|
2
|
+
export { Secured } from './components/Secured';
|
|
3
|
+
export { RemoteWrapper } from './components/RemoteWrapper';
|
|
4
|
+
export { useRemoteConfig } from './hooks/useRemoteConfig';
|
|
5
|
+
export { decodeToken, isTokenExpired, extractPermissions, tokenStorage } from './utils/tokenUtils';
|
|
6
|
+
export { loadRemoteModule, isRemoteAvailable, createRemoteComponentLoader, getRemoteEnvironment } from './utils/moduleUtils';
|
|
7
|
+
export type { RemoteAuthState, RemoteAuthContextValue, AuthProviderProps, SecuredProps, TokenInfo, RemoteConfig, SharedDependencies, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! For license information please see index.js.LICENSE.txt */
|
|
2
|
+
(()=>{var e={20:(e,r,t)=>{"use strict";var o=t(953),n=Symbol.for("react.element"),s=Symbol.for("react.fragment"),a=Object.prototype.hasOwnProperty,i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};r.Fragment=s,r.jsx=function(e,r,t){var o,s={},c=null,d=null;for(o in void 0!==t&&(c=""+t),void 0!==r.key&&(c=""+r.key),void 0!==r.ref&&(d=r.ref),r)a.call(r,o)&&!l.hasOwnProperty(o)&&(s[o]=r[o]);if(e&&e.defaultProps)for(o in r=e.defaultProps)void 0===s[o]&&(s[o]=r[o]);return{$$typeof:n,type:e,key:c,ref:d,props:s,_owner:i.current}}},60:(e,r,t)=>{var o={"./moduleUtils":226,"./moduleUtils.ts":226,"./tokenUtils":433,"./tokenUtils.ts":433};function n(e){return Promise.resolve().then(()=>{if(!t.o(o,e)){var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}return t(o[e])})}n.keys=()=>Object.keys(o),n.id=60,e.exports=n},226:(e,r,t)=>{"use strict";t.r(r),t.d(r,{createRemoteComponentLoader:()=>i,getRemoteEnvironment:()=>l,isRemoteAvailable:()=>a,loadRemoteModule:()=>s});var o=t(953),n=t.n(o);const s=async(e,r)=>{try{return await t(60)(`${e}/${r}`)}catch(t){throw console.error(`Failed to load remote module ${e}/${r}:`,t),t}},a=async e=>{try{const r=window[e];return"function"==typeof r?.get}catch(e){return!1}},i=(e,r,t)=>n().lazy(async()=>{try{return await s(e,r)}catch(o){return console.error(`Remote component loading failed: ${e}/${r}`,o),t?{default:t}:{default:()=>n().createElement("div",{style:{padding:"20px",color:"red"}},`Failed to load remote component: ${e}/${r}`)}}}),l=()=>({isDevelopment:!1,apiBaseUrl:process.env.REACT_APP_API_URL||"",frontUrl:process.env.REACT_APP_FRONT_URL||""})},433:(e,r,t)=>{"use strict";t.r(r),t.d(r,{decodeToken:()=>o,extractPermissions:()=>s,isTokenExpired:()=>n,tokenStorage:()=>a});const o=e=>{try{if(!e)return null;const r=e.split(".");if(3!==r.length)return null;const t=r[1],o=t+"=".repeat((4-t.length%4)%4),n=atob(o);return JSON.parse(n)}catch(e){return console.error("Error decoding token:",e),null}},n=e=>{const r=o(e);if(!r||!r.exp)return!0;const t=Math.floor(Date.now()/1e3);return r.exp<t},s=e=>{if(!e)return[];const r=["permissions","roles","authorities","scope"];for(const t of r){const r=e[t];if(Array.isArray(r))return r;if("string"==typeof r)return r.split(" ").filter(Boolean)}return[]},a={key:"bo-remote-token",save:e=>{try{sessionStorage.setItem(a.key,e)}catch(e){console.error("Error saving token:",e)}},get:()=>{try{return sessionStorage.getItem(a.key)}catch(e){return console.error("Error getting token:",e),null}},remove:()=>{try{sessionStorage.removeItem(a.key)}catch(e){console.error("Error removing token:",e)}},clear:()=>{try{sessionStorage.clear()}catch(e){console.error("Error clearing storage:",e)}}}},848:(e,r,t)=>{"use strict";e.exports=t(20)},953:e=>{"use strict";e.exports=require("react")}},r={};function t(o){var n=r[o];if(void 0!==n)return n.exports;var s=r[o]={exports:{}};return e[o](s,s.exports,t),s.exports}t.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return t.d(r,{a:r}),r},t.d=(e,r)=>{for(var o in r)t.o(r,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},t.e=()=>Promise.resolve(),t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var o={};(()=>{"use strict";t.r(o),t.d(o,{RemoteAuthProvider:()=>l,RemoteWrapper:()=>u,Secured:()=>d,createRemoteComponentLoader:()=>p.createRemoteComponentLoader,decodeToken:()=>n.decodeToken,extractPermissions:()=>n.extractPermissions,getRemoteEnvironment:()=>p.getRemoteEnvironment,isRemoteAvailable:()=>p.isRemoteAvailable,isTokenExpired:()=>n.isTokenExpired,loadRemoteModule:()=>p.loadRemoteModule,tokenStorage:()=>n.tokenStorage,useRemoteAuth:()=>c,useRemoteConfig:()=>m});var e=t(848),r=t(953),n=t(433);const s={token:null,isAuthenticated:!1,permissions:[],user:void 0},a=(e,r)=>{switch(r.type){case"SET_TOKEN":const t=(0,n.decodeToken)(r.payload),o=t?(0,n.extractPermissions)(t):[];return{...e,token:r.payload,isAuthenticated:!(0,n.isTokenExpired)(r.payload),permissions:o,user:t?{id:t.sub||"",email:t.email||"",name:t.name||t.email||""}:void 0};case"CLEAR_AUTH":return{...s};case"SET_USER_INFO":return{...e,user:{id:r.payload.sub||"",email:r.payload.email||"",name:r.payload.name||r.payload.email||""}};default:return e}},i=(0,r.createContext)(void 0),l=({children:t,initialToken:o})=>{const[l,c]=(0,r.useReducer)(a,s);(0,r.useEffect)(()=>{const e=n.tokenStorage.get(),r=o||e;r&&!(0,n.isTokenExpired)(r)?c({type:"SET_TOKEN",payload:r}):e&&n.tokenStorage.remove()},[o]);const d={state:l,setToken:e=>{e&&(n.tokenStorage.save(e),c({type:"SET_TOKEN",payload:e}))},clearAuth:()=>{n.tokenStorage.remove(),c({type:"CLEAR_AUTH"})},hasPermission:e=>!!l.isAuthenticated&&l.permissions.includes(e)};return(0,e.jsx)(i.Provider,{value:d,children:t})},c=()=>{const e=(0,r.useContext)(i);if(void 0===e)throw new Error("useRemoteAuth must be used within a RemoteAuthProvider");return e},d=({children:r,permission:t,fallback:o=null})=>{const{hasPermission:n,state:s}=c();return s.isAuthenticated&&n(t)?(0,e.jsx)(e.Fragment,{children:r}):(0,e.jsx)(e.Fragment,{children:o})},u=({children:r,token:t,apiBaseUrl:o,module:n})=>(0,e.jsx)(l,{initialToken:t,children:(0,e.jsx)("div",{"data-remote-module":n,"data-api-base":o,children:r})}),m=()=>{const e=(0,r.useMemo)(()=>({react:{singleton:!0,eager:!1},"react-dom":{singleton:!0,eager:!1},"aurora-web":{singleton:!0,eager:!1},"react-i18next":{singleton:!0,eager:!1},i18next:{singleton:!0,eager:!1}}),[]);return{sharedDependencies:e,getWebpackConfig:(r,t)=>({name:r,filename:"remoteEntry.js",exposes:t,shared:e})}};var p=t(226)})(),module.exports=o})();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface RemoteAuthState {
|
|
2
|
+
token: string | null;
|
|
3
|
+
isAuthenticated: boolean;
|
|
4
|
+
permissions: string[];
|
|
5
|
+
user?: {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface RemoteAuthContextValue {
|
|
12
|
+
state: RemoteAuthState;
|
|
13
|
+
setToken: (token: string) => void;
|
|
14
|
+
clearAuth: () => void;
|
|
15
|
+
hasPermission: (permission: string) => boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface AuthProviderProps {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
token?: string;
|
|
20
|
+
apiBaseUrl?: string;
|
|
21
|
+
module?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SecuredProps {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
permission: string;
|
|
26
|
+
fallback?: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
export interface TokenInfo {
|
|
29
|
+
iat?: number;
|
|
30
|
+
exp?: number;
|
|
31
|
+
sub?: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
permissions?: string[];
|
|
35
|
+
[key: string]: any;
|
|
36
|
+
}
|
|
37
|
+
export interface RemoteConfig {
|
|
38
|
+
name: string;
|
|
39
|
+
url: string;
|
|
40
|
+
module: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SharedDependencies {
|
|
43
|
+
[key: string]: {
|
|
44
|
+
singleton?: boolean;
|
|
45
|
+
requiredVersion?: string;
|
|
46
|
+
eager?: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for Module Federation and remote module management
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Dynamically imports a remote module
|
|
7
|
+
*/
|
|
8
|
+
export declare const loadRemoteModule: (remoteName: string, moduleName: string) => Promise<any>;
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a remote module is available
|
|
11
|
+
*/
|
|
12
|
+
export declare const isRemoteAvailable: (remoteName: string) => Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a safe remote component loader with error boundary
|
|
15
|
+
*/
|
|
16
|
+
export declare const createRemoteComponentLoader: (remoteName: string, moduleName: string, fallback?: React.ComponentType) => React.LazyExoticComponent<React.ComponentType<any>>;
|
|
17
|
+
/**
|
|
18
|
+
* Environment utilities for remotes
|
|
19
|
+
*/
|
|
20
|
+
export declare const getRemoteEnvironment: () => {
|
|
21
|
+
isDevelopment: boolean;
|
|
22
|
+
apiBaseUrl: string;
|
|
23
|
+
frontUrl: string;
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TokenInfo } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Safely decodes a JWT token without verification
|
|
4
|
+
* For production use, tokens should be verified server-side
|
|
5
|
+
*/
|
|
6
|
+
export declare const decodeToken: (token: string) => TokenInfo | null;
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a token is expired
|
|
9
|
+
*/
|
|
10
|
+
export declare const isTokenExpired: (token: string) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Extracts permissions from a decoded token
|
|
13
|
+
*/
|
|
14
|
+
export declare const extractPermissions: (tokenInfo: TokenInfo) => string[];
|
|
15
|
+
/**
|
|
16
|
+
* Storage utilities for tokens
|
|
17
|
+
*/
|
|
18
|
+
export declare const tokenStorage: {
|
|
19
|
+
key: string;
|
|
20
|
+
save: (token: string) => void;
|
|
21
|
+
get: () => string | null;
|
|
22
|
+
remove: () => void;
|
|
23
|
+
clear: () => void;
|
|
24
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "max-remotes-helper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Helper library for Module Federation remotes in BO ecosystem",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "webpack --config webpack.config.js --mode=production",
|
|
9
|
+
"dev": "webpack --config webpack.config.js --mode=development --watch",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"lint": "eslint src/**/*.{ts,tsx}",
|
|
12
|
+
"clean": "rimraf dist"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"module-federation",
|
|
16
|
+
"remotes",
|
|
17
|
+
"react",
|
|
18
|
+
"bo"
|
|
19
|
+
],
|
|
20
|
+
"author": "BO Team",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"react": ">=18.0.0",
|
|
24
|
+
"react-dom": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^18.0.37",
|
|
28
|
+
"@types/react-dom": "^18.2.6",
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
|
30
|
+
"@typescript-eslint/parser": "^6.17.0",
|
|
31
|
+
"clean-webpack-plugin": "^4.0.0",
|
|
32
|
+
"eslint": "^8.56.0",
|
|
33
|
+
"jest": "^29.5.0",
|
|
34
|
+
"rimraf": "^5.0.0",
|
|
35
|
+
"ts-loader": "^9.4.3",
|
|
36
|
+
"typescript": "^5.9.2",
|
|
37
|
+
"webpack": "^5.87.0",
|
|
38
|
+
"webpack-cli": "^5.1.4"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0",
|
|
42
|
+
"react-dom": ">=18.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RemoteAuthProvider } from '../context/RemoteAuthContext';
|
|
3
|
+
import { AuthProviderProps } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wrapper component that provides authentication context to remote components
|
|
7
|
+
* This is the main component that remote applications should use
|
|
8
|
+
*/
|
|
9
|
+
export const RemoteWrapper: React.FC<AuthProviderProps> = ({
|
|
10
|
+
children,
|
|
11
|
+
token,
|
|
12
|
+
apiBaseUrl,
|
|
13
|
+
module,
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<RemoteAuthProvider initialToken={token}>
|
|
17
|
+
<div data-remote-module={module} data-api-base={apiBaseUrl}>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
</RemoteAuthProvider>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SecuredProps } from '../types';
|
|
3
|
+
import { useRemoteAuth } from '../context/RemoteAuthContext';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Secured component for conditional rendering based on permissions
|
|
7
|
+
* Compatible with Module Federation
|
|
8
|
+
*/
|
|
9
|
+
export const Secured: React.FC<SecuredProps> = ({
|
|
10
|
+
children,
|
|
11
|
+
permission,
|
|
12
|
+
fallback = null
|
|
13
|
+
}) => {
|
|
14
|
+
const { hasPermission, state } = useRemoteAuth();
|
|
15
|
+
|
|
16
|
+
// If not authenticated, don't render
|
|
17
|
+
if (!state.isAuthenticated) {
|
|
18
|
+
return <>{fallback}</>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// If has permission, render children
|
|
22
|
+
if (hasPermission(permission)) {
|
|
23
|
+
return <>{children}</>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Otherwise render fallback
|
|
27
|
+
return <>{fallback}</>;
|
|
28
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react';
|
|
2
|
+
import { RemoteAuthState, RemoteAuthContextValue, TokenInfo } from '../types';
|
|
3
|
+
import { decodeToken, extractPermissions, isTokenExpired, tokenStorage } from '../utils/tokenUtils';
|
|
4
|
+
|
|
5
|
+
// Action types
|
|
6
|
+
type AuthAction =
|
|
7
|
+
| { type: 'SET_TOKEN'; payload: string }
|
|
8
|
+
| { type: 'CLEAR_AUTH' }
|
|
9
|
+
| { type: 'SET_USER_INFO'; payload: TokenInfo };
|
|
10
|
+
|
|
11
|
+
// Initial state
|
|
12
|
+
const initialState: RemoteAuthState = {
|
|
13
|
+
token: null,
|
|
14
|
+
isAuthenticated: false,
|
|
15
|
+
permissions: [],
|
|
16
|
+
user: undefined,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Reducer
|
|
20
|
+
const authReducer = (state: RemoteAuthState, action: AuthAction): RemoteAuthState => {
|
|
21
|
+
switch (action.type) {
|
|
22
|
+
case 'SET_TOKEN':
|
|
23
|
+
const tokenInfo = decodeToken(action.payload);
|
|
24
|
+
const permissions = tokenInfo ? extractPermissions(tokenInfo) : [];
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
token: action.payload,
|
|
29
|
+
isAuthenticated: !isTokenExpired(action.payload),
|
|
30
|
+
permissions,
|
|
31
|
+
user: tokenInfo ? {
|
|
32
|
+
id: tokenInfo.sub || '',
|
|
33
|
+
email: tokenInfo.email || '',
|
|
34
|
+
name: tokenInfo.name || tokenInfo.email || '',
|
|
35
|
+
} : undefined,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
case 'CLEAR_AUTH':
|
|
39
|
+
return {
|
|
40
|
+
...initialState,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
case 'SET_USER_INFO':
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
user: {
|
|
47
|
+
id: action.payload.sub || '',
|
|
48
|
+
email: action.payload.email || '',
|
|
49
|
+
name: action.payload.name || action.payload.email || '',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
default:
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Context
|
|
59
|
+
const RemoteAuthContext = createContext<RemoteAuthContextValue | undefined>(undefined);
|
|
60
|
+
|
|
61
|
+
// Provider props
|
|
62
|
+
interface RemoteAuthProviderProps {
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
initialToken?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Provider component
|
|
68
|
+
export const RemoteAuthProvider: React.FC<RemoteAuthProviderProps> = ({
|
|
69
|
+
children,
|
|
70
|
+
initialToken
|
|
71
|
+
}) => {
|
|
72
|
+
const [state, dispatch] = useReducer(authReducer, initialState);
|
|
73
|
+
|
|
74
|
+
// Initialize with token from storage or props
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const storedToken = tokenStorage.get();
|
|
77
|
+
const tokenToUse = initialToken || storedToken;
|
|
78
|
+
|
|
79
|
+
if (tokenToUse && !isTokenExpired(tokenToUse)) {
|
|
80
|
+
dispatch({ type: 'SET_TOKEN', payload: tokenToUse });
|
|
81
|
+
} else if (storedToken) {
|
|
82
|
+
// Remove expired token
|
|
83
|
+
tokenStorage.remove();
|
|
84
|
+
}
|
|
85
|
+
}, [initialToken]);
|
|
86
|
+
|
|
87
|
+
const setToken = (token: string) => {
|
|
88
|
+
if (!token) return;
|
|
89
|
+
|
|
90
|
+
tokenStorage.save(token);
|
|
91
|
+
dispatch({ type: 'SET_TOKEN', payload: token });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const clearAuth = () => {
|
|
95
|
+
tokenStorage.remove();
|
|
96
|
+
dispatch({ type: 'CLEAR_AUTH' });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const hasPermission = (permission: string): boolean => {
|
|
100
|
+
if (!state.isAuthenticated) return false;
|
|
101
|
+
return state.permissions.includes(permission);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const contextValue: RemoteAuthContextValue = {
|
|
105
|
+
state,
|
|
106
|
+
setToken,
|
|
107
|
+
clearAuth,
|
|
108
|
+
hasPermission,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<RemoteAuthContext.Provider value={contextValue}>
|
|
113
|
+
{children}
|
|
114
|
+
</RemoteAuthContext.Provider>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Hook to use the context
|
|
119
|
+
export const useRemoteAuth = (): RemoteAuthContextValue => {
|
|
120
|
+
const context = useContext(RemoteAuthContext);
|
|
121
|
+
if (context === undefined) {
|
|
122
|
+
throw new Error('useRemoteAuth must be used within a RemoteAuthProvider');
|
|
123
|
+
}
|
|
124
|
+
return context;
|
|
125
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { SharedDependencies } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that provides standard Module Federation configuration for remotes
|
|
6
|
+
*/
|
|
7
|
+
export const useRemoteConfig = () => {
|
|
8
|
+
const sharedDependencies: SharedDependencies = useMemo(() => ({
|
|
9
|
+
react: {
|
|
10
|
+
singleton: true,
|
|
11
|
+
eager: false,
|
|
12
|
+
},
|
|
13
|
+
'react-dom': {
|
|
14
|
+
singleton: true,
|
|
15
|
+
eager: false,
|
|
16
|
+
},
|
|
17
|
+
'aurora-web': {
|
|
18
|
+
singleton: true,
|
|
19
|
+
eager: false,
|
|
20
|
+
},
|
|
21
|
+
'react-i18next': {
|
|
22
|
+
singleton: true,
|
|
23
|
+
eager: false,
|
|
24
|
+
},
|
|
25
|
+
'i18next': {
|
|
26
|
+
singleton: true,
|
|
27
|
+
eager: false,
|
|
28
|
+
},
|
|
29
|
+
}), []);
|
|
30
|
+
|
|
31
|
+
const getWebpackConfig = (remoteName: string, exposedComponents: Record<string, string>) => ({
|
|
32
|
+
name: remoteName,
|
|
33
|
+
filename: 'remoteEntry.js',
|
|
34
|
+
exposes: exposedComponents,
|
|
35
|
+
shared: sharedDependencies,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
sharedDependencies,
|
|
40
|
+
getWebpackConfig,
|
|
41
|
+
};
|
|
42
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Main entry point for bo-remotes-helper
|
|
2
|
+
|
|
3
|
+
// Context and Providers
|
|
4
|
+
export { RemoteAuthProvider, useRemoteAuth } from './context/RemoteAuthContext';
|
|
5
|
+
|
|
6
|
+
// Components
|
|
7
|
+
export { Secured } from './components/Secured';
|
|
8
|
+
export { RemoteWrapper } from './components/RemoteWrapper';
|
|
9
|
+
|
|
10
|
+
// Hooks
|
|
11
|
+
export { useRemoteConfig } from './hooks/useRemoteConfig';
|
|
12
|
+
|
|
13
|
+
// Utils
|
|
14
|
+
export {
|
|
15
|
+
decodeToken,
|
|
16
|
+
isTokenExpired,
|
|
17
|
+
extractPermissions,
|
|
18
|
+
tokenStorage
|
|
19
|
+
} from './utils/tokenUtils';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
loadRemoteModule,
|
|
23
|
+
isRemoteAvailable,
|
|
24
|
+
createRemoteComponentLoader,
|
|
25
|
+
getRemoteEnvironment
|
|
26
|
+
} from './utils/moduleUtils';
|
|
27
|
+
|
|
28
|
+
// Types
|
|
29
|
+
export type {
|
|
30
|
+
RemoteAuthState,
|
|
31
|
+
RemoteAuthContextValue,
|
|
32
|
+
AuthProviderProps,
|
|
33
|
+
SecuredProps,
|
|
34
|
+
TokenInfo,
|
|
35
|
+
RemoteConfig,
|
|
36
|
+
SharedDependencies,
|
|
37
|
+
} from './types';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Types for bo-remotes-helper
|
|
2
|
+
|
|
3
|
+
export interface RemoteAuthState {
|
|
4
|
+
token: string | null;
|
|
5
|
+
isAuthenticated: boolean;
|
|
6
|
+
permissions: string[];
|
|
7
|
+
user?: {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RemoteAuthContextValue {
|
|
15
|
+
state: RemoteAuthState;
|
|
16
|
+
setToken: (token: string) => void;
|
|
17
|
+
clearAuth: () => void;
|
|
18
|
+
hasPermission: (permission: string) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthProviderProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
token?: string;
|
|
24
|
+
apiBaseUrl?: string;
|
|
25
|
+
module?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SecuredProps {
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
permission: string;
|
|
31
|
+
fallback?: React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TokenInfo {
|
|
35
|
+
iat?: number;
|
|
36
|
+
exp?: number;
|
|
37
|
+
sub?: string;
|
|
38
|
+
email?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
permissions?: string[];
|
|
41
|
+
[key: string]: any;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RemoteConfig {
|
|
45
|
+
name: string;
|
|
46
|
+
url: string;
|
|
47
|
+
module: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SharedDependencies {
|
|
51
|
+
[key: string]: {
|
|
52
|
+
singleton?: boolean;
|
|
53
|
+
requiredVersion?: string;
|
|
54
|
+
eager?: boolean;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utilities for Module Federation and remote module management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Dynamically imports a remote module
|
|
9
|
+
*/
|
|
10
|
+
export const loadRemoteModule = async (remoteName: string, moduleName: string) => {
|
|
11
|
+
try {
|
|
12
|
+
// @ts-ignore - Module Federation dynamic import
|
|
13
|
+
const module = await import(`${remoteName}/${moduleName}`);
|
|
14
|
+
return module;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error(`Failed to load remote module ${remoteName}/${moduleName}:`, error);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a remote module is available
|
|
23
|
+
*/
|
|
24
|
+
export const isRemoteAvailable = async (remoteName: string): Promise<boolean> => {
|
|
25
|
+
try {
|
|
26
|
+
// @ts-ignore - Check if remote is available
|
|
27
|
+
const container = window[remoteName];
|
|
28
|
+
return typeof container?.get === 'function';
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a safe remote component loader with error boundary
|
|
36
|
+
*/
|
|
37
|
+
export const createRemoteComponentLoader = (
|
|
38
|
+
remoteName: string,
|
|
39
|
+
moduleName: string,
|
|
40
|
+
fallback?: React.ComponentType
|
|
41
|
+
) => {
|
|
42
|
+
return React.lazy(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const module = await loadRemoteModule(remoteName, moduleName);
|
|
45
|
+
return module;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`Remote component loading failed: ${remoteName}/${moduleName}`, error);
|
|
48
|
+
|
|
49
|
+
if (fallback) {
|
|
50
|
+
return { default: fallback };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Return a default error component
|
|
54
|
+
return {
|
|
55
|
+
default: () => React.createElement('div',
|
|
56
|
+
{ style: { padding: '20px', color: 'red' } },
|
|
57
|
+
`Failed to load remote component: ${remoteName}/${moduleName}`
|
|
58
|
+
)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Environment utilities for remotes
|
|
66
|
+
*/
|
|
67
|
+
export const getRemoteEnvironment = () => {
|
|
68
|
+
return {
|
|
69
|
+
isDevelopment: process.env.NODE_ENV === 'development',
|
|
70
|
+
apiBaseUrl: process.env.REACT_APP_API_URL || '',
|
|
71
|
+
frontUrl: process.env.REACT_APP_FRONT_URL || '',
|
|
72
|
+
};
|
|
73
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { TokenInfo } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely decodes a JWT token without verification
|
|
5
|
+
* For production use, tokens should be verified server-side
|
|
6
|
+
*/
|
|
7
|
+
export const decodeToken = (token: string): TokenInfo | null => {
|
|
8
|
+
try {
|
|
9
|
+
if (!token) return null;
|
|
10
|
+
|
|
11
|
+
const parts = token.split('.');
|
|
12
|
+
if (parts.length !== 3) return null;
|
|
13
|
+
|
|
14
|
+
const payload = parts[1];
|
|
15
|
+
// Add padding if needed
|
|
16
|
+
const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
|
|
17
|
+
const decodedPayload = atob(paddedPayload);
|
|
18
|
+
|
|
19
|
+
return JSON.parse(decodedPayload);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Error decoding token:', error);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a token is expired
|
|
28
|
+
*/
|
|
29
|
+
export const isTokenExpired = (token: string): boolean => {
|
|
30
|
+
const decoded = decodeToken(token);
|
|
31
|
+
if (!decoded || !decoded.exp) return true;
|
|
32
|
+
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
return decoded.exp < now;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extracts permissions from a decoded token
|
|
39
|
+
*/
|
|
40
|
+
export const extractPermissions = (tokenInfo: TokenInfo): string[] => {
|
|
41
|
+
if (!tokenInfo) return [];
|
|
42
|
+
|
|
43
|
+
// Try different possible permission keys
|
|
44
|
+
const permissionKeys = ['permissions', 'roles', 'authorities', 'scope'];
|
|
45
|
+
|
|
46
|
+
for (const key of permissionKeys) {
|
|
47
|
+
const permissions = tokenInfo[key];
|
|
48
|
+
if (Array.isArray(permissions)) {
|
|
49
|
+
return permissions;
|
|
50
|
+
}
|
|
51
|
+
if (typeof permissions === 'string') {
|
|
52
|
+
// Handle space-separated permissions (like OAuth scope)
|
|
53
|
+
return permissions.split(' ').filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Storage utilities for tokens
|
|
62
|
+
*/
|
|
63
|
+
export const tokenStorage = {
|
|
64
|
+
key: 'bo-remote-token',
|
|
65
|
+
|
|
66
|
+
save: (token: string): void => {
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem(tokenStorage.key, token);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Error saving token:', error);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
get: (): string | null => {
|
|
75
|
+
try {
|
|
76
|
+
return sessionStorage.getItem(tokenStorage.key);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error getting token:', error);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
remove: (): void => {
|
|
84
|
+
try {
|
|
85
|
+
sessionStorage.removeItem(tokenStorage.key);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error removing token:', error);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
clear: (): void => {
|
|
92
|
+
try {
|
|
93
|
+
sessionStorage.clear();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error clearing storage:', error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"ES6"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"module": "esnext",
|
|
17
|
+
"moduleResolution": "node",
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"noEmit": false,
|
|
21
|
+
"declaration": true,
|
|
22
|
+
"outDir": "./dist",
|
|
23
|
+
"jsx": "react-jsx"
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"exclude": [
|
|
29
|
+
"node_modules",
|
|
30
|
+
"dist"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
entry: './src/index.ts',
|
|
6
|
+
output: {
|
|
7
|
+
filename: 'index.js',
|
|
8
|
+
path: path.resolve(__dirname, 'dist'),
|
|
9
|
+
libraryTarget: 'commonjs2',
|
|
10
|
+
clean: true,
|
|
11
|
+
},
|
|
12
|
+
resolve: {
|
|
13
|
+
extensions: ['.tsx', '.ts', '.js'],
|
|
14
|
+
},
|
|
15
|
+
externals: {
|
|
16
|
+
react: 'react',
|
|
17
|
+
'react-dom': 'react-dom',
|
|
18
|
+
},
|
|
19
|
+
plugins: [
|
|
20
|
+
new CleanWebpackPlugin(),
|
|
21
|
+
],
|
|
22
|
+
module: {
|
|
23
|
+
rules: [
|
|
24
|
+
{
|
|
25
|
+
test: /\.tsx?$/,
|
|
26
|
+
use: [
|
|
27
|
+
{
|
|
28
|
+
loader: 'ts-loader',
|
|
29
|
+
options: {
|
|
30
|
+
configFile: 'tsconfig.json',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
exclude: /node_modules/,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
};
|