2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-20 09:19:02 +00:00

Merge pull request #104 from iconify/userquin/refactor-modern-loader

feat: refactor modern loader
This commit is contained in:
Vjacheslav Trushkin 2022-01-10 17:43:19 +02:00 committed by GitHub
commit 2b10edef28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 43 deletions

4
package-lock.json generated
View File

@ -4460,7 +4460,6 @@
"minimist": "^1.2.5", "minimist": "^1.2.5",
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
"source-map": "^0.6.1", "source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0" "wordwrap": "^1.0.0"
}, },
"bin": { "bin": {
@ -5245,9 +5244,6 @@
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true, "dev": true,
"dependencies": {
"graceful-fs": "^4.1.6"
},
"optionalDependencies": { "optionalDependencies": {
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }

View File

@ -90,14 +90,6 @@
"require": "./lib/icon/sort.js", "require": "./lib/icon/sort.js",
"import": "./lib/icon/sort.mjs" "import": "./lib/icon/sort.mjs"
}, },
"./lib/modern": {
"require": "./lib/modern/index.js",
"import": "./lib/modern/index.mjs"
},
"./lib/modern/index": {
"require": "./lib/modern/index.js",
"import": "./lib/modern/index.mjs"
},
"./lib/storage/functions": { "./lib/storage/functions": {
"require": "./lib/storage/functions.js", "require": "./lib/storage/functions.js",
"import": "./lib/storage/functions.mjs" "import": "./lib/storage/functions.mjs"
@ -108,17 +100,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@antfu/utils": "^0.3.0",
"@iconify/api-redundancy": "^1.0.2", "@iconify/api-redundancy": "^1.0.2",
"@iconify/types": "^1.0.10", "@iconify/types": "^1.0.10",
"@iconify/utils": "^1.0.16", "@iconify/utils": "^1.0.16",
"cross-fetch": "^3.1.4", "cross-fetch": "^3.1.4"
"debug": "^4.3.3",
"local-pkg": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@iconify/library-builder": "^1.0.3", "@iconify/library-builder": "^1.0.3",
"@types/debug": "^4.1.7",
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/node": "^15.3.0", "@types/node": "^15.3.0",
"@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/eslint-plugin": "^4.31.1",

View File

@ -13,7 +13,8 @@
"@antfu/utils": "^0.3.0", "@antfu/utils": "^0.3.0",
"@iconify/types": "^1.0.12", "@iconify/types": "^1.0.12",
"debug": "^4.3.3", "debug": "^4.3.3",
"kolorist": "^1.5.0" "kolorist": "^1.5.0",
"local-pkg": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@iconify/library-builder": "^1.0.4", "@iconify/library-builder": "^1.0.4",
@ -3972,6 +3973,17 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/local-pkg": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.1.tgz",
"integrity": "sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -8412,6 +8424,11 @@
"type-check": "~0.4.0" "type-check": "~0.4.0"
} }
}, },
"local-pkg": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.1.tgz",
"integrity": "sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw=="
},
"locate-path": { "locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",

View File

@ -128,6 +128,10 @@
"require": "./lib/loader/loaders.js", "require": "./lib/loader/loaders.js",
"import": "./lib/loader/loaders.mjs" "import": "./lib/loader/loaders.mjs"
}, },
"./lib/loader/modern": {
"require": "./lib/loader/modern.js",
"import": "./lib/loader/modern.mjs"
},
"./lib/loader/types": { "./lib/loader/types": {
"require": "./lib/loader/types.js", "require": "./lib/loader/types.js",
"import": "./lib/loader/types.mjs" "import": "./lib/loader/types.mjs"
@ -158,7 +162,8 @@
"@antfu/utils": "^0.3.0", "@antfu/utils": "^0.3.0",
"@iconify/types": "^1.0.12", "@iconify/types": "^1.0.12",
"debug": "^4.3.3", "debug": "^4.3.3",
"kolorist": "^1.5.0" "kolorist": "^1.5.0",
"local-pkg": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@iconify/library-builder": "^1.0.4", "@iconify/library-builder": "^1.0.4",

View File

@ -48,11 +48,14 @@ export { stringToColor, compareColors, colorToString } from './colors/index';
export type { export type {
CustomIconLoader, CustomIconLoader,
CustomCollections, CustomCollections,
IconCustomizer,
IconCustomizations,
InlineCollection, InlineCollection,
} from './loader/types'; } from './loader/types';
export { tryInstallPkg } from './loader/utils'; export { tryInstallPkg, mergeIconProps } from './loader/utils';
export { FileSystemIconLoader } from './loader/loaders'; export { FileSystemIconLoader } from './loader/loaders';
export { getCustomIcon } from './loader/custom'; export { getCustomIcon } from './loader/custom';
export { loadCollection, searchForIcon } from './loader/modern';
// Misc // Misc
export { camelize, camelToKebab, pascalize } from './misc/strings'; export { camelize, camelToKebab, pascalize } from './misc/strings';

View File

@ -1,6 +1,6 @@
import type { Awaitable } from '@antfu/utils';
import createDebugger from 'debug'; import createDebugger from 'debug';
import type { CustomIconLoader, InlineCollection } from './types'; import type { CustomIconLoader, IconCustomizations, InlineCollection } from './types';
import { mergeIconProps } from './utils';
const debug = createDebugger('@iconify-loader:custom'); const debug = createDebugger('@iconify-loader:custom');
@ -11,7 +11,7 @@ export async function getCustomIcon(
custom: CustomIconLoader | InlineCollection, custom: CustomIconLoader | InlineCollection,
collection: string, collection: string,
icon: string, icon: string,
transform?: (svg: string) => Awaitable<string> iconsCustomizations?: IconCustomizations,
): Promise<string | undefined> { ): Promise<string | undefined> {
let result: string | undefined | null; let result: string | undefined | null;
@ -26,10 +26,18 @@ export async function getCustomIcon(
if (result) { if (result) {
if (!result.startsWith('<svg ')) { if (!result.startsWith('<svg ')) {
console.warn( console.warn(`Custom icon "${icon}" in "${collection}" is not a valid SVG`)
`Custom icon "${icon}" in "${collection}" is not a valid SVG` return result
);
} }
return transform ? await transform(result) : result; const { transform, additionalProps = {}, iconCustomizer } = iconsCustomizations || {}
return await mergeIconProps(
transform ? await transform(result) : result,
collection,
icon,
additionalProps,
undefined,
iconCustomizer
)
} }
} }

View File

@ -16,8 +16,8 @@ export function FileSystemIconLoader(
`${dir}/${camelize(name)}.svg`, `${dir}/${camelize(name)}.svg`,
`${dir}/${pascalize(name)}.svg`, `${dir}/${pascalize(name)}.svg`,
]; ];
for (const path of paths) {
let stat: Stats; let stat: Stats;
for (const path of paths) {
try { try {
stat = await fs.lstat(path); stat = await fs.lstat(path);
} catch (err) { } catch (err) {

View File

@ -1,14 +1,17 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import type { IconifyJSON } from '@iconify/types'; import type { IconifyJSON } from '@iconify/types';
import type { FullIconifyIcon } from '@iconify/utils/lib/icon'; import type { FullIconifyIcon } from '../icon';
import { defaultCustomisations as DefaultIconCustomizations, iconToSVG, getIconData, tryInstallPkg } from '@iconify/utils'; import { iconToSVG } from '../svg/build';
import { getIconData } from '../icon-set/get-icon';
import { mergeIconProps, tryInstallPkg } from './utils';
import createDebugger from 'debug'; import createDebugger from 'debug';
import { isPackageExists, resolveModule } from 'local-pkg'; import { isPackageExists, resolveModule } from 'local-pkg';
import type { FullIconCustomisations } from '@iconify/utils/lib/customisations'; import { defaults as DefaultIconCustomizations } from '../customisations';
import type { IconCustomizations } from './types';
const debug = createDebugger('@iconify-core:icon'); const debug = createDebugger('@iconify-loader:icon');
const debugModern = createDebugger('@iconify-core:modern'); const debugModern = createDebugger('@iconify-loader:modern');
const debugLegacy = createDebugger('@iconify-core:legacy'); const debugLegacy = createDebugger('@iconify-loader:legacy');
const _collections: Record<string, Promise<IconifyJSON | undefined>> = {}; const _collections: Record<string, Promise<IconifyJSON | undefined>> = {};
const isLegacyExists = isPackageExists('@iconify/json'); const isLegacyExists = isPackageExists('@iconify/json');
@ -48,13 +51,14 @@ export async function loadCollection(name: string, autoInstall = false): Promise
} }
} }
export function searchForIcon( export async function searchForIcon(
iconSet: IconifyJSON, iconSet: IconifyJSON,
collection: string, collection: string,
ids: string[], ids: string[],
customize?: (defaultCustomizations: FullIconCustomisations) => FullIconCustomisations iconCustomizations?: IconCustomizations,
): string | null { ): Promise<string | undefined> {
let iconData: FullIconifyIcon | null; let iconData: FullIconifyIcon | null;
const { customize, additionalProps = {}, iconCustomizer } = iconCustomizations || {}
for (const id of ids) { for (const id of ids) {
iconData = getIconData(iconSet, id, true); iconData = getIconData(iconSet, id, true);
if (iconData) { if (iconData) {
@ -64,8 +68,14 @@ export function searchForIcon(
iconData, iconData,
typeof customize === 'function' ? customize(defaultCustomizations) : defaultCustomizations typeof customize === 'function' ? customize(defaultCustomizations) : defaultCustomizations
); );
return `<svg ${Object.entries(attributes).map(i => `${i[0]}="${i[1]}"`).join(' ')}>${body}</svg>`; return await mergeIconProps(
`<svg>${body}</svg>`,
collection,
id,
additionalProps,
() => attributes,
iconCustomizer,
)
} }
} }
return null;
} }

