Skip to content

Commit c70b7b7

Browse files
feat: improve registry maintenance with automated build script
- Add build-registry.js script that generates registry files from source components - Replace inline content with file references for better maintainability - Add build:registry script to package.json - Update REGISTRY.md with maintenance documentation - Validate source files exist before generating registry - Eliminate code duplication between source files and registry JSON This solves the maintenance issue where registry files had to be manually updated whenever component source code changed. Now the registry is automatically generated from the actual source files.
1 parent e73df42 commit c70b7b7

19 files changed

+300
-64
lines changed

packages/medusa-forms/REGISTRY.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@ You can install components from this registry using the shadcn CLI:
1010
npx shadcn@latest add --registry https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms/registry.json input
1111
```
1212

13+
## Registry Maintenance
14+
15+
The registry files are automatically generated from source components using a build script. This ensures the registry stays in sync with the actual component code without manual maintenance.
16+
17+
### Regenerating Registry Files
18+
19+
To regenerate the registry files after making changes to components:
20+
21+
```bash
22+
yarn build:registry
23+
```
24+
25+
This script:
26+
- Scans component source files in `src/ui/` and `src/controlled/`
27+
- Generates registry JSON files with file references (not inline content)
28+
- Updates the main `registry.json` with proper metadata
29+
- Validates that all referenced source files exist
30+
31+
### Adding New Components
32+
33+
To add a new component to the registry:
34+
35+
1. Create your component in the appropriate directory (`src/ui/` or `src/controlled/`)
36+
2. Add the component configuration to `scripts/build-registry.js` in the `COMPONENTS` object
37+
3. Run `yarn build:registry` to generate the registry files
38+
4. Commit both the source component and generated registry files
39+
1340
## Available Components
1441

1542
### Base UI Components
@@ -115,4 +142,3 @@ Controlled components use react-hook-form:
115142
- `FieldWrapperProps` - Wrapper component props
116143
- Component-specific props (`InputProps`, `SelectProps`, etc.)
117144
- React Hook Form integration types
118-

packages/medusa-forms/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"scripts": {
4141
"prepublishOnly": "yarn run build",
4242
"build": "vite build",
43+
"build:registry": "node scripts/build-registry.js",
4344
"lint": "biome check .",
4445
"lint:fix": "biome check --apply .",
4546
"type-check": "tsc --noEmit"

packages/medusa-forms/registry.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,4 @@
1414
"components": "src/components",
1515
"utils": "src/lib/utils"
1616
}
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "controlled-checkbox",
33
"type": "registry:ui",
4-
"description": "A checkbox component with react-hook-form integration",
4+
"description": "Checkbox component with react-hook-form integration",
55
"dependencies": [
66
"react-hook-form"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "controlled-checkbox.tsx",
14-
"content": "import {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { FieldCheckbox, type FieldCheckboxProps } from '../ui/FieldCheckbox';\n\nexport type ControlledCheckboxProps<T extends FieldValues> = Omit<FieldCheckboxProps, 'name'> &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>;\n };\n\nexport const ControlledCheckbox = <T extends FieldValues>({\n name,\n rules,\n onChange,\n ...props\n}: ControlledCheckboxProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>();\n\n return (\n <Controller\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => (\n <FieldCheckbox\n {...field}\n {...props}\n formErrors={errors}\n checked={field.value}\n onChange={(checked) => {\n if (onChange) onChange(checked);\n field.onChange(checked);\n }}\n />\n )}\n />\n );\n};\n\n"
14+
"path": "src/controlled/ControlledCheckbox.tsx"
1515
}
1616
]
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "controlled-currency-input",
33
"type": "registry:ui",
4-
"description": "A currency-input component with react-hook-form integration",
4+
"description": "CurrencyInput component with react-hook-form integration",
55
"dependencies": [
66
"react-hook-form"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "controlled-currency-input.tsx",
14-
"content": "import type * as React from 'react';\nimport {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { CurrencyInput, type CurrencyInputProps } from '../ui/CurrencyInput';\n\nexport type ControlledCurrencyInputProps<T extends FieldValues> = CurrencyInputProps &\n Omit<ControllerProps, 'render' | 'control'> & {\n name: Path<T>;\n };\n\nexport const ControlledCurrencyInput = <T extends FieldValues>({\n name,\n rules,\n ...props\n}: ControlledCurrencyInputProps<T>) => {\n const { control } = useFormContext<T>();\n\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => {\n return (\n <CurrencyInput\n {...field}\n {...props}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n field.onChange(e.target.value.replace(/[^0-9.-]+/g, ''));\n }}\n />\n );\n }}\n />\n );\n};\n\n"
14+
"path": "src/controlled/ControlledCurrencyInput.tsx"
1515
}
1616
]
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "controlled-datepicker",
33
"type": "registry:ui",
4-
"description": "A datepicker component with react-hook-form integration",
4+
"description": "DatePicker component with react-hook-form integration",
55
"dependencies": [
66
"react-hook-form"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "controlled-datepicker.tsx",
14-
"content": "import {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { DatePickerInput, type DatePickerProps } from '../ui/DatePicker';\n\nexport type ControlledDatePickerProps<T extends FieldValues> = DatePickerProps &\n Omit<ControllerProps, 'render' | 'control'> & {\n name: Path<T>;\n };\n\nexport const ControlledDatePicker = <T extends FieldValues>({\n name,\n rules,\n ...props\n}: ControlledDatePickerProps<T>) => {\n const { control } = useFormContext<T>();\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => <DatePickerInput {...field} {...props} />}\n />\n );\n};\n\n"
14+
"path": "src/controlled/ControlledDatePicker.tsx"
1515
}
1616
]
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "controlled-input",
33
"type": "registry:ui",
4-
"description": "An input component with react-hook-form integration",
4+
"description": "Input component with react-hook-form integration",
55
"dependencies": [
66
"react-hook-form"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "controlled-input.tsx",
14-
"content": "import type { ComponentProps } from 'react';\nimport {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { Input, type InputProps } from '../ui/Input';\n\nexport type ControlledInputProps<T extends FieldValues> = InputProps &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>;\n } & ComponentProps<typeof Input> &\n Omit<ControllerProps<T>, 'render'>;\n\nexport const ControlledInput = <T extends FieldValues>({\n name,\n rules,\n onChange,\n ...props\n}: ControlledInputProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>();\n\n return (\n <Controller\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => (\n <Input\n {...field}\n {...props}\n labelClassName={props.labelClassName}\n formErrors={errors}\n onChange={(evt) => {\n if (onChange) {\n onChange(evt);\n }\n field.onChange(evt);\n }}\n />\n )}\n />\n );\n};\n\n"
14+
"path": "src/controlled/ControlledInput.tsx"
1515
}
1616
]
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "controlled-select",
33
"type": "registry:ui",
4-
"description": "A select component with react-hook-form integration",
4+
"description": "Select component with react-hook-form integration",
55
"dependencies": [
66
"react-hook-form"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "controlled-select.tsx",
14-
"content": "import type * as React from 'react';\nimport {\n Controller,\n type ControllerProps,\n type FieldPathValue,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { Select, type SelectProps } from '../ui/Select';\n\nexport type ControlledSelectProps<T extends FieldValues> = SelectProps &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n onBlur?: () => void;\n onChange?: (value: unknown) => void;\n } & (\n | {\n options: { label: React.ReactNode; value: FieldPathValue<T, Path<T>> }[];\n children?: never;\n }\n | {\n options?: never;\n children: React.ReactNode;\n }\n );\n\nexport const ControlledSelect = <T extends FieldValues>({\n name,\n rules,\n children,\n options,\n onChange,\n onBlur,\n ...props\n}: ControlledSelectProps<T>) => {\n const { control } = useFormContext<T>();\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => {\n const handleChange = (value: unknown) => {\n if (typeof onChange === 'function') onChange(value);\n field.onChange(value);\n };\n\n if (options) {\n return (\n <Select {...({ ...field, ...props, onValueChange: handleChange } as SelectProps)}>\n <Select.Trigger>\n <Select.Value />\n </Select.Trigger>\n <Select.Content>\n {options.map((option) => (\n <Select.Item key={option.value} value={option.value}>\n {option.label}\n </Select.Item>\n ))}\n </Select.Content>\n </Select>\n );\n }\n\n return <Select {...({ ...field, ...props, onValueChange: handleChange } as SelectProps)}>{children}</Select>;\n }}\n />\n );\n};\n\n"
14+
"path": "src/controlled/ControlledSelect.tsx"
1515
}
1616
]
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "controlled-textarea",
33
"type": "registry:ui",
4-
"description": "A textarea component with react-hook-form integration",
4+
"description": "Textarea component with react-hook-form integration",
55
"dependencies": [
66
"react-hook-form"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "controlled-textarea.tsx",
14-
"content": "import type * as React from 'react';\nimport {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { TextArea, type TextAreaProps } from '../ui/TextArea';\n\nexport type ControlledTextAreaProps<T extends FieldValues> = TextAreaProps &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n rules?: RegisterOptions<T, Path<T>>;\n } & React.ComponentProps<typeof TextArea> &\n Omit<ControllerProps<T>, 'render'>;\n\nexport const ControlledTextArea = <T extends FieldValues>({ name, rules, ...props }: ControlledTextAreaProps<T>) => {\n const { control } = useFormContext<T>();\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules}\n render={({ field }) => <TextArea {...field} {...props} />}\n />\n );\n};\n\n"
14+
"path": "src/controlled/ControlledTextArea.tsx"
1515
}
1616
]
17-
}
18-
17+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "currency-input",
33
"type": "registry:ui",
4-
"description": "currency-input component",
4+
"description": "Base currency input component",
55
"dependencies": [
66
"@medusajs/ui"
77
],
@@ -11,8 +11,7 @@
1111
"files": [
1212
{
1313
"name": "currency-input.tsx",
14-
"content": "import { CurrencyInput as MedusaCurrencyInput } from '@medusajs/ui';\nimport { forwardRef } from 'react';\nimport { FieldWrapper } from './FieldWrapper';\nimport type { BasicFieldProps, MedusaCurrencyInputProps } from './types';\n\nexport type CurrencyInputProps = MedusaCurrencyInputProps & BasicFieldProps;\n\nconst Wrapper = FieldWrapper<CurrencyInputProps>;\n\nexport const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>((props, ref) => (\n <Wrapper {...props}>{(inputProps) => <MedusaCurrencyInput {...inputProps} ref={ref} />}</Wrapper>\n));\n\nCurrencyInput.displayName = 'CurrencyInput';\n"
14+
"path": "src/ui/CurrencyInput.tsx"
1515
}
1616
]
17-
}
18-
17+
}

0 commit comments

Comments
 (0)