| import fs from 'node:fs' |
| import { load as yamlLoad } from 'js-yaml' |
| import { z } from 'zod' |
| import { zLanguageCode, zPxSizeOrEmpty, zVersionMajorMinor, zVersionSemver } from './validation' |
| import { capitalizeFirstLetter } from './utils' |
| |
| // An object containing all the data types and their associated schema. The key should match the name of the data file |
| // in the `./site/data/` directory. |
| const dataDefinitions = { |
| breakpoints: z |
| .object({ |
| breakpoint: z.string(), |
| abbr: z.string(), |
| name: z.string(), |
| 'min-width': zPxSizeOrEmpty, |
| container: zPxSizeOrEmpty |
| }) |
| .array(), |
| colors: z |
| .object({ |
| name: z.string() |
| }) |
| .array(), |
| 'core-team': z |
| .object({ |
| name: z.string(), |
| user: z.string() |
| }) |
| .array(), |
| 'docs-versions': z |
| .object({ |
| group: z.string(), |
| baseurl: z.url(), |
| description: z.string(), |
| versions: z.union([zVersionSemver, zVersionMajorMinor]).array() |
| }) |
| .array(), |
| examples: z |
| .object({ |
| category: z.string(), |
| external: z.boolean().optional(), |
| description: z.string(), |
| examples: z |
| .object({ |
| description: z.string(), |
| indexPath: z.string().optional(), |
| name: z.string(), |
| url: z.string().optional() |
| }) |
| .array() |
| }) |
| .array(), |
| icons: z.object({ |
| preferred: z |
| .object({ |
| name: z.string(), |
| website: z.url() |
| }) |
| .array(), |
| more: z |
| .object({ |
| name: z.string(), |
| website: z.url() |
| }) |
| .array() |
| }), |
| plugins: z |
| .object({ |
| description: z.string(), |
| link: z.string().regex(/^(components|forms)\//), |
| name: z.string() |
| }) |
| .array(), |
| sidebar: z |
| .object({ |
| title: z.string(), |
| section: z.string().optional(), |
| icon: z.string().optional(), |
| icon_color: z.string().optional(), |
| pages: z |
| .object({ |
| title: z.string().optional(), |
| href: z.string().optional(), |
| group: z.string().optional(), |
| meta: z.object({ added: z.string() }).array().optional(), |
| pages: z |
| .object({ |
| title: z.string(), |
| meta: z.object({ added: z.string() }).array().optional() |
| }) |
| .array() |
| .optional() |
| }) |
| .array() |
| .optional() |
| }) |
| .array(), |
| 'theme-colors': z |
| .object({ |
| name: z.string() |
| }) |
| .array() |
| .transform((val) => { |
| // Add a `title` property to each theme color object being the capitalized version of the `name` property. |
| return val.map((themeColor) => ({ ...themeColor, title: capitalizeFirstLetter(themeColor.name) })) |
| }), |
| translations: z |
| .object({ |
| name: z.string(), |
| code: zLanguageCode, |
| description: z.string(), |
| url: z.url() |
| }) |
| .array() |
| } satisfies Record<string, DataSchema> |
| |
| // Inferred types for individual data files. Exported so consumers can avoid |
| // re-deriving them via `ReturnType<typeof getData<'sidebar'>>` and don't have |
| // to fall back to `any` when iterating nested arrays. |
| export type SidebarGroup = z.infer<typeof dataDefinitions.sidebar>[number] |
| export type SidebarItem = NonNullable<SidebarGroup['pages']>[number] |
| export type SidebarSubItem = NonNullable<SidebarItem['pages']>[number] |
| |
| let data = new Map<DataType, z.infer<DataSchema>>() |
| |
| // A helper to get data loaded fom a yml file in the `./site/data/` directory. If the data does not match its associated |
| // schema from `dataDefinitions`, an error is thrown to indicate that the data file is invalid and some action is |
| // required. |
| export function getData<TType extends DataType>(type: TType): z.infer<(typeof dataDefinitions)[TType]> { |
| if (data.has(type)) { |
| // Returns the data if it has already been loaded. |
| return data.get(type) as z.infer<(typeof dataDefinitions)[TType]> |
| } |
| |
| const dataPath = `./site/data/${type}.yml` |
| |
| try { |
| // Load the data from the yml file. |
| const rawData = yamlLoad(fs.readFileSync(dataPath, 'utf8')) |
| |
| // Parse the data using the data schema to validate its content and get back a fully typed data object. |
| const parsedData = dataDefinitions[type].parse(rawData) as z.infer<(typeof dataDefinitions)[TType]> |
| |
| // Cache the data. |
| data.set(type, parsedData) |
| |
| return parsedData |
| } catch (error) { |
| if (error instanceof z.ZodError) { |
| console.error(`The \`${dataPath}\` file content is invalid:`, error.issues) |
| } |
| |
| throw new Error(`Failed to load data from \`${dataPath}\``, { cause: error }) |
| } |
| } |
| |
| type DataType = keyof typeof dataDefinitions |
| type DataSchema = z.ZodTypeAny |