View File

@ -1,10 +1,54 @@
import type { Awaitable } from '@antfu/utils'; import type { Awaitable } from '@antfu/utils';
import type { FullIconCustomisations } from '../customisations';
/** /**
* Custom icon loader, used by getCustomIcon() * Custom icon loader, used by `getCustomIcon`.
*/ */
export type CustomIconLoader = (name: string) => Awaitable<string | undefined>; export type CustomIconLoader = (name: string) => Awaitable<string | undefined>;
/**
* Custom icon customizer, it will allow to customize all icons on a collection or individual icons.
*/
export type IconCustomizer = (collection: string, icon: string, props: Record<string, string>) => Awaitable<void>;
/**
* Icon customizations: will be applied to all resolved icons.
*
* For each loaded icon, the customizations will be applied in this order:
* - apply `transform` to raw `svg`, if provided and using custom icon collection
* - apply `customize` with default customizations, if provided
* - apply `iconCustomizer` with `customize` customizations, if provided
* - apply `additionalProps` with `iconCustomizer` customizations, if provided
*/
export type IconCustomizations = {
/**
* Transform raw `svg`.
*
* **WARNING**: `transform` will be only applied when using `custom` icon collection: it will be applied only when using `getCustomIcon` and excluded when using `searchForIcon`.
*
* @param svg The loaded `svg`
* @return The transformed `svg`.
*/
transform?: (svg: string) => Awaitable<string>
/**
* Change default icon customizations values.
*
* @param defaultCustomizations Default icon's customizations values.
* @return The modified icon's customizations values.
*/
customize?: (defaultCustomizations: FullIconCustomisations) => FullIconCustomisations
/**
* Custom icon customizer.
*/
iconCustomizer?: IconCustomizer
/**
* Additional icon properties.
*
* All properties without value will not be applied.
*/
additionalProps?: Record<string, string | undefined>
};
/** /**
* List of icons as object. Key is icon name, value is icon data or callback (can be async) to get icon data * List of icons as object. Key is icon name, value is icon data or callback (can be async) to get icon data
*/ */

