diff --git a/apps/www/package.json b/apps/www/package.json index fce418d..9487b1e 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -20,6 +20,7 @@ "@fontsource-variable/inter": "^5.0.20", "@nanostores/react": "^0.7.3", "@nanostores/vue": "^0.10.0", + "@ui/lib": "workspace:*", "@ui/react": "workspace:*", "@ui/vue": "workspace:*", "astro": "^4.14.6", @@ -30,6 +31,7 @@ "react": "catalog:", "react-dom": "catalog:", "recharts": "^2.12.7", + "scule": "^1.3.0", "sharp": "^0.32.6", "tailwind-merge": "catalog:" }, diff --git a/apps/www/src/atoms/framework.ts b/apps/www/src/atoms/framework.ts index 3340f4b..91f332c 100644 --- a/apps/www/src/atoms/framework.ts +++ b/apps/www/src/atoms/framework.ts @@ -2,4 +2,6 @@ import { atom } from "nanostores" export type Framework = "react" | "vue" +export const frameworks: Framework[] = ["react", "vue"] + export const $framework = atom("react") diff --git a/apps/www/src/components/content/code-wrapper/code-wrapper.tsx b/apps/www/src/components/content/code-wrapper/code-wrapper.tsx index a1a502c..b7d1ec9 100644 --- a/apps/www/src/components/content/code-wrapper/code-wrapper.tsx +++ b/apps/www/src/components/content/code-wrapper/code-wrapper.tsx @@ -6,9 +6,10 @@ import { cn } from "@/lib/utils" interface Props { collapsible?: boolean + copy?: boolean children?: React.ReactNode - className: string | undefined - src: string + className?: string + src?: string [key: string]: unknown } @@ -17,6 +18,7 @@ export default function CodeWrapper({ className, src, collapsible, + copy = true, ...props }: Props) { const [open, setOpen] = useState(false) @@ -45,7 +47,9 @@ export default function CodeWrapper({ ) : null} - + {copy ? ( + + ) : null} ) } diff --git a/apps/www/src/components/content/code-wrapper/index.astro b/apps/www/src/components/content/code-wrapper/index.astro index 77c26d7..611d617 100644 --- a/apps/www/src/components/content/code-wrapper/index.astro +++ b/apps/www/src/components/content/code-wrapper/index.astro @@ -1,25 +1,21 @@ --- import { Code } from "astro/components" import type { HTMLAttributes } from "astro/types" +import type { BuiltinLanguage } from "shiki" import CodeWrapper from "./code-wrapper" interface Props extends HTMLAttributes<"div"> { - lang?: "tsx" | "vue" | "ts" | "json" + src?: string + lang?: BuiltinLanguage className?: string collapsible?: boolean - src: string + copy?: boolean } -const { src, lang, className, collapsible, ...props } = Astro.props +const { src, lang, className, ...props } = Astro.props --- - - + + diff --git a/apps/www/src/components/content/component-preview/framework-switcher.tsx b/apps/www/src/components/content/component-preview/framework-switcher.tsx index 45ddc61..53a9cfe 100644 --- a/apps/www/src/components/content/component-preview/framework-switcher.tsx +++ b/apps/www/src/components/content/component-preview/framework-switcher.tsx @@ -1,7 +1,8 @@ import { useStore } from "@nanostores/react" import { useEffect, useState } from "react" +import { pascalCase } from "scule" -import { $framework, type Framework } from "@/atoms/framework" +import { $framework, type Framework, frameworks } from "@/atoms/framework" import { Select, SelectContent, @@ -12,21 +13,10 @@ import { SelectValueText, } from "@ui/react/select" -const frameworks = [ - { - value: "react", - label: "React", - }, - { - value: "vue", - label: "Vue", - }, - { - value: "solid", - label: "Solid (WIP)", - disabled: true, - }, -] +const frameworksItems = frameworks.map((framework) => ({ + value: framework, + label: pascalCase(framework), +})) export function FrameworkSwitcher() { const framework = useStore($framework) @@ -45,7 +35,7 @@ export function FrameworkSwitcher() { }} className="w-[160px]" positioning={{ sameWidth: true }} - items={frameworks} + items={frameworksItems} > @@ -55,13 +45,9 @@ export function FrameworkSwitcher() { - {frameworks.map((framework) => ( - - {framework.label} + {frameworksItems.map((item) => ( + + {item.label} ))} diff --git a/apps/www/src/components/content/component-preview/index.astro b/apps/www/src/components/content/component-preview/index.astro index 7c6730c..47b4f49 100644 --- a/apps/www/src/components/content/component-preview/index.astro +++ b/apps/www/src/components/content/component-preview/index.astro @@ -2,6 +2,7 @@ import { CopyButton } from "@/components/copy-button" import { getExampleSource } from "@/lib/source" +import { frameworks } from "@/atoms/framework" import CodeWrapper from "../code-wrapper/index.astro" import { FrameworkSwitcher } from "./framework-switcher" import NotFound from "./not-found.astro" @@ -14,8 +15,11 @@ interface Props { } const { name } = Astro.props -const source = await getExampleSource("react", name) -const sourceVue = await getExampleSource("vue", name) +const frameworkSources = await Promise.all( + frameworks.map(async (framework) => { + return await getExampleSource(framework, name) + }) +) ---
@@ -24,84 +28,50 @@ const sourceVue = await getExampleSource("vue", name)
- -
-
- { - source ? ( - - ) : ( - - ) - } -
- + { + frameworks.map((framework, index) => ( + + )) + }
- -
- - diff --git a/apps/www/src/components/content/component-source.astro b/apps/www/src/components/content/component-source.astro index 9b0c410..703046e 100644 --- a/apps/www/src/components/content/component-source.astro +++ b/apps/www/src/components/content/component-source.astro @@ -1,4 +1,5 @@ --- +import { frameworks } from "@/atoms/framework" import { getComponentSource } from "@/lib/source" import CodeWrapper from "./code-wrapper/index.astro" @@ -7,25 +8,29 @@ export interface Props { } const { component } = Astro.props -const source = await getComponentSource("react", component) -const sourceVue = await getComponentSource("vue", component) +const frameworkSources = await Promise.all( + frameworks.map((framework) => getComponentSource(framework, component)) +) --- { - source.map((src) => ( - - )) -} -{ - sourceVue.map((src) => ( - + frameworks.map((framework, index) => ( +
+ {frameworkSources[index]?.length ? ( + frameworkSources[index]?.map((src) => ( +
+

{src.filename}

+ +
+ )) + ) : ( + + )} +
)) } diff --git a/apps/www/src/components/copy-button.tsx b/apps/www/src/components/copy-button.tsx index 8516e00..87a66f3 100644 --- a/apps/www/src/components/copy-button.tsx +++ b/apps/www/src/components/copy-button.tsx @@ -5,8 +5,7 @@ import { cn } from "@/lib/utils" import { Button, type ButtonProps } from "@ui/react/button" interface CopyButtonProps extends ButtonProps { - value: string - src?: string + value?: string | undefined } export async function copyToClipboardWithMeta(value: string) { @@ -16,7 +15,6 @@ export async function copyToClipboardWithMeta(value: string) { export function CopyButton({ value, className, - src, variant = "ghost", ...props }: CopyButtonProps) { @@ -42,7 +40,7 @@ export function CopyButton({ className )} onClick={() => { - copyToClipboardWithMeta(value) + copyToClipboardWithMeta(value || "") setHasCopied(true) }} {...props} diff --git a/apps/www/src/lib/source.ts b/apps/www/src/lib/source.ts index 76bb257..0921d9a 100644 --- a/apps/www/src/lib/source.ts +++ b/apps/www/src/lib/source.ts @@ -1,62 +1,68 @@ import fs from "node:fs/promises" import path from "node:path" import type { Framework } from "@/atoms/framework" -import { type ResolveOptions, findExports, resolvePath } from "mlly" - -const resolvePathAndReadFile = async (id: string, options: ResolveOptions) => { - try { - const filePath = await resolvePath(id, options) - return fs.readFile(filePath, "utf-8") - } catch (error) { - console.log("error", error) - return false - } -} +import { safeReadFile } from "@ui/lib/utils/fs" +import { safeResolvePath } from "@ui/lib/utils/mlly" +import { findExports } from "mlly" +import type { BuiltinLanguage } from "shiki" export const getExampleSource = async (framework: Framework, name: string) => { - return resolvePathAndReadFile(`@ui/${framework}/examples/${name}`, { + const entryPath = await safeResolvePath(`@ui/${framework}/examples/${name}`, { conditions: ["source"], }) + if (!entryPath.success) return null + const entryFile = await safeReadFile(entryPath.result) + if (!entryFile.success) return null + const entryFileInfo = extractFileInfo(entryPath.result) + return { + filename: `${entryFileInfo[1]}.${entryFileInfo[2]}`, + source: entryFile.result, + lang: entryFileInfo[2] as BuiltinLanguage, + } } export const getComponentSource = async ( framework: Framework, name: string ) => { - switch (framework) { - case "react": { - return [ - await resolvePathAndReadFile(`@ui/react/${name}`, { - conditions: ["source"], - }), - ] - } + const entryPath = await safeResolvePath(`@ui/${framework}/${name}`, { + conditions: ["source"], + }) + if (!entryPath.success) return [] + const entryFile = await safeReadFile(entryPath.result) + if (!entryFile.success) return [] - case "vue": { - const entryPath = await resolvePath(`@ui/vue/${name}`, { - conditions: ["source"], - }) - const entry = await resolvePathAndReadFile(`@ui/vue/${name}`, { - conditions: ["source"], + const exports = await findExports(entryFile.result) + const sources = await Promise.all( + exports + .filter((exp) => exp.specifier?.startsWith("./")) + .map(async (exp) => { + // biome-ignore lint/style/noNonNullAssertion: + const [, name, lang] = extractFileInfo(exp.specifier!) + + return { + filename: `${name}.${lang}`, + source: await fs.readFile( + // biome-ignore lint/style/noNonNullAssertion: + path.resolve(path.dirname(entryPath.result), exp.specifier!), + "utf-8" + ), + lang: lang as BuiltinLanguage, + } }) + ) - if (entry) { - const exports = await findExports(entry) - const sources = await Promise.all( - exports - .filter((exp) => exp.specifier?.startsWith("./")) - .map(async (exp) => { - console.log("exp", exp) - return fs.readFile( - // biome-ignore lint/style/noNonNullAssertion: - path.resolve(path.dirname(entryPath), exp.specifier!), - "utf-8" - ) - }) - ) - return [...sources, entry] - } - return [] - } - } + const entryFileInfo = extractFileInfo(entryPath.result) + return [ + ...sources, + { + filename: `${entryFileInfo[1]}.${entryFileInfo[2]}`, + source: entryFile.result, + lang: entryFileInfo[2] as BuiltinLanguage, + }, + ] +} + +export const extractFileInfo = (p: string) => { + return p.match(/([\w-]+)\.(vue|ts|tsx)$/) || [] } diff --git a/apps/www/src/pages/docs/[...slug].astro b/apps/www/src/pages/docs/[...slug].astro index 3add5a1..5d0e85b 100644 --- a/apps/www/src/pages/docs/[...slug].astro +++ b/apps/www/src/pages/docs/[...slug].astro @@ -102,3 +102,30 @@ const toc = generateToC(headings, { minHeadingLevel: 2, maxHeadingLevel: 4 }) + + diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json index ac4c2ac..d938a36 100644 --- a/apps/www/tsconfig.json +++ b/apps/www/tsconfig.json @@ -2,6 +2,7 @@ "extends": "astro/tsconfigs/strictest", "compilerOptions": { "verbatimModuleSyntax": true, + "exactOptionalPropertyTypes": false, // react "jsxImportSource": "react", diff --git a/packages/lib/package.json b/packages/lib/package.json index ca368b0..913c2e7 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -9,6 +9,10 @@ "./ai": { "types": "./ai/index.ts", "default": "./ai/index.ts" + }, + "./utils/*": { + "types": "./utils/*.ts", + "default": "./utils/*.ts" } }, "devDependencies": { @@ -17,6 +21,7 @@ "dependencies": { "@ai-sdk/anthropic": "^0.0.41", "@ai-sdk/openai": "^0.0.46", - "ai": "catalog:" + "ai": "catalog:", + "mlly": "^1.7.1" } } diff --git a/packages/lib/utils/fs.ts b/packages/lib/utils/fs.ts new file mode 100644 index 0000000..67eed12 --- /dev/null +++ b/packages/lib/utils/fs.ts @@ -0,0 +1,28 @@ +import fs from "node:fs/promises" + +type SafeReadFileResult = + | { + success: true + result: string + } + | { + success: false + error: Error + } + +export const safeReadFile = async ( + filePath: string +): Promise => { + try { + const file = await fs.readFile(filePath, "utf-8") + return { + success: true, + result: file, + } + } catch (error) { + return { + success: false, + error: error as Error, + } + } +} diff --git a/packages/lib/utils/mlly.ts b/packages/lib/utils/mlly.ts new file mode 100644 index 0000000..b5032a4 --- /dev/null +++ b/packages/lib/utils/mlly.ts @@ -0,0 +1,28 @@ +import { type ResolveOptions, findExports, resolvePath } from "mlly" + +type SafeResolvePathResult = + | { + success: true + result: string + } + | { + success: false + error: Error + } + +export const safeResolvePath = async ( + id: string, + options: ResolveOptions +): Promise => { + try { + return { + success: true, + result: await resolvePath(id, options), + } + } catch (error) { + return { + success: false, + error: error as Error, + } + } +} diff --git a/packages/vue/package.json b/packages/vue/package.json index d687025..530a14f 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -75,7 +75,6 @@ "vite": "catalog:", "vite-tsconfig-paths": "^4.3.2", "vue": "catalog:", - "vue-tsc": "^2.0.29", - "zod": "catalog:" + "vue-tsc": "^2.0.29" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99b3aea..b368d32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,9 +51,6 @@ catalogs: vue: specifier: ^3.4.38 version: 3.4.38 - zod: - specifier: ^3.23.8 - version: 3.23.8 importers: @@ -119,6 +116,9 @@ importers: '@nanostores/vue': specifier: ^0.10.0 version: 0.10.0(nanostores@0.11.3)(vue@3.4.38(typescript@5.5.4)) + '@ui/lib': + specifier: workspace:* + version: link:../../packages/lib '@ui/react': specifier: workspace:* version: link:../../packages/react @@ -149,6 +149,9 @@ importers: recharts: specifier: ^2.12.7 version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + scule: + specifier: ^1.3.0 + version: 1.3.0 sharp: specifier: ^0.32.6 version: 0.32.6 @@ -219,6 +222,9 @@ importers: ai: specifier: 'catalog:' version: 3.3.20(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.4.38(typescript@5.5.4))(zod@3.23.8) + mlly: + specifier: ^1.7.1 + version: 1.7.1 devDependencies: '@ui/tsconfig': specifier: workspace:* @@ -409,9 +415,6 @@ importers: vue-tsc: specifier: ^2.0.29 version: 2.0.29(typescript@5.5.4) - zod: - specifier: 'catalog:' - version: 3.23.8 packages: @@ -5829,6 +5832,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -6797,6 +6803,9 @@ packages: vue-component-type-helpers@2.0.29: resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==} + vue-component-type-helpers@2.1.2: + resolution: {integrity: sha512-URuxnrOhO9lUG4LOAapGWBaa/WOLDzzyAbL+uKZqT7RS+PFy0cdXI2mUSh7GaMts6vtHaeVbGk7trd0FPJi65Q==} + vue-docgen-api@4.79.2: resolution: {integrity: sha512-n9ENAcs+40awPZMsas7STqjkZiVlIjxIKgiJr5rSohDP0/JCrD9VtlzNojafsA1MChm/hz2h3PDtUedx3lbgfA==} peerDependencies: @@ -9005,7 +9014,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.38(typescript@5.5.4) - vue-component-type-helpers: 2.0.29 + vue-component-type-helpers: 2.1.2 '@swc/helpers@0.5.12': dependencies: @@ -14022,6 +14031,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scule@1.3.0: {} + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -14539,7 +14550,7 @@ snapshots: is-glob: 4.0.3 jiti: 1.21.6 lilconfig: 2.1.0 - micromatch: 4.0.7 + micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.1 @@ -15194,6 +15205,8 @@ snapshots: vue-component-type-helpers@2.0.29: {} + vue-component-type-helpers@2.1.2: {} + vue-docgen-api@4.79.2(vue@3.4.38(typescript@5.5.4)): dependencies: '@babel/parser': 7.25.3