2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-28 04:59:07 +00:00

chore: restructure tailwind plugin

This commit is contained in:
Vjacheslav Trushkin 2024-05-06 21:23:05 +03:00
parent 879f21637b
commit ca87add889
8 changed files with 271 additions and 155 deletions

View File

@ -1,7 +1,7 @@
import { getIconsCSSData } from '@iconify/utils/lib/css/icons';
import { loadIconSet } from './loader';
import { getIconNames } from './names';
import type { CleanIconifyPluginOptions } from './options';
import { loadIconSet } from './helpers/loader';
import { getIconNames } from './helpers/names';
import type { CleanIconifyPluginOptions } from './helpers/options';
/**
* Get CSS rules for icons list
@ -17,7 +17,7 @@ export function getCSSRulesForIcons(
// Parse all icon sets
for (const prefix in prefixes) {
const iconSet = loadIconSet(prefix, options);
const iconSet = loadIconSet(options.iconSets?.[prefix] || prefix);
if (!iconSet) {
throw new Error(
`Cannot load icon set for "${prefix}". Install "@iconify-json/${prefix}" as dev dependency?`

View File

@ -1,7 +1,7 @@
import { getIconsCSSData } from '@iconify/utils/lib/css/icons';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import { loadIconSet } from './loader';
import type { DynamicIconifyPluginOptions } from './options';
import { loadIconSet } from './helpers/loader';
import type { DynamicIconifyPluginOptions } from './helpers/options';
/**
* Get dynamic CSS rules
@ -20,7 +20,7 @@ export function getDynamicCSSRules(
throw new Error(`Invalid icon name: "${icon}"`);
}
const iconSet = loadIconSet(prefix, options);
const iconSet = loadIconSet(options.iconSets?.[prefix] || prefix);
if (!iconSet) {
throw new Error(
`Cannot load icon set for "${prefix}". Install "@iconify-json/${prefix}" as dev dependency?`
@ -35,11 +35,11 @@ export function getDynamicCSSRules(
}
if (scale) {
generated.common.rules.height = scale + 'em'
generated.common.rules.width = scale + 'em'
generated.common.rules.height = scale + 'em';
generated.common.rules.width = scale + 'em';
} else {
delete generated.common.rules.height
delete generated.common.rules.width
delete generated.common.rules.height;
delete generated.common.rules.width;
}
return {

View File

@ -0,0 +1,112 @@
import { readFileSync } from 'node:fs';
import type { IconifyJSON } from '@iconify/types';
import { IconifyIconSetSource } from './options';
import { matchIconName } from '@iconify/utils/lib/icon/name';
/**
* Locate icon set
*/
interface LocatedIconSet {
main: string;
info?: string;
}
export function locateIconSet(prefix: string): LocatedIconSet | undefined {
// Try `@iconify-json/{$prefix}`
try {
const main = require.resolve(`@iconify-json/${prefix}/icons.json`);
const info = require.resolve(`@iconify-json/${prefix}/info.json`);
return {
main,
info,
};
} catch {}
// Try `@iconify/json`
try {
const main = require.resolve(`@iconify/json/json/${prefix}.json`);
return {
main,
};
} catch {}
}
/**
* Cache for loaded icon sets
*
* Tailwind CSS can send multiple separate requests to plugin, this will
* prevent same data from being loaded multiple times.
*
* Key is filename, not prefix!
*/
const cache = Object.create(null) as Record<string, IconifyJSON>;
/**
* Load icon set from file
*/
function loadIconSetFromFile(source: LocatedIconSet): IconifyJSON | undefined {
try {
const result = JSON.parse(readFileSync(source.main, 'utf8'));
if (!result.info && source.info) {
// Load info from a separate file
result.info = JSON.parse(readFileSync(source.info, 'utf8'));
}
return result;
} catch {}
}
/**
* Load icon set from source
*/
export function loadIconSet(
source: IconifyIconSetSource
): IconifyJSON | undefined {
let filename: LocatedIconSet;
if (typeof source === 'function') {
// Callback
return source();
}
if (typeof source === 'object') {
// IconifyJSON
return source;
}
// String
// Try to parse JSON
if (source.startsWith('{')) {
try {
return JSON.parse(source);
} catch {
// Invalid JSON
}
}
// Check for cache
if (cache[source]) {
return cache[source];
}
// Icon set prefix
if (source.match(matchIconName)) {
const filename = locateIconSet(source);
if (filename) {
// Load icon set
const result = loadIconSetFromFile(filename);
if (result) {
cache[source] = result;
}
return result;
}
}
// Filename
const result = loadIconSetFromFile({
main: source,
});
if (result) {
cache[source] = result;
}
return result;
}

