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:
parent
51c5b29fcd
commit
1fad38b291
@ -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"
|
||||
|
@ -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
|
||||
|
15
packages/core/src/api/loaders.ts
Normal file
15
packages/core/src/api/loaders.ts
Normal 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;
|
||||
}
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
*
|
||||
|
253
packages/core/tests/api/custom-loader-test.ts
Normal file
253
packages/core/tests/api/custom-loader-test.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user