-
Notifications
You must be signed in to change notification settings - Fork 231
Description
Hi there,
As usual, it's a great library 👏. I appreciate the fact that the lesser-known importmaps
standard is utilized ;)
Goal
I've previously employed importmaps
in one of my micro-frontend projects alongside single-spa and the importmap overrides library. This approach significantly enhances the developer experience in complex environments by allowing direct integration of a local server into an integration server (more info in the YouTube video from Joel Denning).
It also proves beneficial for testing, enabling the generation of an affected importmap
on PRs and its use to override bundles in an existing environment without the need for cloning or deploying anything.
It also facilitates incremental and canary deployment.
Issue
My intention was to apply the same strategy with native federation due to its utilization. However, I found it unfeasible due to a globalCache that does not account for potential modifications to the importmaps
overrides. This capability is a default feature and it is supported by the es-module-shims library.
Propositions
First, let's examine the current behavior:
flowchart TD
subgraph Remote [On Load Remote Module]
1[loadRemoteModule on routing or in component]
2[getRemote Infos From Cache]
3[importShim from remote url]
1 --> 2
2 --> 3
end
subgraph Host [On Host Initialization]
direction TB
a[initFederation In Host]
b[Fetch federation.manifest.json]
h[Load remoteEntry.json of the host]
i[Generate importmap with key/url]
y[Combine Host importmap and remotes importmaps]
z[Write importmaps in DOM]
a --> b
a --> A
b --> B
subgraph A [hostImportMap]
direction TB
h[Load remoteEntry.json]
i[Generate importmap with key/url]
h --> i
end
subgraph B [remotesImportMap]
direction TB
c[Load remoteEntry.json]
d[Generate importmap with key/url]
e[Add remote entry infos to globalCache]
c --> d
c --> e
end
A --> y
B --> y
y --> z
end
Cache((globalCache))
e .-> Cache
2 .-> Cache
1. Allow importmap overrides in the es-module-shims
library
<script type="esms-options">
{
"shimMode": true
"mapOverrides": true
}
</script>
2. Import the importmap directly as a file in the index.html
instead of runtime code in initFederation
The browser can combine directly multiple importmaps and load them for us. I would suggest that instead of executing runtime code, integrating all importmap already generated at compile time directly in the index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>host</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script type="importmap-shim" src="assets/host-shared.importmap.json"></script>
<script type="importmap-shim" src="assets/remotes.importmap.json"></script>
</head>
<body>
<nx-nf-root></nx-nf-root>
</body>
</html>
3. Ensure that when we loadRemoteModule
, we obtain the actual version from the importmap
By listening to the DOM mutation like es-module-shim and refreshing the cache
new MutationObserver((mutations) => {
for (const { addedNodes, type } of mutations) {
if (type !== 'childList') continue;
for (const node of addedNodes) {
if (node.tagName === 'SCRIPT') {
if (node.type === 'importmap-shim' || node.type === 'importmap') {
const remoteNamesToRemote = globalcache.remoteNamesToRemote;
// TODO: should update the remoteNamesToRemote by changing the base url
}
}
}
}
}).observe(document, { childList: true, subtree: true });
The challenge here is that the cache is grouped per remote and only maintains a single base URL, so overriding one exposes
would change the URL for others as well.
By directly reading the importmap
The impact on performance is uncertain. Having an API library to manipulate importmaps
would be advantageous.
Work Around
I have succeeded in overriding the loadRemoteModule
function:
export function getImportMapOverride(importMapKey: string): string | undefined {
// @ts-ignore
const imports = window?.importMapOverrides?.getOverrideMap()?.imports;
return imports && imports[importMapKey];
}
export async function loadRemoteOverrideUtils<T = any>(
remoteName: string,
exposedModule: string
): Promise<T> {
const remoteKey = `${remoteName}/${exposedModule}`;
const importMapOverrideUrl = getImportMapOverride(remoteKey);
// If override found for remoteKey, load it separately
// Else, use the default function
return importMapOverrideUrl
? importShim<T>(importMapOverrideUrl)
: loadRemoteModule(remoteName, exposedModule);
}
But I don't like the fact that the globalCache
of native federation is still invalid.
full code here https://github.com/jogelin/nx-nf/tree/poc-load-remote-overrides
What about directly overriding the federation.manifest.json
?
In my opinion, it is the best approach because overriding only one exposes
does not make sense. Usually, we want to override an entire remote URL.
By using a custom approach
I implemented an easy way, but custom, but it keeps the globalCache
in sync:
If you have that override in your localStorage
:
Directly in the main.ts
you can use:
initFederation('/assets/federation.manifest.json')
.then(() => initFederationOverrides()) // <-- HERE
.catch((err) => console.error(err))
.then((_) => import('./bootstrap'))
.catch((err) => console.error(err));
utilities functions:
import { processRemoteInfo } from '@angular-architects/native-federation';
import { ImportMap } from './import-map.type';
const NATIVE_FEDERATION_LOCAL_STORAGE_PREFIX = 'native-federation-override:';
export function initFederationOverrides(): Promise<ImportMap[]> {
const overrides = loadNativeFederationOverridesFromStorage();
const processRemoteInfoPromises = Object.entries(overrides).map(
([remoteName, url]) => processRemoteInfo(url, remoteName)
);
return Promise.all(processRemoteInfoPromises);
}
function loadNativeFederationOverridesFromStorage(): Record<string, string> {
return Object.entries(localStorage).reduce((overrides, [key, url]) => {
return {
...overrides,
...(key.startsWith(NATIVE_FEDERATION_LOCAL_STORAGE_PREFIX) && {
[key.replace(NATIVE_FEDERATION_LOCAL_STORAGE_PREFIX, '')]: url,
}),
};
}, {});
}
But why not using an importmap
to load the remoteEntry.json
files?
The federation.manifest.json
would then appear as:
{
imports: {
"host": "http://localhost:4200/remoteEntry.json",
"mfAccount": "http://localhost:4203/remoteEntry.json",
"mfHome": "http://localhost:4201/remoteEntry.json",
"mfLogin": "http://localhost:4202/remoteEntry.json"
}
}
and directly integrate it into the index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>host</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script type="importmap-shim" src="assets/federation.manifest.json"></script>
</head>
<body>
<nx-nf-root></nx-nf-root>
</body>
</html>
and in the initFederation
, we just need to use:
import('mfAccount').then((remoteEntry) => // same as before, inject exposes to inline importmap)
In this way:
✅ It is standard
✅ We use importmap
everywhere
✅ We can use default override behaviour
✅ It allows to override a full remote AND exposes separately
What do you Think?
Do you want to make a PR?
Yes of course after discussion :)