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

feat: Add support for deferred routes registration #100

Merged
merged 22 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A federated web application shell built on top of [Module Federation](https://we
| [@squide/core](packages/core/README.md) | [![npm version](https://img.shields.io/npm/v/@squide/core)](https://www.npmjs.com/package/@squide/core) |
| [@squide/react-router](packages/react-router/README.md) | [![npm version](https://img.shields.io/npm/v/@squide/react-router)](https://www.npmjs.com/package/@squide/react-router) |
| [@squide/webpack-module-federation](packages/webpack-module-federation/README.md) | [![npm version](https://img.shields.io/npm/v/@squide/webpack-module-federation)](https://www.npmjs.com/package/@squide/webpack-module-federation) |
| [@squide/msw](packages/msw/README.md) | [![npm version](https://img.shields.io/npm/v/@squide/msw)](https://www.npmjs.com/package/@squide/msw) |
| [@squide/fakes](packages/fakes/README.md) | [![npm version](https://img.shields.io/npm/v/@squide/fakes)](https://www.npmjs.com/package/@squide/fakes) |

## Have a question or found an issue?
Expand Down
6 changes: 3 additions & 3 deletions docs/getting-started/create-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const context: AppContext = {
};

// Register the remote module.
registerRemoteModules(Remotes, runtime, { context });
await registerRemoteModules(Remotes, runtime, { context });

const root = createRoot(document.getElementById("root")!);

Expand Down Expand Up @@ -290,10 +290,10 @@ const context: AppContext = {
};

// Register the newly created local module.
registerLocalModules([registerHost], runtime, { context });
await registerLocalModules([registerHost], runtime, { context });

// Register the remote module.
registerRemoteModules(Remotes, runtime, { context });
await registerRemoteModules(Remotes, runtime, { context });

const root = createRoot(document.getElementById("root")!);

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/create-local-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const context: AppContext = {
};

// Register the remote module.
registerRemoteModules(Remotes, runtime, context);
await registerRemoteModules(Remotes, runtime, context);

// Register the local module.
registerLocalModule([registerLocalModule], runtime, context);
Expand Down
28 changes: 14 additions & 14 deletions docs/getting-started/learn-the-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ const runtime = new Runtime({

Then, [register the modules MSW request handlers](../reference/msw/MswPlugin.md#register-request-handlers) at registration:

```ts !#12 remote-module/src/register.tsx
```ts !#10,12 remote-module/src/register.tsx
import { getMswPlugin } from "@squide/msw";
import type { ModuleRegisterFunction, Runtime } from "@squide/react-router";

Expand All @@ -196,23 +196,23 @@ Don't forget to mark the registration function as `async` since there's a dynami

Then, [retrieve the modules MSW request handlers](../reference/msw/MswPlugin.md#retrieve-the-request-handlers) in the host application and start MSW:

```ts !#10,13
```ts !#9,12
import { registerRemoteModules } from "@squide/webpack-module-federation";
import { setMswAsStarted } from "@squide/msw";

registerRemoteModules(Remotes, runtime).then(() => {
if (process.env.USE_MSW) {
// Files including an import to the "msw" package are included dynamically to prevent adding
// MSW stuff to the bundled when it's not used.
import("../mocks/browser.ts").then(({ startMsw }) => {
// Will start MSW with the request handlers provided by every module.
startMsw(mswPlugin.requestHandlers);
await registerRemoteModules(Remotes, runtime);

// Indicate to resources that are dependent on MSW that the service has been started.
setMswAsStarted();
});
}
});
if (process.env.USE_MSW) {
// Files including an import to the "msw" package are included dynamically to prevent adding
// MSW stuff to the bundled when it's not used.
const startMsw = (await import("../mocks/browser.ts")).startMsw;

// Will start MSW with the request handlers provided by every module.
startMsw(mswPlugin.requestHandlers);

// Indicate to resources that are dependent on MSW that the service has been started.
setMswAsStarted();
}
```

Finally, make sure that the [application rendering is delayed](../reference/msw/useIsMswReady.md) until MSW is started:
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/add-a-shared-dependency.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 30
order: 10
label: Add a shared dependency
---

Expand Down Expand Up @@ -57,7 +57,7 @@ If the version difference between a host application and a remote module is a **

Libraries matching the following criterias are strong candidates to be configured as shared dependencies:

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

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/add-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export const registerHost: ModuleRegisterFunction<Runtime> = runtime => {
children: [
{
// The root error boundary is a named route to be able to nest
// the loging / logout page under it with the "parentName" optioné
// the loging / logout page under it with the "parentName" option.
$name: "root-error-boundary",
errorElement: <RootErrorBoundary />,
children: [
Expand Down
18 changes: 9 additions & 9 deletions docs/guides/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ expanded: true

# Guides

- [Override the host layout](override-the-host-layout.md)
- [Isolate module failures](isolate-module-failures.md)
- [Add authentication](add-authentication.md)
- [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)
- [Add a shared dependency](add-a-shared-dependency.md)
- [Override a React context](override-a-react-context.md)
- [Migrating from a monolithic application](migrating-from-a-monolith.md)
- [Override the host layout](./override-the-host-layout.md)
- [Isolate module failures](./isolate-module-failures.md)
- [Add authentication](./add-authentication.md)
- [Develop a module in isolation](./develop-a-module-in-isolation.md)
- [Federated tabs](./federated-tabs.md)
- [Override a React context](./override-a-react-context.md)
- [Implement a custom logger](./implement-a-custom-logger.md)
- [Add a shared dependency](./add-a-shared-dependency.md)
- [Migrating from a monolithic application](./migrating-from-a-monolith.md)
8 changes: 4 additions & 4 deletions docs/guides/develop-a-module-in-isolation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 50
order: 60
---

# Develop a module in isolation
Expand Down Expand Up @@ -149,9 +149,9 @@ const context: AppContext = {
};

// Register the shell module.
registerLocalModules([registerShell, registerHost], runtime, { context });
await registerLocalModules([registerShell, registerHost], runtime, { context });

registerRemoteModules(Remotes, runtime, { context });
await registerRemoteModules(Remotes, runtime, { context });

const root = createRoot(document.getElementById("root")!);

Expand Down Expand Up @@ -214,7 +214,7 @@ const runtime = new Runtime({

// Registering the remote module as a static module because the "register" function
// is local when developing in isolation.
registerLocalModules([registerModule, registerDev, registerShell], runtime);
await registerLocalModules([registerModule, registerDev, registerShell], runtime);

const root = createRoot(document.getElementById("root")!);

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/federated-tabs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 40
order: 50
---

# Federated tabs
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/implement-a-custom-logger.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 60
order: 20
---

# Implement a custom logger
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/migrating-from-a-monolith.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 10
order: 0
label: Migrating from a monolith
---

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/override-a-react-context.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 20
order: 30
label: Override a React context
---

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/override-the-host-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function RootLayout() {
}
```

In the previous code sample, the `RootLayout` serves as the default layout for the homepage as well as for every page (route) registered by a module that are not nested with either the [parentPath](../reference/runtime/runtime-class.md#register-nested-routes-under-an-existing-route) or the [parentName](../reference/runtime/runtime-class.md#register-a-named-route) option.
In the previous code sample, the `RootLayout` serves as the default layout for the homepage as well as for every page (route) registered by a module that are not nested under a parent route with either the [parentPath](../reference/runtime/runtime-class.md#register-nested-routes-under-an-existing-route) or the [parentName](../reference/runtime/runtime-class.md#register-a-named-route) option.

For most pages, this is the behavior expected by the author. However, for pages such as a login, the default `RootLayout` isn't suitable because the page is not bound to a user session (the user is not even authenticated yet).

Expand Down
5 changes: 4 additions & 1 deletion docs/reference/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ expanded: true

- [registerLocalModules](registration/registerLocalModules.md)
- [registerRemoteModules](registration/registerRemoteModules.md)
- [registrationStatus](registration/registrationStatus.md)
- [completeModuleRegistrations](registration/completeModuleRegistrations.md)
- [completeLocalModuleRegistrations](registration/completeLocalModuleRegistrations.md)
- [completeRemoteModuleRegistrations](registration/completeRemoteModuleRegistrations.md)
- [useAreModulesRegistered](registration/useAreModulesRegistered.md)
- [useAreModulesReady](registration/useAreModulesReady.md)

### Routing
Expand Down
8 changes: 5 additions & 3 deletions docs/reference/msw/MswPlugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ None

## Usage

!!!info
Do not include MSW in production code. To address this, we recommend conditionally importing the code that includes the [msw](https://www.npmjs.com/package/msw) package based on an environment variable.
!!! warning
Don't forget to [activate the msw feature](../webpack/defineDevHostConfig.md#activate-optional-features) on the host application as well as every remote module.
!!!

Do not include MSW in production code. To address this, we recommend conditionally importing the code that includes the [msw](https://www.npmjs.com/package/msw) package based on an environment variable.

To do so, first use [cross-env](https://www.npmjs.com/package/cross-env) to define a `USE_MSW` environment variable in a PNPM script:

```json package.json
Expand Down Expand Up @@ -93,7 +95,7 @@ const runtime = new Runtime({

### Register request handlers

```ts !#3,10
```ts !#3,8,10
import { getMswPlugin } from "@squide/msw";

if (process.env.USE_MSW) {
Expand Down
116 changes: 116 additions & 0 deletions docs/reference/registration/completeLocalModuleRegistrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
toc:
depth: 2-3
order: 70
---

# completeLocalModuleRegistrations

Completes the registration process for modules that have been registred using [registerLocalModules](./registerLocalModules.md) by executing the registered **deferred registration** functions.

!!!info
This function should only be used by applications that support [deferred registrations](./registerLocalModules.md#defer-the-registration-of-routes-or-navigation-items).
!!!

## Reference

```ts
completeLocalModuleRegistrations(runtime, data?)
```

### Parameters

- `runtime`: A `Runtime` instance.
- `data`: An optional object with data to forward to the deferred registration functions.

### Returns

A `Promise` object with an array of `LocalModuleRegistrationError` if any error happens during the completion of the local modules registration process.

- `LocalModuleRegistrationError`:
- `error`: The original error object.

## Usage

### Complete local module registrations

```tsx !#15,18 host/src/bootstrap.tsx
import { completeLocalModuleRegistrations, registerLocalModules, Runtime } from "@squide/react-router";
import { register } from "@sample/local-module";
import { fetchFeatureFlags, type AppContext } from "@sample/shared";

const runtime = new Runtime();

const context: AppContext = {
name: "Test app"
};

await registerLocalModules([register], runtime, { context });

// Don't fetch data in the bootstrapping code for a real application. This is done here
// strictly for demonstration purpose.
const featureFlags = await fetchFeatureFlags();

// Complete the local module registrations with the feature flags data.
await completeLocalModuleRegistrations(runtime, { featureFlags });
```

```tsx !#19-32 local-module/src/register.tsx
import type { ModuleRegisterFunction, Runtime } from "@squide/react-router";
import type { AppContext, FeatureFlags } from "@sample/shared";
import { AboutPage } from "./AboutPage.tsx";
import { FeatureAPage } from "./FeatureAPage.tsx";

export function register: ModuleRegisterFunction<Runtime, AppContext>(runtime, context) {
runtime.registerRoute({
path: "/about",
element: <AboutPage />
});

runtime.registerNavigationItem({
$label: "About",
to: "/about"
});

// Once the feature flags has been loaded by the host application, by completing the module registrations process,
// the deferred registration function will be called with the feature flags data.
return ({ featureFlags }: { featureFlags: FeatureFlags }) => {
// Only register the "feature-a" route and navigation item if the feature is active.
if (featureFlags.featureA) {
runtime.registerRoute({
path: "/feature-a",
element: <FeatureAPage />
});

runtime.registerNavigationItem({
$label: "Feature A",
to: "/feature-a"
});
}
};
}
```

### Handle the completion errors

```tsx !#17-19 host/src/bootstrap.tsx
import { completeLocalModuleRegistrations, registerLocalModules, Runtime } from "@squide/react-router";
import { register } from "@sample/local-module";
import { fetchFeatureFlags, type AppContext } from "@sample/shared";

const runtime = new Runtime();

const context: AppContext = {
name: "Test app"
};

await registerLocalModules([register], runtime, { context });

// Don't fetch data in the bootstrapping code for a real application. This is done here
// strictly for demonstration purpose.
const featureFlags = await fetchFeatureFlags();

await (completeLocalModuleRegistrations(runtime, { featureFlags })).forEach(x => {
console.log(x);
});
```
Loading