2
0
mirror of https://github.com/iconify/iconify.git synced 2024-11-09 23:00:56 +00:00

feat: allow external collections packages

This commit is contained in:
userquin 2024-01-26 18:03:02 +01:00
parent 0d3e578b45
commit a14a3c4b0e
15 changed files with 1542 additions and 946 deletions

View File

@ -277,6 +277,11 @@
"require": "./lib/loader/custom.cjs",
"import": "./lib/loader/custom.mjs"
},
"./lib/loader/external-pkg": {
"types": "./lib/loader/external-pkg.d.ts",
"require": "./lib/loader/external-pkg.cjs",
"import": "./lib/loader/external-pkg.mjs"
},
"./lib/loader/fs": {
"types": "./lib/loader/fs.d.ts",
"require": "./lib/loader/fs.cjs",
@ -409,9 +414,10 @@
"debug": "^4.3.4",
"kolorist": "^1.8.0",
"local-pkg": "^0.4.3"
},
},
"devDependencies": {
"@iconify-json/flat-color-icons": "^1.1.6",
"@test-scope/test-color-icons": "file:./tests/fixtures/@test-scope/test-color-icons",
"@types/debug": "^4.1.8",
"@types/jest": "^29.5.3",
"@types/node": "^18.17.1",
@ -419,6 +425,7 @@
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.9.0",
"eslint-plugin-prettier": "^5.0.0",
"plain-color-icons": "file:./tests/fixtures/plain-color-icons",
"rimraf": "^5.0.1",
"typescript": "^5.1.6",
"unbuild": "^1.2.1",

View File

@ -79,6 +79,7 @@ export { getIconsCSS, getIconsContentCSS } from './css/icons';
export type {
CustomIconLoader,
CustomCollections,
ExternalPkgInfo,
IconCustomizer,
IconCustomizations,
IconifyLoaderOptions,

View File

@ -0,0 +1,71 @@
import type { AutoInstall, CustomIconLoader, ExternalPkgInfo } from './types';
import { loadCollectionFromFS } from './fs';
import { searchForIcon } from './modern';
import { warnOnce } from './warn';
/**
* Creates a CustomIconLoader collection from an external scoped package collection.
*
* @param packageName The scoped package name.
* @param autoInstall {AutoInstall} [autoInstall=false] - whether to automatically install
*/
export function createExternalPackageIconLoader(
packageName: ExternalPkgInfo,
autoInstall: AutoInstall = false
) {
let scope: string;
let collection: string;
const collections: Record<string, CustomIconLoader> = {};
if (typeof packageName === 'string') {
if (packageName.length === 0) {
warnOnce(`invalid package name, it is empty`);
return collections;
}
if (packageName[0] === '@') {
if (packageName.indexOf('/') === -1) {
warnOnce(`invalid scoped package name "${packageName}"`);
return collections;
}
[scope, collection] = packageName.split('/');
} else {
scope = '';
collection = packageName;
}
} else {
[scope, collection] = packageName;
}
collections[collection] = createCustomIconLoader(
scope,
collection,
autoInstall
);
return collections;
}
function createCustomIconLoader(
scope: string,
collection: string,
autoInstall: AutoInstall
) {
// create the custom collection loader
const iconSetPromise = loadCollectionFromFS(collection, autoInstall, scope);
return <CustomIconLoader>(async (icon) => {
// await until the collection is loaded
const iconSet = await iconSetPromise;
// copy/paste from ./node-loader.ts
let result: string | undefined;
if (iconSet) {
// possible icon names
const ids = [
icon,
icon.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(),
icon.replace(/([a-z])(\d+)/g, '$1-$2'),
];
result = await searchForIcon(iconSet, collection, ids);
}
return result;
});
}

View File

@ -1,5 +1,5 @@
import { promises as fs, Stats } from 'fs';
import { isPackageExists, resolveModule } from 'local-pkg';
import { isPackageExists, resolveModule, importModule } from 'local-pkg';
import type { IconifyJSON } from '@iconify/types';
import { tryInstallPkg } from './install-pkg';
import type { AutoInstall } from './types';
@ -7,9 +7,18 @@ import type { AutoInstall } from './types';
const _collections: Record<string, Promise<IconifyJSON | undefined>> = {};
const isLegacyExists = isPackageExists('@iconify/json');
/**
* Asynchronously loads a collection from the file system.
*
* @param name {string} the name of the collection, e.g. 'mdi'
* @param autoInstall {AutoInstall} [autoInstall=false] - whether to automatically install
* @param scope {string} [scope='@iconify-json'] - the scope of the collection, e.g. '@my-company-json'
* @return {Promise<IconifyJSON | undefined>} the loaded IconifyJSON or undefined
*/
export async function loadCollectionFromFS(
name: string,
autoInstall: AutoInstall = false
autoInstall: AutoInstall = false,
scope = '@iconify-json'
): Promise<IconifyJSON | undefined> {
if (!(await _collections[name])) {
_collections[name] = task();
@ -17,16 +26,40 @@ export async function loadCollectionFromFS(
return _collections[name];
async function task() {
let jsonPath = resolveModule(`@iconify-json/${name}/icons.json`);
if (!jsonPath && isLegacyExists) {
jsonPath = resolveModule(`@iconify/json/json/${name}.json`);
const packageName = scope.length === 0 ? name : `${scope}/${name}`;
let jsonPath = resolveModule(`${packageName}/icons.json`);
// Legacy support for @iconify/json
if (scope === '@iconify-json') {
if (!jsonPath && isLegacyExists) {
jsonPath = resolveModule(`@iconify/json/json/${name}.json`);
}
// Try to install the package if it doesn't exist
if (!jsonPath && !isLegacyExists && autoInstall) {
await tryInstallPkg(packageName, autoInstall);
jsonPath = resolveModule(`${packageName}/icons.json`);
}
} else if (!jsonPath && autoInstall) {
await tryInstallPkg(packageName, autoInstall);
jsonPath = resolveModule(`${packageName}/icons.json`);
}
if (!jsonPath && !isLegacyExists && autoInstall) {
await tryInstallPkg(`@iconify-json/${name}`, autoInstall);
jsonPath = resolveModule(`@iconify-json/${name}/icons.json`);
// Try to import module if it exists
if (!jsonPath) {
let packagePath = resolveModule(packageName);
if (packagePath?.match(/^[a-z]:/i)) {
packagePath = `file:///${packagePath}`.replace(/\\/g, '/');
}
if (packagePath) {
const { icons }: { icons?: IconifyJSON } = await importModule(
packagePath
);
if (icons) return icons;
}
}
// Load from file
let stat: Stats | undefined;
try {
stat = jsonPath ? await fs.lstat(jsonPath) : undefined;

View File

@ -2,6 +2,11 @@ import type { Awaitable } from '@antfu/utils';
import type { FullIconCustomisations } from '../customisations/defaults';
import type { IconifyJSON } from '@iconify/types';
/**
* The external scoped package name: e.g. @my-collections/collection-a.
*/
export type ExternalPkgInfo = string | [name: string, collection: string];
/**
* Type for universal icon loader.
*/

View File

@ -0,0 +1,21 @@
import { describe } from 'vitest';
import { loadNodeIcon } from '../lib/loader/node-loader';
import { createExternalPackageIconLoader } from '../lib/loader/external-pkg';
describe('external-pkg', () => {
test('loadNodeIcon works with importModule and plain package name', async () => {
const result = await loadNodeIcon('plain-color-icons', 'about', {
customCollections:
createExternalPackageIconLoader('plain-color-icons'),
});
expect(result).toBeTruthy();
});
test('loadNodeIcon works with importModule and scoped package name', async () => {
const result = await loadNodeIcon('test-color-icons', 'about', {
customCollections: createExternalPackageIconLoader(
'@test-scope/test-color-icons'
),
});
expect(result).toBeTruthy();
});
});

View File

@ -0,0 +1,13 @@
import type {
IconifyJSON,
IconifyInfo,
IconifyMetaData,
IconifyChars,
} from '@iconify/types';
export { IconifyJSON, IconifyInfo, IconifyMetaData, IconifyChars };
export declare const icons: IconifyJSON;
export declare const info: IconifyInfo;
export declare const metadata: IconifyMetaData;
export declare const chars: IconifyChars;

View File

@ -0,0 +1,28 @@
/* eslint-disable prettier/prettier */
exports.icons = {
prefix: 'test-color-icons',
icons: {
about: {
body: '<path fill="#2196F3" d="M37 40H11l-6 6V12c0-3.3 2.7-6 6-6h26c3.3 0 6 2.7 6 6v22c0 3.3-2.7 6-6 6z"/><g fill="#fff"><path d="M22 20h4v11h-4z"/><circle cx="24" cy="15" r="2"/></g>',
},
},
lastModified: 1672652184,
width: 48,
height: 48,
};
exports.info = {
prefix: 'test-color-icons',
name: 'Test Color Icons',
total: 1,
author: {
name: 'test',
},
license: {
title: 'MIT',
},
samples: ['about'],
height: 32,
displayHeight: 16,
};
exports.metadata = {};
exports.chars = {};

View File

@ -0,0 +1,31 @@
/* eslint-disable prettier/prettier */
const icons = {
prefix: 'test-color-icons',
icons: {
about: {
body: '<path fill="#2196F3" d="M37 40H11l-6 6V12c0-3.3 2.7-6 6-6h26c3.3 0 6 2.7 6 6v22c0 3.3-2.7 6-6 6z"/><g fill="#fff"><path d="M22 20h4v11h-4z"/><circle cx="24" cy="15" r="2"/></g>'
}
},
lastModified: 1672652184,
width: 48,
height: 48
}
const info = {
prefix: 'test-color-icons',
name: 'Test Color Icons',
total: 1,
author: {
name: 'test'
},
license: {
title: 'MIT'
},
samples: ['about'],
height: 32,
displayHeight: 16
}
const metadata = {}
const chars = {}
export { icons, info, metadata, chars }

View File

@ -0,0 +1,13 @@
{
"name": "@test-scope/test-color-icons",
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"exports": {
"./*": "./*",
".": {
"require": "./index.js",
"import": "./index.mjs"
}
}
}

View File

@ -0,0 +1,13 @@
import type {
IconifyJSON,
IconifyInfo,
IconifyMetaData,
IconifyChars,
} from '@iconify/types';
export { IconifyJSON, IconifyInfo, IconifyMetaData, IconifyChars };
export declare const icons: IconifyJSON;
export declare const info: IconifyInfo;
export declare const metadata: IconifyMetaData;
export declare const chars: IconifyChars;

View File

@ -0,0 +1,28 @@
/* eslint-disable prettier/prettier */
exports.icons = {
prefix: 'test-color-icons',
icons: {
about: {
body: '<path fill="#2196F3" d="M37 40H11l-6 6V12c0-3.3 2.7-6 6-6h26c3.3 0 6 2.7 6 6v22c0 3.3-2.7 6-6 6z"/><g fill="#fff"><path d="M22 20h4v11h-4z"/><circle cx="24" cy="15" r="2"/></g>',
},
},
lastModified: 1672652184,
width: 48,
height: 48,
};
exports.info = {
prefix: 'test-color-icons',
name: 'Test Color Icons',
total: 1,
author: {
name: 'test',
},
license: {
title: 'MIT',
},
samples: ['about'],
height: 32,
displayHeight: 16,
};
exports.metadata = {};
exports.chars = {};

View File

@ -0,0 +1,31 @@
/* eslint-disable prettier/prettier */
const icons = {
prefix: 'test-color-icons',
icons: {
about: {
body: '<path fill="#2196F3" d="M37 40H11l-6 6V12c0-3.3 2.7-6 6-6h26c3.3 0 6 2.7 6 6v22c0 3.3-2.7 6-6 6z"/><g fill="#fff"><path d="M22 20h4v11h-4z"/><circle cx="24" cy="15" r="2"/></g>'
}
},
lastModified: 1672652184,
width: 48,
height: 48
}
const info = {
prefix: 'test-color-icons',
name: 'Test Color Icons',
total: 1,
author: {
name: 'test'
},
license: {
title: 'MIT'
},
samples: ['about'],
height: 32,
displayHeight: 16
}
const metadata = {}
const chars = {}
export { icons, info, metadata, chars }

View File

@ -0,0 +1,13 @@
{
"name": "plain-color-icons",
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"exports": {
"./*": "./*",
".": {
"require": "./index.js",
"import": "./index.mjs"
}
}
}

File diff suppressed because it is too large Load Diff