View File

@ -0,0 +1,96 @@
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
import type { IconifyJSON } from '@iconify/types';
// Callback for loading icon set
type IconifyJSONLoaderCallback = () => IconifyJSON;
// Source for icon set: icon set, filename, or callback that loads icon set
export type IconifyIconSetSource =
| IconifyJSON
| string
| IconifyJSONLoaderCallback;
/**
* Common options
*/
export interface CommonIconifyPluginOptions {
// Custom icon sets
// Value can be loaded icon set or callback that loads icon set
iconSets?: Record<string, IconifyIconSetSource>;
}
/**
* Options for clean class names
*/
export interface CleanIconifyPluginOptions
extends CommonIconifyPluginOptions,
IconCSSIconSetOptions {
//
}
/**
* Options for dynamic class names
*/
export interface DynamicIconifyPluginOptions
extends CommonIconifyPluginOptions {
// Class prefix
prefix?: string;
// Include icon-specific selectors only
overrideOnly?: true;
// Sets the default height/width value (ex. scale: 2 = 2em)
scale?: number;
}
/**
* Options for reusable selectors
*/
export interface ReusableIconifyPluginOptions {
// Selector for mask, defaults to ".iconify"
mask?: string;
// Selector for background, defaults to ".iconify-color"
background?: string;
// Variable name, defaults to "svg"
varName?: string;
}
/**
* Options for main plugin
*/
// Icons to include: array of names or callback
type IconsListOption = string[] | ((name: string) => boolean);
// Source filename or icon set
type IconSetSource = string | IconifyJSON;
// Full icon set options
interface IconSetOptions {
// Prefix, required if `source` is not set
// If both `source` and `prefix` are set, `prefix` will be used
prefix?: string;
// Source
source?: IconSetSource;
// Icons to load
icons?: IconsListOption;
}
// Array of icon sets to load
type IconifyPluginListOptions = (string | IconSetOptions)[];
// Full object
export interface IconifyPluginOptionsObject
extends ReusableIconifyPluginOptions {
// Prefixes to load
prefixes: IconifyPluginListOptions;
}
// Full options
export type IconifyPluginOptions =
| IconifyPluginOptionsObject
| IconifyPluginListOptions;

View File

