feat: Lazy load icons

Co-authored-by: Alois Klink <alois@aloisklink.com>
This commit is contained in:
Sidharth Vinod 2024-09-02 19:51:14 +05:30
parent c68ae309e5
commit 0edfab1048
No known key found for this signature in database
GPG Key ID: FB5CCD378D3907CD
10 changed files with 183 additions and 148 deletions

View File

@ -28,8 +28,13 @@
startOnLoad: false,
logLevel: 0,
});
const logos = await fetch('https://unpkg.com/@iconify-json/logos/icons.json');
mermaid.registerIconPacks(await logos.json());
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
]);
await mermaid.run();
if (window.Cypress) {
window.rendered = true;

View File

@ -232,17 +232,25 @@
service s3(logos:aws-s3)[Cloud Store]
service ec2(logos:aws-ec2)[Server]
service api(logos:aws-api-gateway)[Api Gateway]
service fa(fa:image)[Font Awesome Icon]
</pre>
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
startOnLoad: false,
logLevel: 0,
});
const logos = await fetch('https://unpkg.com/@iconify-json/logos/icons.json');
mermaid.registerIconPacks(await logos.json());
mermaid.init();
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
{
name: 'fa',
loader: () =>
fetch('https://unpkg.com/@iconify-json/fa6-regular/icons.json').then((res) =>
res.json()
),
},
]);
</script>
</body>
</html>

View File

@ -224,17 +224,17 @@ Used to register external diagram types.
### registerIconPacks
**registerIconPacks**: (...`iconPacks`: `IconifyJSON`\[]) => `void`
**registerIconPacks**: (`iconLoaders`: `IconLoader`\[]) => `void`
#### Type declaration
▸ (`...iconPacks`): `void`
▸ (`iconLoaders`): `void`
##### Parameters
| Name | Type |
| :------------- | :--------------- |
| `...iconPacks` | `IconifyJSON`\[] |
| Name | Type |
| :------------ | :-------------- |
| `iconLoaders` | `IconLoader`\[] |
##### Returns

View File

@ -197,41 +197,69 @@ By default, architecture diagram supports the following icons: `cloud`, `databas
Users can use any of the 200,000+ icons available in iconify.design, or add their own custom icons, by following the steps below.
The icon packs available can be found at [icones.js.org](https://icones.js.org/).
We use the name defined when registering the icon pack, to override the prefix field of the iconify pack. This allows the user to use shorter names for the icons. It also allows us to load a particular pack only when it is used in a diagram.
Using JSON file directly from CDN:
```js
import mermaid from 'CDN/mermaid.esm.mjs';
// You have to call `initialize` with startOnLoad:false before calling `registerIconPacks`,
// to prevent mermaid from starting before the icons are loaded
mermaid.initialize({
startOnLoad: false,
logLevel: 0,
});
const logos = await fetch('https://unpkg.com/@iconify-json/logos/icons.json');
mermaid.registerIconPacks(await logos.json());
mermaid.init();
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
]);
```
Using packages and a bundler:
```js
import mermaid from 'mermaid';
// npm install @iconify-json/logos
import { icons as logos } from '@iconify-json/logos';
mermaid.initialize({
startOnLoad: false,
logLevel: 0,
});
mermaid.registerIconPacks(logos);
mermaid.init();
```bash
npm install @iconify-json/logos
```
After the icons are installed, they can be used in the architecture diagram by using the format "prefix:icon-name", where prefix comes from the icon pack you selected.
With lazy loading
```js
import mermaid from 'mermaid';
mermaid.registerIconPacks([
{
name: 'logos',
loader: () => import('@iconify-json/logos').then((module) => module.icons),
},
]);
```
Without lazy loading
```js
import mermaid from 'mermaid';
import { icons } from '@iconify-json/logos';
mermaid.registerIconPacks([
{
name: icons.prefix, // To use the prefix defined in the icon pack
icons,
},
]);
```
After the icons are installed, they can be used in the architecture diagram by using the format "name:icon-name", where name is the value used when registering the icon pack.
```mermaid-example
architecture-beta
group api(logos:aws-lambda)[API]
service db(logos:aws-aurora)[Database] in api
service disk1(logos:aws-glacier)[Storage] in api
service disk2(logos:aws-s3)[Storage] in api
service server(logos:aws-ec2)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
```
````
```mermaid
architecture-beta
group api(logos:aws-lambda)[API]
@ -245,29 +273,3 @@ architecture-beta
disk1:T -- B:server
disk2:T -- B:db
```
````
<div id="arch-example">loading...</div>
<script>
const main = async () => {
const logos = await fetch('https://unpkg.com/@iconify-json/logos/icons.json');
mermaid.registerIconPacks(await logos.json());
const svg = await window.render('d'+Date.now().toString(), `architecture-beta
group api(logos:aws-api-gateway)[API]
service db(logos:aws-aurora)[Database] in api
service disk1(logos:aws-glacier)[Storage] in api
service disk2(logos:aws-s3)[Storage] in api
service server(logos:aws-ec2)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db`, {});
document.getElementById('arch-example').innerHTML = svg;
};
if (!import.meta.env.SSR) {
setTimeout(main, 100);
}
</script>

