UNPKG

vueless

Version:

Vue Styleless UI Component Library, powered by Tailwind CSS.

243 lines (196 loc) 8.29 kB
import fs from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; import { vuelessConfig } from "./vuelessConfig.js"; import { COMPONENTS, GRAYSCALE_COLOR, INHERIT_COLOR, PRIMARY_COLOR, TEXT_COLOR, } from "../../constants.js"; const OPTIONAL_MARK = "?"; const CLOSING_BRACKET = "}"; const IGNORE_PROP = "@ignore"; const CUSTOM_PROP = "@custom"; const PROPS_INTERFACE_REG_EXP = /export\s+interface\s+Props(?:<[^>]+>)?\s*{([^}]*)}/s; const UNION_SYMBOLS_REG_EXP = /[?|:"|;]/g; const WORD_IN_QUOTE_REG_EXP = /"([^"]+)"/g; const DEFAULT_SAFE_COLORS = [PRIMARY_COLOR, GRAYSCALE_COLOR, INHERIT_COLOR, TEXT_COLOR]; export async function setCustomPropTypes(srcDir) { for await (const [componentName, componentDir] of Object.entries(COMPONENTS)) { let componentGlobalConfig = vuelessConfig.components?.[componentName]; if (vuelessConfig.colors && vuelessConfig.colors.length && componentGlobalConfig) { const customProps = componentGlobalConfig.props || []; const colorPropsIndex = customProps.findIndex((prop) => prop.name === "color"); const isCustomColorProp = colorPropsIndex !== -1; const modifiedCustomColorProp = isCustomColorProp ? customProps.with(colorPropsIndex, { ...customProps[colorPropsIndex], name: "color", values: [ ...new Set([ ...(customProps[colorPropsIndex]?.values || []), ...vuelessConfig.colors, ...DEFAULT_SAFE_COLORS, ]), ], }) : undefined; const customPropsWithColor = [ ...customProps, { name: "color", values: [...new Set([...vuelessConfig.colors, ...DEFAULT_SAFE_COLORS])], required: false, }, ]; componentGlobalConfig = { ...componentGlobalConfig, props: isCustomColorProp ? modifiedCustomColorProp : customPropsWithColor, }; } if (vuelessConfig.colors && vuelessConfig.colors.length && !componentGlobalConfig) { componentGlobalConfig = { props: [ { name: "color", values: [...new Set([...vuelessConfig.colors, ...DEFAULT_SAFE_COLORS])], required: false, }, ], }; } const isCustomProps = componentGlobalConfig && componentGlobalConfig.props; const isHiddenStories = componentGlobalConfig && componentGlobalConfig.storybook === false; if (isCustomProps && !isHiddenStories) { await cacheComponentTypes(path.join(srcDir, componentDir)); await modifyComponentTypes(path.join(srcDir, componentDir), componentGlobalConfig.props); } } } export async function removeCustomPropTypes(srcDir) { for await (const componentDir of Object.values(COMPONENTS)) { await restoreComponentTypes(path.join(srcDir, componentDir)); await clearComponentTypesCache(path.join(srcDir, componentDir)); } } async function cacheComponentTypes(filePath) { const cacheDir = path.join(filePath, ".cache"); const sourceFile = path.join(filePath, "types.ts"); const destFile = path.join(cacheDir, "types.ts"); if (existsSync(cacheDir)) { return; } if (existsSync(sourceFile)) { await fs.mkdir(cacheDir); await fs.cp(sourceFile, destFile); } } async function clearComponentTypesCache(filePath) { await fs.rm(path.join(filePath, ".cache"), { force: true, recursive: true }); } async function restoreComponentTypes(filePath) { const cacheDir = path.join(filePath, ".cache"); const sourceFile = path.join(cacheDir, "types.ts"); const destFile = path.join(filePath, "types.ts"); if (existsSync(sourceFile)) { await fs.copyFile(sourceFile, destFile); } } function getMultiLineUnionValues(lines, propIndex, propEndIndex) { return lines .slice(propIndex) .slice(1, propEndIndex + 1) .map((item) => item.replace(UNION_SYMBOLS_REG_EXP, "").trim()); } function getInlineUnionValues(lines, propIndex, propEndIndex) { const types = lines .slice(propIndex) .slice(0, propEndIndex + 1) .at(0) .match(WORD_IN_QUOTE_REG_EXP); return types ? types.map((value) => value.replace(/"/g, "")) : []; } /** * Updates or add a prop types dynamically. * @param {string} filePath - The path to the TypeScript file. * @param {Array} props - Array of prop objects to add or update. */ async function modifyComponentTypes(filePath, props) { try { const targetFile = path.join(filePath, "types.ts"); /* Read `types.ts` and split it by lines. */ let fileContent = await fs.readFile(targetFile, "utf-8"); const propsInterface = fileContent.match(PROPS_INTERFACE_REG_EXP)?.at(0)?.trim(); /* Remove props interface and double returns from fileContent */ fileContent = fileContent.replace(propsInterface, "").replace(/\n\s*\n/g, "\n"); const lines = propsInterface.split("\n"); for (const prop of props) { const { name, type, values = [], description, required, ignore } = prop; if (!name) return; /* Find line with prop. */ const propRegex = new RegExp(`^\\s*${name}[?:]?\\s*:`); const propIndex = lines.findIndex((line) => propRegex.test(line)); const propEndIndex = lines.slice(propIndex).findIndex((line) => line.endsWith(";")); const propTypes = propEndIndex ? getMultiLineUnionValues(lines, propIndex, propEndIndex) : getInlineUnionValues(lines, propIndex, propEndIndex); const defaultUnionType = propTypes.map((value) => `"${value}"`).join(" | "); /* Prepare prop params. */ const uniqueValues = [...new Set(values)]; const isAssignableValue = uniqueValues.every((value) => propTypes.includes(value)); const unionType = uniqueValues.map((value) => `"${value}"`).join(" | "); const userOptionalMark = required ? "" : OPTIONAL_MARK; const defaultOptionalMark = lines[propIndex]?.includes(OPTIONAL_MARK) ? OPTIONAL_MARK : ""; const optionalMark = required === undefined ? defaultOptionalMark : userOptionalMark; const isExtendOnly = lines .slice(propIndex - 2, propIndex) .join("") .includes("@extendOnly"); const propDescription = description?.replaceAll(/[\n\s]+/g, " ").trim() || "–"; // removes new lines and double spaces. const propType = unionType.length ? unionType : type; /* Add ignore JSDoc property. */ if (ignore) { const ignoreDefinition = [` * ${IGNORE_PROP}`, ` */`]; const deleteLinesCount = lines[propIndex - 2].includes(IGNORE_PROP) ? 2 : 1; lines.splice(propIndex - deleteLinesCount, deleteLinesCount, ...ignoreDefinition); } /* Check if the prop type already exists. */ if (~propIndex) { if (unionType.length && (isAssignableValue || !isExtendOnly)) { // Remove multiline union types; lines.splice(propIndex + 1, propEndIndex); lines.splice(propIndex, 1, ` ${name}${defaultOptionalMark}: ${propType};`); } if (unionType.length && isExtendOnly && !isAssignableValue) { // eslint-disable-next-line no-console console.warn(`${unionType} is not assignable to type ${defaultUnionType}.`); } const isCustomProp = lines[propIndex - 2].includes(CUSTOM_PROP); if (!isCustomProp && (type || description || required)) { // eslint-disable-next-line no-console, prettier/prettier console.warn("Changing of prop type, description or required are not allowed for the default props."); } continue; } /* Add new prop. */ const closingBracketIndex = lines.findIndex((line) => line.trim() === CLOSING_BRACKET); const propDefinition = [ "", ` /**`, ` * ${propDescription}`, ` * ${CUSTOM_PROP}`, ` */`, ` ${name}${optionalMark}: ${type};`, ]; lines.splice(closingBracketIndex, 0, ...propDefinition); } lines.unshift(fileContent); /* Update `types.ts` file. */ await fs.writeFile(targetFile, lines.join("\n"), "utf-8"); } catch (error) { // eslint-disable-next-line no-console console.error("Error updating file:", error.message, filePath); } }