diff --git a/docs/building-apps/esm-views.md b/docs/building-apps/esm-views.md new file mode 100644 index 000000000..762a65bc1 --- /dev/null +++ b/docs/building-apps/esm-views.md @@ -0,0 +1,164 @@ +--- +parent: Building your Apps +title: ESM Views +--- + +modular builds packages of `"type": "esm-view"` as +[ES Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), +rewriting all of a subset of their imports to make use of a configurable ESM CDN +(e.g. [Skypack](https://www.skypack.dev) or [esm.sh](https://esm.sh/)). This +allows users to implement the +[microfrontend pattern](../concepts/microfrontends.md), by creating an artifact +that can be +[`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports)ed +at runtime by a host application, or loaded stand-alone thanks to the automatic +generation of the `index.html` and trampoline file. + +## Why ESM Views need an external CDN + +ESM Views are designed to exclude external dependencies from the output bundle +and `import` them at runtime. This simplifies the build process by removing the +need for explicit dependency de-duplication as the browser will do so +automatically though its cache so long as all dependencies are served from the +same origin, i.e. a single CDN. + +This is particularly useful for a host application that lazily loads several +independently developed and hosted applications onto a browser tab at runtime; +If each of those applications naively bundled all their dependencies this would +result in inefficiencies as a copy of each dependency that is used by more than +one application would be included in each bundle. For stateful dependencies like +React that don't allow multiple instances of themselves in the same page +context, this would cause crashes. Importing these external dependencies from a +CDN, instead, means that every shared dependency is loaded from the server +exactly once at runtime and re-used in every point of the code where it's +imported. This improves efficiency and, since every dependency is loaded and +evaluated only once, it plays well with stateful libraries. + +## How to build + +ESM views are built with the [`modular build`](../commands/build.md) command. +The default behaviour when building an ESM view is: + +1. All the non-local `import`s in the package's `src` directory are extracted + and matched with their version in the package's `package.json` or the + `package.json` in the repository root and with their exact version in the + repo's lockfile. +2. The main entrypoint (as defined in the ESM view's `package.json`'s `main` + field) and its local imports are bundled in a single file. +3. All the `import` statements to non-local dependencies encountered in the + process are rewritten to `import` from an external ESM CDN (by default + https://www.skypack.dev/), using the versions extracted in step 1. By + default, versions extracted from `package.json` will be used, but users can + customize the rewrite template to use versions from the lockfile instead. +4. All the local CSS is bundled in a single file. +5. The `dist` directory is generated, containing: + - The js file + - The css file + - A [package manifest](#package-manifest) (`package.json`) file containing: + - The location of the js bundle (`"module"` field) + - The location of the css bundle (`"style"` field) + - An object with the whole set of dependencies and their version ranges + (`"dependencies"` field) + - An array of bundled dependencies (`bundledDependencies` field) + - A synthetically generated `index.html` file, linking the trampoline file + and the css bundle + - A synthetically generated trampoline file, dynamically `import`ing the js + bundle and `React.render`ing its default export to a `#root` div. + +The ESM view build result can either be +[dynamically imported](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports) +from a host application, +([including the css bundle](https://web.dev/css-module-scripts/)) or served +statically as a standalone application (for example, using `modular serve` or +from a web server). + +## Customise the ESM CDN + +You can specify a CDN template to rewrite dependencies using the environment +variable `EXTERNAL_CDN_TEMPLATE`. + +For example: + +- The default template for the [Skypack](https://www.skypack.dev/) public CDN is + `EXTERNAL_CDN_TEMPLATE="https://cdn.skypack.dev/[name]@[resolution]"` +- A valid template to work with the esm.sh public CDN can be specified with + `EXTERNAL_CDN_TEMPLATE="https://esm.sh/[name]@[version]"` + +These are the substrings that are replaced in the template: + +- `[name]` is replaced with the name of the imported dependency +- `[version]` is replaced with the version of the imported dependency as + extracted from the package's or the root's (hoisted) `package.json`. +- `[resolution]` is replaced with the version of the imported dependency as + extracted from the yarn lockfile (`yarn.lock`). + +## Customise bundling / rewriting strategy + +By default, all external dependencies are rewritten to a CDN URL and none is +bundled. This logic can be controlled using two environment variables: + +1. `EXTERNAL_ALLOW_LIST` is a comma-separated string that specifies which + dependencies are allowed to be rewritten to the CDN; if not specified, its + default value is `**` ( -> all dependencies are rewritten) +2. `EXTERNAL_BLOCK_LIST` is a comma-separated string that specifies which + dependencies are **not** allowed to be rewritten to the CDN; if not specified + its default value is empty ( -> no dependency excluded, i.e. all dependencies + are rewritten) + +The allow / block lists are parsed and processed according to this logic: + +- If a dependency is only in the allow list, it will be rewritten +- If a dependency is only in the block list, it will be bundled +- If a dependency is in both lists, it will be bundled (`EXTERNAL_BLOCK_LIST` + wins) +- If a dependency is in none of the lists, it will be bundled (but remember that + all dependencies are in allow list by default) + +The dependencies will be reflected in the output package manifest +(`package.json`) according to these rules: + +- All dependencies and their versions are listed in the `dependencies` field, as + an object +- The dependencies that are bundled are listed in the `bundledDependencies` + field, as an array + +It is possible to specify wildcards in the block and allow list. +[Micromatch](https://github.com/micromatch/micromatch) syntax is supported. + +## Package manifest + +Every build of an ESM view will generate a package manifest (`package.json`), +which will contain a selection of the original `package.json` fields, plus a set +of added / modified fields: + +- [`style`](https://jaketrent.com/post/package-json-style-attribute): the + location of the js bundle (example: + `"style": "static/css/main.c6ac0a5c.css"`), useful for an host to dynamically + load the styles and add them to the page `` or the + [adopted stylesheet](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets). +- [`module`](https://github.com/dherman/defense-of-dot-js/blob/f31319be735b21739756b87d551f6711bd7aa283/proposal.md): + the location of the js bundle (example: + `"module": "static/js/main.5077b483.js"`), useful for an host to dynamically + load the ESM view and render it in a React tree. +- [`dependencies`](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#dependencies): + an object containing all the dependencies imported in the package source + (including the hoisted ones) and their correspondent version ranges. +- [`bundledDependencies`](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bundleddependencies): + an array of bundled dependencies. Since `dependencies` contains all the + dependencies (bundled and not bundled), it is always possible to know which + dependencies were rewritten and which were bundled. + +## External CSS imports + +CSS imports pointing to an external package (for example: +[`import 'regular-table/dist/css/material.css'`](https://www.npmjs.com/package/regular-table) +) will be rewritten to a CDN URL (for example, using Skypack, +`https://cdn.skypack.dev/regular-table@[version]/dist/css/material.css`). The +only difference is that they will be rewritten in the bundle as code that +applies the CSS into the page, either by simply adding it to the `head` or, +depending on the build `target`, using +[CSS Module scripts](https://web.dev/css-module-scripts/) and adding the script +to the +[adopted stylesheet](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets). + +This feature is experimental and feedback is appreciated. diff --git a/docs/building-apps/web workers.md b/docs/building-apps/web-workers.md similarity index 100% rename from docs/building-apps/web workers.md rename to docs/building-apps/web-workers.md diff --git a/docs/commands/add.md b/docs/commands/add.md index 4ed76e908..693f5fb56 100644 --- a/docs/commands/add.md +++ b/docs/commands/add.md @@ -10,18 +10,22 @@ Adds a new package by creating a new workspace at `packages/` (i.e. `modular add my-app` would create a package in `packages/my-app` and `modular add libs/lib-a` would create a package in `packages/libs/lib-a`) -Packages can currently be one of 3 types: +Packages can currently be one of the following types: -- A standalone application. This corresponds to a single `create-react-app` - project in a workspace. Inside this workspace, you can import packages from - other workspaces freely, and features like jsx and typechecking work out of - the box. +- A standalone `app`. This corresponds to a single `create-react-app` project in + a workspace. Inside this workspace, you can import packages from other + workspaces freely, and features like jsx and typechecking work out of the box. -- A View, which is a package that exports a React component by default. Views - are primary, top-level components in `modular`. Read more about Views in - [this explainer](../concepts/views.md). +- An `esm-view`, which is a package that typically exports a React component by + default. ESM Views are built as ES modules that can be `import`ed at runtime + by a host to implement a [micro frontend](../concepts/microfrontends.md) + architecture or started as a normal standalone application. See also + [the view building reference](../building-apps/esm-views.md) -- A typical javascript package. You can use this to create any other kind of +- A `view`, which is a package that exports a React component by default. Read + more about Views in [this explainer](../concepts/views.md). + +- A typical JavaScript `package`. You can use this to create any other kind of utility, tool, or whatever your needs require you to do. As an example, you could build a node.js server inside one of these. diff --git a/docs/concepts/microfrontends.md b/docs/concepts/microfrontends.md new file mode 100644 index 000000000..f21b8958e --- /dev/null +++ b/docs/concepts/microfrontends.md @@ -0,0 +1,76 @@ +--- +parent: Concepts +--- + +## ESM micro frontends in Modular + +Micro frontends are a pattern in which discrete UIs (frontends) are composed of +independent fragments that can be built and deployed separately by different +teams and loaded on-demand at runtime. Modular gives developers the opportunity +to implement micro frontends through [ESM Views](../building-apps/esm-views.md), +which are applications built as ES Modules. ESM Views can be served standalone +or dynamically imported by a host application at runtime and rendered in the +host application's own React tree, without the need of using Iframes. ESM Views +also allow automatic dependency de-duplication in the browser, thanks to how +Modular offloads third-party dependencies to a configurable ESM CDN. + +## How we build micro frontends + +An ESM View can be added to a Modular monorepo via the `modular add` command, +which will set the package's Modular type to `"esm-view"` in its `package.json`. + +```json +"modular": { + "type": "esm-view" +} +``` + +This will instruct the `modular build` command to output an +[ES Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). + +The ESM View's local dependencies will be bundled in the dist result, while the +ESM View's external dependencies will be rewritten to import from an ESM CDN by +default, like (for example) [Skypack](https://www.skypack.dev/) or +[esm.sh](https://esm.sh/). This allows the ESM View's external dependencies to +be loaded on-demand at runtime and to be automatically de-duplicated (i.e. the +same dependency will be not re-fetched and re-evaluated, but simply re-used if +any view tries to use it the second time). This is especially useful in a React +micro frontend scenario, where multiple ESM views share the same copy of React, +without incurring errors associated with +[multiple copies of React in the same page](https://reactjs.org/warnings/invalid-hook-call-warning.html). + +Since dynamically importing ES Modules leverages a +[widely supported standard](https://caniuse.com/es6-module-dynamic-import), +where the browser does the heavy lifting of fetching, de-duplicating and making +dependencies available, ES Modules are Modular's preferred building blocks to +implement a micro frontend architecture. + +## How to load micro frontends + +Building an ESM View generates a single JavaScript entrypoint and a single CSS +entrypoint that can be imported at runtime by any application (or other ESM +View) using dynamic import (or any viable technique in case of styles). ESM +Views will generate a package manifest (`package.json` file) that contains: + +- Information regarding the location of the built files, whose names are + uniquely hashed to facilitate caching +- Lists of bundled and rewritten dependencies along with their dependencies, in + order to decouple importing of ESM Views from the actual build result + structure. For more information, visit the + [ESM Views reference page](../building-apps/esm-views.md) + +## Standalone support + +As ESM views typically export a React component by default, a synthetic +`index.html` capable of loading the view as a standalone web page is provided in +the `dist/{{view-name}}` directory. This means that ESM views can additionally +be served (for example with the `modular serve` command or any HTTP server) as +normal applications, while retaining the ability of loading their dependencies +via an ESM CDN. + +## Customisation + +ESM views can be customised by editing the template used to rewrite the external +imports and by setting which dependency imports are rewritten to point to a CDN +and which are bundled. For more information on how to customise the build of ESM +views, visit the [ESM Views reference page](../building-apps/esm-views.md)