View File

@ -1,6 +1,7 @@
import { installPackage } from '@antfu/install-pkg'; import { installPackage } from '@antfu/install-pkg';
import { sleep } from '@antfu/utils'; import { Awaitable, sleep } from '@antfu/utils';
import { cyan, yellow } from 'kolorist'; import { cyan, yellow } from 'kolorist';
import type { IconCustomizer } from './types';
const warned = new Set<string>(); const warned = new Set<string>();
@ -14,6 +15,25 @@ export function warnOnce(msg: string): void {
let pending: Promise<void> | undefined; let pending: Promise<void> | undefined;
const tasks: Record<string, Promise<void> | undefined> = {}; const tasks: Record<string, Promise<void> | undefined> = {};
export async function mergeIconProps(
svg: string,
collection: string,
icon: string,
additionalProps: Record<string, string | undefined>,
propsProvider?: () => Awaitable<Record<string, string>>,
iconCustomizer?: IconCustomizer,
): Promise<string> {
const props: Record<string, string> = await propsProvider?.() ?? {}
await iconCustomizer?.(collection, icon, props)
Object.keys(additionalProps).forEach((p) => {
const v = additionalProps[p]
if (v !== undefined && v !== null)
props[p] = v
})
const replacement = svg.startsWith('<svg ') ? '<svg ' : '<svg'
return svg.replace(replacement, `${replacement}${Object.keys(props).map(p => `${p}="${props[p]}"`).join(' ')}`)
}
export async function tryInstallPkg(name: string): Promise<void | undefined> { export async function tryInstallPkg(name: string): Promise<void | undefined> {
if (pending) { if (pending) {
await pending; await pending;

View File

@ -16,9 +16,12 @@ describe('Testing getCustomIcon', () => {
() => svg, () => svg,
'a', 'a',
'b', 'b',
(icon) => { {
transform(icon) {
return icon.replace('<svg ', '<svg width="1em" height="1em" '); return icon.replace('<svg ', '<svg width="1em" height="1em" ');
} }
}
); );
expect(result && result.indexOf('width="1em"') > -1).toBeTruthy(); expect(result && result.indexOf('width="1em"') > -1).toBeTruthy();
expect(result && result.indexOf('height="1em"') > -1).toBeTruthy(); expect(result && result.indexOf('height="1em"') > -1).toBeTruthy();