diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..e89684b0 --- /dev/null +++ b/config.yaml @@ -0,0 +1,10 @@ +buildConfiguration: + buildCommand: pnpm run generate-registry + installDependenciesStep: + command: npm install -g pnpm && pnpm i --ignore-scripts + requiredFiles: + - package.json + - pnpm-lock.yaml + - tsconfig.json +livePreviewConfiguration: + setupCommand: ":" diff --git a/package.json b/package.json index e609efa9..f40eda5e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "ci-publish": "tsx scripts/publishCI.ts", "release": "tsx scripts/release.ts", "prepare": "husky && pnpm build", - "generate-notices": "generate-license-file" + "generate-notices": "generate-license-file", + "generate-registry": "tsx src/components/puck/registry/build-registry.ts" }, "dependencies": { "@measured/puck": "0.16.2-canary.a1988b5", @@ -109,7 +110,8 @@ "typescript": "^5.5.4", "vite": "^5.3.5", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^2.0.5" + "vitest": "^2.0.5", + "zod": "^3.23.8" }, "peerDependencies": { "react": "^17.0.2 || ^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1676ccb3..a1fd3b0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,6 +207,9 @@ importers: vitest: specifier: ^2.0.5 version: 2.1.4(@types/node@20.17.6)(jsdom@24.1.3) + zod: + specifier: ^3.23.8 + version: 3.23.8 packages: diff --git a/src/components/puck/BodyText.tsx b/src/components/puck/BodyText.tsx index a666c5b9..ad16c265 100644 --- a/src/components/puck/BodyText.tsx +++ b/src/components/puck/BodyText.tsx @@ -1,14 +1,14 @@ import * as React from "react"; import { ComponentConfig, Fields } from "@measured/puck"; -import { Body, BodyProps, bodyVariants } from "./atoms/body.tsx"; -import { useDocument } from "../../hooks/useDocument.tsx"; -import { resolveYextEntityField } from "../../utils/resolveYextEntityField.ts"; -import { EntityField } from "../editor/EntityField.tsx"; +import { Body, BodyProps, bodyVariants } from "./atoms/body.js"; import { + useDocument, + resolveYextEntityField, + EntityField, YextEntityField, YextEntityFieldSelector, -} from "../editor/YextEntityFieldSelector.tsx"; -import { NumberFieldWithDefaultOption } from "../editor/NumberOrDefaultField.tsx"; + NumberFieldWithDefaultOption, +} from "../../index.ts"; export interface BodyTextProps extends BodyProps { text: YextEntityField; diff --git a/src/components/puck/HeadingText.tsx b/src/components/puck/HeadingText.tsx index a35b65fb..07067362 100644 --- a/src/components/puck/HeadingText.tsx +++ b/src/components/puck/HeadingText.tsx @@ -1,14 +1,14 @@ -import React from "react"; +import * as React from "react"; import { ComponentConfig, Fields } from "@measured/puck"; -import { Heading, HeadingProps, headingVariants } from "./atoms/heading.tsx"; -import { useDocument } from "../../hooks/useDocument.tsx"; -import { resolveYextEntityField } from "../../utils/resolveYextEntityField.ts"; -import { EntityField } from "../editor/EntityField.tsx"; +import { Heading, HeadingProps, headingVariants } from "./atoms/heading.js"; import { + useDocument, + resolveYextEntityField, + EntityField, YextEntityField, YextEntityFieldSelector, -} from "../editor/YextEntityFieldSelector.tsx"; -import { NumberFieldWithDefaultOption } from "../editor/NumberOrDefaultField.tsx"; + NumberFieldWithDefaultOption, +} from "../../index.ts"; export interface HeadingTextProps extends HeadingProps { text: YextEntityField; diff --git a/src/components/puck/atoms/body.tsx b/src/components/puck/atoms/body.tsx index 7ca3f2d2..8ebc8d9a 100644 --- a/src/components/puck/atoms/body.tsx +++ b/src/components/puck/atoms/body.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import clsx from "clsx"; +import { clsx } from "clsx"; import { NumberOrDefault } from "../../editor/NumberOrDefaultField.tsx"; // Define the variants for the body component diff --git a/src/components/puck/atoms/heading.tsx b/src/components/puck/atoms/heading.tsx index 890d5fbf..c6138c7c 100644 --- a/src/components/puck/atoms/heading.tsx +++ b/src/components/puck/atoms/heading.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { type NumberOrDefault } from "../../editor/NumberOrDefaultField.tsx"; +import { type NumberOrDefault } from "../../editor/NumberOrDefaultField.js"; // Define the variants for the heading component const headingVariants = cva("components", { diff --git a/src/components/puck/registry/README.md b/src/components/puck/registry/README.md new file mode 100644 index 00000000..33fcbd61 --- /dev/null +++ b/src/components/puck/registry/README.md @@ -0,0 +1,33 @@ +# Component Shadcn Registry + +## Generate registry locally + +`pnpm run generate-registry` + +## Add a new component + +Add a new object to `ui` in `components.ts`. + +| Field | Description | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| name | must be unique | +| type | "registry:ui" if it should be directly pulled by a user. "registry:component" if it's a sub-component of multiple ui files | +| files | array of files relative to `src/components/puck` | +| registryDependencies | array of component dependencies using the names defined in this file | + +## Requirements for components + +- Do not use .ts or .tsx imports because of the starter's tsconfig +- Default package imports should be avoided because of the starter's tsconfig +- Imports from the @yext/visual-editor package must have a local path that is captured by the + `IMPORT_PATTERN` in `build-registry.ts` + +## Running shadcn locally + +Useful for debugging our registry with the shadcn CLI. + +1. `git clone https://github.com/shadcn-ui/ui.git` +2. `pnpm i` +3. `cd packages/shadcn` (`packages/cli` is the old cli) +4. `pnpm run dev` +5. `REGISTRY_URL=https://reliably-numerous-kit.pgsdemo.com/components node ui/packages/shadcn/dist/index.js add` diff --git a/src/components/puck/registry/artifacts.json b/src/components/puck/registry/artifacts.json new file mode 100644 index 00000000..8c8c267e --- /dev/null +++ b/src/components/puck/registry/artifacts.json @@ -0,0 +1,10 @@ +{ + "artifactStructure": { + "assets": [ + { + "root": "dist", + "pattern": "components/**/*" + } + ] + } +} diff --git a/src/components/puck/registry/build-registry.ts b/src/components/puck/registry/build-registry.ts new file mode 100644 index 00000000..756c560f --- /dev/null +++ b/src/components/puck/registry/build-registry.ts @@ -0,0 +1,143 @@ +// Based on https://github.com/bwestwood11/ui-cart/blob/main/scripts/build-registry.ts +// which is a simplified version of https://github.com/shadcn-ui/ui/blob/main/apps/www/scripts/build-registry.mts +import { writeFile, copyFile, mkdir, readFile } from "node:fs/promises"; +import { existsSync, rmSync } from "fs"; +import z from "zod"; +import path from "path"; +import { registryComponents } from "./registry.ts"; +import { registryItemFileSchema } from "./schema.ts"; + +const DIST_DIR = `./dist`; +const SLUG = `components`; +const COLORS_PATH = `${DIST_DIR}/${SLUG}/colors`; +const ICONS_PATH = `${DIST_DIR}/${SLUG}/icons`; +const STYLES_PATH = `${DIST_DIR}/${SLUG}/styles`; +const YEXT_STYLE_PATH = `${STYLES_PATH}/yext`; +const COMPONENTS_SRC_PATH = `./src/components/puck`; + +// matches import { ... } from "..." where the import path starts with ../../ +const IMPORT_PATTERN = /from "(\.\.\/\.\.\/[^"]+)"/g; + +const styleIndex = { + name: "yext", + type: "registry:style", + cssVars: {}, + files: [], +}; + +const colorsIndex = { + inlineColors: { + light: {}, + dark: {}, + }, + cssVars: { + light: {}, + dark: {}, + }, + inlineColorsTemplate: "", + cssVarsTemplate: "", +}; + +const iconsIndex = {}; + +const stylesIndex = [{ name: "yext", label: "Yext" }]; + +async function writeFileRecursive(filePath: string, data: string) { + const dir = path.dirname(filePath); // Extract the directory path + + try { + // Ensure the directory exists, recursively creating directories as needed + await mkdir(dir, { recursive: true }); + await writeFile(filePath, data, "utf-8"); + } catch (error) { + console.error("Error writing file: ", error); + } +} + +type File = z.infer; +// Read the files specified in registry.ts, insert the @yext/visual-editor import and output JSON +const getComponentFiles = async (files: File[]) => { + const filesArrayPromises = (files ?? []).map(async (file) => { + if (typeof file === "string") { + const filePath = `${COMPONENTS_SRC_PATH}/${file}`; + const fileContent = await readFile(filePath, "utf-8"); + return { + type: "registry:component", + content: fileContent.replace( + IMPORT_PATTERN, + `from "@yext/visual-editor"` + ), + path: file, + target: `${SLUG}/${file}`, + }; + } + }); + const filesArray = await Promise.all(filesArrayPromises); + + return filesArray; +}; + +export const buildRegistry = async () => { + // Delete dist folder if it exists + if (existsSync(DIST_DIR)) { + rmSync(DIST_DIR, { recursive: true }); + } + + // Write all index files + await Promise.all([ + writeFileRecursive( + `${YEXT_STYLE_PATH}/index.json`, + JSON.stringify(styleIndex) + ), + writeFileRecursive( + `${COLORS_PATH}/neutral.json`, + JSON.stringify(colorsIndex) + ), + writeFileRecursive(`${ICONS_PATH}/index.json`, JSON.stringify(iconsIndex)), + writeFileRecursive( + `${STYLES_PATH}/index.json`, + JSON.stringify(stylesIndex) + ), + writeFileRecursive( + `${DIST_DIR}/${SLUG}/index.json`, + JSON.stringify(registryComponents) + ), + writeFileRecursive( + `${DIST_DIR}/${SLUG}/registry/index.json`, + JSON.stringify(registryComponents) + ), + ]); + + // copy artifacts.json after to ensure that the `dist` dir exists + await copyFile( + `${COMPONENTS_SRC_PATH}/registry/artifacts.json`, + `${DIST_DIR}/artifacts.json` + ); + + // generate JSON files for each component + for (let i = 0; i < registryComponents.length; i++) { + const component = registryComponents[i]; + const files = component.files; + if (!files) throw new Error("No files found for component"); + + const filesArray = await getComponentFiles(files); + + const json = JSON.stringify( + { + ...component, + files: filesArray, + }, + null, + 2 + ); + await writeFileRecursive(`${YEXT_STYLE_PATH}/${component.name}.json`, json); + } +}; + +buildRegistry() + .then(() => { + console.log("Registry files created successfully"); + }) + .catch((err) => { + console.error("Error creating registry files: ", err); + }); diff --git a/src/components/puck/registry/components.ts b/src/components/puck/registry/components.ts new file mode 100644 index 00000000..771fc332 --- /dev/null +++ b/src/components/puck/registry/components.ts @@ -0,0 +1,29 @@ +import { Registry } from "./schema.ts"; + +// type: registry:ui => appears in the `npx shadcn add` list +// type: registry:component => building blocks that shouldn't be used on their own + +export const ui: Registry = [ + { + name: "heading", + type: "registry:component", + files: ["atoms/heading.tsx"], + }, + { + name: "HeadingText", + type: "registry:ui", + registryDependencies: ["heading"], + files: ["HeadingText.tsx"], + }, + { + name: "body", + type: "registry:component", + files: ["atoms/body.tsx"], + }, + { + name: "BodyText", + type: "registry:ui", + registryDependencies: ["body"], + files: ["BodyText.tsx"], + }, +]; diff --git a/src/components/puck/registry/registry.ts b/src/components/puck/registry/registry.ts new file mode 100644 index 00000000..f3b32a88 --- /dev/null +++ b/src/components/puck/registry/registry.ts @@ -0,0 +1,4 @@ +import { ui } from "./components.ts"; + +// Allows for later expansion into other shadcn registry types +export const registryComponents = [...ui]; diff --git a/src/components/puck/registry/schema.ts b/src/components/puck/registry/schema.ts new file mode 100644 index 00000000..4437fc30 --- /dev/null +++ b/src/components/puck/registry/schema.ts @@ -0,0 +1,91 @@ +// Based on https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/schema.ts +import { z } from "zod"; + +export const blockChunkSchema = z.object({ + name: z.string(), + description: z.string(), + component: z.any(), + file: z.string(), + code: z.string().optional(), + container: z + .object({ + className: z.string().nullish(), + }) + .optional(), +}); + +export const registryItemTypeSchema = z.enum([ + "registry:style", + "registry:lib", + "registry:example", + "registry:block", + "registry:component", + "registry:ui", + "registry:hook", + "registry:theme", + "registry:page", +]); + +export const registryItemFileSchema = z.union([ + z.string(), + z.object({ + path: z.string(), + content: z.string().optional(), + type: registryItemTypeSchema, + target: z.string().optional(), + }), +]); + +export const registryItemTailwindSchema = z.object({ + config: z.object({ + content: z.array(z.string()).optional(), + theme: z.record(z.string(), z.any()).optional(), + plugins: z.array(z.string()).optional(), + }), +}); + +export const registryItemCssVarsSchema = z.object({ + light: z.record(z.string(), z.string()).optional(), + dark: z.record(z.string(), z.string()).optional(), +}); + +export const registryEntrySchema = z.object({ + name: z.string(), + type: registryItemTypeSchema, + description: z.string().optional(), + dependencies: z.array(z.string()).optional(), + devDependencies: z.array(z.string()).optional(), + registryDependencies: z.array(z.string()).optional(), + files: z.array(registryItemFileSchema).optional(), + tailwind: registryItemTailwindSchema.optional(), + cssVars: registryItemCssVarsSchema.optional(), + source: z.string().optional(), + category: z.string().optional(), + subcategory: z.string().optional(), + chunks: z.array(blockChunkSchema).optional(), + docs: z.string().optional(), +}); + +export const registrySchema = z.array(registryEntrySchema); + +export type RegistryEntry = z.infer; + +export type Registry = z.infer; + +export const blockSchema = registryEntrySchema.extend({ + type: z.literal("registry:block"), + style: z.enum(["default", "new-york"]), + component: z.any(), + container: z + .object({ + height: z.string().nullish(), + className: z.string().nullish(), + }) + .optional(), + code: z.string(), + highlightedCode: z.string(), +}); + +export type Block = z.infer; + +export type BlockChunk = z.infer; diff --git a/tailwind.config.ts b/tailwind.config.ts index 635c8337..ef4b504a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,7 +1,10 @@ import type { Config } from "tailwindcss"; export default { - content: ["./src/**/*.{html,js,jsx,ts,tsx}"], + content: [ + "./src/**/*.{html,js,jsx,ts,tsx}", + "!./src/components/puck/registry/**", // exclude the registry + ], prefix: "ve-", theme: { extend: { diff --git a/tsconfig.json b/tsconfig.json index 0c2bc2d0..b7656788 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "target": "ESNext", "jsx": "react-jsx", @@ -26,7 +25,8 @@ "strict": true, "skipDefaultLibCheck": true, - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true, }, "include": ["src", "index.d.ts", ".eslintrc.json"], "exclude": ["node_modules"]