@ -1,109 +0,0 @@
import { readFileSync } from 'fs';
import type { IconifyJSON } from '@iconify/types';
/**
* Callback for loading icon set
*/
type IconifyJSONLoaderCallback = () => IconifyJSON;
/**
* Options for icon set loaders
*/
export interface IconifyPluginLoaderOptions {
// Custom icon sets
// Value can be loaded icon set or callback that loads icon set
iconSets?: Record<string, IconifyJSON | string | IconifyJSONLoaderCallback>;
}
/**
* Locate icon set
*/
interface LocatedIconSet {
main: string;
info?: string;
}
export function locateIconSet(prefix: string): LocatedIconSet | undefined {
try {
const main = require.resolve(`@iconify-json/${prefix}/icons.json`);
const info = require.resolve(`@iconify-json/${prefix}/info.json`);
return {
main,
info,
};
} catch {}
try {
const main = require.resolve(`@iconify/json/json/${prefix}.json`);
return {
main,
};
} catch {}
}
/**
* Cache for loaded icon sets
*
* Tailwind CSS can send multiple separate requests to plugin, this will
* prevent same data from being loaded multiple times.
*
* Key is filename, not prefix!
*/
const cache = Object.create(null) as Record<string, IconifyJSON>;
/**
* Load icon set
*/
export function loadIconSet(
prefix: string,
options: IconifyPluginLoaderOptions
): IconifyJSON | undefined {
let filename: LocatedIconSet;
// Check for custom icon set
const customIconSet = options.iconSets?.[prefix];
if (customIconSet) {
switch (typeof customIconSet) {
case 'function': {
// Callback. Store result in options to avoid loading it again
const result = customIconSet();
options.iconSets[prefix] = result;
return result;
}
case 'string': {
// Filename to load it from
filename = {
main: customIconSet,
};
break;
}
default:
return customIconSet;
}
} else {
// Find icon set
filename = locateIconSet(prefix);
}
if (!filename) {
return;
}
const main = typeof filename === 'string' ? filename : filename.main;
// Check for cache
if (cache[main]) {
return cache[main];
}
// Attempt to load it
try {
const result = JSON.parse(readFileSync(main, 'utf8'));
if (!result.info && typeof filename === 'object' && filename.info) {
// Load info from a separate file
result.info = JSON.parse(readFileSync(filename.info, 'utf8'));
}
cache[main] = result;
return result;
} catch {}
}

View File

@ -1,33 +0,0 @@
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
import type { IconifyPluginLoaderOptions } from './loader';
/**
* Common options
*/
export interface CommonIconifyPluginOptions extends IconifyPluginLoaderOptions {
//
}
/**
* Options for clean class names
*/
export interface CleanIconifyPluginOptions
extends CommonIconifyPluginOptions,
IconCSSIconSetOptions {
//
}
/**
* Options for dynamic class names
*/
export interface DynamicIconifyPluginOptions
extends CommonIconifyPluginOptions {
// Class prefix
prefix?: string;
// Include icon-specific selectors only
overrideOnly?: true;
// Sets the default height/width value (ex. scale: 2 = 2em)
scale?: number
}

View File

@ -4,7 +4,10 @@ import { getDynamicCSSRules } from './dynamic';
import type {
CleanIconifyPluginOptions,
DynamicIconifyPluginOptions,
} from './options';
IconifyPluginOptions,
IconifyPluginOptionsObject,
} from './helpers/options';
import { getCommonCSSRules } from '@iconify/utils/lib/css/common';
/**
* Generate styles for dynamic selector: class="icon-[mdi-light--home]"
@ -38,7 +41,54 @@ export function addCleanIconSelectors(
});
}
/**
* Iconify plugin
*
* TODO: export it when ready
*/
function iconifyPlugin(options: IconifyPluginOptions) {
return plugin(({ addUtilities }) => {
const rules = Object.create(null) as Record<
string,
Record<string, string>
>;
// Convert options to object
const fullOptions: IconifyPluginOptionsObject = Array.isArray(options)
? {
prefixes: options,
}
: options;
// Variable name, default to 'svg' (cannot be empty string)
const varName = fullOptions.varName || 'svg';
// Add common rules
const mask = fullOptions.mask ?? '.iconify';
const background = fullOptions.background ?? '.iconify-color';
if (mask) {
rules[mask] = getCommonCSSRules({
mode: 'mask',
varName,
});
}
if (background) {
rules[background] = getCommonCSSRules({
mode: 'background',
varName,
});
}
addUtilities(rules);
// TODO: add icon sets
});
}
/**
* Export types
*/
export type { CleanIconifyPluginOptions, DynamicIconifyPluginOptions };
export type {
CleanIconifyPluginOptions,
DynamicIconifyPluginOptions,
IconifyPluginOptions,
};