vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
416 lines (346 loc) • 11 kB
text/typescript
import { COMPONENTS } from "../constants";
import type {
WebTypes,
Tag,
Attribute,
EventProperty,
SlotBinding,
ExposeProperty,
UnknownObject,
} from "../types";
interface Types {
[key: string]: ArgType | undefined;
}
export interface ArgType {
control?: "text" | "date" | "number" | "boolean" | "array" | "select" | "object" | false;
options?: string[];
table?: TableConfig;
name?: string;
description?: string;
type?: { required?: boolean } | string | null;
action?: string;
}
interface TableConfig {
disable?: boolean;
defaultValue?: { summary: unknown };
category?: "props" | "slots" | "expose" | "Storybook Events";
type?: Record<string, string | string[]>;
}
type ComponentNames = keyof typeof COMPONENTS;
/* Load Web-Types from cache. */
const [webTypes]: WebTypes[] = Object.values(
import.meta.glob("/node_modules/.cache/vueless/web-types.json", {
eager: true,
import: "default",
}),
);
const getComponentData = (componentName: ComponentNames) => {
const component = webTypes?.contributions?.html?.tags?.find(
(item: Tag) => item.name === componentName,
);
if (!component) {
// eslint-disable-next-line no-console
console.warn(
"The component docs data is missing. Probably the `web-types.json` file is missing or have incorrect format.",
);
}
return component;
};
export function getSlotNames(componentName: string | undefined) {
if (!componentName) return;
return getComponentData(componentName as ComponentNames)?.slots?.map((item) => item.name);
}
export function getArgTypes(componentName: string | undefined) {
if (!componentName) return;
const component = getComponentData(componentName as ComponentNames);
if (!component) return;
const types: Partial<Types> = {
// Hide default template arg in docs.
defaultTemplate: { table: { disable: true } },
// Hide slot template arg in docs.
slotTemplate: { table: { disable: true } },
};
component.attributes?.forEach((attribute: Attribute) => {
const type = attribute.value.type;
if (attribute.enum) {
attribute.enum = attribute.enum.map((item) => {
if (item === "UnknownObject") return "Object";
if (item === "UnknownArray") return "Array";
return item;
});
}
const nonUnionTypes = [
"null",
"string",
"number",
"boolean",
"Date",
"Array",
"Object",
"UnknownArray",
"UnknownObject",
];
if (attribute.enum?.some((value) => !nonUnionTypes.includes(value))) {
types[attribute.name] = {
options: attribute.enum,
control: "select",
table: {
type: { summary: attribute.enum.join(" | ") },
defaultValue: { summary: attribute.default || "" },
},
};
} else if (attribute.enum?.some((value) => nonUnionTypes.includes(value))) {
let control = attribute.enum[0];
if (control === "string") {
control = "text";
}
if (control === "Date") {
control = "date";
}
types[attribute.name] = {
control: control as ArgType["control"],
table: {
type: { summary: attribute.enum.join(" | ") },
defaultValue: { summary: attribute.default || "" },
},
};
}
if (attribute.enum?.length === 1) {
types[attribute.name] = {
control: "object",
table: {
type: { summary: attribute.enum.join(" | ") },
defaultValue: { summary: attribute.default || "" },
},
};
}
if (type === "string") {
types[attribute.name] = {
control: "text",
table: {
type: { summary: "string" },
defaultValue: { summary: attribute.default || "" },
},
};
}
if (type === "number") {
types[attribute.name] = {
control: "number",
table: {
type: { summary: "number" },
defaultValue: { summary: attribute.default || "" },
},
};
}
if (type === "Date") {
types[attribute.name] = {
control: "date",
table: {
type: { summary: "Date" },
defaultValue: { summary: attribute.default || "" },
},
};
}
if (type === "TModelValue") {
types[attribute.name] = {
control: "date",
table: {
defaultValue: { summary: attribute.default || "" },
type: {
summary: ["string", "Date", "from: string, to: string", "from: Date, to: Date"].join(
" | ",
),
},
},
};
}
if (type === "boolean") {
types[attribute.name] = {
control: "boolean",
table: {
type: { summary: "boolean" },
defaultValue: { summary: attribute.default || "" },
},
};
}
if (type === "array") {
types[attribute.name] = {
control: "array",
table: {
type: { summary: "array" },
defaultValue: { summary: attribute.default || [] },
},
};
}
/* Hide ignored props. */
if (attribute.description?.includes("@ignore")) {
types[attribute.name] = {
table: {
disable: true,
},
};
}
/* Hide strange undefiled argType. */
types["undefined"] = {
table: {
disable: true,
},
};
/* Applying category, required and description for all props. */
types[attribute.name] = {
...types[attribute.name],
table: {
...types[attribute.name]?.table,
category: "props",
},
type: {
required: attribute.required,
},
description: attribute.description,
};
});
component.slots?.forEach((slot) => {
const bindings: string[] = [];
slot.bindings?.forEach((binding: SlotBinding) => {
if (binding.name === "name") return;
const description = binding.description ? ` (${binding.description})` : "";
bindings.push(`${binding.name}: ${binding.type}${description}`);
});
types[`${slot.name}Slot`] = {
name: slot.name,
description: slot.description,
type: slot.bindings ? `{ ${bindings.join(", ")} }` : null,
control: "text",
table: { category: "slots" },
};
// Hide autogenerated slot docs, but keep props with the same name
if (!component.attributes?.map((item) => item.name)?.includes(slot.name)) {
types[slot.name] = {
table: {
disable: true,
},
};
}
});
component.exposes?.forEach((expose) => {
const properties: string[] = [];
expose.properties?.forEach((property: ExposeProperty) => {
const description = property.description ? ` (${property.description})` : "";
properties.push(`${property.type}${description}`);
});
types[`${expose.name}Expose`] = {
type: expose.properties ? properties.join(", ") : null,
name: expose.name,
description: expose.description,
control: false,
table: { category: "expose" },
};
// Hide autogenerated expose docs, but keep props with the same name
if (!component.attributes?.map((item) => item.name)?.includes(expose.name)) {
types[expose.name] = {
table: {
disable: true,
},
};
}
});
component.events?.forEach((event) => {
const properties: string[] = [];
event.properties?.forEach((property: EventProperty) => {
const description = property.description ? ` (${property.description})` : "";
properties.push(`${property.name}: ${property.type}${description}`);
});
types[event.name] = {
type: event.properties ? properties.join(", ") : null,
name: event.name,
description: event.description,
};
if (import.meta.env.DEV) {
const eventName = "on" + event.name.charAt(0).toUpperCase() + event.name.slice(1);
types[eventName] = {
action: event.name,
table: { category: "Storybook Events" },
};
}
});
return types;
}
export function getSource(defaultConfig: string) {
return defaultConfig.replace("export default /*tw*/ ", "").replace(";", "");
}
export function getSlotsFragment(defaultTemplate: string) {
return `
<template v-for="(slot, index) of slots" :key="index" v-slot:[slot]>
<template v-if="slot === 'default' && !args['defaultSlot']">${defaultTemplate || ""}</template>
<template v-else-if="slot === 'default' && args['defaultSlot']">{{ args['defaultSlot'] }}</template>
<template v-else-if="args[slot + 'Slot']">{{ args[slot + 'Slot'] }}</template>
</template>
`;
}
/**
* Create story param config to show component description with a link on GitHub.
*/
export function getDocsDescription(componentName: string | undefined, afterText = "") {
if (!componentName) {
return {};
}
let viewOnGitHub = "";
if (COMPONENTS[componentName as ComponentNames]) {
viewOnGitHub = `| <a href="https://github.com/vuelessjs/vueless/tree/main/src/${COMPONENTS[componentName as ComponentNames]}" target="_blank">View on GitHub</a><br/>${afterText}`;
}
return {
description: {
component: `The \`${componentName}\` component. ${viewOnGitHub}`,
},
};
}
/**
* Generate args (props) for templates with loops.
*/
export function getArgs(args: UnknownObject, option: string, outerOption?: string) {
const outerEnumProps: UnknownObject = {};
const enumProps: UnknownObject = {};
Object.entries(args)
.filter(([, value]) => JSON.stringify(value)?.includes("{enumValue}"))
.map(([key]) => key)
.forEach((key) => {
enumProps[key] = option;
const isNotPrimitive =
Object.keys(args[key] ?? {}).length || (Array.isArray(args[key]) && args[key].length);
if (key in args && isNotPrimitive) {
const replacedOption = JSON.stringify(args[key])?.replaceAll("{enumValue}", option);
enumProps[key] = JSON.parse(replacedOption);
}
});
Object.entries(args)
.filter(([, value]) => JSON.stringify(value)?.includes("{outerEnumValue}"))
.map(([key]) => key)
.forEach((key) => {
outerEnumProps[key] = option;
const isNotPrimitive =
Object.keys(args[key] ?? {}).length || (Array.isArray(args[key]) && args[key].length);
if (key in args && isNotPrimitive) {
const replacedOption = JSON.stringify(args[key])?.replaceAll("{outerEnumValue}", option);
outerEnumProps[key] = JSON.parse(replacedOption);
}
});
return {
...args,
...enumProps,
...outerEnumProps,
[args.enum as string]: option,
[args.outerEnum as string]: outerOption,
};
}
export function getEnumVariantDescription(message = "Hover over a variant to see its value.") {
return {
docs: {
description: {
story: message,
},
},
};
}
export function trimText(text: string) {
return text.replace(/\s+/g, " ").trim();
}