@servicetitan/docs-uikit 32.1.0 → 32.2.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/docs/BREAKING_CHANGES.mdx +1 -0
- package/docs/launchdarkly-service.mdx +1 -1
- package/docs/microfrontends/developing/authentication.mdx +91 -0
- package/docs/microfrontends/developing/developing.mdx +124 -0
- package/docs/microfrontends/developing/headless-bundle.mdx +62 -0
- package/docs/microfrontends/hosting.mdx +84 -0
- package/docs/microfrontends/microfrontends.mdx +45 -0
- package/docs/microfrontends/publishing.mdx +93 -0
- package/docs/microfrontends/sharing-dependencies.mdx +96 -0
- package/docs/microfrontends/troubleshooting.mdx +55 -0
- package/docs/startup/build.mdx +281 -8
- package/docs/startup/lint.mdx +23 -0
- package/docs/startup/mfe-publish.mdx +48 -11
- package/docs/startup/start.mdx +46 -1
- package/docs/startup/startup.mdx +5 -375
- package/docs/startup/test/jest.mdx +114 -0
- package/docs/startup/test/test.mdx +72 -0
- package/docs/startup/test/vitest.mdx +117 -0
- package/docs/web-components/headless-loader.mdx +2 -47
- package/docs/web-components/loader.mdx +2 -2
- package/package.json +2 -2
- package/docs/startup/test.mdx +0 -205
|
@@ -142,7 +142,7 @@ export const HostApp: FC = () => {
|
|
|
142
142
|
|
|
143
143
|
### LDService
|
|
144
144
|
|
|
145
|
-
The `LDService` interface enables [headless bundles](./
|
|
145
|
+
The `LDService` interface enables [headless bundles](./microfrontends/developing/headless-bundle) to create and reuse client connections for LaunchDarkly projects.
|
|
146
146
|
|
|
147
147
|
```ts
|
|
148
148
|
export interface LDService {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Authentication
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
To develop against backend APIs that require authentication, you can configure [proxies](https://webpack.js.org/configuration/dev-server/#devserverproxy) that add cookies to local requests.
|
|
6
|
+
|
|
7
|
+
## Create proxy script
|
|
8
|
+
|
|
9
|
+
Create a script that generates a `webpack-dev-server` proxy configuration from a local `config.js` file that maps URLs to cookies. For example,
|
|
10
|
+
|
|
11
|
+
```js title="local/proxy-settings.js"
|
|
12
|
+
const proxy = [];
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
/**
|
|
16
|
+
* Here map environments to the endpoints (prefixes) that require authentication
|
|
17
|
+
* (if all do, use the ** wildcard). If all requests are sent to the host,
|
|
18
|
+
* remove or comment out the "ms" (microservice) environment.
|
|
19
|
+
*/
|
|
20
|
+
const environments = {
|
|
21
|
+
mainApp: ['/app/api'],
|
|
22
|
+
ms: ['**'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Read local config
|
|
26
|
+
const config = require('./config');
|
|
27
|
+
|
|
28
|
+
// Add proxy for each URL and cookie in local config
|
|
29
|
+
Object.entries(environments).forEach(([name, prefixes]) => {
|
|
30
|
+
const { cookie, url } = config[name] || {};
|
|
31
|
+
if (url && cookie) {
|
|
32
|
+
proxy.push(createProxyConfig({ target: url, cookie, context: prefixes }));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore error
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createProxyConfig({ context, cookie, target }) {
|
|
40
|
+
return {
|
|
41
|
+
context,
|
|
42
|
+
target,
|
|
43
|
+
changeOrigin: true,
|
|
44
|
+
secure: false,
|
|
45
|
+
onProxyReq: req => {
|
|
46
|
+
if (cookie) {
|
|
47
|
+
req.setHeader('Cookie', cookie);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = proxy.length ? proxy : undefined;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Put cookies in local configuration file
|
|
57
|
+
|
|
58
|
+
Login to the application and use the web browser's Developer Tools to copy the cookies from backend requests to `config.js`.
|
|
59
|
+
|
|
60
|
+
```js title="local/config.js"
|
|
61
|
+
module.exports = {
|
|
62
|
+
mainApp: {
|
|
63
|
+
url: 'https://go.servicetitan.com/',
|
|
64
|
+
cookie: '', // actual cookie goes here
|
|
65
|
+
},
|
|
66
|
+
ms: {
|
|
67
|
+
url: 'http://localhost:8888',
|
|
68
|
+
cookie: '', // actual cookie goes here
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
:::caution
|
|
74
|
+
Add `config.js` to `.gitignore` so the cookies are not exposed. And, note that
|
|
75
|
+
you will have to replace cookies when they expire.
|
|
76
|
+
:::
|
|
77
|
+
|
|
78
|
+
## Configure `webpack-dev-server`
|
|
79
|
+
|
|
80
|
+
Set `cli.webpack.proxy` to the relative path to the proxy script.
|
|
81
|
+
|
|
82
|
+
```json title="packages/mfe/package.json"
|
|
83
|
+
{
|
|
84
|
+
"cli": {
|
|
85
|
+
"webpack": {
|
|
86
|
+
"port": 8888,
|
|
87
|
+
"proxy": "../../local/proxy-settings.js"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Developing
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
An MFE is a React component that is bundled in such a way that it renders inside an isolated host container.
|
|
6
|
+
|
|
7
|
+
A best practice is to use different MFEs for different purposes. Creating multi-tasking MFEs that serve multiple, unrelated purposes is usually a suboptimal approach.
|
|
8
|
+
Components that conditionally render different behaviors based on inputs from the host should probably be implemented as separate MFEs.
|
|
9
|
+
|
|
10
|
+
To create an MFE, create a file named `app.tsx` in the root directory of a package's source folder (the `rootDir` in `tsconfig.json`) that exports a component named `App`. The `App` component will be the MFE's entry point.
|
|
11
|
+
|
|
12
|
+
Remove any existing "index" file from the root directory and, if the MFE uses `@servicetitan/design-system`, remove all imports of design system styles from the code (`startup` automatically imports design system styles when needed).
|
|
13
|
+
|
|
14
|
+
:::tip
|
|
15
|
+
|
|
16
|
+
- If the MFE is large, use [`lazyModule`](../../lazy-module) to split it into smaller modules that are loaded on demand.
|
|
17
|
+
- See [Headless Bundle](./headless-bundle) for how to create a separate bundle for preloading setup or configuration information.
|
|
18
|
+
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
In the MFE's `package.json`, set,
|
|
24
|
+
|
|
25
|
+
- `private` to `true`
|
|
26
|
+
- `cli.web-component` to `true`,
|
|
27
|
+
- `cli.webpack.port` to a free port from where to serve the MFE locally
|
|
28
|
+
- `publishConfig.access` to `"private"`
|
|
29
|
+
|
|
30
|
+
E.g.,
|
|
31
|
+
|
|
32
|
+
```json title="package.json"
|
|
33
|
+
{
|
|
34
|
+
"private": true,
|
|
35
|
+
"cli": {
|
|
36
|
+
"web-component": true,
|
|
37
|
+
"webpack": {
|
|
38
|
+
"port": 8888
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "restricted"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Required dependencies
|
|
48
|
+
|
|
49
|
+
Add these required **dependencies** (include other dependencies as needed):
|
|
50
|
+
|
|
51
|
+
- `@servicetitan/log-service`
|
|
52
|
+
- `@servicetitan/react-ioc`
|
|
53
|
+
- `@servicetitan/web-components`
|
|
54
|
+
- `history`
|
|
55
|
+
- `react`
|
|
56
|
+
- `react-dom`
|
|
57
|
+
|
|
58
|
+
## Running
|
|
59
|
+
|
|
60
|
+
Use [startup start](../../startup/start) to run the MFE. It will serve it from the port specified [above](#configuration).
|
|
61
|
+
|
|
62
|
+
:::tip
|
|
63
|
+
See [Authentication](./authentication) for how to develop against backend APIs that require authentication.
|
|
64
|
+
:::
|
|
65
|
+
|
|
66
|
+
### Inside host application
|
|
67
|
+
|
|
68
|
+
To run the MFE in a local host, pass [Loader](../../web-components/loader) the MFE's local URL. For example,
|
|
69
|
+
|
|
70
|
+
```tsx title="Local Host Application"
|
|
71
|
+
import { Loader } from '@servicetitan/web-components';
|
|
72
|
+
|
|
73
|
+
export function MyMfe() {
|
|
74
|
+
return <Loader src="http://localhost:8888" />;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Using `getValueForEnvironment` in the Monolith
|
|
79
|
+
|
|
80
|
+
In the Monolith, you can use [getValueForEnvironment](../../web-components/get-value-for-environment) to automatically use the MFE's local URL in development. E.g.,
|
|
81
|
+
|
|
82
|
+
```tsx title="Monolith Host Application"
|
|
83
|
+
import { Loader, getValueForEnvironment } from '@servicetitan/web-components';
|
|
84
|
+
|
|
85
|
+
const baseUrl = 'https://unpkg.servicetitan.com/@servicetitan/my-mfe';
|
|
86
|
+
|
|
87
|
+
export function MyMfe() {
|
|
88
|
+
return <Loader src={getMfeUrl()} />;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getMfeUrl() {
|
|
92
|
+
return (
|
|
93
|
+
getValueForEnvironment({
|
|
94
|
+
dev: 'http://localhost:8888',
|
|
95
|
+
qa: `${baseUrl}@qa`,
|
|
96
|
+
stage: `${baseUrl}@stage`,
|
|
97
|
+
go: `${baseUrl}@prod`,
|
|
98
|
+
next: `${baseUrl}@prod`,
|
|
99
|
+
}) ?? ''
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Without start
|
|
105
|
+
|
|
106
|
+
To run the MFE without `start`, [build](#building) it, then point any static http server to the MFE's root folder, where [Loader](../../web-components/loader) will find the `dist` directory. For example, to serve with [`http-server`](https://www.npmjs.com/package/http-server) on port 8888,
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npx --yes http-server packages/mfe -p 8888 --cors
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Building
|
|
113
|
+
|
|
114
|
+
Use [startup build](../../startup/build) to build the MFE.
|
|
115
|
+
|
|
116
|
+
## See Also
|
|
117
|
+
|
|
118
|
+
- [Authentication](./authentication) - how to use backend API's that require authentication
|
|
119
|
+
- [Example MFE](https://github.com/servicetitan/frontend-example/tree/master/packages/mfe)
|
|
120
|
+
- [Headless Bundle](./headless-bundle) - how to preloading setup or configuration information
|
|
121
|
+
- [Hosting](../hosting) - how to load MFEs
|
|
122
|
+
- [Publishing](../publishing) - how to publish MFEs
|
|
123
|
+
- [Sharing Dependencies](../sharing-dependencies) - how to customize shared dependencies
|
|
124
|
+
- [Web Components](../../web-components) - how to use `Loader`, `getValueForEnvironment` and other runtime APIs
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Headless Bundle
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A headless bundle is a tiny bundle that allows hosts to preload configuration or setup information without having to load the entire MFE. Headless bundles are not rendered into or associated with DOM elements so they cannot use React.
|
|
6
|
+
|
|
7
|
+
To generate a headless bundle, create a file named `headless.ts` in the root directory of the MFE's source folder (the `rootDir` in `tsconfig.json`) and export a function named `connectedCallback`.
|
|
8
|
+
|
|
9
|
+
:::info
|
|
10
|
+
`Startup` includes everything imported by `headless.ts` in the bundle.
|
|
11
|
+
Headless bundles are wholly self-contained and cannot [share dependencies](../sharing-dependencies) with hosts.
|
|
12
|
+
:::
|
|
13
|
+
|
|
14
|
+
## connectedCallback
|
|
15
|
+
|
|
16
|
+
The host calls `connectedCallback` when the bundle is loaded.
|
|
17
|
+
|
|
18
|
+
```ts title="src/headless.ts"
|
|
19
|
+
import type { HeadlessCallback } from '@servicetitan/web-components';
|
|
20
|
+
|
|
21
|
+
export const connectedCallback: HeadlessCallback<ExampleData> = ({ mfeData, eventBus }) => {
|
|
22
|
+
// Process host data
|
|
23
|
+
doSomethingWithMfeData(mfeData);
|
|
24
|
+
|
|
25
|
+
// Send message to host
|
|
26
|
+
eventBus?.emit('example-module:status', { status: 'connected', timestamp: Date.now() });
|
|
27
|
+
};
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See [HeadlessCallback](#headlesscallback) for a detailed description of `connectedCallback`s parameters.
|
|
31
|
+
|
|
32
|
+
## disconnectedCallback
|
|
33
|
+
|
|
34
|
+
If `headless.ts` exports a function named `disconnectedCallback`, the host calls it when the bundle is unmounted.
|
|
35
|
+
|
|
36
|
+
```ts title="src/headless.ts"
|
|
37
|
+
import type { HeadlessCallback } from '@servicetitan/web-components';
|
|
38
|
+
|
|
39
|
+
export const disconnectedCallback: HeadlessCallback = ({ eventBus }) => {
|
|
40
|
+
// Send message to host
|
|
41
|
+
eventBus?.emit('example-module:status', { status: 'disconnected', timestamp: Date.now() });
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See [HeadlessCallback](#headlesscallback) for a detailed description of `disconnectedCallback`s parameters.
|
|
46
|
+
|
|
47
|
+
## HeadlessCallback
|
|
48
|
+
|
|
49
|
+
`HeadlessCallback<TMfeData = any, TEventBus extends EventBus = EventBus>`
|
|
50
|
+
|
|
51
|
+
The `connectedCallback` and `disconnectedCallback` functions are passed a single argument that is an object with the following fields:
|
|
52
|
+
|
|
53
|
+
| Name | Type | Description |
|
|
54
|
+
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `ldService` | `LDService` | The [LaunchDarkly LDService instance](../../launchdarkly-service#ldservice) passed from the Host. |
|
|
56
|
+
| `logService` | `Log` | The [Log instance](../../log-service#log) passed from the Host. |
|
|
57
|
+
| `eventBus` | `TEventBus` | The [EventBus instance](../../web-components/event-bus) passed from the Host, typed as `TEventBus`. |
|
|
58
|
+
| `mfeData` | `Serializable<TMfeData>` | The [data passed from the host to `HeadlessLoader`](../../web-components/headless-loader#headless-loader-props), typed as `TMfeData`. |
|
|
59
|
+
|
|
60
|
+
## Loading
|
|
61
|
+
|
|
62
|
+
Use [HeadlessLoader](../../web-components/headless-loader) to load headless bundles.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hosting
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A host is an application, or MFE, that uses [`Loader`](../web-components/loader) to dynamically fetch and render MFEs.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
Set `cli.webpack.expose-shared-dependencies` to `true` in the application's `package.json`.
|
|
10
|
+
|
|
11
|
+
:::caution
|
|
12
|
+
`expose-shared-dependencies` only applies to applications. MFEs that load other MFEs do not need this setting or any additional configuration.
|
|
13
|
+
:::
|
|
14
|
+
|
|
15
|
+
```json title="package.json"
|
|
16
|
+
{
|
|
17
|
+
"cli": {
|
|
18
|
+
"webpack": {
|
|
19
|
+
"expose-shared-dependencies": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Design system
|
|
26
|
+
|
|
27
|
+
If the host uses `@servicetitan/design-system` remove all imports of design system styles form the code; `startup` handles that automatically. Alternatively, to customize the design system styles, move the imports to a file named `design-system.css` in the root directory of the application's source folder (the `rootDir` in `tsconfig.json`). E.g.,
|
|
28
|
+
|
|
29
|
+
```css title="design-system.css"
|
|
30
|
+
@import '@servicetitan/tokens/dist/tokens.css';
|
|
31
|
+
@import '@servicetitan/anvil-fonts/dist/css/anvil-fonts.css';
|
|
32
|
+
@import '@servicetitan/design-system/dist/system.min.css';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Loading MFEs
|
|
36
|
+
|
|
37
|
+
ServiceTitan serves MFEs from a private UNPKG instance at https://unpkg.servicetitan.com.
|
|
38
|
+
To load an MFE, pass [`Loader`](../web-components/loader) its UNPKG url. For example,
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { Loader } from '@servicetitan/web-components';
|
|
42
|
+
|
|
43
|
+
const mfeUrl = 'https://unpkg.servicetitan.com/@servicetitan/example-mfe';
|
|
44
|
+
|
|
45
|
+
export function ExampleMfe() {
|
|
46
|
+
return <Loader src={mfeUrl} />;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Using tags
|
|
51
|
+
|
|
52
|
+
In practice hosts usually append a tag of the form `@{version}`to the URL to specify which version to load. Tags can be an exact version (e.g., **@0.0.0-next.152efeb**) or an release tag like **@prod**.
|
|
53
|
+
|
|
54
|
+
In the Monolith, you can use [getValueForEnvironment](../web-components/get-value-for-environment) to determine the release tag from the host's environment,
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Loader, getValueForEnvironment } from '@servicetitan/web-components';
|
|
58
|
+
|
|
59
|
+
const baseUrl = 'https://unpkg.servicetitan.com/@servicetitan/example-mfe';
|
|
60
|
+
|
|
61
|
+
export function ExampleMfe() {
|
|
62
|
+
return <Loader src={getMfeUrl()} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getMfeUrl() {
|
|
66
|
+
return (
|
|
67
|
+
getValueForEnvironment({
|
|
68
|
+
dev: 'http://localhost:8888',
|
|
69
|
+
qa: `${baseUrl}@qa`,
|
|
70
|
+
stage: `${baseUrl}@stage`,
|
|
71
|
+
go: `${baseUrl}@prod`,
|
|
72
|
+
next: `${baseUrl}@prod`,
|
|
73
|
+
}) ?? ''
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## See Also
|
|
79
|
+
|
|
80
|
+
- [Developing](./developing) - how to create MFEs
|
|
81
|
+
- [Example host application](https://github.com/servicetitan/frontend-example/tree/master/packages/application)
|
|
82
|
+
- [Publishing](./publishing) - how to publish MFEs
|
|
83
|
+
- [Sharing Dependencies](./sharing-dependencies) - how to customize shared dependencies
|
|
84
|
+
- [Web Components](../web-components) - how to use `Loader`, `getValueForEnvironment` and other runtime APIs.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Microfrontends (MFEs)
|
|
3
|
+
sidebar_position: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Microfrontends (MFEs) break a large frontend application into independently developed, deployed, and versioned pieces so teams can move faster and reduce cross-team coupling.
|
|
7
|
+
|
|
8
|
+
:::info
|
|
9
|
+
ServiceTitan's MFE solution predates Webpack's module federation.
|
|
10
|
+
For a discussion of the pros and cons and why we continue to use ServiceTitan's solution see this [document](https://github.com/servicetitan/dispatch-docs/blob/5fd062ec6a7f292be593484a633c892ca34c8de7/architecture-decisions/0007-mfe-monolith-shared-code-and-dependencies.md).
|
|
11
|
+
:::
|
|
12
|
+
|
|
13
|
+
## Benefits
|
|
14
|
+
|
|
15
|
+
- Enable independent release cadence and ownership
|
|
16
|
+
- Let teams choose and upgrade dependencies independently
|
|
17
|
+
- Improve fault isolation and reliability at scale
|
|
18
|
+
- Allow reuse of UI pieces that are standalone
|
|
19
|
+
- Maintain performance by reusing compatible dependencies
|
|
20
|
+
|
|
21
|
+
## Restrictions
|
|
22
|
+
|
|
23
|
+
- Stores or services from the host application cannot be shared with MFEs
|
|
24
|
+
- Passing data via `window` or global variables is discouraged
|
|
25
|
+
- Use [Loader's data prop](../web-components/loader#passing-data-from-host-to-mfe) or [`EventBus`](../web-components/event-bus) to exchange data between hosts and MFEs
|
|
26
|
+
- Data must be serializable
|
|
27
|
+
- Changes must be backward compatible, because hosts and MFEs release independently
|
|
28
|
+
|
|
29
|
+
## Design
|
|
30
|
+
|
|
31
|
+
[`Startup`](../startup/) builds MFEs. It outputs two bundles, and metadata about the MFE's capabilities and dependencies. The **full** bundle is self-contained and includes all code required by the MFE. The **light** bundle contains only private dependencies that aren't shared with hosts.
|
|
32
|
+
|
|
33
|
+
Teams publish MFE's to ServiceTitan private [Verdaccio + UNPKG](/docs/frontend/verdaccio-unpkg) infrastructure.
|
|
34
|
+
|
|
35
|
+
At runtime, [`Loader`](../web-components/loader) fetches the MFE's metadata, selects either the **light** or **full** bundle depending on whether the host's dependencies are compatible, then renders the MFE inside a Shadow DOM that minimizes conflicts.
|
|
36
|
+
|
|
37
|
+
## Topics
|
|
38
|
+
|
|
39
|
+
See the following topics for how to build, deploy and use MFEs:
|
|
40
|
+
|
|
41
|
+
- [Developing](./developing/) - how to create MFEs
|
|
42
|
+
- [Hosting](./hosting) - how to load MFEs
|
|
43
|
+
- [Publishing](./publishing) - how to publish MFEs
|
|
44
|
+
- [Sharing Dependencies](./sharing-dependencies) - how to customize shared dependencies
|
|
45
|
+
- [Troubleshooting](./troubleshooting) - how to fix common issues
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Publishing
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import Tabs from '@theme/Tabs';
|
|
6
|
+
import TabItem from '@theme/TabItem';
|
|
7
|
+
|
|
8
|
+
MFEs must be published before they can be loaded in production.
|
|
9
|
+
|
|
10
|
+
:::note
|
|
11
|
+
This document describes using UNPKG + Verdaccio, the recommended way to publish MFEs. There are alternative ways of serving MFEs (e.g., from a microservice) that are not recommended and are not described in this document.
|
|
12
|
+
:::
|
|
13
|
+
|
|
14
|
+
## Publishing Locally
|
|
15
|
+
|
|
16
|
+
Before you can publish locally, run the following command to add an authorization token for Verdaccio to the local NPM configuration:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx verdaccio-okta-oauth@latest --registry https://verdaccio.servicetitan.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
[Build](../startup/build) the MFE, then to publish it run:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx startup mfe-publish
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
:::info
|
|
29
|
+
If the project contains multiple MFEs `startup` publishes all of them.
|
|
30
|
+
:::
|
|
31
|
+
|
|
32
|
+
## Publishing From CI
|
|
33
|
+
|
|
34
|
+
To publish an MFE from a Github or Teamcity workflow, use the `ST_VERDACCIO_PUBLISH_TOKEN` organization secret to authorize access to Verdaccio. For example,
|
|
35
|
+
|
|
36
|
+
<Tabs
|
|
37
|
+
defaultValue="github"
|
|
38
|
+
values={[{ label: "Github Action", value: "github"}, {label: "Dockerfile", value: "docker"}]}>
|
|
39
|
+
|
|
40
|
+
<TabItem value="github">
|
|
41
|
+
|
|
42
|
+
```yml
|
|
43
|
+
- name: Build project
|
|
44
|
+
run: npm run build
|
|
45
|
+
|
|
46
|
+
- name: Configure Verdaccio token
|
|
47
|
+
run: npm config set --location=project "//verdaccio.servicetitan.com/:_authToken"="${{ secrets.ST_VERDACCIO_PUBLISH_TOKEN }}"
|
|
48
|
+
|
|
49
|
+
- name: Publish MFE
|
|
50
|
+
run: npm run mfe-publish
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
</TabItem>
|
|
54
|
+
<TabItem value="docker">
|
|
55
|
+
|
|
56
|
+
```dockerfile
|
|
57
|
+
ARG ST_VERDACCIO_PUBLISH_TOKEN
|
|
58
|
+
|
|
59
|
+
RUN npm run build
|
|
60
|
+
RUN npm config set --location=project "//verdaccio.servicetitan.com/:_authToken=$ST_VERDACCIO_PUBLISH_TOKEN"
|
|
61
|
+
RUN npm run mfe-publish
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
</TabItem>
|
|
65
|
+
</Tabs>
|
|
66
|
+
|
|
67
|
+
## Managing Releases With Tags
|
|
68
|
+
|
|
69
|
+
Release tags like **dev**, **next**, and **prod** are a lightweight way to control which MFE bundle consumers load, without changing host code. You can use tags to promote, test, or roll back releases independently from the host application.
|
|
70
|
+
|
|
71
|
+
### Key concepts
|
|
72
|
+
|
|
73
|
+
- A release tag points to a specific published package version.
|
|
74
|
+
- Hosts pass a tag instead of an exact version to [Loader](../web-components/loader).
|
|
75
|
+
- Switching the tag to a different version changes what hosts load.
|
|
76
|
+
|
|
77
|
+
:::caution
|
|
78
|
+
When a tag is switched to a new version, it could take ServiceTitan's infrastructure up to fifteen minutes to deliver the new version to consumers.
|
|
79
|
+
:::
|
|
80
|
+
|
|
81
|
+
### Common workflows
|
|
82
|
+
|
|
83
|
+
- **Canary testing**: publish MFE as `0.0.0-develop.{hash}` and tag as **dev** for early testing.
|
|
84
|
+
- **Staging promotion**: point **next** to the version used to use in pre-production environments.
|
|
85
|
+
- **Production promotion**: point **prod** to the release that passed staging.
|
|
86
|
+
- **Rollback**: repoint a tag to the previous good version to revert consumers.
|
|
87
|
+
|
|
88
|
+
## See Also
|
|
89
|
+
|
|
90
|
+
- [Example `mfe-publish` workflow](https://github.com/servicetitan/frontend-example/blob/master/.github/workflows/mfe-publish.yml)
|
|
91
|
+
- [startup mfe-publish](../startup/mfe-publish) -- how to tag releases
|
|
92
|
+
- [Using tags](./hosting#using-tags) -- how to load MFEs with tags
|
|
93
|
+
- [Verdaccio + UNPKG](/docs/frontend/verdaccio-unpkg) -- publishing infrastructure
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sharing Dependencies
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
When `startup` builds an MFE, it creates two bundles, named **full** and **light**:[^headless]
|
|
6
|
+
|
|
7
|
+
- The **full** bundle is completely self-contained and includes all code required by the MFE.
|
|
8
|
+
- The **light** bundle is smaller and faster because it omits dependencies that already exist in the host (e.g., `react`, `@servicetitan/design-system`).
|
|
9
|
+
|
|
10
|
+
At runtime, the host loads the **light** bundle when its shared dependencies are compatible with the MFE's, else it loads the **full** bundle.
|
|
11
|
+
|
|
12
|
+
To be compatible, the host's version of a dependency must satisfy the MFE's [semver](https://semver.org) range. To allow the widest compatibility MFEs should use `^` instead of `~` for shared dependencies. For example, a host using `react` `18.3.1` will consider itself compatible with an MFEs that depends on `^18.2.0`, and incompatible with an MFE that depends on `~18.2.0`.
|
|
13
|
+
|
|
14
|
+
By default, MFEs and hosts share the following dependencies (see [here](https://github.com/search?q=repo%3Aservicetitan%2Fuikit+getDefaultSharedDependencies&type=code) for the most up-to-date list):
|
|
15
|
+
|
|
16
|
+
- @servicetitan/anvil2
|
|
17
|
+
- @servicetitan/design-system
|
|
18
|
+
- classnames
|
|
19
|
+
- formstate
|
|
20
|
+
- mobx
|
|
21
|
+
- mobx-react
|
|
22
|
+
- mobx-utils
|
|
23
|
+
- react
|
|
24
|
+
- react-dom
|
|
25
|
+
|
|
26
|
+
:::info
|
|
27
|
+
MFE's are not required to use all the default dependencies; `startup` and `Loader` ignore any that aren't used.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
To customize which dependencies are shared, set `cli.webpack.shared-dependencies` to an object that maps dependencies to `SharedDependencies.{name}`. By convention `{name}` is the camel-cased package name.
|
|
31
|
+
|
|
32
|
+
- Use the special **defaults** key with the empty string to incorporate the default list.
|
|
33
|
+
- To exclude a default dependency, map it to the empty string.
|
|
34
|
+
|
|
35
|
+
## Adding a Shared Dependency
|
|
36
|
+
|
|
37
|
+
To add a dependency, use the special **defaults** key to first include the defaults. For example, to add `lodash` as a shared dependency,
|
|
38
|
+
|
|
39
|
+
```json title="package.json"
|
|
40
|
+
{
|
|
41
|
+
"cli": {
|
|
42
|
+
"webpack": {
|
|
43
|
+
"shared-dependencies": {
|
|
44
|
+
"defaults": "", // add default list
|
|
45
|
+
"lodash": "SharedDependencies.Lodash" // add lodash
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
:::caution
|
|
53
|
+
When you add a shared dependency its code is completely omitted from the **light** bundle. The host must include and share the same dependency, else it will always use the MFE's **full** bundle.
|
|
54
|
+
:::
|
|
55
|
+
|
|
56
|
+
## Omitting a Default Dependency
|
|
57
|
+
|
|
58
|
+
:::tip
|
|
59
|
+
Because monorepos's hoist dependencies into the project's root `package.json`, if a default shared dependency exists in the root `package.json`, `startup` assumes it is used by the MFE.
|
|
60
|
+
If the MFE doesn't actually use a hoisted dependency, customize its shared dependencies to omit it.
|
|
61
|
+
:::
|
|
62
|
+
|
|
63
|
+
To omit an default dependency, map it to the empty string. For example, to omit `@servicetitan/design-system`:
|
|
64
|
+
|
|
65
|
+
```json title="package.json"
|
|
66
|
+
{
|
|
67
|
+
"cli": {
|
|
68
|
+
"webpack": {
|
|
69
|
+
"shared-dependencies": {
|
|
70
|
+
"defaults": "", // add default list
|
|
71
|
+
"@servicetitan/design-system": "" // drop @servicetitan/design-system
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Specifying All Dependencies
|
|
79
|
+
|
|
80
|
+
To completely customize the list of shared dependencies, omit the **defaults** key. For example, to only share Anvil2 and React,
|
|
81
|
+
|
|
82
|
+
```json title="package.json"
|
|
83
|
+
{
|
|
84
|
+
"cli": {
|
|
85
|
+
"webpack": {
|
|
86
|
+
"shared-dependencies": {
|
|
87
|
+
"@servicetitan/anvil2": "SharedDependencies.ServiceTitan.Anvil2",
|
|
88
|
+
"react": "SharedDependencies.React",
|
|
89
|
+
"react-dom": "SharedDependencies.ReactDOM"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
[^headless]: `startup` can also created **headless** bundles for preloading configuration or setup information. Headless bundles never share dependencies with hosts. See [Headless Bundle](./developing/headless-bundle) for more information.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Troubleshooting
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Styled-components
|
|
6
|
+
|
|
7
|
+
We advise against using `styled-components` in MFEs. If you do, you may encounter an issue where styles are not applied because they are not injected properly.
|
|
8
|
+
To fix, use `StyleSheetManager` to inject the styles into the MFE's ShadowDOM. For example,
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { useMFEMetadataContext } from '@servicetitan/web-components';
|
|
12
|
+
import styled, { StyleSheetManager } from 'styled-components';
|
|
13
|
+
|
|
14
|
+
const Title = styled.h1`
|
|
15
|
+
color: #bf4f74;
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export const MFETitle: FC = () => {
|
|
19
|
+
const { shadowRoot } = useMFEMetadataContext();
|
|
20
|
+
return (
|
|
21
|
+
<StyleSheetManager target={shadowRoot}>
|
|
22
|
+
<Title>MFE Title</Title>
|
|
23
|
+
</StyleSheetManager>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For styled components inside portalled elements (e.g., Popover), use `StyleSheetManager` to inject the styles into the MFE's portal ShadowDOM. For example,
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { Popover } from '@servicetitan/anvil2';
|
|
32
|
+
import { useMFEMetadataContext } from '@servicetitan/web-components';
|
|
33
|
+
import styled, { StyleSheetManager } from 'styled-components';
|
|
34
|
+
|
|
35
|
+
const PopoverTitle = styled.h2`
|
|
36
|
+
color: #bf4f75;
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
export const MFEPopover: FC = () => {
|
|
40
|
+
const { portalShadowRoot } = useMFEMetadataContext();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Popover>
|
|
44
|
+
<Popover.Button>Click me!</Popover.Button>
|
|
45
|
+
<Popover.Content>
|
|
46
|
+
<StyleSheetManager target={portalShadowRoot}>
|
|
47
|
+
<PopoverTitle>Title</PopoverTitle>
|
|
48
|
+
</StyleSheetManager>
|
|
49
|
+
</Popover.Content>
|
|
50
|
+
</Popover>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See [useMFEMetadataContext](../web-components/use-mfe-metadata-context) for more information about accessing an MFE's ShadowDOM elements.
|