2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-04 18:23:17 +00:00

feat: custom loader in core

This commit is contained in:
Vjacheslav Trushkin 2024-11-03 17:58:14 +02:00
parent 51c5b29fcd
commit 1fad38b291
7 changed files with 334 additions and 32 deletions

View File

@ -38,6 +38,10 @@
"require": "./lib/api/icons.cjs",
"import": "./lib/api/icons.mjs"
},
"./lib/api/loaders": {
"require": "./lib/api/loaders.cjs",
"import": "./lib/api/loaders.mjs"
},
"./lib/api/modules": {
"require": "./lib/api/modules.cjs",
"import": "./lib/api/modules.mjs"

View File

@ -1,6 +1,6 @@
import type { IconifyIcon, IconifyJSON } from '@iconify/types';
import {
IconifyIconName,
type IconifyIconName,
matchIconName,
stringToIcon,
} from '@iconify/utils/lib/icon/name';
@ -93,32 +93,31 @@ function parseLoaderResponse(
icons: string[],
data: unknown
) {
const fail = () => {
function checkMissing() {
const pending = storage.pendingIcons;
icons.forEach((name) => {
storage.missing.add(name);
// Remove added icons from pending list
if (pending) {
pending.delete(name);
}
// Mark as missing if icon is not in storage
if (!storage.icons[name]) {
storage.missing.add(name);
}
});
};
}
// Check for error
if (typeof data !== 'object' || !data) {
fail();
} else {
if (data && typeof data === 'object') {
// Add icons to storage
try {
const parsed = addIconSet(storage, data as IconifyJSON);
if (!parsed.length) {
fail();
checkMissing();
return;
}
// Remove added icons from pending list
const pending = storage.pendingIcons;
if (pending) {
parsed.forEach((name) => {
pending.delete(name);
});
}
// Cache API response
storeInBrowserStorage(storage, data as IconifyJSON);
} catch (err) {
@ -126,6 +125,9 @@ function parseLoaderResponse(
}
}
// Check for some icons from request were not in response: mark as missing
checkMissing();
// Trigger update on next tick
loadedNewIcons(storage);
}
@ -149,15 +151,35 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
const { provider, prefix } = storage;
// Get icons and delete queue
// Icons should not be undefined, but just in case assume it can be
const icons = storage.iconsToLoad;
delete storage.iconsToLoad;
if (!icons || !icons.length) {
// Icons should not be undefined or empty, but just in case check it
return;
}
// TODO: check for custom loader
// Check for custom loader
if (storage.customLoader) {
const response = storage.customLoader(icons, prefix, provider);
if (response instanceof Promise) {
// Custom loader is async
response
.then((data) => {
parseLoaderResponse(storage, icons, data);
})
.catch(() => {
parseLoaderResponse(storage, icons, null);
});
} else {
// Sync loader
parseLoaderResponse(storage, icons, response);
}
return;
}
// Using API loader
// Validate icon names for API
const { valid, invalid } = checkIconNamesForAPI(icons || []);
const { valid, invalid } = checkIconNamesForAPI(icons);
if (invalid.length) {
// Invalid icons

View File

@ -0,0 +1,15 @@
import { getStorage } from '../storage/storage.js';
import type { IconifyCustomLoader, IconStorageWithAPI } from './types.js';
/**
* Set custom loader
*/
export function setCustomLoader(
loader: IconifyCustomLoader,
prefix: string,
provider?: string
): void {
// Assign loader directly to storage
(getStorage(provider || '', prefix) as IconStorageWithAPI).customLoader =
loader;
}

View File

@ -7,13 +7,13 @@ import type {
import { initRedundancy } from '@iconify/api-redundancy';
import {
getAPIModule,
IconifyAPIQueryParams,
IconifyAPISendQuery,
type IconifyAPIQueryParams,
type IconifyAPISendQuery,
} from './modules';
import {
createAPIConfig,
IconifyAPIConfig,
PartialIconifyAPIConfig,
type IconifyAPIConfig,
type PartialIconifyAPIConfig,
} from './config';
import { getAPIConfig } from './config';

View File

@ -4,6 +4,16 @@ import type {
} from './icons';
import type { SortedIcons } from '../icon/sort';
import type { IconStorage } from '../storage/storage';
import { IconifyJSON } from '@iconify/types';
/**
* Custom icons loader
*/
export type IconifyCustomLoader = (
icons: string[],
prefix: string,
provider: string
) => Promise<IconifyJSON | null> | IconifyJSON | null;
/**
* Storage for callbacks
@ -26,6 +36,9 @@ export interface APICallbackItem {
* Add custom stuff to storage
*/
export interface IconStorageWithAPI extends IconStorage {
// Custom loader
customLoader?: IconifyCustomLoader;
/**
* List of icons that are being loaded, added to storage
*

View File

@ -0,0 +1,253 @@
import { defaultIconProps } from '@iconify/utils';
import { loadIcons, loadIcon } from '../../lib/api/icons';
import { setCustomLoader } from '../../lib/api/loaders';
import { listIcons } from '../../lib/storage/storage';
describe('Testing API loadIcons', () => {
let prefixCounter = 0;
function nextPrefix(): string {
prefixCounter++;
return `loader-test-${prefixCounter < 10 ? '0' : ''}${prefixCounter}`;
}
it('Custom async loader with loadIcon', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set loader
setCustomLoader(
(icons, requestedPrefix, requestedProvider) => {
return new Promise((resolve, reject) => {
try {
// Check params
expect(icons).toEqual(['icon1']);
expect(requestedPrefix).toBe(prefix);
expect(requestedProvider).toBe(provider);
} catch (err) {
reject(err);
return;
}
// Send data
resolve({
prefix,
icons: {
icon1: {
body: '<path d="" />',
},
icon_2: {
body: '<path d="" />',
},
},
});
});
},
prefix,
provider
);
// Load icon
loadIcon(provider + ':' + prefix + ':icon1')
.then((data) => {
try {
// Test response
expect(data).toEqual({
...defaultIconProps,
body: '<path d="" />',
});
// Check storage
expect(listIcons(provider, prefix)).toEqual([
`@${provider}:${prefix}:icon1`,
`@${provider}:${prefix}:icon_2`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
})
.catch(reject);
});
});
it('Custom sync loader with loadIcon', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set loader
setCustomLoader(
(icons, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(icons).toEqual(['Icon_1']);
expect(requestedPrefix).toBe(prefix);
expect(requestedProvider).toBe(provider);
} catch (err) {
reject(err);
return null;
}
// Send data
return {
prefix,
icons: {
// Use name that is not allowed in API
Icon_1: {
body: '<path d="" />',
},
},
};
},
prefix,
provider
);
// Load icon
loadIcon(`${provider}:${prefix}:Icon_1`)
.then((data) => {
try {
// Test response
expect(data).toEqual({
...defaultIconProps,
body: '<path d="" />',
});
// Check storage
expect(listIcons(provider, prefix)).toEqual([
`@${provider}:${prefix}:Icon_1`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
})
.catch(reject);
});
});
it('Missing icons', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set loaders: one is sync, one is async
setCustomLoader(
(icons, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(icons).toEqual(['icon1']);
expect(requestedPrefix).toBe(prefix1);
expect(requestedProvider).toBe(provider);
} catch (err) {
reject(err);
return null;
}
// Send data
return {
prefix: prefix1,
icons: {
icon1: {
body: '<path d="" />',
},
},
};
},
prefix1,
provider
);
setCustomLoader(
(icons, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(icons).toEqual(['BadIcon', 'Icon_2']);
expect(requestedPrefix).toBe(prefix2);
expect(requestedProvider).toBe(provider);
} catch (err) {
reject(err);
return null;
}
// Send data asynchronously, without 'BadIcon'
return new Promise((resolve) => {
setTimeout(() => {
resolve({
prefix: prefix2,
icons: {
Icon_1: {
body: '<path d="" />',
},
Icon_2: {
body: '<path d="" />',
},
Icon_3: {
body: '<path d="" />',
},
},
});
}, 150);
});
},
prefix2,
provider
);
// Load icon
loadIcons(
[
`${provider}:${prefix1}:icon1`,
`${provider}:${prefix2}:Icon_2`,
`${provider}:${prefix2}:BadIcon`,
],
(loaded, missing, pending) => {
if (pending.length) {
// Could be called before all icons are loaded because of async loader
return;
}
try {
// Test response
expect(loaded).toEqual([
{
provider,
prefix: prefix1,
name: 'icon1',
},
{
provider,
prefix: prefix2,
name: 'Icon_2',
},
]);
expect(missing).toEqual([
{
provider,
prefix: prefix2,
name: 'BadIcon',
},
]);
// Check storage
expect(listIcons(provider, prefix1)).toEqual([
`@${provider}:${prefix1}:icon1`,
]);
expect(listIcons(provider, prefix2)).toEqual([
`@${provider}:${prefix2}:Icon_1`,
`@${provider}:${prefix2}:Icon_2`,
`@${provider}:${prefix2}:Icon_3`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
}
);
});
});
});

View File

@ -12,11 +12,7 @@ describe('Testing API loadIcons', () => {
let prefixCounter = 0;
function nextPrefix(): string {
prefixCounter++;
return (
'api-load-test-' +
(prefixCounter < 10 ? '0' : '') +
prefixCounter.toString()
);
return `api-load-test-${prefixCounter < 10 ? '0' : ''}${prefixCounter}`;
}
it('Loading few icons', () => {
@ -311,9 +307,9 @@ describe('Testing API loadIcons', () => {
expect(loadedIcon).toBe(false);
// Test isPending
// After change to naming convention, icon name is valid and should be pending
// Filtering invalid names is done in loader, not in API module
expect(isPending({ provider, prefix, name: 'BadIconName' })).toBe(true);
expect(isPending({ provider, prefix, name: 'BadIconName' })).toBe(
false
);
});
it('Loading one icon twice with Promise', () => {
@ -551,7 +547,6 @@ describe('Testing API loadIcons', () => {
callback: QueryModuleResponse
): void => {
queryCounter++;
params;
switch (queryCounter) {
case 1:
// First call on api1