# Dynamic Components

This directory contains a collection of components which can be dynamically loaded.
The main advantage to this is that it allows updates to be pushed to the dynamic components
without the consuming applications needing to be re-deployed - though, of course, care
should always be taken to validate all consuming applications before deploying.

# Structure

* `components/` - Actual component definitions. These are stock-standard Vue components.
* `exports/` - The files to export to the outside world. Each of these will become
  a standalone, dynamically-importable bundle.
* `wrappers/` - Contains wrappers for each dynamic export. These are what will actually
  be imported by the consuming application and handle the loading of the dynamic bundles.
* `util/` - Shared utility functions

# How does it work?

In our vue.config.js, we load in a custom webpack plugin from 
`__build-utils__/DynamicExportsBuilderPlugin.js`. This plugin creates a child build of
the main webpack build. This child build lists all files in `exports/` and runs them
through a special version of the build process.

For each file in `exports/`, this generates an entrypoint in `dist/exports/` with the same name.
This export pulls all 'externalized' dependencies from specially-named global variables and
exposes the exports of the file on a global variable named something like
`__ns_account_components__exports__{file name}__`, as defined in `util/exported-names.js`.
Other than those externalized dependencies, each of these bundles is self-contained.

## Consuming the bundles

Each component has an entry in `wrappers/`. These wrap the dynamically-imported Vue components 
and will be bundled with the consuming application. The work because, when you register a Vue component,
if that 'component' is actually just a function which returns a promise, Vue will invoke that function and
resolve the promise the first time the component is mounted 
(See [Vue Async Components](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).

So, to consume the Invite Link Builder dynamic component, you can just do this:

```html
<template>
  <div>
    <invite-link-builder />
  </div>
</template>
<script>
import { InviteLinkBuilder } from '@nuskin/account-components';

export {
  components: {
    InviteLinkBuilder
  }
}
</script>
```

This will actually import the file in `wrappers/invite-link-builder.js` and will lazily load the
component bundle and render the actual invite link builder component.

## Externalized Dependencies

The following dependencies are externalized. These are defined in `util/external-dependencies.js`.

* `@nuskin/ns-util` as `__ns_account_components__deps___nuskin_ns_util__`
* `vue` as `__ns_account_components__deps__vue__`
* `vuetify` as `__ns_account_components__deps__vuetify__`

When a wrapper component is loaded, it will import the dependency and put it into the specified global
variable, where the dynamic component bundle can grab it.

## Build Process

In order to have a seamless build process and integration with Storybook, Vue-CLI, etc, we had to 
build a custom Webpack plugin. This is because the other tools insist on being the masters of their
Webpack build processes, which means we can't use a number of webpack features. This plugin, in
`__build-utils__/DynamicExportsBuilderPlugin.js`, has to do some low-level work to make it so that
we can produce two separate sets of outputs: those for the main build, which includes the wrapper components
but does not include the dynamic component bundles, and a separate set with different build rules for the
dynamic component bundles. It accomplishes this by setting up a 'child compilation' - a way of
running a webpack build within a webpack build. The child build inherits the settings of its parent,
but can have its own settings. However, those settings are more limited, as a lot of the configuration
'sugar' provided by webpack (`entry`, `externals`, chunking, etc) is lost and has to be manually replicated
using the plugin which implement that webpack 'sugar'.

See the plugin itself for details on how it works.

# Building a dynamic component

## 1 - Write a stubbed component

In `src/dynamic/components/my-widget.vue`:

```html
<template>
  <h1>My Fancy Widget</h1>
</template>
<script>
export default {

};
</script>
```

## 2 - Write an exports file

The export file MUST have a major version number on it - this allows us to
make breaking changes to our component and wrappers later, without breaking
those who haven't updated the library yet!

In `src/dynamic/exports/my-widget-v1.js`

```js
export { default } from "../components/my-widget.vue";
```

## 3 - Write a wrapper file

In `src/dynamic/wrappers/my-widget.js`

```js
import wrapperFactory from "../util/wrapper-factory.js"; // wrapperFactory does most of the work for you

export default async function MyWidgetWrapper() { // Must be a function
  const exported = await wrapperFactory({ name: "my-widget-v1" }); // This string matches the file name in exports/
  return exported.default;
}
```

TODO: Add info about testing this - we should write a simple test harness.

## 4 - When ready, add the wrapper to the package exports

Add to `src/dynamic/index.js`

```js
export { default as MyWidget } from "./wrappers/my-widget.js";
```