View File

@ -34,7 +34,12 @@ import {
} from './architectureTypes.js';
import { drawEdges, drawGroups, drawJunctions, drawServices } from './svgDraw.js';
registerIconPacks(architectureIcons);
registerIconPacks([
{
name: architectureIcons.prefix,
icons: architectureIcons,
},
]);
cytoscape.use(fcose);
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {

View File

@ -212,7 +212,7 @@ export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Cor
if (data.icon) {
const bkgElem = groupLabelContainer.append('g');
bkgElem.html(
`<g>${getIconSVG(data.icon, { height: groupIconSize, width: groupIconSize, fallbackPrefix: architectureIcons.prefix })}</g>`
`<g>${await getIconSVG(data.icon, { height: groupIconSize, width: groupIconSize, fallbackPrefix: architectureIcons.prefix })}</g>`
);
bkgElem.attr(
'transform',
@ -297,11 +297,11 @@ export const drawServices = async function (
// throw new Error(`Invalid SVG Icon name: "${service.icon}"`);
// }
bkgElem.html(
`<g>${getIconSVG(service.icon, { height: iconSize, width: iconSize, fallbackPrefix: architectureIcons.prefix })}</g>`
`<g>${await getIconSVG(service.icon, { height: iconSize, width: iconSize, fallbackPrefix: architectureIcons.prefix })}</g>`
);
} else if (service.iconText) {
bkgElem.html(
`<g>${getIconSVG('blank', { height: iconSize, width: iconSize, fallbackPrefix: architectureIcons.prefix })}</g>`
`<g>${await getIconSVG('blank', { height: iconSize, width: iconSize, fallbackPrefix: architectureIcons.prefix })}</g>`
);
const textElemContainer = bkgElem.append('g');
const fo = textElemContainer

View File

@ -86,9 +86,11 @@ onUnmounted(() => mut.disconnect());
const renderChart = async () => {
console.log('rendering chart' + props.id + code.value);
const hasDarkClass = document.documentElement.classList.contains('dark');
const mermaidConfig = {
securityLevel: 'loose',
startOnLoad: false,
theme: hasDarkClass ? 'dark' : 'default',
};
let svgCode = await render(props.id, code.value, mermaidConfig);
// This is a hack to force v-html to re-render, otherwise the diagram disappears

View File

@ -2,29 +2,17 @@ import mermaid, { type MermaidConfig } from 'mermaid';
import zenuml from '../../../../../mermaid-zenuml/dist/mermaid-zenuml.core.mjs';
const init = mermaid.registerExternalDiagrams([zenuml]);
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
]);
export const render = async (id: string, code: string, config: MermaidConfig): Promise<string> => {
await init;
const hasDarkClass = document.documentElement.classList.contains('dark');
const theme = hasDarkClass ? 'dark' : 'default';
mermaid.initialize({ ...config, theme });
mermaid.initialize(config);
const { svg } = await mermaid.render(id, code);
return svg;
};
declare global {
interface Window {
mermaid: typeof mermaid;
render: typeof render;
}
interface ImportMeta {
env: {
SSR: boolean;
};
}
}
if (!import.meta.env.SSR) {
window.mermaid = mermaid;
window.render = render;
}

View File

@ -159,42 +159,56 @@ By default, architecture diagram supports the following icons: `cloud`, `databas
Users can use any of the 200,000+ icons available in iconify.design, or add their own custom icons, by following the steps below.
The icon packs available can be found at [icones.js.org](https://icones.js.org/).
We use the name defined when registering the icon pack, to override the prefix field of the iconify pack. This allows the user to use shorter names for the icons. It also allows us to load a particular pack only when it is used in a diagram.
Using JSON file directly from CDN:
```js
import mermaid from 'CDN/mermaid.esm.mjs';
// You have to call `initialize` with startOnLoad:false before calling `registerIconPacks`,
// to prevent mermaid from starting before the icons are loaded
mermaid.initialize({
startOnLoad: false,
logLevel: 0,
});
const logos = await fetch('https://unpkg.com/@iconify-json/logos/icons.json');
mermaid.registerIconPacks(await logos.json());
mermaid.init();
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
]);
```
Using packages and a bundler:
```js
import mermaid from 'mermaid';
// npm install @iconify-json/logos
import { icons as logos } from '@iconify-json/logos';
mermaid.initialize({
startOnLoad: false,
logLevel: 0,
});
mermaid.registerIconPacks(logos);
mermaid.init();
```bash
npm install @iconify-json/logos
```
After the icons are installed, they can be used in the architecture diagram by using the format "prefix:icon-name", where prefix comes from the icon pack you selected.
With lazy loading
````
```mermaid
```js
import mermaid from 'mermaid';
mermaid.registerIconPacks([
{
name: 'logos',
loader: () => import('@iconify-json/logos').then((module) => module.icons),
},
]);
```
Without lazy loading
```js
import mermaid from 'mermaid';
import { icons } from '@iconify-json/logos';
mermaid.registerIconPacks([
{
name: icons.prefix, // To use the prefix defined in the icon pack
icons,
},
]);
```
After the icons are installed, they can be used in the architecture diagram by using the format "name:icon-name", where name is the value used when registering the icon pack.
```mermaid-example
architecture-beta
group api(logos:aws-lambda)[API]
@ -207,29 +221,3 @@ architecture-beta
disk1:T -- B:server
disk2:T -- B:db
```
````
<div id="arch-example">loading...</div>
<script>
const main = async () => {
const logos = await fetch('https://unpkg.com/@iconify-json/logos/icons.json');
mermaid.registerIconPacks(await logos.json());
const svg = await window.render('d'+Date.now().toString(), `architecture-beta
group api(logos:aws-api-gateway)[API]
service db(logos:aws-aurora)[Database] in api
service disk1(logos:aws-glacier)[Storage] in api
service disk2(logos:aws-s3)[Storage] in api
service server(logos:aws-ec2)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db`, {});
document.getElementById('arch-example').innerHTML = svg;
};
if (!import.meta.env.SSR) {
setTimeout(main, 100);
}
</script>

View File

@ -3,21 +3,47 @@ import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/typ
import type { IconifyIconCustomisations } from '@iconify/utils';
import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } from '@iconify/utils';
interface AsyncIconLoader {
name: string;
loader: () => Promise<IconifyJSON>;
}
interface SyncIconLoader {
name: string;
icons: IconifyJSON;
}
export type IconLoader = AsyncIconLoader | SyncIconLoader;
export const unknownIcon: IconifyIcon = {
body: '<g><rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/><text transform="translate(21.16 64.67)" style="fill: #fff; font-family: ArialMT, Arial; font-size: 67.75px;"><tspan x="0" y="0">?</tspan></text></g>',
height: 80,
width: 80,
};
export const iconsStore = new Map<string, IconifyJSON>();
const iconsStore = new Map<string, IconifyJSON>();
const loaderStore = new Map<string, AsyncIconLoader['loader']>();
export const registerIconPacks = (...iconPacks: IconifyJSON[]) => {
for (const pack of iconPacks) {
iconsStore.set(pack.prefix, pack);
export const registerIconPacks = (iconLoaders: IconLoader[]) => {
for (const iconLoader of iconLoaders) {
if (!iconLoader.name) {
throw new Error(
'Invalid icon loader. Must have a "name" property with non-empty string value.'
);
}
log.debug('Registering icon pack:', iconLoader.name);
if ('loader' in iconLoader) {
loaderStore.set(iconLoader.name, iconLoader.loader);
} else if ('icons' in iconLoader) {
iconsStore.set(iconLoader.name, iconLoader.icons);
} else {
log.error('Invalid icon loader:', iconLoader);
throw new Error('Invalid icon loader. Must have either "icons" or "loader" property.');
}
}
};
const getRegisteredIconData = (iconName: string, fallbackPrefix?: string) => {
const getRegisteredIconData = async (iconName: string, fallbackPrefix?: string) => {
const data = stringToIcon(iconName, true, fallbackPrefix !== undefined);
if (!data) {
throw new Error(`Invalid icon name: ${iconName}`);
@ -26,9 +52,20 @@ const getRegisteredIconData = (iconName: string, fallbackPrefix?: string) => {
if (!prefix) {
throw new Error(`Icon name must contain a prefix: ${iconName}`);
}
const icons = iconsStore.get(prefix);
let icons = iconsStore.get(prefix);
if (!icons) {
throw new Error(`Icon set not found: ${data.prefix}`);
const loader = loaderStore.get(prefix);
if (!loader) {
throw new Error(`Icon set not found: ${data.prefix}`);
}
try {
const loaded = await loader();
icons = { ...loaded, prefix };
iconsStore.set(prefix, icons);
} catch (e) {
log.error(e);
throw new Error(`Failed to load icon set: ${data.prefix}`);
}
}
const iconData = getIconData(icons, data.name);
if (!iconData) {
@ -37,22 +74,22 @@ const getRegisteredIconData = (iconName: string, fallbackPrefix?: string) => {
return iconData;
};
export const isIconAvailable = (iconName: string) => {
export const isIconAvailable = async (iconName: string) => {
try {
getRegisteredIconData(iconName);
await getRegisteredIconData(iconName);
return true;
} catch {
return false;
}
};
export const getIconSVG = (
export const getIconSVG = async (
iconName: string,
customisations?: IconifyIconCustomisations & { fallbackPrefix?: string }
) => {
let iconData: ExtendedIconifyIcon;
try {
iconData = getRegisteredIconData(iconName, customisations?.fallbackPrefix);
iconData = await getRegisteredIconData(iconName, customisations?.fallbackPrefix);
} catch (e) {
log.error(e);
iconData = unknownIcon;