Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: added a shared dependency guide #86

Merged
merged 11 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/getting-started/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ With this mecanism in place, all federated parts of an application can now be lo

By sharing the same browsing context (e.g. the same [Document object](https://developer.mozilla.org/en-US/docs/Web/API/Document), the same [Window object](https://developer.mozilla.org/en-US/docs/Web/API/Window), and the same DOM), federated parts now **form a unified and cohesive single application**, addressing the second challenge.

With Module Federation, we believe that we can develop federated applications that provide the same user experience as monolithic applications :rocket:
With Module Federation, we hope to develop federated applications that provide the same user experience as monolithic applications :rocket:.

### React Router

React Router [nested routes](https://reactrouter.com/en/main/start/tutorial#nested-routes) feature is ideal for federated applications as it enables highly **composable** and **decoupled** UI. Besides, what else would you use? :joy:
React Router [nested routes](https://reactrouter.com/en/main/start/tutorial#nested-routes) feature is ideal for federated applications as it enables highly **composable** and **decoupled** UI. For a more in-depth explanation, refer to this [article](https://www.infoxicator.com/why-react-router-is-excellent-for-micro-frontends).

## Module registration

Expand Down
2 changes: 2 additions & 0 deletions docs/getting-started/learn-the-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,7 @@ const service = useService("user-service") as UserService;

The services are also available from the [Runtime](/reference/runtime/runtime-class.md) instance.

## Fakes

For development purposes, have a look at the available [fake implementations](../reference/default.md#fakes).

155 changes: 155 additions & 0 deletions docs/guides/add-a-shared-dependency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
order: 30
label: Add a shared dependency
---

# Add a shared dependency

[Shared dependencies](https://webpack.js.org/plugins/module-federation-plugin/#sharing-libraries) represent one of the most powerful concepts within [Module Federation](https://webpack.js.org/plugins/module-federation-plugin). However, mastering this aspect can be quite challenging. **Failure** to configure shared dependencies properly in a federated application using Module Federation can significantly **impact** both **user** and **developer experiences**.

`@squide` aims to simplify the configuration of shared dependencies by abstracting the [fundamental shared dependencies](#default-shared-dependencies) necessary for building an application with React and React Router. Nevertheless, every federated application will inevitably require the configuration of additional custom shared dependencies.

For a more comprehensive documentation of the Module Federation APIs, their functionality, and their benefits, please refer to this [article](https://www.infoxicator.com/en/module-federation-shared-api).

## Default shared dependencies

Since `@squide` has dependencies on React and React Router, the [define*](../reference/default.md#webpack) functions automatically configure shared dependencies for these packages by default, in addition to `@squide` own packages. The following shared dependencies are set as [singleton](#singleton-dependency) by default:

- [react](https://www.npmjs.com/package/react)
- [react-dom](https://www.npmjs.com/package/react-dom)
- [react-router-dom](https://www.npmjs.com/package/react-router-dom)
- [@squide/core](https://www.npmjs.com/package/@squide/core)
- [@squide/react-router](https://www.npmjs.com/package/@squide/react-router)
- [@squide/webpack-module-federation](https://www.npmjs.com/package/@squide/webpack-module-federation)

For the full shared dependencies configuration, have a look at the [defineConfig.ts](https://github.com/gsoft-inc/wl-squide/blob/main/packages/webpack-module-federation/src/defineConfig.ts) file on Github.

> You can [extend](../reference/webpack/defineDevHostConfig.md#extend-a-default-shared-dependency) or [override](../reference/webpack/defineDevHostConfig.md#override-a-default-shared-dependency) the default shared dependencies configuration.

## What should be configured as a shared dependency?

Candidates for shared dependencies:

- Medium to large libraries that are used by multiple modules..
- Libraries that requires a [single instance](#react--react-dom) to work properly (like `react`).
- Libraries exporting [React contexts](#react-context-limitations).

## Adding shared dependencies

To configure shared dependencies, use the `sharedDependencies` option of any [define*](../reference/default.md#webpack) function:

```js !#7-11 host/webpack.dev.js
// @ts-check

import { defineDevHostConfig } from "@squide/webpack-module-federation/defineConfig.js";
import { swcConfig } from "./swc.dev.js";

export default defineDevHostConfig(swcConfig, "host", 8080, {
sharedDependencies: {
"@sample/shared": {
singleton: true
}
}
});
```

When a dependency is shared between a host application and a remote module, the sharing options must be **configured on both ends**. Let's also set the `@sample/shared` as a `singleton` for the remote module:
patricklafrance marked this conversation as resolved.
Show resolved Hide resolved

```js !#7-11 remote-module/webpack.dev.js
// @ts-check

import { defineDevRemoteModuleConfig } from "@squide/webpack-module-federation/defineConfig.js";
import { swcConfig } from "./swc.dev.js";

export default defineDevRemoteModuleConfig(swcConfig, "remote1", 8081, {
sharedDependencies: {
"@sample/shared": {
singleton: true
}
}
});
```

## Singleton dependency

A [singleton](https://webpack.js.org/plugins/module-federation-plugin/#singleton) shared dependency does exactly what its name suggests: it loads only a single instance of the dependency. This means that the dependency will be included in just one bundle file of the federated application.

### `strictVersion`

Sometimes, a `singleton` shared dependency is paired with the [strictVersion](https://webpack.js.org/plugins/module-federation-plugin/#strictversion) option:

```js !#10 webpack.config.js
// @ts-check

import { defineDevHostConfig } from "@squide/webpack-module-federation/defineConfig.js";
import { swcConfig } from "./swc.dev.js";

export default defineDevHostConfig(swcConfig, "host", 8080, {
sharedDependencies: {
"@sample/shared": {
singleton: true,
strictVersion: "1.2.1"
}
}
});
```

"When specified, the `strictVersion` option will generate a runtime error if a module attempts to load a version of the dependency that is incompatible with the specified version. It's often unnecessary to use a strict version, and omitting it provides greater flexibility when it comes time to update the shared dependency version."

### Expected behaviors

#### Minor or patch version

When the version difference between a host application and a remote module is a **minor** or **patch** version, the higher version of the dependency will be loaded. For example:

- The host application is on `10.1.0` and a remote module is on `10.3.1` -> `10.3.1` will be loaded
- The host application is on `10.3.1` and a remote module is on `10.1.0` -> `10.3.1` will be loaded

#### Major version

If the version difference between a host application and a remote module is a **major** version, once again, the higher version of the dependency will be loaded. However, a warning will also be issued. For example:

- The host application is on `11.0.0` and a remote module is on `10.3.1` -> `11.0.0` will be loaded
- The host application is on `10.3.1` and a remote module is on `11.0.0` -> `11.0.0` will be loaded

## Eager dependency

An [eager](https://webpack.js.org/plugins/module-federation-plugin/#eager) shared dependency becomes available as soon as the host application starts. In simple terms, it is included in the host application bundle rather than being loaded lazily when it is first requested."

```js !#10 webpack.config.js
// @ts-check

import { defineDevHostConfig } from "@squide/webpack-module-federation/defineConfig.js";
import { swcConfig } from "./swc.dev.js";

export default defineDevHostConfig(swcConfig, "host", 8080, {
sharedDependencies: {
"@sample/shared": {
singleton: true,
eager: true
}
}
});
```

All the shared dependencies that are configured by [default](#default-shared-dependencies) for a `@squide` federated application are set as `eager` shared dependencies.

The key point to remember about `eager` dependencies is that only one application or remote module should configure a shared dependency as eager. Otherwise, the dependency will be included in the bundler of every application or remote module that set the dependency as `eager`.

## React context limitations

For a React context to be provided by the host application and consumed by the remodules, the library exporting the React context must be set as a `singleton`.
patricklafrance marked this conversation as resolved.
Show resolved Hide resolved

To troubleshoot a React context issue or gor find more information, refer to the [troubleshooting](../troubleshooting.md#react-context-values-are-undefined) page.
patricklafrance marked this conversation as resolved.
Show resolved Hide resolved

## `react` & `react-dom`

`react` and `react-dom` dependencies must be configured as a `singleton`, otherwise either an error will be thrown at bootstrapping if the loaded `react` versions are incompatible, or features like `useState` will not work.

## `react-router-dom`

The `react-router-dom` dependency must be configured as a `singleton` because it relies on a global React context that needs to be declared in the host application and is consumed by remote modules.

## Learn more

To learn more about Module Federation shared dependencies read this [article](https://www.infoxicator.com/en/module-federation-shared-api) about the shared APIs and refer to this [POC](https://github.com/patricklafrance/wmf-versioning) on GitHub.
3 changes: 2 additions & 1 deletion docs/guides/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ expanded: true
- [Implement a custom logger](implement-a-custom-logger.md)
- [Develop a module in isolation](develop-a-module-in-isolation.md)
- [Federated tabs](federated-tabs.md)
- [Overriding a React context for a specific remote module](overriding-a-react-context.md)
- [Add a shared dependency](add-a-shared-dependency.md)
- [Override a React context](overriding-a-react-context.md)
- [Migrating from a monolithic application](migrating-from-a-monolith.md)
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
order: 30
label: Overriding a React context
order: 20
label: Override a React context
---

# Overriding a React context
# Override a React context

In a federated application using [Module Federation](https://webpack.js.org/concepts/module-federation/), it's typical to configure various global [React contexts](https://legacy.reactjs.org/docs/context.html) at the root of the host application. These contexts are usually consumed down the line by the layouts and pages of the remote modules.

Expand Down Expand Up @@ -67,7 +67,7 @@ In the previous code samples, the host application provides a value for the `Bac

## Override the context for the remote module

Now, suppose the requirements change, and the remote module's pages need to have a `red` background. The context can be overriden for the remote module by declaring a new provider directly in the routes registration:
Now, suppose the requirements change, and one remote module's pages need to have a `red` background. The context can be overriden for the remote module by declaring a new provider directly in the routes registration:

```tsx !#10-12 remote-module/src/register.tsx
import type { ModuleRegisterFunction, Runtime } from "@squide/react-router";
Expand All @@ -90,7 +90,7 @@ export const register: ModuleRegisterFunction<Runtime> = runtime => {

## Extract an utility function

If there was multiple routes to setup with the new provider, an utility function could also be extracted:
Since there are multiple routes to setup with the new provider, an utility function can be extracted:

```tsx !#6-12,18 remote-module/src/register.tsx
import type { ModuleRegisterFunction, Runtime } from "@squide/react-router";
Expand Down
1 change: 1 addition & 0 deletions docs/reference/fakes/index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
order: 30
1 change: 1 addition & 0 deletions docs/reference/webpack/index.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
order: 30
label: "webpack"
1 change: 1 addition & 0 deletions docs/samples.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ icon: command-palette

- :icon-mark-github: [Host application](https://github.com/gsoft-inc/wl-squide/tree/main/sample/host)
- :icon-mark-github: [Remote module](https://github.com/gsoft-inc/wl-squide/tree/main/sample/remote-module)
- :icon-mark-github: [Another remote module](https://github.com/gsoft-inc/wl-squide/tree/main/sample/another-remote-module)
- :icon-mark-github: [Local module](https://github.com/gsoft-inc/wl-squide/tree/main/sample/local-module)
- :icon-mark-github: [Shared application shell](https://github.com/gsoft-inc/wl-squide/tree/main/sample/shell)
- :icon-mark-github: [Shared library](https://github.com/gsoft-inc/wl-squide/tree/main/sample/shared)
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ icon: question

## React context values are undefined

If you are encountering undefined values when sharing React context between the host application and its modules, it is likely due to two possible reasons: either you have two instances of React, or you have multiple instances of that specific React context.
If you are encountering undefined values when providing a React context from the host application and consuming the context in modules, it is likely due to two possible reasons: either you have two instances of React, or you have multiple instances of that React context.

To resolve this issue:

Expand All @@ -19,4 +19,4 @@ To resolve this issue:

If the issue persists, update your host application and remote module's webpack build configuration with the `optimize: false` option. Afterward, build the bundles and serve them. Open a web browser, access the DevTools, switch to the Network tab (ensure that JS files are listed), navigate to the application's homepage, and inspect the downloaded bundle files. The problematic React context definition should appear only once; otherwise, you may have multiple instances of the React context.

For additional information on shared dependency versioning, please refer to: https://github.com/patricklafrance/wmf-versioning."
For additional information on shared dependency versioning, please refer to: https://github.com/patricklafrance/wmf-versioning.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
"devDependencies": {
"@changesets/changelog-github": "0.4.8",
"@changesets/cli": "2.26.2",
"@typescript-eslint/parser": "6.7.0",
"@typescript-eslint/parser": "6.7.2",
"@workleap/eslint-plugin": "2.1.1",
"@workleap/typescript-configs": "3.0.2",
"cross-env": "7.0.3",
"eslint": "8.49.0",
"jest": "29.7.0",
"netlify-cli": "16.3.5",
"netlify-cli": "16.4.1",
"retypeapp": "3.5.0",
"ts-node": "10.9.1",
"typescript": "5.2.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"react-dom": "*"
},
"devDependencies": {
"@types/react": "18.2.21",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@workleap/eslint-plugin": "2.1.1",
"@workleap/tsup-configs": "3.0.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/react-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@
"react-router-dom": "*"
},
"devDependencies": {
"@swc/core": "1.3.85",
"@swc/core": "1.3.86",
"@swc/helpers": "0.5.2",
"@swc/jest": "0.2.29",
"@testing-library/react": "14.0.0",
"@types/jest": "29.5.5",
"@types/react": "18.2.21",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@types/react-test-renderer": "18.0.1",
"@types/react-test-renderer": "18.0.2",
"@workleap/eslint-plugin": "2.1.1",
"@workleap/swc-configs": "2.1.2",
"@workleap/tsup-configs": "3.0.1",
Expand Down
8 changes: 4 additions & 4 deletions packages/webpack-module-federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@
"webpack": ">=5.0.0"
},
"devDependencies": {
"@swc/core": "1.3.85",
"@swc/core": "1.3.86",
"@swc/helpers": "0.5.2",
"@swc/jest": "0.2.29",
"@types/jest": "29.5.5",
"@types/node": "20.6.2",
"@types/react": "18.2.21",
"@types/node": "20.6.3",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@workleap/eslint-plugin": "2.1.1",
"@workleap/swc-configs": "2.1.2",
"@workleap/tsup-configs": "3.0.1",
"@workleap/typescript-configs": "3.0.2",
"@workleap/webpack-configs": "1.0.7",
"@workleap/webpack-configs": "1.0.8",
"jest": "29.7.0",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
Loading
Loading