# bluebell

<div align="center">

English | [简体中文](./README_CN.md)

</div>

---

A high concurrency [micro-frontend](https://micro-frontends.org/) framework inspired by and compatible with [single-spa](https://single-spa.js.org/).

## features

-   [x] Compatible with single-spa
-   [x] Support multiple instances
-   [x] Support router & manual modes
-   [x] Support safe destroying
-   [x] Support safe nested
-   [x] Support async navigation cancelation
-   [x] Fast reaction in concurrency
-   [x] Multiple types of application entry
-   [x] Support built-in ESM loading
-   [x] Support unified resources management
-   [x] Support more chances for retry
-   [x] Support keep alive
-   [x] Support preload(intelligent optional)
-   [x] Support dead loop detect
-   [ ] Support sandbox
-   [ ] Support shadow DOM
-   [ ] Support iframe

## usage

### router mode

```ts
const { RouterContainer } from 'bluebell';

const container = new RouterContainer({
    name: 'top',
    root: '#app',
    fallbackUrl: '/foo',
    fallbackOnlyWhen: loc => loc.pathname === '/',
    cancelActivateApp: (app) => Promise.resolve(false),
});

const [fooApp, barApp, bazApp] = container.registerApps([
    {
        // use entry
        name: 'foo',
        entry: '/foo-config/assets.json',
        activeWhen: '/foo',
    },
    {
        // use assetsMap
        name: 'bar',
        entry: '/foo-config/assets.json',
        assetsMap: { module: 'esm', initial: {}, async: {} },
        activeWhen: (location: Location) => location.pathname.startsWith('/foo'),
    },
    {
        // use lifecycle
        name: 'baz',
        lifecycle: {
            mount: async () => {},
            unmount: async () => {},
            bootstrap: async () => {},
        },
        activeWhen: ['/baz', '/bas'],
    }
]);

container.on('appactivating', ({ appname }) => {});
container.on('appactivated', ({ appname }) => {});
container.on('appactivateerror', ({ appname, error }) => {});
container.on('noappactivated', ({ error }) => {}); // No emitted if fallback.

container.run();

await container.destroy();
```

> 💡 Once a RouterContainer starts running, a popstate event will be dispatched after `pushState` or `replaceState` called, which changes the default behavior of browser.

### manual mode

```ts
const { ManualContainer } from 'bluebell';

const container = new ManualContainer({
    name: 'top',
    root: '#app',
});

const [fooApp, barApp, bazApp] = container.registerApps([
    {
        // use entry
        name: 'foo',
        entry: '/foo-config/assets.json',
    },
    {
        // use assetsMap
        name: 'bar',
        entry: '/foo-config/assets.json',
        assetsMap: { module: 'esm', initial: {}, async: {} },
    },
    {
        // use lifecycle
        name: 'baz',
        lifecycle: {
            mount: async () => {},
            unmount: async () => {},
            bootstrap: async () => {},
        },
    }
]);

container.on('appactivating', ({ appname }) => {});
container.on('appactivated', ({ appname }) => {});
container.on('appactivateerror', ({ appname, error }) => {});
container.on('noappactivated', ({ error }) => {});

await container.activateApp('foo');

await container.destroy();
```

### events

```ts
fooApp.on('beforestart', () => {});
fooApp.on('afterstart', () => {});
fooApp.on('starterror', ({ error: AppError; prevState: AppState }) => {});
fooApp.on('beforestop', () => {});
fooApp.on('afterstop', () => {});
fooApp.on('stoperror', ({ error: AppError; prevState: AppState }) => {});
fooApp.on('beforeupdate', () => {});
fooApp.on('afterupdate', () => {});
fooApp.on('updateerror', ({ error: AppError; prevState: AppState }) => {});
fooApp.on('beforeunload', () => {});
fooApp.on('afterunload', () => {});

fooApp.lifecycle.on('beforeload'), () => {});
fooApp.lifecycle.on('afterload'), () => {});
fooApp.lifecycle.on('loaderror'), ({ error: AppError }) => {});
fooApp.lifecycle.on('beforebootstrap'), () => {});
fooApp.lifecycle.on('afterbootstrap'), () => {});
fooApp.lifecycle.on('bootstraperror'), ({ error: AppError }) => {});
fooApp.lifecycle.on('beforemount'), () => {});
fooApp.lifecycle.on('aftermount'), () => {});
fooApp.lifecycle.on('mountinterrupted'), ({ error: AppError }) => {});
fooApp.lifecycle.on('mounterror'), ({ error: AppError }) => {});
fooApp.lifecycle.on('beforeunmount'), () => {});
fooApp.lifecycle.on('afterunmount'), () => {});
fooApp.lifecycle.on('unmounterror'), ({ error: AppError }) => {});
fooApp.lifecycle.on('beforeupdate'), () => {});
fooApp.lifecycle.on('afterupdate'), () => {});
fooApp.lifecycle.on('updateinterrupted'), ({ error: AppError }) => {});
fooApp.lifecycle.on('updateerror'), ({ error: AppError}) => {});
```

### strict vs loose

```ts
container.registerApps([
    {
        name: 'foo',
        strict: true,
        // HTML entry
        entry: 'http://localhost:9000',
    },
]);
```

In loose mode, app tries to load CSS and JavaScript by creating HTML tags, otherwise by loading and evalating source code.

App is in loose mode by default.

## contributors

-   <yanni4night@gmail.com>
