vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
317 lines (266 loc) • 9.69 kB
JavaScript
/**
This scrypt find icon names from the UIcon props and objects across the project
and copy SVG icons from the default icons library (@material-symbols or other from config)
to the `VUELESS_ICONS_CACHED_DIR` folder.
*/
/* eslint-disable no-console */
import fs from "node:fs";
import path from "node:path";
import { cwd } from "node:process";
import { rm, cp } from "node:fs/promises";
import { createRequire } from "module";
import { merge } from "lodash-es";
import { vuelessConfig } from "./vuelessConfig.js";
import { getDirFiles, getComponentDefaultConfig } from "./helper.js";
import {
COMPONENTS,
VUELESS_DIR,
VUELESS_LOCAL_DIR,
VUELESS_LIBRARY,
VUELESS_ICONS_DIR,
VUELESS_ICONS_LOCAL_DIR,
VUELESS_ICONS_CACHED_DIR,
VUELESS_CONFIG_FILE_NAME,
ICONS_DIR,
} from "../../constants.js";
const DEFAULT_ICONS_DIR = path.join(cwd(), VUELESS_ICONS_DIR);
const DEFAULT_ICONS_LOCAL_DIR = path.join(cwd(), VUELESS_ICONS_LOCAL_DIR);
const CACHED_ICONS_DIR = path.join(cwd(), VUELESS_ICONS_CACHED_DIR);
const U_ICON = "UIcon";
let isDebug = false;
let isVuelessEnv = false;
let isStorybookMode = false;
let isVuelessIconsMode = false;
/**
* Dynamically find icons across the project and cache it.
* Icons cache magick happens here... ✨
* @param {string} mode
* @param {string} env
* @param {boolean} debug
* @param {Array} targetFiles
*/
export async function cacheIcons({ mode, env, debug, targetFiles = [] } = {}) {
isDebug = debug || false;
isVuelessEnv = env === "vueless";
isStorybookMode = mode === "storybook";
isVuelessIconsMode = mode === "vuelessIcons";
if (isVuelessIconsMode) {
targetFiles = isVuelessEnv
? [path.join(cwd(), VUELESS_LOCAL_DIR)]
: [path.join(cwd(), VUELESS_DIR)];
}
const commonExcludes = ["/types.ts", "/constants.ts"];
const exclude = isStorybookMode
? [...commonExcludes]
: [...commonExcludes, "/stories.js", "/stories.ts", ".d.ts"];
const vueFiles = targetFiles.map((componentPath) => getDirFiles(componentPath, ".vue"));
const jsFiles = targetFiles.map((jsFilePath) => getDirFiles(jsFilePath, ".js", { exclude }));
const tsFiles = targetFiles.map((tsFilePath) => getDirFiles(tsFilePath, ".ts", { exclude }));
const iconFiles = await Promise.all([...vueFiles, ...jsFiles, ...tsFiles]);
if ((isVuelessIconsMode && isVuelessEnv) || !isVuelessIconsMode) {
await findAndCopyIcons([
...iconFiles.flat(),
`${VUELESS_CONFIG_FILE_NAME}.js`,
`${VUELESS_CONFIG_FILE_NAME}.ts`,
]);
}
if (isVuelessIconsMode) {
await copyCachedVuelessIcons(isVuelessEnv);
}
}
/**
* Remove cached icons.
* @param {string} mirrorCacheDir
* @param {boolean} debug
* @returns {Promise<void>}
*/
export async function removeIconsCache(mirrorCacheDir, debug) {
if (fs.existsSync(CACHED_ICONS_DIR)) {
await rm(CACHED_ICONS_DIR, { recursive: true, force: true });
}
if (mirrorCacheDir) {
const mirrorCacheIconsPath = path.join(cwd(), mirrorCacheDir, ICONS_DIR);
if (fs.existsSync(mirrorCacheIconsPath)) {
await rm(mirrorCacheIconsPath, { recursive: true, force: true });
}
}
if (debug) {
console.log("Icons cache was successfully removed.");
}
}
/**
* Copy cached icons in the provided folder by path.
* @param {string} mirrorCacheDir
* @param {boolean} debug
* @returns {Promise<void>}
*/
export async function copyIconsCache(mirrorCacheDir, debug) {
const cachePath = path.join(cwd(), VUELESS_ICONS_CACHED_DIR);
if (mirrorCacheDir && fs.existsSync(cachePath)) {
await cp(cachePath, path.join(cwd(), mirrorCacheDir, ICONS_DIR), { recursive: true });
}
if (debug) {
console.log(`Vueless cached icons was successfully copied into: ${mirrorCacheDir}.`);
}
}
/**
* Copy icons which using in vueless components to the cache.
* @returns {Promise<void>}
*/
async function copyCachedVuelessIcons(isVuelessEnv) {
if (isVuelessEnv && fs.existsSync(CACHED_ICONS_DIR)) {
await cp(CACHED_ICONS_DIR, DEFAULT_ICONS_LOCAL_DIR, {
recursive: true,
});
}
if (!isVuelessEnv && fs.existsSync(DEFAULT_ICONS_DIR)) {
await cp(DEFAULT_ICONS_DIR, CACHED_ICONS_DIR, {
recursive: true,
});
}
}
/**
* Scan the project for icon names and copy found icons to the cache.
* @param {Array} files
*/
async function findAndCopyIcons(files) {
const defaults = await getDefaults();
const safelistIcons = vuelessConfig.components?.[U_ICON]?.safelistIcons;
safelistIcons?.forEach((iconName) => {
copyIcon(iconName, defaults);
});
files.forEach((file) => {
const fileContents = fs.existsSync(file) ? fs.readFileSync(file).toString() : "";
/* Objects across the project */
const iconNameRegex = /\w*(icon)\w*:\s*["']([^"'\s]+)["']/gi;
const objectMatchNameArray = fileContents.match(iconNameRegex);
if (objectMatchNameArray) {
for (const match of objectMatchNameArray) {
const iconNameMatch = iconNameRegex.exec(match);
const iconName = iconNameMatch && iconNameMatch[2];
try {
if (iconName) {
copyIcon(iconName, defaults);
}
} catch (error) {
isDebug && console.log(error);
}
iconNameRegex.lastIndex = 0;
}
}
/* Vueless components props */
const iconPropsPattern = `\\b\\w*(name|icon)\\w*\\s*=\\s*(['"])(.*?)\\2`;
const uComponentIconNamePattern =
/<U\w+\b[^>]*?\b\w*(name|icon)\w*\s*[:=]\s*(['"])(.*?)\2[^>]*?>/;
const uComponentIconNameArray = fileContents.match(new RegExp(uComponentIconNamePattern, "g"));
if (!uComponentIconNameArray) return;
for (const match of uComponentIconNameArray) {
const groupMatch = match.match(new RegExp(iconPropsPattern));
const iconName = groupMatch ? groupMatch[3] : null;
try {
if (!iconName) continue;
if (iconName?.includes("?") && iconName?.includes(":")) {
const [trueName, falseName] = getTernaryValues(iconName);
copyIcon(trueName, defaults);
copyIcon(falseName, defaults);
} else {
copyIcon(iconName, defaults);
}
} catch (error) {
isDebug && console.log(error);
}
}
});
}
/**
* Retrieve values from ternary of strings.
* @param {string} expression
* @returns {Array} of ternary values
*/
function getTernaryValues(expression) {
const [, values] = expression
.replace(/\s/g, "") // newlines and spaces
.replace(/\?\./g, "") // conditional chaining `?.`
.replace(/['"]/g, "") // single and double quotes
.split("?");
const [trueValue, falseValue] = values.split(":");
return [trueValue, falseValue];
}
/**
* Copy icon from icon package into cache folder.
* @param {string} name
* @param {object} defaults
*/
function copyIcon(name, defaults) {
name = name.toLowerCase();
const iconNameRegex = /^[a-z0-9_-]+$/;
/* Stop the scrypt if the icon name is irrelevant. */
if (!iconNameRegex.test(name)) {
return;
}
const { source, destination } = getIconLibraryPaths(name, defaults);
if (!fs.existsSync(source) || fs.existsSync(destination)) {
return;
}
const require = createRequire(import.meta.url);
const destDir = path.dirname(destination);
fs.mkdirSync(destDir, {
recursive: true,
});
fs.copyFile(require.resolve(source), destination, (error) => {
if (isDebug) {
error
? console.error(`Error copying icon "${name}":`, error)
: console.log(`Icon "${name}" copied successfully!`);
}
});
}
/**
* Build a path to the icon source in the selected icon library and cache destination path.
* @param {string} name
* @param {object} defaults
* @returns {source: string, destination: string}
*/
function getIconLibraryPaths(name, defaults) {
const customLibraryPath = defaults.path;
const library = defaults.library;
const weight = defaults.weight;
const style = defaults.style;
/* eslint-disable prettier/prettier */
const libraries = {
[VUELESS_LIBRARY]: {
// @material-symbols icons which used across the components (this works only at Vueless env).
source: `${cwd()}/node_modules/${library}/svg-${weight}/${style}/${name}.svg`,
destination: `${CACHED_ICONS_DIR}/${VUELESS_LIBRARY}/${name}.svg`
},
"@material-symbols": {
source: `${cwd()}/node_modules/${library}/svg-${weight}/${style}/${name}.svg`,
destination: `${CACHED_ICONS_DIR}/${library}/svg-${weight}/${style}/${name}.svg`
},
"bootstrap-icons": {
source: `${cwd()}/node_modules/${library}/icons/${name}.svg`,
destination: `${CACHED_ICONS_DIR}/${library}/${name}.svg`
},
"heroicons": {
source: `${cwd()}/node_modules/${library}/24/${name.endsWith("-fill") ? "solid" : "outline"}/${name}.svg`,
destination: `${CACHED_ICONS_DIR}/${library}/${name.endsWith("-fill") ? "solid" : "outline"}/${name}.svg`
},
"custom-icons": {
source: `${cwd()}/${customLibraryPath}/${name}.svg`,
destination: `${CACHED_ICONS_DIR}/${library}/${name}.svg`
},
};
/* eslint-enable prettier/prettier */
const libraryName = isVuelessIconsMode && isVuelessEnv ? VUELESS_LIBRARY : library;
return libraries[libraryName];
}
/**
* Merge global and local defaults config for UIcon.
* @returns {Object}
*/
async function getDefaults() {
const defaultIconsDir = isVuelessEnv ? VUELESS_LOCAL_DIR : VUELESS_DIR;
const defaultConfigPath = path.join(cwd(), defaultIconsDir, COMPONENTS[U_ICON], "config.ts");
const uIconDefaultConfig = await getComponentDefaultConfig(U_ICON, defaultConfigPath);
return merge({}, uIconDefaultConfig?.defaults, vuelessConfig.components?.[U_ICON]?.defaults);
}